agentic-dev-loop 0.9.2__tar.gz → 0.10.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. {agentic_dev_loop-0.9.2/src/agentic_dev_loop.egg-info → agentic_dev_loop-0.10.0}/PKG-INFO +44 -22
  2. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/README.md +43 -21
  3. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/pyproject.toml +1 -1
  4. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/core/config.py +39 -5
  5. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/graph.py +14 -0
  6. agentic_dev_loop-0.10.0/src/agentic_dev_loop/engine/llm.py +101 -0
  7. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/stages.py +14 -6
  8. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0/src/agentic_dev_loop.egg-info}/PKG-INFO +44 -22
  9. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_config.py +88 -0
  10. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_graph.py +66 -1
  11. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_llm.py +89 -36
  12. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_sdk_compat.py +6 -5
  13. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_stages.py +80 -0
  14. agentic_dev_loop-0.9.2/src/agentic_dev_loop/engine/llm.py +0 -67
  15. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/LICENSE +0 -0
  16. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/setup.cfg +0 -0
  17. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/__init__.py +0 -0
  18. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/core/__init__.py +0 -0
  19. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/core/paths.py +0 -0
  20. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/core/results.py +0 -0
  21. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/core/verdict.py +0 -0
  22. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/__init__.py +0 -0
  23. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/actions.py +0 -0
  24. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
  25. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
  26. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/agents.py +0 -0
  27. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/allowlist.py +0 -0
  28. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
  29. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
  30. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
  31. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/cli.py +0 -0
  32. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/containers.py +0 -0
  33. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/deps.py +0 -0
  34. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/dryrun.py +0 -0
  35. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
  36. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
  37. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
  38. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
  39. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/forbidden.py +0 -0
  40. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
  41. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/isolation.py +0 -0
  42. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/loaders.py +0 -0
  43. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/mounts.py +0 -0
  44. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/outputs.py +0 -0
  45. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/presets.py +0 -0
  46. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/proc.py +0 -0
  47. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/registry.py +0 -0
  48. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
  49. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
  50. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/secrets.py +0 -0
  51. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
  52. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/engine/sync_back.py +0 -0
  53. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/py.typed +0 -0
  54. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/viz/__init__.py +0 -0
  55. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/viz/__main__.py +0 -0
  56. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop/viz/server.py +0 -0
  57. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
  58. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
  59. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
  60. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
  61. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
  62. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_029_merge_on_conflict.py +0 -0
  63. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_033_brief_step.py +0 -0
  64. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_035_finalize_nudge.py +0 -0
  65. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
  66. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_041_brief_coverage.py +0 -0
  67. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_046_brief_prompt.py +0 -0
  68. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_actions.py +0 -0
  69. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_agent_error_guide.py +0 -0
  70. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_agents.py +0 -0
  71. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_aidlc.py +0 -0
  72. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_cli.py +0 -0
  73. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_containers.py +0 -0
  74. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_deps.py +0 -0
  75. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_dry_run.py +0 -0
  76. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_e2e_fixtures.py +0 -0
  77. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_egress_proxy.py +0 -0
  78. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_hostboundary.py +0 -0
  79. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_hostboundary_integration.py +0 -0
  80. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_isolation.py +0 -0
  81. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_isolation_integration.py +0 -0
  82. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_isolation_pbt.py +0 -0
  83. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_loaders.py +0 -0
  84. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_module_contract.py +0 -0
  85. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_outputs.py +0 -0
  86. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_outputs_pbt.py +0 -0
  87. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_proc.py +0 -0
  88. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_publish.py +0 -0
  89. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_qa_prompt.py +0 -0
  90. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_registry.py +0 -0
  91. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_repo_ascii.py +0 -0
  92. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_results.py +0 -0
  93. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_secrets.py +0 -0
  94. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_secrets_integration.py +0 -0
  95. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_secrets_pipeline.py +0 -0
  96. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_source_resolution.py +0 -0
  97. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_step_workspace.py +0 -0
  98. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_sync_back.py +0 -0
  99. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_trigger_script.py +0 -0
  100. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_unit5_config.py +0 -0
  101. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_unit5_pbt.py +0 -0
  102. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_unit5_pure.py +0 -0
  103. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_verdict.py +0 -0
  104. {agentic_dev_loop-0.9.2 → agentic_dev_loop-0.10.0}/tests/test_viz.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -202,6 +202,17 @@ llm:
202
202
  api_key_env: OPENAI_API_KEY
203
203
  # reasoning_effort: medium # optional: low | medium | high | xhigh | none
204
204
 
