Skip to content

feat(loop-agent): profile-owned Layer-1 system prompt — provider default + fail-loud#81

Open
bkrabach wants to merge 7 commits into
mainfrom
feat/profile-owned-system-prompt
Open

feat(loop-agent): profile-owned Layer-1 system prompt — provider default + fail-loud#81
bkrabach wants to merge 7 commits into
mainfrom
feat/profile-owned-system-prompt

Conversation

@bkrabach

Copy link
Copy Markdown
Collaborator

Summary

attractor's loop-pipeline spawned provider node agents (anthropic/openai/gemini) that received a STUB system prompt ("You are a coding agent."), causing hallucination and over-fragmented output. Root cause: loop-agent's Layer-1 base prompt was read ONLY from system_prompt/system_prompt_file in session.orchestrator.config — it never consumed the kernel context manager's context.include factory (the in-code comment claiming it did was stale). Provider prompts were parked in context.include side-files, so they never reached Layer-1.

Fix: loop-agent now resolves Layer-1 with precedence:

  1. explicit system_prompt
  2. explicit system_prompt_file
  3. provider default context/system-<provider>.md (keyed on the SAME provider source used for the actual completion)
  4. fail-loud RuntimeError for an unknown provider (never a silent stub)

Path resolution is CWD-independent with clear missing-file errors. The stale comment is corrected. attractor-expert (a non-coding consultant) gets a dedicated persona base.

Net simplification: The provider default replaced ~24 hard-coded system_prompt_file config lines across the bundle/agent/profile YAMLs (only attractor-expert keeps an explicit override).

Consumer impact — zero-config: attractor users, the dot-graph resolver, and any external loop-agent consumer now get the correct provider base prompt with NO changes on their side — they just pick up this version.

Verification checklist

  • Unit tests pass (pytest modules/loop-pipeline/) — 1378 tests passing
  • Live pipeline run exercising changed code path — verified in DTU; see evidence section
  • AGENTS.md reviewed; repo-specific gates met
  • Backward-compat path unchanged — existing explicit system_prompt/system_prompt_file configs continue to work (tested)
  • PR body includes verification evidence (below)

Verification evidence

Cross-provider Layer-1 resolution (DTU integration test):

  • Each spawned loop-agent node received its provider's verbatim base header via the DEFAULT — no cross-contamination between anthropic/openai/gemini
  • wiki-weaver ingest scenario: base + task prompt delivered, no fail-loud
  • app-cli attractor-tool (root→tool→node chain): base reaches the deepest node; no fail-loud

Live Resolve stack validation (real consumer, v0.2.0 dot-graph resolver):

  • Real DotGraphResolver (v0.2.0) drove its own example pipeline (expert_builder_explorer.dot) in a real worker with this branch loaded
  • Spawned loop-agent nodes received Anthropic base via the DEFAULT
  • Zero fail-loud; dot-graph UNCHANGED
  • Provider base correctly injected at Layer-1 (verified in session events)

Backward-compatibility:

  • Existing bundles with explicit system_prompt_file config remain unchanged — they still use their explicit override
  • Fallback hierarchy tested: explicit > file > default > fail-loud

Notes for reviewers

Design reference: See docs/designs/layer-1-profile-owned-system-prompt.md for the design document.

Known follow-ups (not blockers):

  1. Provider default ships anthropic/openai/gemini; any other provider fails loud unless it sets an explicit base (by design — never a silent wrong base).
  2. Multi-provider fan-out routing (separate, pre-existing): when all providers are mounted on a parent that fans out, the per-node provider isn't promoted. Not this feature; tracked separately.
  3. Wheel packaging: the default resolves the prompt files via the amplifier full-clone source path (how all real consumers load loop-agent). A standalone pip-WHEEL install of loop-agent wouldn't bundle context/system-*.md → fail-loud (not silent). Package them into the wheel if standalone-wheel installs are ever needed.

bkrabach and others added 7 commits June 27, 2026 15:09
…ide + fail-loud

Deliver the provider base prompt as a first-class loop-agent Layer-1
SessionConfig field (spec §6.1 ProviderProfile), rather than through the
context.include side-file channel that silently goes empty on spawned
sessions.

