agentic-dev-loop 1.0.0a46__tar.gz → 1.0.0a49__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.
Potentially problematic release.
This version of agentic-dev-loop might be problematic. Click here for more details.
- {agentic_dev_loop-1.0.0a46/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a49}/PKG-INFO +16 -14
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/README.md +15 -13
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/core/config.py +62 -31
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/agents.py +6 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/dryrun.py +10 -3
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/graph.py +3 -4
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/isolation.py +66 -21
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/stages.py +9 -8
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49/src/agentic_dev_loop.egg-info}/PKG-INFO +16 -14
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_033_brief_step.py +29 -62
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_041_brief_coverage.py +10 -10
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_agent_error_guide.py +1 -1
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_config.py +83 -7
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_e2e_fixtures.py +7 -8
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_isolation.py +80 -2
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_isolation_integration.py +79 -1
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_publish.py +35 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/core/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/agent_error_guide.md +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/agent_error_guide.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/allowlist.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/brief_coverage.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/brief_prompt.md +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/brief_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/containers.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/forbidden.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop.egg-info/SOURCES.txt +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_035_finalize_nudge.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_036_banned_commands_and_lane_boundary.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_046_brief_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_containers.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_sdk_compat.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a46/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a49}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentic-dev-loop
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a49
|
|
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
|
|
@@ -206,10 +206,11 @@ agents_dir: sdk_agents # your <name>.agent.md f
|
|
|
206
206
|
|
|
207
207
|
steps:
|
|
208
208
|
- name: brief
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
209
|
+
reasoning_effort: medium
|
|
210
|
+
brief_inputs:
|
|
211
|
+
include: [AGENTS.md, coding-standards-ddd.yaml]
|
|
212
|
+
sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
|
|
213
|
+
env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
|
|
213
214
|
on_pass: dev
|
|
214
215
|
|
|
215
216
|
- name: dev
|
|
@@ -249,9 +250,9 @@ steps:
|
|
|
249
250
|
|
|
250
251
|
| Key | Notes |
|
|
251
252
|
| --- | --- |
|
|
252
|
-
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
|
|
253
|
+
| `agent:` / `command:` / `action:` / `brief_inputs:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). `brief_inputs:` is a native brief step - see its row below. |
|
|
253
254
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
254
|
-
| `brief_inputs:` |
|
|
255
|
+
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
255
256
|
| `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. |
|
|
256
257
|
| `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). |
|
|
257
258
|
| `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. |
|
|
@@ -294,9 +295,9 @@ Recommended lane defaults (guidance only, not enforced):
|
|
|
294
295
|
| review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
|
|
295
296
|
|
|
296
297
|
A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
|
|
297
|
-
run start (or when its inputs change), its `
|
|
298
|
-
starts, and the dev/review agents read that digest
|
|
299
|
-
rules and environment facts every attempt.
|
|
298
|
+
run start (or when its inputs change), its engine-owned `attempt-brief.md`
|
|
299
|
+
digest survives attempt starts, and the dev/review agents read that digest
|
|
300
|
+
instead of re-deriving repo rules and environment facts every attempt.
|
|
300
301
|
|
|
301
302
|
The brief digest is mechanically coverage-checked: the configured
|
|
302
303
|
`brief_inputs.sections` headings must each appear on their own line, every
|
|
@@ -384,10 +385,11 @@ steps:
|
|
|
384
385
|
structurally enforced.** A networked step runs on a per-run internal
|
|
385
386
|
docker network with no route to the host. Its traffic is forced
|
|
386
387
|
through a containerized egress proxy (injected via
|
|
387
|
-
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy
|
|
388
|
-
on the internal network
|
|
389
|
-
|
|
390
|
-
|
|
388
|
+
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy's internal IP
|
|
389
|
+
on the internal network; each distinct egress policy gets its own
|
|
390
|
+
proxy with a distinct internal IP); a request to an undeclared host
|
|
391
|
+
is denied and logged as an enforcement event. The engine never adds
|
|
392
|
+
an undeclared target. `egress: { class: browser }` is the explicit
|
|
391
393
|
broad-but-logged class (every connection logged, nothing filtered); command
|
|
392
394
|
and agent steps default to a strict allowlist.
|
|
393
395
|
- **Command, agent, and sandbox-action steps** may declare it. The host-side
|
|
@@ -162,10 +162,11 @@ agents_dir: sdk_agents # your <name>.agent.md f
|
|
|
162
162
|
|
|
163
163
|
steps:
|
|
164
164
|
- name: brief
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
reasoning_effort: medium
|
|
166
|
+
brief_inputs:
|
|
167
|
+
include: [AGENTS.md, coding-standards-ddd.yaml]
|
|
168
|
+
sections: [Repo rules digest, Unit scope digest, Environment facts, Open risks]
|
|
169
|
+
env_facts: ["the workspace is an export with no .git; verify with find/ls, never git status"]
|
|
169
170
|
on_pass: dev
|
|
170
171
|
|
|
171
172
|
- name: dev
|
|
@@ -205,9 +206,9 @@ steps:
|
|
|
205
206
|
|
|
206
207
|
| Key | Notes |
|
|
207
208
|
| --- | --- |
|
|
208
|
-
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). |
|
|
209
|
+
| `agent:` / `command:` / `action:` / `brief_inputs:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call). `commit` is the sole host-side built-in action; consumer actions are sandbox-executed, command-shaped scripts (see [Host boundary](#host-boundary-security-model)). `brief_inputs:` is a native brief step - see its row below. |
|
|
209
210
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
210
|
-
| `brief_inputs:` |
|
|
211
|
+
| `brief_inputs:` | The discriminator for a native brief step: `{ include: [<paths/globs>], env_facts: [<strings>], sections: [<headings>] }`. There is no `agent:`, `produces:`, or `max_iterations:` - the prompt is engine-embedded, the capture file is `attempt-brief.md`, and the iteration cap is 12, all engine-owned. The engine resolves the include list against the workspace, passes the resolved docs + include contents + env facts as the step's task message, and regenerates the step only when those inputs change. `sections` is the ordered list of digest headings the brief must emit (default `[Repo rules digest, Unit scope digest, Environment facts, Open risks]`; non-empty and unique). `name` stays free-form, so multiple brief steps are allowed. |
|
|
211
212
|
| `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. |
|
|
212
213
|
| `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). |
|
|
213
214
|
| `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. |
|
|
@@ -250,9 +251,9 @@ Recommended lane defaults (guidance only, not enforced):
|
|
|
250
251
|
| review | xhigh | shortest lane, highest stakes: a missed detection sails past the last gate into commit |
|
|
251
252
|
|
|
252
253
|
A `brief_inputs:` step is the per-run context-digestion pattern: it runs once at
|
|
253
|
-
run start (or when its inputs change), its `
|
|
254
|
-
starts, and the dev/review agents read that digest
|
|
255
|
-
rules and environment facts every attempt.
|
|
254
|
+
run start (or when its inputs change), its engine-owned `attempt-brief.md`
|
|
255
|
+
digest survives attempt starts, and the dev/review agents read that digest
|
|
256
|
+
instead of re-deriving repo rules and environment facts every attempt.
|
|
256
257
|
|
|
257
258
|
The brief digest is mechanically coverage-checked: the configured
|
|
258
259
|
`brief_inputs.sections` headings must each appear on their own line, every
|
|
@@ -340,10 +341,11 @@ steps:
|
|
|
340
341
|
structurally enforced.** A networked step runs on a per-run internal
|
|
341
342
|
docker network with no route to the host. Its traffic is forced
|
|
342
343
|
through a containerized egress proxy (injected via
|
|
343
|
-
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy
|
|
344
|
-
on the internal network
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
`HTTP_PROXY`/`HTTPS_PROXY`, pointed at the proxy's internal IP
|
|
345
|
+
on the internal network; each distinct egress policy gets its own
|
|
346
|
+
proxy with a distinct internal IP); a request to an undeclared host
|
|
347
|
+
is denied and logged as an enforcement event. The engine never adds
|
|
348
|
+
an undeclared target. `egress: { class: browser }` is the explicit
|
|
347
349
|
broad-but-logged class (every connection logged, nothing filtered); command
|
|
348
350
|
and agent steps default to a strict allowlist.
|
|
349
351
|
- **Command, agent, and sandbox-action steps** may declare it. The host-side
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.0a49"
|
|
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"
|
|
@@ -35,6 +35,13 @@ DEFAULT_BRIEF_SECTIONS = [
|
|
|
35
35
|
"Open risks",
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
+
# 049: a brief step is engine-owned end to end - the capture file and the
|
|
39
|
+
# iteration cap are fixed, not configurable. Downstream consumers (capture
|
|
40
|
+
# write, hygiene, context, retry classification) keep reading `produces` and
|
|
41
|
+
# `max_iterations`; parse-time normalization applies these after validation.
|
|
42
|
+
BRIEF_PRODUCES = ["attempt-brief.md"]
|
|
43
|
+
BRIEF_MAX_ITERATIONS = 12
|
|
44
|
+
|
|
38
45
|
# 047: the full SDK reasoning_effort literal, passed through verbatim. The
|
|
39
46
|
# engine is provider-generic, so it validates shape only - whether a given
|
|
40
47
|
# provider accepts every value (notably "none") is operator risk (D3).
|
|
@@ -372,9 +379,9 @@ class BriefInputsSpec(BaseModel):
|
|
|
372
379
|
|
|
373
380
|
class StepConfig(BaseModel):
|
|
374
381
|
"""One pipeline step. Its type IS the key it carries: exactly one of
|
|
375
|
-
`agent`, `action`, or `
|
|
376
|
-
else fails to load. See the `kind` property (the single
|
|
377
|
-
graph-building dispatches on)."""
|
|
382
|
+
`agent`, `action`, `command`, or `brief_inputs` - empty values do not
|
|
383
|
+
count, anything else fails to load. See the `kind` property (the single
|
|
384
|
+
discriminator graph-building dispatches on)."""
|
|
378
385
|
model_config = ConfigDict(extra="forbid")
|
|
379
386
|
|
|
380
387
|
name: str
|
|
@@ -413,10 +420,12 @@ class StepConfig(BaseModel):
|
|
|
413
420
|
|
|
414
421
|
@property
|
|
415
422
|
def kind(self) -> str:
|
|
416
|
-
"""The step's type: 'agent', 'command', or 'action'. Which
|
|
417
|
-
|
|
423
|
+
"""The step's type: 'brief', 'agent', 'command', or 'action'. Which
|
|
424
|
+
of the four keys is set is the ONLY signal; validation has already
|
|
418
425
|
guaranteed exactly one is set and non-empty by the time any instance
|
|
419
426
|
exists."""
|
|
427
|
+
if self.brief_inputs is not None:
|
|
428
|
+
return "brief"
|
|
420
429
|
if self.agent is not None:
|
|
421
430
|
return "agent"
|
|
422
431
|
if self.command is not None:
|
|
@@ -470,11 +479,48 @@ class StepConfig(BaseModel):
|
|
|
470
479
|
raise ValueError(
|
|
471
480
|
f"step {self.name!r}: {key!r} must be a non-empty string"
|
|
472
481
|
)
|
|
473
|
-
if sum(k is not None for k in
|
|
482
|
+
if sum(k is not None for k in
|
|
483
|
+
(self.agent, self.action, self.command, self.brief_inputs)) != 1:
|
|
474
484
|
raise ValueError(
|
|
475
|
-
f"step {self.name!r}: exactly one of 'agent', 'action',
|
|
476
|
-
"'command' is required"
|
|
485
|
+
f"step {self.name!r}: exactly one of 'agent', 'action', "
|
|
486
|
+
"'command', or 'brief_inputs' is required"
|
|
477
487
|
)
|
|
488
|
+
if self.brief_inputs is not None:
|
|
489
|
+
if self.produces:
|
|
490
|
+
raise ValueError(
|
|
491
|
+
f"step {self.name!r}: 'brief_inputs' steps write "
|
|
492
|
+
f"{BRIEF_PRODUCES[0]!r}; drop 'produces'")
|
|
493
|
+
if self.artifact is not None:
|
|
494
|
+
raise ValueError(
|
|
495
|
+
f"step {self.name!r}: 'brief_inputs' steps have no "
|
|
496
|
+
"verdicts; drop 'artifact'")
|
|
497
|
+
if self.verdicts is not None:
|
|
498
|
+
raise ValueError(
|
|
499
|
+
f"step {self.name!r}: 'brief_inputs' steps have no "
|
|
500
|
+
"verdicts; drop 'verdicts'")
|
|
501
|
+
if self.params:
|
|
502
|
+
raise ValueError(
|
|
503
|
+
f"step {self.name!r}: 'brief_inputs' steps take no "
|
|
504
|
+
"'params'")
|
|
505
|
+
if self.max_iterations is not None:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"step {self.name!r}: 'brief_inputs' steps use the "
|
|
508
|
+
f"engine cap ({BRIEF_MAX_ITERATIONS}); drop "
|
|
509
|
+
"'max_iterations'")
|
|
510
|
+
if self.timeout is not None:
|
|
511
|
+
raise ValueError(
|
|
512
|
+
f"step {self.name!r}: 'timeout' is command-only; drop it "
|
|
513
|
+
"from a 'brief_inputs' step")
|
|
514
|
+
if self.custom_prompt is not None:
|
|
515
|
+
raise ValueError(
|
|
516
|
+
f"step {self.name!r}: the brief prompt is engine-owned; "
|
|
517
|
+
"drop 'custom_prompt'")
|
|
518
|
+
if self.finalize_nudge_turns is not None:
|
|
519
|
+
raise ValueError(
|
|
520
|
+
f"step {self.name!r}: 'brief_inputs' steps have no "
|
|
521
|
+
"verdicts; drop 'finalize_nudge_turns'")
|
|
522
|
+
self.produces = list(BRIEF_PRODUCES)
|
|
523
|
+
self.max_iterations = BRIEF_MAX_ITERATIONS
|
|
478
524
|
if self.command is not None:
|
|
479
525
|
for field, empty in (("artifact", None), ("verdicts", None),
|
|
480
526
|
("produces", []), ("params", {}),
|
|
@@ -495,9 +541,11 @@ class StepConfig(BaseModel):
|
|
|
495
541
|
raise ValueError(
|
|
496
542
|
f"step {self.name!r}: 'timeout' requires 'command'"
|
|
497
543
|
)
|
|
498
|
-
if self.terminal_timeout is not None
|
|
544
|
+
if (self.terminal_timeout is not None
|
|
545
|
+
and self.agent is None and self.brief_inputs is None):
|
|
499
546
|
raise ValueError(
|
|
500
|
-
f"step {self.name!r}: 'terminal_timeout' requires 'agent'"
|
|
547
|
+
f"step {self.name!r}: 'terminal_timeout' requires 'agent' or "
|
|
548
|
+
"'brief_inputs'"
|
|
501
549
|
)
|
|
502
550
|
if self.finalize_nudge_turns is not None and self.agent is None:
|
|
503
551
|
raise ValueError(
|
|
@@ -514,37 +562,20 @@ class StepConfig(BaseModel):
|
|
|
514
562
|
f"step {self.name!r}: 'finalize_nudge_turns' must be less "
|
|
515
563
|
"than 'max_iterations'"
|
|
516
564
|
)
|
|
517
|
-
if self.brief_inputs is not None and self.agent is None:
|
|
518
|
-
raise ValueError(
|
|
519
|
-
f"step {self.name!r}: 'brief_inputs' requires 'agent'"
|
|
520
|
-
)
|
|
521
565
|
if self.custom_prompt is not None and self.agent is None:
|
|
522
566
|
raise ValueError(
|
|
523
567
|
f"step {self.name!r}: 'custom_prompt' requires 'agent'"
|
|
524
568
|
)
|
|
525
|
-
if (self.
|
|
526
|
-
and
|
|
527
|
-
raise ValueError(
|
|
528
|
-
f"step {self.name!r}: 'brief_inputs' is only valid on a "
|
|
529
|
-
"produces-only agent step"
|
|
530
|
-
)
|
|
531
|
-
if self.brief_inputs is not None and not self.produces:
|
|
532
|
-
raise ValueError(
|
|
533
|
-
f"step {self.name!r}: 'brief_inputs' requires 'produces'"
|
|
534
|
-
)
|
|
535
|
-
if self.setup is not None and self.command is None and self.agent is None:
|
|
569
|
+
if (self.setup is not None and self.command is None
|
|
570
|
+
and self.agent is None and self.brief_inputs is None):
|
|
536
571
|
raise ValueError(
|
|
537
|
-
f"step {self.name!r}: 'setup' is only allowed on 'command'
|
|
572
|
+
f"step {self.name!r}: 'setup' is only allowed on 'command', "
|
|
573
|
+
"'agent', or 'brief_inputs' steps"
|
|
538
574
|
)
|
|
539
575
|
if self.setup_probe is not None and self.setup is None:
|
|
540
576
|
raise ValueError(
|
|
541
577
|
f"step {self.name!r}: 'setup_probe' requires 'setup'"
|
|
542
578
|
)
|
|
543
|
-
# `network` is NOT rejected here: the schema can't tell a host-side
|
|
544
|
-
# built-in action (e.g. `commit`, which must never take egress) from
|
|
545
|
-
# a sandbox-executed consumer action (which may, like a command step).
|
|
546
|
-
# That distinction is enforced once the registry is known - see
|
|
547
|
-
# `validate_action_network` (020).
|
|
548
579
|
if self.secrets and self.action is not None:
|
|
549
580
|
raise ValueError(
|
|
550
581
|
f"step {self.name!r}: 'secrets' is only allowed on 'agent' or "
|
{agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
@@ -25,6 +25,12 @@ if TYPE_CHECKING:
|
|
|
25
25
|
|
|
26
26
|
from .isolation import Sandbox
|
|
27
27
|
|
|
28
|
+
# 049, D3: the internal brief agent name is a fixed UUID4 string, never the
|
|
29
|
+
# literal "brief", so a consumer vendoring a `brief.agent.md` cannot mint a
|
|
30
|
+
# second AgentDefinition under the same name. The vendored-file warning still
|
|
31
|
+
# keys on the literal filename `brief.agent.md` (that is the file to delete).
|
|
32
|
+
BRIEF_AGENT_NAME = "4f1c9e2b-7a3d-4e5f-8b6c-0d1a2f3c4d5e"
|
|
33
|
+
|
|
28
34
|
|
|
29
35
|
def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
|
|
30
36
|
"""Load `<agents_dir>/<name>.agent.md` and build the Agent via the SDK
|
{agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/dryrun.py
RENAMED
|
@@ -27,10 +27,11 @@ def _validate_agents(cfg: PipelineConfig, agents_dir: Path) -> None:
|
|
|
27
27
|
"""Every `agent:` step must resolve to an existing file under agents_dir
|
|
28
28
|
(the same fail-fast `load_agent` enforces at run time).
|
|
29
29
|
|
|
30
|
-
The brief prompt is engine-embedded (046), so a ``brief_inputs:``
|
|
31
|
-
|
|
30
|
+
The brief prompt is engine-embedded (046), so a native ``brief_inputs:``
|
|
31
|
+
step (kind 'brief') has `agent is None` and is exempt from the agents_dir
|
|
32
|
+
lookup - it never reads a vendored file."""
|
|
32
33
|
for step in cfg.steps:
|
|
33
|
-
if step.agent is None
|
|
34
|
+
if step.agent is None:
|
|
34
35
|
continue
|
|
35
36
|
path = agents_dir / f"{step.agent}.agent.md"
|
|
36
37
|
if not path.is_file():
|
|
@@ -79,6 +80,12 @@ def _step_summary(step: StepConfig) -> str:
|
|
|
79
80
|
parts.append(f"produces={','.join(step.produces)}")
|
|
80
81
|
if step.max_iterations is not None:
|
|
81
82
|
parts.append(f"max_iterations={step.max_iterations}")
|
|
83
|
+
elif step.kind == "brief":
|
|
84
|
+
parts = [f"sections={len(step.brief_inputs.sections)}"]
|
|
85
|
+
if step.produces:
|
|
86
|
+
parts.append(f"produces={','.join(step.produces)}")
|
|
87
|
+
if step.max_iterations is not None:
|
|
88
|
+
parts.append(f"max_iterations={step.max_iterations}")
|
|
82
89
|
elif step.kind == "command":
|
|
83
90
|
parts = [f"command={step.command}"]
|
|
84
91
|
if step.timeout is not None:
|
{agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/graph.py
RENAMED
|
@@ -93,10 +93,9 @@ def build_graph(runtime: Runtime, checkpointer=None):
|
|
|
93
93
|
graph = StateGraph(LoopState)
|
|
94
94
|
for step in runtime.cfg.steps:
|
|
95
95
|
if step.kind == "agent":
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
node = make_agent_node(runtime, step)
|
|
96
|
+
node = make_agent_node(runtime, step)
|
|
97
|
+
elif step.kind == "brief":
|
|
98
|
+
node = make_brief_node(runtime, step)
|
|
100
99
|
elif step.kind == "command":
|
|
101
100
|
node = make_command_node(runtime, step)
|
|
102
101
|
else:
|
{agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -31,7 +31,7 @@ import sys
|
|
|
31
31
|
import time
|
|
32
32
|
import uuid
|
|
33
33
|
from abc import ABC, abstractmethod
|
|
34
|
-
from collections.abc import Mapping
|
|
34
|
+
from collections.abc import Iterable, Mapping
|
|
35
35
|
from dataclasses import dataclass
|
|
36
36
|
from datetime import datetime, timezone
|
|
37
37
|
from pathlib import Path, PurePosixPath
|
|
@@ -299,10 +299,10 @@ def proxy_container_name(run_id: str, profile_key: str) -> str:
|
|
|
299
299
|
return f"{run_id}-proxy-{profile_key}"
|
|
300
300
|
|
|
301
301
|
|
|
302
|
-
def proxy_url(
|
|
303
|
-
"""The sandbox-reachable proxy URL: the proxy
|
|
304
|
-
|
|
305
|
-
return f"http://{
|
|
302
|
+
def proxy_url(proxy_host: str, port: int = EGRESS_PROXY_PORT) -> str:
|
|
303
|
+
"""The sandbox-reachable proxy URL: the proxy's internal address on the
|
|
304
|
+
internal network (038, D3). Never 127.0.0.1 or the host-gateway."""
|
|
305
|
+
return f"http://{proxy_host}:{port}"
|
|
306
306
|
|
|
307
307
|
|
|
308
308
|
def egress_events_path(iso_state_dir: str | Path, run_id: str) -> Path:
|
|
@@ -310,8 +310,10 @@ def egress_events_path(iso_state_dir: str | Path, run_id: str) -> Path:
|
|
|
310
310
|
return Path(iso_state_dir) / f"{EGRESS_EVENTS_PREFIX}-{run_id}.jsonl"
|
|
311
311
|
|
|
312
312
|
|
|
313
|
-
def
|
|
314
|
-
"""
|
|
313
|
+
def proxy_usable_ips(subnets: str) -> list[str]:
|
|
314
|
+
"""Usable non-gateway IPv4 host addresses from the first viable subnet
|
|
315
|
+
(048 D1): ``hosts[1:]``, skipping the gateway at ``hosts[0]``. Empty when
|
|
316
|
+
no subnet offers a non-gateway host."""
|
|
315
317
|
for token in subnets.split():
|
|
316
318
|
try:
|
|
317
319
|
network = ipaddress.ip_network(token, strict=False)
|
|
@@ -322,8 +324,8 @@ def first_usable_ip(subnets: str) -> str | None:
|
|
|
322
324
|
hosts = list(network.hosts())
|
|
323
325
|
if len(hosts) < 2:
|
|
324
326
|
continue
|
|
325
|
-
return str(hosts[1]
|
|
326
|
-
return
|
|
327
|
+
return [str(host) for host in hosts[1:]]
|
|
328
|
+
return []
|
|
327
329
|
|
|
328
330
|
|
|
329
331
|
|
|
@@ -964,6 +966,7 @@ class DockerBackend(Sandbox):
|
|
|
964
966
|
self._run_net = run_network_name(self._run_id)
|
|
965
967
|
self._network_created = False
|
|
966
968
|
self._proxy_containers: list[str] = []
|
|
969
|
+
self._proxy_ips: dict[str, str] = {}
|
|
967
970
|
self._egress_events_path = egress_events_path(self._iso_state_dir,
|
|
968
971
|
self._run_id)
|
|
969
972
|
self._sandbox_venv_dir = sandbox_venv_dir # 022 relocated venv (opt-in)
|
|
@@ -1309,9 +1312,9 @@ class DockerBackend(Sandbox):
|
|
|
1309
1312
|
proxy_url_value: str | None = None
|
|
1310
1313
|
if network and self._export_root is not None:
|
|
1311
1314
|
proxy_name = proxy_container_name(self._run_id, key[0][:16])
|
|
1312
|
-
self._start_proxy_container(proxy_name, policy,
|
|
1313
|
-
|
|
1314
|
-
proxy_url_value = proxy_url(
|
|
1315
|
+
proxy_ip = self._start_proxy_container(proxy_name, policy,
|
|
1316
|
+
self._dial_rewrites)
|
|
1317
|
+
proxy_url_value = proxy_url(proxy_ip)
|
|
1315
1318
|
payload = docker_create_payload(
|
|
1316
1319
|
run_id=self._run_id, image=self._image, profile=self._profile,
|
|
1317
1320
|
workspace=self._workspace, network=network,
|
|
@@ -1334,7 +1337,7 @@ class DockerBackend(Sandbox):
|
|
|
1334
1337
|
started=True)
|
|
1335
1338
|
|
|
1336
1339
|
def _start_proxy_container(self, name: str, policy: EgressPolicy,
|
|
1337
|
-
dial_rewrites: dict[str, str] | None = None) ->
|
|
1340
|
+
dial_rewrites: dict[str, str] | None = None) -> str:
|
|
1338
1341
|
"""Create one containerized proxy (038 D2/D5, 039 D1-D4): host-access
|
|
1339
1342
|
network at create time with a pinned internal IP, then attach the internal
|
|
1340
1343
|
network; the proxy binds that address only and is readiness probed from
|
|
@@ -1350,7 +1353,7 @@ class DockerBackend(Sandbox):
|
|
|
1350
1353
|
policy_json = egress_proxy_module.policy_to_json(policy)
|
|
1351
1354
|
dial_rewrites = dict(dial_rewrites or {})
|
|
1352
1355
|
dial_rewrites_json = json.dumps(dial_rewrites, sort_keys=True)
|
|
1353
|
-
bind_host = self._proxy_internal_ip()
|
|
1356
|
+
bind_host = self._proxy_internal_ip(name)
|
|
1354
1357
|
payload = docker_proxy_payload(
|
|
1355
1358
|
run_id=self._run_id, image=self._image, name=name,
|
|
1356
1359
|
policy_json=policy_json, dial_rewrites_json=dial_rewrites_json,
|
|
@@ -1379,22 +1382,63 @@ class DockerBackend(Sandbox):
|
|
|
1379
1382
|
f"{bind_host}:{EGRESS_PROXY_PORT} after "
|
|
1380
1383
|
f"{PROXY_READY_ATTEMPTS} readiness probes")
|
|
1381
1384
|
self._proxy_containers.append(name)
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
+
return bind_host
|
|
1386
|
+
|
|
1387
|
+
def _proxy_internal_ip(self, proxy_name: str) -> str:
|
|
1388
|
+
"""Return the per-proxy pinned IPv4 address on the internal network
|
|
1389
|
+
(048 D1): inspect the subnet and the addresses containers already hold
|
|
1390
|
+
on it, then allocate the next free host (profile containers join the
|
|
1391
|
+
run network with a daemon auto-assigned IP, so those must be skipped
|
|
1392
|
+
or the later ``network connect --ip`` collides)."""
|
|
1385
1393
|
inspect = self._runner.run(
|
|
1386
1394
|
["docker", "network", "inspect", "-f",
|
|
1387
|
-
"{{range .IPAM.Config}}{{.Subnet}} {{end}}"
|
|
1395
|
+
"{{range .IPAM.Config}}{{.Subnet}} {{end}}|"
|
|
1396
|
+
"{{range .Containers}}{{.IPv4Address}} {{end}}",
|
|
1397
|
+
self._run_net],
|
|
1388
1398
|
timeout=30)
|
|
1389
1399
|
if inspect.exit_code != 0:
|
|
1390
1400
|
raise SandboxError(
|
|
1391
1401
|
f"AE-SBX-021: internal network inspect failed: "
|
|
1392
1402
|
f"{inspect.stderr.strip() or inspect.stdout.strip()}")
|
|
1393
|
-
|
|
1394
|
-
if
|
|
1403
|
+
subnets, sep, in_use_part = inspect.stdout.partition("|")
|
|
1404
|
+
in_use = in_use_part.split() if sep else ()
|
|
1405
|
+
return self._allocate_proxy_ip(subnets, proxy_name, in_use)
|
|
1406
|
+
|
|
1407
|
+
def _allocate_proxy_ip(self, subnets: str, proxy_name: str,
|
|
1408
|
+
in_use: Iterable[str] = ()) -> str:
|
|
1409
|
+
"""Allocate one internal IPv4 address per proxy (048 D1/D2). An
|
|
1410
|
+
existing allocation for ``proxy_name`` is reused; otherwise the first
|
|
1411
|
+
free non-gateway host wins (the first proxy keeps ``hosts[1]``).
|
|
1412
|
+
Addresses in ``in_use`` (held by other containers on the network) are
|
|
1413
|
+
skipped alongside the already-allocated proxy IPs. Raises when the
|
|
1414
|
+
subnet has no free host instead of letting the daemon emit a
|
|
1415
|
+
confusing collision error."""
|
|
1416
|
+
existing = self._proxy_ips.get(proxy_name)
|
|
1417
|
+
if existing is not None:
|
|
1418
|
+
return existing
|
|
1419
|
+
hosts = proxy_usable_ips(subnets)
|
|
1420
|
+
if not hosts:
|
|
1395
1421
|
raise SandboxError(
|
|
1396
1422
|
f"AE-SBX-021: internal network has no usable IPv4 subnet: "
|
|
1397
|
-
f"{
|
|
1423
|
+
f"{subnets.strip()!r}")
|
|
1424
|
+
taken = set(self._proxy_ips.values())
|
|
1425
|
+
for token in in_use:
|
|
1426
|
+
addr = token.split("/", 1)[0].strip()
|
|
1427
|
+
try:
|
|
1428
|
+
ip = ipaddress.ip_address(addr)
|
|
1429
|
+
except ValueError:
|
|
1430
|
+
continue
|
|
1431
|
+
if isinstance(ip, ipaddress.IPv4Address):
|
|
1432
|
+
taken.add(str(ip))
|
|
1433
|
+
free = [host for host in hosts if host not in taken]
|
|
1434
|
+
if not free:
|
|
1435
|
+
raise SandboxError(
|
|
1436
|
+
f"AE-SBX-021: internal network {subnets.strip()!r} has "
|
|
1437
|
+
f"{len(hosts)} usable proxy address(es) but "
|
|
1438
|
+
f"{len(self._proxy_ips) + 1} egress proxies were requested - "
|
|
1439
|
+
f"no free address to assign")
|
|
1440
|
+
ip = free[0]
|
|
1441
|
+
self._proxy_ips[proxy_name] = ip
|
|
1398
1442
|
return ip
|
|
1399
1443
|
|
|
1400
1444
|
def _wait_proxy_ready(self, name: str, host: str) -> bool:
|
|
@@ -1541,6 +1585,7 @@ class DockerBackend(Sandbox):
|
|
|
1541
1585
|
for name in self._proxy_containers:
|
|
1542
1586
|
self._rm_container(name)
|
|
1543
1587
|
self._proxy_containers.clear()
|
|
1588
|
+
self._proxy_ips.clear()
|
|
1544
1589
|
if self._network_created:
|
|
1545
1590
|
self._runner.run(["docker", "network", "rm", self._run_net], timeout=60)
|
|
1546
1591
|
self._network_created = False
|
{agentic_dev_loop-1.0.0a46 → agentic_dev_loop-1.0.0a49}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -32,7 +32,8 @@ from langgraph.graph import END
|
|
|
32
32
|
from openhands.sdk import Conversation
|
|
33
33
|
|
|
34
34
|
from .agent_error_guide import guide_section
|
|
35
|
-
from .agents import load_agent, load_brief_agent,
|
|
35
|
+
from .agents import (BRIEF_AGENT_NAME, load_agent, load_brief_agent,
|
|
36
|
+
register_sandbox_terminal)
|
|
36
37
|
from .brief_coverage import extract_anchors, missing_anchors
|
|
37
38
|
from .brief_prompt import BRIEF_PROMPT_VERSION
|
|
38
39
|
from agentic_dev_loop.core.config import TERMINALS, PipelineConfig, Route, StepConfig, route_target
|
|
@@ -182,7 +183,7 @@ class Runtime:
|
|
|
182
183
|
if step is not None and step.reasoning_effort is not None:
|
|
183
184
|
llm = build_llm(self.cfg.llm,
|
|
184
185
|
reasoning_effort=step.reasoning_effort)
|
|
185
|
-
if step is not None and step.
|
|
186
|
+
if step is not None and step.kind == "brief":
|
|
186
187
|
agent = load_brief_agent(
|
|
187
188
|
agent_name, step.brief_inputs.sections, llm)
|
|
188
189
|
else:
|
|
@@ -398,7 +399,7 @@ def build_context(runtime: Runtime, docs: dict[str, str], subject: str,
|
|
|
398
399
|
parts.append("## Dependency policy\n\n" + policy_injection(runtime.cfg))
|
|
399
400
|
lines = []
|
|
400
401
|
for step in runtime.cfg.steps:
|
|
401
|
-
if step.agent
|
|
402
|
+
if step.kind not in ("agent", "brief"):
|
|
402
403
|
continue
|
|
403
404
|
for produced in step.produces:
|
|
404
405
|
lines.append(f"{step.name} agent writes: `{prompt_dir(runtime)}/{produced}`")
|
|
@@ -428,7 +429,7 @@ def run_scoped_artifacts(cfg: PipelineConfig) -> list[str]:
|
|
|
428
429
|
outputs are exempt from per-attempt hygiene."""
|
|
429
430
|
files = []
|
|
430
431
|
for step in cfg.steps:
|
|
431
|
-
if step.
|
|
432
|
+
if step.kind != "brief":
|
|
432
433
|
continue
|
|
433
434
|
files.extend(step.produces)
|
|
434
435
|
if step.artifact is not None:
|
|
@@ -778,7 +779,7 @@ def _warn_vendored_brief(runtime: Runtime, step: StepConfig) -> None:
|
|
|
778
779
|
The embedded prompt is authoritative, so a leftover
|
|
779
780
|
``<agents_dir>/<agent>.agent.md`` for a ``brief_inputs:`` step is ignored;
|
|
780
781
|
the warning names it without failing the run."""
|
|
781
|
-
vendored = runtime.agents_dir /
|
|
782
|
+
vendored = runtime.agents_dir / "brief.agent.md"
|
|
782
783
|
if vendored.is_file():
|
|
783
784
|
print(
|
|
784
785
|
f"[{runtime.cfg.name}] ignoring vendored brief prompt {vendored} - "
|
|
@@ -845,8 +846,8 @@ def make_brief_node(runtime: Runtime, step: StepConfig):
|
|
|
845
846
|
state.get("doc_sources"),
|
|
846
847
|
coverage_note=coverage_note)
|
|
847
848
|
|
|
848
|
-
print(f"[{subject}] Running {step.
|
|
849
|
-
result = runtime.run_agent(
|
|
849
|
+
print(f"[{subject}] Running {step.name} agent...", flush=True)
|
|
850
|
+
result = runtime.run_agent(BRIEF_AGENT_NAME, message,
|
|
850
851
|
max_iterations=max_iterations, step=step)
|
|
851
852
|
if not result.ok:
|
|
852
853
|
print(f"[{subject}] {step.name} agent failed on attempt {attempt}.",
|
|
@@ -1125,7 +1126,7 @@ def make_bump_node(runtime: Runtime):
|
|
|
1125
1126
|
# re-enter that brief step so it regenerates once and refreshes its
|
|
1126
1127
|
# hash. Unchanged inputs keep the failing step's retry_target.
|
|
1127
1128
|
for step in runtime.cfg.steps:
|
|
1128
|
-
if step.
|
|
1129
|
+
if step.kind != "brief":
|
|
1129
1130
|
continue
|
|
1130
1131
|
try:
|
|
1131
1132
|
current = brief_inputs_hash(runtime, step,
|