feat(loop-agent): profile-owned Layer-1 system prompt — provider default + fail-loud#81
Open
bkrabach wants to merge 7 commits into
Open
feat(loop-agent): profile-owned Layer-1 system prompt — provider default + fail-loud#81bkrabach wants to merge 7 commits into
bkrabach wants to merge 7 commits into
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_fileinsession.orchestrator.config— it never consumed the kernel context manager'scontext.includefactory (the in-code comment claiming it did was stale). Provider prompts were parked incontext.includeside-files, so they never reached Layer-1.Fix: loop-agent now resolves Layer-1 with precedence:
system_promptsystem_prompt_filecontext/system-<provider>.md(keyed on the SAME provider source used for the actual completion)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_fileconfig lines across the bundle/agent/profile YAMLs (onlyattractor-expertkeeps 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
pytest modules/loop-pipeline/) — 1378 tests passingsystem_prompt/system_prompt_fileconfigs continue to work (tested)Verification evidence
Cross-provider Layer-1 resolution (DTU integration test):
Live Resolve stack validation (real consumer, v0.2.0 dot-graph resolver):
expert_builder_explorer.dot) in a real worker with this branch loadedBackward-compatibility:
system_prompt_fileconfig remain unchanged — they still use their explicit overrideNotes for reviewers
Design reference: See
docs/designs/layer-1-profile-owned-system-prompt.mdfor the design document.Known follow-ups (not blockers):
context/system-*.md→ fail-loud (not silent). Package them into the wheel if standalone-wheel installs are ever needed.