## What changed

### loop-agent/__init__.py
- Load system_prompt from orchestrator_config at session init via the
  new system_prompt_file: context/system-<provider>.md path (resolved
  relative to bundle root via Path(__file__) 4-level traversal).
- Fail-loud on empty Layer-1 for LLM-capable sessions (RuntimeError
  with a diagnostic pointing to the fix).

### loop-agent/agent_session.py
- Thread system_prompt into SessionConfig so it becomes the canonical
  Layer-1 slot the provider adapter sees.

### loop-agent/config.py
- Add system_prompt_file field to LoopAgentConfig.

### bundles/attractor-pipeline.yaml
- Add system_prompt_file: context/system-<provider>.md to each
  provider's agents-map entry (anthropic/openai/gemini).
- Keep upstream #80 changes (claude-sonnet-4-6 default_model).

### agents/attractor-agent-{anthropic,openai,gemini}{,-isolated}.yaml
### agents/pipeline-runner.yaml
- Add system_prompt_file per provider; add explanatory comment
  referencing the design doc and nlspec §6.1.

### profiles/attractor-profile-{anthropic,openai,gemini}.yaml
- Set system_prompt_file in each profile's orchestrator config.

### modules/loop-pipeline/amplifier_module_loop_pipeline/backend.py
- Thread runtime user_instructions override (Layer-5) from
  node.attrs["user_instructions"] or PipelineContext["user_instructions"]
  into orchestrator_config so callers can supply per-run overrides.