205
+ # Named LLM profiles (054): a step names one with `llm: <name>` to move the
206
+ # whole profile (model, endpoint, key, effort default) together - no partial
207
+ # merges. The `llm:` block above remains the default. Profile keys are
208
+ # host-side: sandbox access still goes through `secrets:`, never a profile key.
209
+ # llms:
210
+ # review:
211
+ # model: openai/deepseek-v4-pro
212
+ # base_url: https://api.deepseek.com
213
+ # api_key_env: REVIEW_API_KEY
214
+ # reasoning_effort: xhigh
215
+
205
216
  max_attempts: 10 # CLI --max-attempts overrides
206
217
  agents_dir: sdk_agents # your <name>.agent.md files
207
218
 
@@ -231,7 +242,8 @@ steps:
231
242
  agent: review
232
243
  artifact: review-findings.md
233
244
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
234
- # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
245
+ # reasoning_effort: xhigh # per-step override of the effective profile's effort
246
+ # llm: review # (054) select a named `llms:` profile for this step
235
247
  on_pass: qa
236
248
  on_fail: { goto: dev, retry: true }
237
249
 
@@ -257,7 +269,8 @@ steps:
257
269
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
258
270
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
259
271
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
260
- | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
272
+ | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
273
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
261
274
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
262
275
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
263
276
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
@@ -272,17 +285,25 @@ steps:
272
285
 
273
286
  ### Reasoning effort (opt-in)
274
287
 
275
- `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
276
- on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
277
- or `none`; unset means the field is never passed and the SDK default
278
- (`high`) applies, so a pipeline that omits it behaves exactly as before. A
279
- step's value overrides the `llm` block's value, so the review lane can hold
280
- a higher effort than the rest.
281
-
282
- The value is forwarded only for models the SDK/LiteLLM mark as supporting
283
- it; an unsupported value fails at the first LLM call (the schema rejects
284
- unknown names at load time, but cannot know provider capability). No
285
- pipeline ships a value: adopt one only after an observed run proves lower
288
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
289
+ named `llms:` profile, and individual agent steps. It takes one of `low`,
290
+ `medium`, `high`, `xhigh`, or `none`; unset means reasoning is off - the
291
+ field is forwarded as none, reasoning is stripped from requests, and a
292
+ thinking model whose history still carries reasoning fails fast with an
293
+ actionable engine error rather than a provider 400. A step's value overrides
294
+ the effective profile's value, so the review lane can hold a higher effort
295
+ than the rest, or a named profile can move an entire lane to a different
296
+ provider.
297
+
298
+ Reasoning is opt-in and provider-generic: any openai-compatible endpoint
299
+ expressible as an `LlmConfig` works, and reasoning passback is gated on the
300
+ configured effort level, never on a model-name list. A thinking model without
301
+ an effort level (or with `none` where the provider ignores it) is invalid
302
+ config - it still reasons but its API requires the passback the engine strips,
303
+ so the engine raises before the doomed request is sent. Set
304
+ `reasoning_effort` on the step or its profile, or use a non-thinking model.
305
+ The schema rejects unknown effort names at load time, but cannot know provider
306
+ capability; adopt an effort value only after an observed run proves lower
286
307
  per-turn latency with a green suite and no provider 400s.
287
308
 
288
309
  Recommended lane defaults (guidance only, not enforced):
@@ -515,22 +536,23 @@ adapt per repo.
515
536
  Adaptation checklist for a new repo:
516
537
 
517
538
  1. `llm:` - model, `base_url`, `api_key_env`.
518
- 2. `isolation:` - toolchain image override (the consumer repo pins a
539
+ 2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
540
+ 3. `isolation:` - toolchain image override (the consumer repo pins a
519
541
  toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
520
542
  only if a stage runs Chromium.
521
- 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
543
+ 4. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
522
544
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
523
545
  `frontend-editor/node_modules`); caches are never synced back, and a
524
546
  step's `setup:` hook runs with them mounted so it can warm them.
525
- 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
526
- 5. `secrets:` - refs only, resolved from host env; each step opts in to the
547
+ 5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
548
+ 6. `secrets:` - refs only, resolved from host env; each step opts in to the
527
549
  refs it needs.
528
- 6. `egress:` - `presets: [package-registries]` for fetch steps;
550
+ 7. `egress:` - `presets: [package-registries]` for fetch steps;
529
551
  `class: browser` (broad-but-logged) for browser-tier qa only.
530
- 7. `deps_policy:` and `lockfiles:` - see
552
+ 8. `deps_policy:` and `lockfiles:` - see
531
553
  [Dependency policy](#dependency-policy-locked-by-default).
532
- 8. The dev prompt's impossible set - write it for your toolchain.
533
- 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
554
+ 9. The dev prompt's impossible set - write it for your toolchain.
555
+ 10. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
534
556
  docs: shared context for every agent step.
535
557
 
536
558
  Validate before the first run - dry-run loads and prints the resolved plan
@@ -158,6 +158,17 @@ llm:
158
158
  api_key_env: OPENAI_API_KEY
159
159
  # reasoning_effort: medium # optional: low | medium | high | xhigh | none
160
160
 
161
+ # Named LLM profiles (054): a step names one with `llm: <name>` to move the
162
+ # whole profile (model, endpoint, key, effort default) together - no partial
163
+ # merges. The `llm:` block above remains the default. Profile keys are
164
+ # host-side: sandbox access still goes through `secrets:`, never a profile key.
165
+ # llms:
166
+ # review:
167
+ # model: openai/deepseek-v4-pro
168
+ # base_url: https://api.deepseek.com
169
+ # api_key_env: REVIEW_API_KEY
170
+ # reasoning_effort: xhigh
171
+
161
172
  max_attempts: 10 # CLI --max-attempts overrides
162
173
  agents_dir: sdk_agents # your <name>.agent.md files
163
174
 
@@ -187,7 +198,8 @@ steps:
187
198
  agent: review
188
199
  artifact: review-findings.md
189
200
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
190
- # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
201
+ # reasoning_effort: xhigh # per-step override of the effective profile's effort
202
+ # llm: review # (054) select a named `llms:` profile for this step
191
203
  on_pass: qa
192
204
  on_fail: { goto: dev, retry: true }
193
205
 
@@ -213,7 +225,8 @@ steps:
213
225
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
214
226
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
215
227
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
216
- | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
228
+ | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
229
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
217
230
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
218
231
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
219
232
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
@@ -228,17 +241,25 @@ steps:
228
241
 
229
242
  ### Reasoning effort (opt-in)
230
243
 
231
- `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
232
- on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
233
- or `none`; unset means the field is never passed and the SDK default
234
- (`high`) applies, so a pipeline that omits it behaves exactly as before. A
235
- step's value overrides the `llm` block's value, so the review lane can hold
236
- a higher effort than the rest.
237
-
238
- The value is forwarded only for models the SDK/LiteLLM mark as supporting
239
- it; an unsupported value fails at the first LLM call (the schema rejects
240
- unknown names at load time, but cannot know provider capability). No
241
- pipeline ships a value: adopt one only after an observed run proves lower
244
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
245
+ named `llms:` profile, and individual agent steps. It takes one of `low`,
246
+ `medium`, `high`, `xhigh`, or `none`; unset means reasoning is off - the
247
+ field is forwarded as none, reasoning is stripped from requests, and a
248
+ thinking model whose history still carries reasoning fails fast with an
249
+ actionable engine error rather than a provider 400. A step's value overrides
250
+ the effective profile's value, so the review lane can hold a higher effort
251
+ than the rest, or a named profile can move an entire lane to a different
252
+ provider.
253
+
254
+ Reasoning is opt-in and provider-generic: any openai-compatible endpoint
255
+ expressible as an `LlmConfig` works, and reasoning passback is gated on the
256
+ configured effort level, never on a model-name list. A thinking model without
257
+ an effort level (or with `none` where the provider ignores it) is invalid
258
+ config - it still reasons but its API requires the passback the engine strips,
259
+ so the engine raises before the doomed request is sent. Set
260
+ `reasoning_effort` on the step or its profile, or use a non-thinking model.
261
+ The schema rejects unknown effort names at load time, but cannot know provider
262
+ capability; adopt an effort value only after an observed run proves lower
242
263
  per-turn latency with a green suite and no provider 400s.
243
264
 
244
265
  Recommended lane defaults (guidance only, not enforced):
@@ -471,22 +492,23 @@ adapt per repo.
471
492
  Adaptation checklist for a new repo:
472
493
 
473
494
  1. `llm:` - model, `base_url`, `api_key_env`.
474
- 2. `isolation:` - toolchain image override (the consumer repo pins a
495
+ 2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
496
+ 3. `isolation:` - toolchain image override (the consumer repo pins a
475
497
  toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
476
498
  only if a stage runs Chromium.
477
- 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
499
+ 4. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
478
500
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
479
501
  `frontend-editor/node_modules`); caches are never synced back, and a
480
502
  step's `setup:` hook runs with them mounted so it can warm them.
481
- 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
482
- 5. `secrets:` - refs only, resolved from host env; each step opts in to the
503
+ 5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
504
+ 6. `secrets:` - refs only, resolved from host env; each step opts in to the
483
505
  refs it needs.
484
- 6. `egress:` - `presets: [package-registries]` for fetch steps;
506
+ 7. `egress:` - `presets: [package-registries]` for fetch steps;
485
507
  `class: browser` (broad-but-logged) for browser-tier qa only.
486
- 7. `deps_policy:` and `lockfiles:` - see
508
+ 8. `deps_policy:` and `lockfiles:` - see
487
509
  [Dependency policy](#dependency-policy-locked-by-default).
488
- 8. The dev prompt's impossible set - write it for your toolchain.
489
- 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
510
+ 9. The dev prompt's impossible set - write it for your toolchain.
511
+ 10. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
490
512
  docs: shared context for every agent step.
491
513
 
492
514
  Validate before the first run - dry-run loads and prints the resolved plan
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agentic-dev-loop"
7
- version = "0.9.2"
7
+ version = "0.10.0"
8
8
  description = "Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -396,9 +396,13 @@ class StepConfig(BaseModel):
396
396
  terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
397
397
  finalize_nudge_turns: int | None = Field(default=None, gt=0) # agent steps only (035)
398
398
  brief_inputs: BriefInputsSpec | None = None # agent steps only (033)
399
- # 047 (D7): per-step opt-in override; unset falls back to the llm block,
400
- # then the SDK default. Full SDK literal, passthrough semantics.
399
+ # 047 (D7) / 054 (D6): per-step opt-in override; unset falls back to the
400
+ # effective profile's value (the named `llms:` profile, else the `llm:`
401
+ # block), else reasoning is off. Full SDK literal, passthrough semantics.
401
402
  reasoning_effort: ReasoningEffort | None = None
403
+ # 054 (D3): names a top-level `llms:` profile; the whole profile moves
404
+ # together (model, endpoint, key, effort default). None = the `llm:` block.
405
+ llm: str | None = None
402
406
  custom_prompt: str | None = None # agent steps only (040); file path resolved at load
403
407
  env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
404
408
  setup: str | None = None # command or agent steps; runs fresh at each step entry (052)
@@ -486,6 +490,16 @@ class StepConfig(BaseModel):
486
490
  f"step {self.name!r}: exactly one of 'agent', 'action', "
487
491
  "'command', or 'brief_inputs' is required"
488
492
  )
493
+ if self.llm is not None:
494
+ if not self.llm.strip():
495
+ raise ValueError(
496
+ f"step {self.name!r}: 'llm' must be a non-empty profile name"
497
+ )
498
+ if self.kind not in ("agent", "brief"):
499
+ raise ValueError(
500
+ f"step {self.name!r}: 'llm' names an LLM profile and is "
501
+ "only allowed on 'agent' or 'brief_inputs' steps"
502
+ )
489
503
  if self.brief_inputs is not None:
490
504
  if self.produces:
491
505
  raise ValueError(
@@ -587,9 +601,9 @@ class LlmConfig(BaseModel):
587
601
  base_url: str | None = None
588
602
  api_key_env: str
589
603
  timeout: float | None = None # omitted → opinionated no-timeout default
590
- # 047 (D1): opt-in passthrough; unset means the field is not passed at
591
- # all, so the SDK default ("high") applies and today's behavior is
592
- # byte-identical. No default may move without an observed run (D6).
604
+ # 047 (D1) / 054 (D6): opt-in passthrough; unset means reasoning is off
605
+ # (the engine forwards none and strips reasoning passback). The SDK
606
+ # "high" default never engages because build_llm always passes the value.
593
607
  reasoning_effort: ReasoningEffort | None = None
594
608
 
595
609
 
@@ -606,6 +620,9 @@ class PipelineConfig(BaseModel):
606
620
  extensions_dir: str = "~/.agentic-dev-loop/extensions"
607
621
  env_allowlist: list[str] = []
608
622
  llm: LlmConfig
623
+ # 054 (D1): named LLM profiles, each a strict LlmConfig. Optional and
624
+ # purely additive - the `llm:` block above remains the default.
625
+ llms: dict[str, LlmConfig] = {}
609
626
  isolation: IsolationConfig | None = None # None = always-on defaults
610
627
  state_dir: str = ".pr" # default relocated to ~/.agentic-dev-loop/runs/<hash>/ at load
611
628
  secrets: dict[str, SecretSpec] = {} # ref name -> locator; values never appear here
@@ -690,6 +707,23 @@ class PipelineConfig(BaseModel):
690
707
  "enforces, or set 'deps_policy: open'")
691
708
  return self
692
709
 
710
+ @model_validator(mode="after")
711
+ def _check_llm_profiles(self) -> "PipelineConfig":
712
+ """054 (D5): the registry is additive and referential. A profile
713
+ literally named 'default' is rejected (the `llm:` block already is the
714
+ default), and any step naming an undeclared profile fails at load,
715
+ never mid-run."""
716
+ if "default" in self.llms:
717
+ raise ValueError(
718
+ "'llms' cannot declare a profile named 'default' - the "
719
+ "top-level 'llm:' block is already the default")
720
+ for step in self.steps:
721
+ if step.llm is not None and step.llm not in self.llms:
722
+ raise ValueError(
723
+ f"step {step.name!r}: unknown LLM profile {step.llm!r} - "
724
+ f"declared profiles: {sorted(self.llms)}")
725
+ return self
726
+
693
727
  @model_validator(mode="after")
694
728
  def _mounts_avoid_state_dir(self) -> "PipelineConfig":
695
729
  """BR-1.3: the host-only run-state dir is never a mount path.
@@ -177,11 +177,25 @@ def run_pipeline(cfg_path, args, *, viz_bus=None,
177
177
  sandbox.close()
178
178
 
179
179
 
180
+ def _build_profile_llms(cfg) -> dict:
181
+ """054 (D7): build the LLM for each profile referenced by any step,
182
+ eagerly, so a missing api_key_env fails at run start, not mid-run.
183
+ Declared-but-unreferenced profiles are shape-validated at load; their
184
+ keys are not checked here."""
185
+ referenced = {step.llm for step in cfg.steps if step.llm is not None}
186
+ return {name: build_llm(cfg.llms[name]) for name in sorted(referenced)}
187
+
188
+
180
189
  def _run_pipeline(cfg, base, registry, state_dir, max_attempts, args, viz_bus,
181
190
  sandbox, resolved_secrets, tooling_paths) -> tuple[bool, int]:
182
191
  export_root = getattr(sandbox, "export_root", None)
183
192
  engine_allowlist = resolve_engine_allowlist(registry, args)
193
+ # 054 (D7): build the default LLM plus one per profile referenced by any
194
+ # step, eagerly, so a missing api_key_env for a referenced profile fails
195
+ # at run start, not mid-run.
196
+ profile_llms = _build_profile_llms(cfg)
184
197
  runtime = Runtime(cfg=cfg, registry=registry, llm=build_llm(cfg.llm),
198
+ llms=profile_llms,
185
199
  agents_dir=base / cfg.agents_dir, state_dir=state_dir,
186
200
  workspace=Path.cwd(), sandbox=sandbox,
187
201
  sandbox_cwd=Path(export_root) if export_root else None,
@@ -0,0 +1,101 @@
1
+ """LLM construction from pipeline config. Fail fast on a missing key -
2
+ never fall back to another env var (no silent fallbacks)."""
3
+
4
+ import os
5
+
6
+ from openhands.sdk import LLM
7
+
8
+ from agentic_dev_loop.core.config import LlmConfig
9
+
10
+ # 054 (D8): reasoning passback is config-gated, never name-list-gated. The
11
+ # SDK gates reasoning_content passback on a closed model-name list
12
+ # (SEND_REASONING_CONTENT_MODELS) the engine never consults; instead each
13
+ # LLM instance carries the engine-resolved effort on
14
+ # ``_agentic_dev_loop_reasoning_effort`` and the serializer below gates
15
+ # passback on it. Gate on = the effort is set and not "none"; gate off =
16
+ # reasoning is stripped and, when history still carries reasoning_content,
17
+ # a clear engine error fires before the doomed request is sent (D12).
18
+ _ORIGINAL_FORMAT_MESSAGES = LLM.format_messages_for_llm
19
+
20
+
21
+ def _reasoning_gate_on(llm: LLM) -> bool:
22
+ """The engine-configured effort for this instance, if any. The SDK's own
23
+ ``reasoning_effort`` field defaults to "high", so a direct (non-engine)
24
+ instance would read as gate-on; the dedicated attribute is only set by
25
+ ``build_llm``, keeping absent effort = reasoning off."""
26
+ effort = getattr(llm, "_agentic_dev_loop_reasoning_effort", None)
27
+ return effort is not None and effort != "none"
28
+
29
+
30
+ def _reasoning_stripped_error(llm: LLM) -> RuntimeError:
31
+ """054 (D12): gate off while the conversation still carries
32
+ reasoning_content - a request that strips it would 400 at the provider."""
33
+ effort = getattr(llm, "_agentic_dev_loop_reasoning_effort", None)
34
+ return RuntimeError(
35
+ f"model {llm.model!r} is configured with reasoning off "
36
+ f"(reasoning_effort={effort!r}) but the conversation history carries "
37
+ "reasoning_content that would be stripped from the request. Set "
38
+ "'reasoning_effort' (low/medium/high/xhigh) on the step or its llm "
39
+ "profile, use 'none' only where the provider honors it, or use a "
40
+ "non-thinking model."
41
+ )
42
+
43
+
44
+ if not getattr(_ORIGINAL_FORMAT_MESSAGES, "_agentic_dev_loop_reasoning_repair", False):
45
+ def _config_gated_reasoning_serializer(self, messages: list) -> list[dict]:
46
+ """The engine-owned reasoning passback serializer (054 D8).
47
+
48
+ Gate on: every assistant message that carries reasoning_content
49
+ writes it back (the SDK only does so for listed models and only on
50
+ the first message of a split tool-call turn), and a tool-call
51
+ message without its own reasoning borrows the turn's last reasoning
52
+ (reset on user turns). Gate off: reasoning is stripped from the
53
+ outgoing request and, if history still carries reasoning_content,
54
+ the D12 error fires instead of sending a doomed request.
55
+ """
56
+ out = _ORIGINAL_FORMAT_MESSAGES(self, messages)
57
+ if not _reasoning_gate_on(self):
58
+ for d in out:
59
+ d.pop("reasoning_content", None)
60
+ if any(getattr(m, "reasoning_content", None) for m in messages):
61
+ raise _reasoning_stripped_error(self)
62
+ return out
63
+
64
+ last = None
65
+ for msg, d in zip(messages, out):
66
+ if msg.role == "user":
67
+ last = None
68
+ continue
69
+ if msg.role != "assistant":
70
+ continue
71
+ rc = getattr(msg, "reasoning_content", None)
72
+ if msg.tool_calls:
73
+ if rc is not None:
74
+ last = rc
75
+ if last is not None:
76
+ d["reasoning_content"] = last
77
+ elif rc:
78
+ d["reasoning_content"] = rc
79
+ return out
80
+
81
+ _config_gated_reasoning_serializer._agentic_dev_loop_reasoning_repair = True
82
+ LLM.format_messages_for_llm = _config_gated_reasoning_serializer
83
+
84
+
85
+ def build_llm(cfg: LlmConfig, reasoning_effort: str | None = None) -> LLM:
86
+ api_key = os.environ.get(cfg.api_key_env)
87
+ if not api_key:
88
+ raise RuntimeError(f"{cfg.api_key_env} environment variable is required")
89
+ # 054 (D6): a step's value wins, else the effective profile's value, else
90
+ # off. reasoning_effort is always passed (None when unset) so the SDK
91
+ # "high" default cannot engage; absent effort = reasoning off.
92
+ effort = reasoning_effort if reasoning_effort is not None else cfg.reasoning_effort
93
+ llm = LLM(
94
+ model=cfg.model,
95
+ api_key=api_key,
96
+ base_url=cfg.base_url,
97
+ timeout=cfg.timeout, # policy: no LLM timeout unless explicitly set
98
+ reasoning_effort=effort,
99
+ )
100
+ llm._agentic_dev_loop_reasoning_effort = effort
101
+ return llm
@@ -120,6 +120,9 @@ class Runtime:
120
120
  agents_dir: Path
121
121
  state_dir: Path
122
122
  workspace: Path
123
+ # 054 (D7): named-profile LLMs built eagerly at run start, keyed by the
124
+ # `llms:` profile name. `llm` above remains the default (`llm:` block).
125
+ llms: dict[str, "LLM"] = field(default_factory=dict)
123
126
  sandbox: Sandbox | None = None
124
127
  # unit-5: the .git-free export root sandboxed steps run in (BR-2); None =
125
128
  # the real workspace (no export materialised - fake/legacy paths).
@@ -176,13 +179,18 @@ class Runtime:
176
179
  if step is not None else None),
177
180
  forbidden_commands=forbidden_commands,
178
181
  allowlist=allowlist)
179
- # 047 (D7): a step's reasoning_effort overrides the llm block's;
180
- # self.llm already carries the block value (or the SDK default),
181
- # so only build a fresh LLM when the step overrides it.
182
+ # 054 (D6/D7): a named profile selects the whole config (model,
183
+ # endpoint, key, effort default move together); the step's own
184
+ # reasoning_effort then overrides the effective profile's value.
182
185
  llm = self.llm
183
- if step is not None and step.reasoning_effort is not None:
184
- llm = build_llm(self.cfg.llm,
185
- reasoning_effort=step.reasoning_effort)
186
+ if step is not None:
187
+ effective_cfg = (self.cfg.llms[step.llm]
188
+ if step.llm is not None else self.cfg.llm)
189
+ if step.llm is not None:
190
+ llm = self.llms.get(step.llm, self.llm)
191
+ if step.reasoning_effort is not None:
192
+ llm = build_llm(effective_cfg,
193
+ reasoning_effort=step.reasoning_effort)
186
194
  if step is not None and step.kind == "brief":
187
195
  agent = load_brief_agent(
188
196
  agent_name, step.brief_inputs.sections, llm)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-dev-loop
3
- Version: 0.9.2
3
+ Version: 0.10.0
4
4
  Summary: Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.
5
5
  Author: Luke Wilson
6
6
  License-Expression: Apache-2.0
@@ -202,6 +202,17 @@ llm:
202
202
  api_key_env: OPENAI_API_KEY
203
203
  # reasoning_effort: medium # optional: low | medium | high | xhigh | none
204
204
 
205
+ # Named LLM profiles (054): a step names one with `llm: <name>` to move the
206
+ # whole profile (model, endpoint, key, effort default) together - no partial
207
+ # merges. The `llm:` block above remains the default. Profile keys are
208
+ # host-side: sandbox access still goes through `secrets:`, never a profile key.
209
+ # llms:
210
+ # review:
211
+ # model: openai/deepseek-v4-pro
212
+ # base_url: https://api.deepseek.com
213
+ # api_key_env: REVIEW_API_KEY
214
+ # reasoning_effort: xhigh
215
+
205
216
  max_attempts: 10 # CLI --max-attempts overrides
206
217
  agents_dir: sdk_agents # your <name>.agent.md files
207
218
 
@@ -231,7 +242,8 @@ steps:
231
242
  agent: review
232
243
  artifact: review-findings.md
233
244
  verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
234
- # reasoning_effort: xhigh # per-step override; falls back to the llm block, then SDK default
245
+ # reasoning_effort: xhigh # per-step override of the effective profile's effort
246
+ # llm: review # (054) select a named `llms:` profile for this step
235
247
  on_pass: qa
236
248
  on_fail: { goto: dev, retry: true }
237
249
 
@@ -257,7 +269,8 @@ steps:
257
269
  | `terminal_timeout:` | Optional agent-step per-action timeout in seconds (gt 0). `SandboxTerminalExecutor` uses it when the agent leaves the terminal action timeout unset, overriding the SDK tool's default. `None` = current behavior. |
258
270
  | `finalize_nudge_turns:` | Optional agent-step final-response nudge (gt 0). Requires `max_iterations:` and must be less than it. At `max_iterations - finalize_nudge_turns` completed turns the engine injects one advisory turn ("`N turns remain: produce your final response now`"), exactly once; the agent may still exhaust its turns and existing stuck/unclear handling applies unchanged. Absent = current behavior. Provisional pending one consumer run (revert if noisy or the raised cap inflates cost). |
259
271
  | `custom_prompt:` | Planned (planning-docs/040). Optional agent-step value: literal text, or a path whose file contents are read at load time, appended verbatim to the agent prompt at context assembly (after the prompt, before the task/artifacts). A value that looks like a path but names no file is a validation error. Agent steps only. |
260
- | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block (pipeline-global) or per-step (overrides the block). Unset anywhere = the field is not passed and the SDK default (`high`) applies, so today's behavior is unchanged. Forwarded only for models the SDK/LiteLLM mark as supporting it; an unsupported value fails at the first LLM call. See [Reasoning effort](#reasoning-effort-opt-in). |
272
+ | `llms:` (top-level) / step `llm:` | Named LLM profiles, each a full `LlmConfig` (`model`, `base_url`, `api_key_env`, `timeout`, `reasoning_effort`). A step names one with `llm: <profile-name>`; the whole profile moves together (no partial merges). Allowed only on agent and brief steps. The top-level `llm:` block remains the default. Profile keys are host-side - sandbox access still goes through `secrets:`. |
273
+ | `reasoning_effort:` | Optional `low` / `medium` / `high` / `xhigh` / `none`. Set on the `llm` block, a `llms:` profile, or per-step (a step value overrides the effective profile's). Unset anywhere = reasoning off: nothing is forwarded and reasoning is stripped from requests. A thinking model without an effort level is invalid config and fails at the first request (crap in, crap out). See [Reasoning effort](#reasoning-effort-opt-in). |
261
274
  | `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
262
275
  | `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` runs fresh at every step boundary in a one-shot provision container targeting the export (command or agent steps; network ON; no stamping); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
263
276
  | `mounts:` / `egress:` | Per-step workspace isolation. `mounts:` lists `{path, mode}` entries (`rw`/`ro`, default `ro`) and is **strict-by-default** - empty/missing mounts nothing; only declared-rw paths sync back to the repo. `egress:` is `{class, presets, targets}` (`class: browser` = broad-but-logged; otherwise a strict allowlist). See [Per-stage networking](#per-stage-networking) and [docs/setup.md](docs/setup.md). |
@@ -272,17 +285,25 @@ steps:
272
285
 
273
286
  ### Reasoning effort (opt-in)
274
287
 
275
- `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block and
276
- on individual agent steps. It takes one of `low`, `medium`, `high`, `xhigh`,
277
- or `none`; unset means the field is never passed and the SDK default
278
- (`high`) applies, so a pipeline that omits it behaves exactly as before. A
279
- step's value overrides the `llm` block's value, so the review lane can hold
280
- a higher effort than the rest.
281
-
282
- The value is forwarded only for models the SDK/LiteLLM mark as supporting
283
- it; an unsupported value fails at the first LLM call (the schema rejects
284
- unknown names at load time, but cannot know provider capability). No
285
- pipeline ships a value: adopt one only after an observed run proves lower
288
+ `reasoning_effort` is an opt-in latency dial on the pipeline `llm` block, a
289
+ named `llms:` profile, and individual agent steps. It takes one of `low`,
290
+ `medium`, `high`, `xhigh`, or `none`; unset means reasoning is off - the
291
+ field is forwarded as none, reasoning is stripped from requests, and a
292
+ thinking model whose history still carries reasoning fails fast with an
293
+ actionable engine error rather than a provider 400. A step's value overrides
294
+ the effective profile's value, so the review lane can hold a higher effort
295
+ than the rest, or a named profile can move an entire lane to a different
296
+ provider.
297
+
298
+ Reasoning is opt-in and provider-generic: any openai-compatible endpoint
299
+ expressible as an `LlmConfig` works, and reasoning passback is gated on the
300
+ configured effort level, never on a model-name list. A thinking model without
301
+ an effort level (or with `none` where the provider ignores it) is invalid
302
+ config - it still reasons but its API requires the passback the engine strips,
303
+ so the engine raises before the doomed request is sent. Set
304
+ `reasoning_effort` on the step or its profile, or use a non-thinking model.
305
+ The schema rejects unknown effort names at load time, but cannot know provider
306
+ capability; adopt an effort value only after an observed run proves lower
286
307
  per-turn latency with a green suite and no provider 400s.
287
308
 
288
309
  Recommended lane defaults (guidance only, not enforced):
@@ -515,22 +536,23 @@ adapt per repo.
515
536
  Adaptation checklist for a new repo:
516
537
 
517
538
  1. `llm:` - model, `base_url`, `api_key_env`.
518
- 2. `isolation:` - toolchain image override (the consumer repo pins a
539
+ 2. `llms:` (optional) - named profiles for multi-provider lanes (keep review on the strongest model); a step opts in with `llm: <name>`.
540
+ 3. `isolation:` - toolchain image override (the consumer repo pins a
519
541
  toolchain image by digest) and `seccomp_profile: bundled:chromium-userns`
520
542
  only if a stage runs Chromium.
521
- 3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
543
+ 4. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
522
544
  (`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
523
545
  `frontend-editor/node_modules`); caches are never synced back, and a
524
546
  step's `setup:` hook runs with them mounted so it can warm them.
525
- 4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
526
- 5. `secrets:` - refs only, resolved from host env; each step opts in to the
547
+ 5. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
548
+ 6. `secrets:` - refs only, resolved from host env; each step opts in to the
527
549
  refs it needs.
528
- 6. `egress:` - `presets: [package-registries]` for fetch steps;
550
+ 7. `egress:` - `presets: [package-registries]` for fetch steps;
529
551
  `class: browser` (broad-but-logged) for browser-tier qa only.
530
- 7. `deps_policy:` and `lockfiles:` - see
552
+ 8. `deps_policy:` and `lockfiles:` - see
531
553
  [Dependency policy](#dependency-policy-locked-by-default).
532
- 8. The dev prompt's impossible set - write it for your toolchain.
533
- 9. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
554
+ 9. The dev prompt's impossible set - write it for your toolchain.
555
+ 10. `additional_files:` - the repo's `AGENTS.md`, coding standards, design
534
556
  docs: shared context for every agent step.
535
557
 
536
558
  Validate before the first run - dry-run loads and prints the resolved plan