- Both model-resolution (#78) and user_instructions threading are
  present; they operate at different points in _run_with_spawn and
  do not interact.

### docs/designs/layer-1-profile-owned-system-prompt.md
- Design doc grounding the fix to nlspec line refs and empirical
  code findings; documents the E1/E2/E3 DTU eval results.

### modules/loop-agent/tests/
- Update test fixtures to supply system_prompt_file where required by
  the new fail-loud guard.

## Verified
- loop-agent: 478 passed
- loop-pipeline: 1378 passed (3 pre-existing DOT-attribute warnings)
- ruff format + lint: clean on all changed Python
- Re-applied cleanly onto b50843c (origin/main); no conflicts

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…awn paths

The Layer-1 fail-loud guard crashes any loop-agent node spawned by
loop-pipeline that lacks both a system_prompt_file and a base
context.include. A sweep found the per-provider agent definitions in
two more spawn entry points still on the broken context.include channel.

## attractor-interactive.yaml (run_pipeline tool entry point)
The interactive bundle's run_pipeline tool spawns attractor-pipeline-runner
(loop-pipeline), which spawns the per-provider child agents from this
bundle's own agents: map. Those three inline agents
(attractor-agent-{anthropic,openai,gemini}) lacked system_prompt_file and
would fail-loud on spawn. Added system_prompt_file: context/system-<provider>.md
to each — same shape as attractor-pipeline.yaml / pipeline-runner.yaml.
The top-level interactive session's own context.include is left untouched
(correct main-session channel).

## profiles/attractor-e2e-pipeline-{anthropic,gemini}.yaml (DTU eval fixtures)
These e2e pipeline profiles run loop-pipeline and spawn a loop-agent child
from their agents: map. The child config carried no system_prompt_file;
the profile's top-level context.include feeds the PIPELINE orchestrator,
not the spawned child — so the child would fail-loud. Added
system_prompt_file to the spawned child in both. These are the spawn-based
eval fixtures the cross-provider DTU runs exercise, so leaving them would
crash those very evals.

## Sweep verdict (no other gaps)
- bundles/attractor-agent.yaml: standalone main loop-agent session with
  context.include — correct, NOT a spawn path. No change.
- bundles/attractor-pipeline.yaml, agents/pipeline-runner.yaml,
  agents/attractor-agent-*.yaml: already carry system_prompt_file.
- agents/attractor-agent-*-isolated.yaml: carry system_prompt_file. Safe.
- profiles/attractor-profile-*.yaml: carry system_prompt_file. Safe.
- profiles/attractor-e2e-{anthropic,gemini}.yaml: single main loop-agent
  sessions with context.include (no pipeline spawn). Correct. No change.
- examples/pipelines/**/*.dot: set only node attrs (llm_provider/llm_model);
  they define NO orchestrator/session and resolve agent configs from the
  running bundle's agents: map — they cannot carry the gap themselves.

## Verified
- loop-agent: 478 passed
- loop-pipeline: 1378 passed
- All changed YAML parses; spawned-child system_prompt_file confirmed present
- No Python changed in this commit

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…loop-agent session + fix profile session.context

A DTU disproved the prior premise: in core 1.6.0, context.include does
NOT populate a loop-agent's Layer-1 base prompt — ONLY system_prompt /
system_prompt_file does. So every MAIN loop-agent session that relied on
context.include for its base was silently stubbed before, and now
fail-louds on the empty-Layer-1 guard. This completes the migration so
EVERY loop-agent session (main and spawned) carries a working base.

## Main sessions migrated to system_prompt_file (were context.include-only)
- bundles/attractor-agent.yaml (anthropic) — base-only context.include removed
- bundles/attractor-interactive.yaml ROOT session (anthropic) — kept the
  genuinely-additive includes (pipeline-awareness, dot-reference); removed
  system-anthropic.md from the include (now owned by system_prompt_file)
- profiles/attractor-e2e-anthropic.yaml (anthropic) — base-only include removed
- profiles/attractor-e2e-gemini.yaml (gemini) — base-only include removed

Single-owner pattern (matches the established agents/attractor-agent-*.yaml):
base prompt is owned by system_prompt_file; context.include carries only
genuinely additive supplementary context.

## Profile session.context ValueError fixed
attractor-profile-{anthropic,openai,gemini}.yaml failed at load with
'ValueError: Configuration must specify session.context'. Root cause: these
profiles include ONLY attractor-core (which provides no context module),
unlike attractor-agent.yaml which inherits session.context from
amplifier-foundation. Fix: declare session.context (context-simple)
explicitly in each profile.

## Inventory result — every loop-agent session now has a working base
All main + spawned loop-agent sessions across bundles/, profiles/, agents/
now resolve a system_prompt_file. One provider-agnostic exception is FLAGGED
for human decision (not guessed): behaviors/attractor-core.yaml's
attractor-expert agent (see commit notes / report).

## Verified
- loop-agent: 478 passed
- loop-pipeline: 1378 passed
- All 7 changed YAML parse; per-session base-source audit confirms
  system_prompt_file present on every loop-agent session
- No Python changed in this commit

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
… + correct stale comment + give attractor-expert a base prompt

Three council-mandated must-fixes.

## Must-fix 1 (BLOCKER) — robust, CWD-independent path resolution + clear error
Extracted system_prompt_file resolution into a module-level helper
_resolve_system_prompt_file() in loop-agent/__init__.py. Behavior:
- ABSOLUTE path -> used as-is (must exist).
- RELATIVE path -> resolved against the module's owning bundle root,
  anchored on __file__ (NEVER the process CWD). The documented anchor
  (parents[3] = <bundle-root>) is tried FIRST for determinism; if a
  future layout change moves it, we walk up the remaining __file__
  ancestors and accept the first under which the path actually exists.
- MISSING file -> raises a CLEAR, ACTIONABLE FileNotFoundError naming
  the configured value AND the absolute path tried, and stating that
  resolution is CWD-independent — instead of the old silent warning +
  empty Layer-1 that only tripped the generic guard much later.

The prior code already used __file__ (not CWD), so it was technically
CWD-independent — but it was a blind fixed-depth parent^4 with a
silent-warning miss path. The helper keeps parent^3-of-package as the
primary anchor (verified correct for the editable install Amplifier's
foundation activator always uses) and adds a walk-up fallback so the
fixed depth is self-checking rather than blindly trusted.

Added 5 unit tests (test_system_prompt_wiring.py) proving: relative
resolution succeeds after chdir to an unrelated tmp dir; resolution is
identical from two different CWDs; a missing relative file raises the
clear error (names value + path + 'working directory'); a missing
absolute file errors clearly; an existing absolute file is used as-is.

## Must-fix 2 — stale loop-agent comment corrected
agent_session.py ~537 claimed Layer-1 came from the context.include
_system_prompt_factory. It does NOT (disproven in core 1.6.0). Replaced
with an accurate note: Layer-1 comes ONLY from config.system_prompt /
system_prompt_file; context.include is additive context, not the base.

## Must-fix 3 — attractor-expert given a base prompt (PREFERRED fix)
behaviors/attractor-core.yaml's attractor-expert is a provider-agnostic
consultant with no base prompt -> would hard-crash on the fail-loud guard
if ever spawned as an LLM node. Authored a dedicated persona base
(context/system-attractor-expert.md, a concise DOT/pipeline-design
consultant persona) and pointed its session.orchestrator.config.
system_prompt_file at it. Chose author-a-persona over gating because it
makes the agent actually work if spawned, rather than just suppressing
the guard.

## Verified
- loop-agent: 483 passed (was 478; +5 resolution tests)
- loop-pipeline: 1378 passed
- ruff format + lint: clean on all changed Python

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
…ard-coded system_prompt_file lines

Root-cause cleanup (council Option A): replace the per-YAML system_prompt_file
config lines with a single provider-keyed DEFAULT in loop-agent, so a provider
agent needs NO base config — the provider supplies it.

## Scope gate (PASSED)
At base-prompt resolution loop-agent knows its intended provider:
__init__.py derives provider_name = next(iter(providers.keys())), the SAME
value it uses for the actual completion (provider = providers[provider_name]).
So a provider-derived default base always matches the model actually called —
it is the agent's own mounted provider, not a post-routing driver. The base
default and provider selection read ONE source and cannot disagree; a wrong
base would already be a (louder) wrong-API bug. Spawned pipeline children get
the right provider via foundation apply_provider_preferences (promotes the
node's llm_provider to priority 0 in the child mount plan).

## Mechanism (4-step precedence) — _resolve_base_prompt
  1. explicit system_prompt config            -> use as-is
  2. explicit system_prompt_file config       -> load (robust CWD-independent resolver)
  3. provider DEFAULT context/system-<provider>.md -> load (the common case)
  4. unknown provider / missing file          -> fail-loud clear error
Explicit config (1,2) still overrides the default, so attractor-expert keeps
its persona base. Provider normalization shared via canonical_provider() /
KNOWN_PROVIDERS in agent_session.py (also used by project-doc/env filtering).

## Removed 24 redundant system_prompt_file lines across 17 YAMLs
agents/attractor-agent-*{,-isolated}.yaml (6), agents/pipeline-runner.yaml (3),
bundles/attractor-{agent,interactive,pipeline}.yaml (1+4+3),
profiles/attractor-profile-* (3), profiles/attractor-e2e-* (2),
profiles/attractor-e2e-pipeline-* (2). KEPT behaviors/attractor-core.yaml's
attractor-expert system_prompt_file (non-coding persona override). Orphaned
explanatory comments cleaned; all YAML re-validated.

## Tests
- Repurposed test_empty_system_prompt_raises_loud_error ->
  test_unknown_provider_with_no_base_raises_loud_error (precedence 4 is now the
  unknown-provider path; known providers resolve a default).
- Added test_provider_default_base_prompt_loaded_for_known_provider (precedence 3)
  and test_explicit_config_overrides_provider_default (1 > 3).
- Reverted test_orchestrator_passes_provider_name dummy (known provider now
  covered by default). Ripple is otherwise largely irreducible: the bulk of
  dummies are {"test": provider} tests (deliberately neutral, unknown provider
  -> must keep an explicit base) or AgentSession-direct tests that bypass the
  orchestrator default — both legitimately retained.

## Verified
- loop-agent: 485 passed
- loop-pipeline: 1378 passed
- ruff format + lint: clean on changed Python
- Design doc updated with the 4-step precedence (§A')

Co-Authored-By: Amplifier <amplifier@microsoft.com>
… proofs

🤖 Generated with [Amplifier](https://github.com/microsoft/amplifier)

Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant