agentic-dev-loop 1.0.0a27__tar.gz → 1.0.0a29__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.0a27/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a29}/PKG-INFO +9 -4
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/README.md +8 -3
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/pyproject.toml +1 -1
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/__init__.py +3 -1
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/config.py +42 -2
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/agents.py +17 -3
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/isolation.py +24 -7
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/stages.py +121 -3
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29/src/agentic_dev_loop.egg-info}/PKG-INFO +9 -4
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop.egg-info/SOURCES.txt +1 -0
- agentic_dev_loop-1.0.0a29/tests/test_032_stop_when_green.py +363 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_config.py +12 -8
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_isolation.py +40 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_sdk_compat.py +27 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/LICENSE +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/setup.cfg +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/paths.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/results.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/verdict.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/actions.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/cli.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/deps.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/dryrun.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/egress_policy.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/extensions/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/extensions/aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/graph.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/llm.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/loaders.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/mounts.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/outputs.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/presets.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/proc.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/registry.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/seccomp/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/seccomp/chromium-userns.json +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/secrets.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/viz/__init__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/viz/__main__.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/viz/server.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_028_declared_caches.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_029_merge_on_conflict.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_actions.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_agents.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_aidlc.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_cli.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_deps.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_dry_run.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_e2e_fixtures.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_egress_proxy.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_graph.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_hostboundary.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_hostboundary_integration.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_isolation_integration.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_isolation_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_llm.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_module_contract.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_outputs_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_proc.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_publish.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_qa_prompt.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_registry.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_repo_ascii.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_results.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_secrets.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_secrets_integration.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_secrets_pipeline.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_source_resolution.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_stages.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_step_workspace.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_sync_back.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_trigger_script.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_unit5_config.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_unit5_pbt.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_unit5_pure.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_verdict.py +0 -0
- {agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/tests/test_viz.py +0 -0
{agentic_dev_loop-1.0.0a27/src/agentic_dev_loop.egg-info → agentic_dev_loop-1.0.0a29}/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.0a29
|
|
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
|
|
@@ -195,6 +195,8 @@ steps:
|
|
|
195
195
|
- name: dev
|
|
196
196
|
agent: dev
|
|
197
197
|
produces: implementation-summary.md
|
|
198
|
+
terminal_timeout: 900
|
|
199
|
+
stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
|
|
198
200
|
on_pass: test
|
|
199
201
|
|
|
200
202
|
- name: test
|
|
@@ -227,8 +229,10 @@ steps:
|
|
|
227
229
|
| --- | --- |
|
|
228
230
|
| `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)). |
|
|
229
231
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
232
|
+
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
233
|
+
| `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. |
|
|
230
234
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
231
|
-
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps
|
|
235
|
+
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
232
236
|
| `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). |
|
|
233
237
|
| `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
|
|
234
238
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -236,7 +240,7 @@ steps:
|
|
|
236
240
|
| `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
|
|
237
241
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
238
242
|
| `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
|
|
239
|
-
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
243
|
+
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
240
244
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
241
245
|
|
|
242
246
|
### Secrets (`secrets:`) - refs only, never values
|
|
@@ -432,7 +436,8 @@ Adaptation checklist for a new repo:
|
|
|
432
436
|
only if a stage runs Chromium.
|
|
433
437
|
3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
|
|
434
438
|
(`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
|
|
435
|
-
`frontend-editor/node_modules`); caches are never synced back
|
|
439
|
+
`frontend-editor/node_modules`); caches are never synced back, and a
|
|
440
|
+
step's `setup:` hook runs with them mounted so it can warm them.
|
|
436
441
|
4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
|
|
437
442
|
5. `secrets:` - refs only, resolved from host env; each step opts in to the
|
|
438
443
|
refs it needs.
|
|
@@ -153,6 +153,8 @@ steps:
|
|
|
153
153
|
- name: dev
|
|
154
154
|
agent: dev
|
|
155
155
|
produces: implementation-summary.md
|
|
156
|
+
terminal_timeout: 900
|
|
157
|
+
stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
|
|
156
158
|
on_pass: test
|
|
157
159
|
|
|
158
160
|
- name: test
|
|
@@ -185,8 +187,10 @@ steps:
|
|
|
185
187
|
| --- | --- |
|
|
186
188
|
| `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)). |
|
|
187
189
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
190
|
+
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
191
|
+
| `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. |
|
|
188
192
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
189
|
-
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps
|
|
193
|
+
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
190
194
|
| `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). |
|
|
191
195
|
| `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
|
|
192
196
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -194,7 +198,7 @@ steps:
|
|
|
194
198
|
| `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
|
|
195
199
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
196
200
|
| `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
|
|
197
|
-
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
201
|
+
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
198
202
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
199
203
|
|
|
200
204
|
### Secrets (`secrets:`) - refs only, never values
|
|
@@ -390,7 +394,8 @@ Adaptation checklist for a new repo:
|
|
|
390
394
|
only if a stage runs Chromium.
|
|
391
395
|
3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
|
|
392
396
|
(`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
|
|
393
|
-
`frontend-editor/node_modules`); caches are never synced back
|
|
397
|
+
`frontend-editor/node_modules`); caches are never synced back, and a
|
|
398
|
+
step's `setup:` hook runs with them mounted so it can warm them.
|
|
394
399
|
4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
|
|
395
400
|
5. `secrets:` - refs only, resolved from host env; each step opts in to the
|
|
396
401
|
refs it needs.
|
|
@@ -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.0a29"
|
|
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"
|
{agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/core/__init__.py
RENAMED
|
@@ -6,7 +6,8 @@ test fails if this package imports engine/viz or heavy deps.
|
|
|
6
6
|
|
|
7
7
|
from .config import (PipelineConfig, StepConfig, LlmConfig, IsolationConfig,
|
|
8
8
|
VerdictSpec, Route, route_target, load_config,
|
|
9
|
-
default_state_dir, TERMINALS, MountSpec, EgressSpec
|
|
9
|
+
default_state_dir, TERMINALS, MountSpec, EgressSpec,
|
|
10
|
+
StopWhenSpec)
|
|
10
11
|
from .results import Result
|
|
11
12
|
from .verdict import Verdict, VerdictError, parse_verdict, RETRY
|
|
12
13
|
|
|
@@ -14,5 +15,6 @@ __all__ = [
|
|
|
14
15
|
"PipelineConfig", "StepConfig", "LlmConfig", "IsolationConfig",
|
|
15
16
|
"VerdictSpec", "Route", "route_target", "load_config",
|
|
16
17
|
"default_state_dir", "TERMINALS", "MountSpec", "EgressSpec",
|
|
18
|
+
"StopWhenSpec",
|
|
17
19
|
"Result", "Verdict", "VerdictError", "parse_verdict", "RETRY",
|
|
18
20
|
]
|
|
@@ -305,6 +305,36 @@ class EgressSpec(BaseModel):
|
|
|
305
305
|
return [_validate_egress_target(t) for t in v]
|
|
306
306
|
|
|
307
307
|
|
|
308
|
+
class StopWhenSpec(BaseModel):
|
|
309
|
+
"""Optional ``stop_when:`` termination signal on an agent step (032).
|
|
310
|
+
|
|
311
|
+
``green_command`` is a regex matched against the agent's terminal
|
|
312
|
+
observation command; a matching observation that exits 0 - and no
|
|
313
|
+
workspace writes since - triggers one nudge turn, then
|
|
314
|
+
``post_green_turns`` further turns before the engine closes the
|
|
315
|
+
conversation. Absent ``stop_when`` keeps the current run-to-finish
|
|
316
|
+
behaviour unchanged.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
model_config = ConfigDict(extra="forbid")
|
|
320
|
+
|
|
321
|
+
green_command: str
|
|
322
|
+
post_green_turns: int = Field(default=2, ge=0)
|
|
323
|
+
|
|
324
|
+
@field_validator("green_command")
|
|
325
|
+
@classmethod
|
|
326
|
+
def _valid_regex(cls, v: str) -> str:
|
|
327
|
+
if not isinstance(v, str) or not v.strip():
|
|
328
|
+
raise ValueError(
|
|
329
|
+
"stop_when.green_command must be a non-empty string")
|
|
330
|
+
try:
|
|
331
|
+
re.compile(v)
|
|
332
|
+
except re.error as e:
|
|
333
|
+
raise ValueError(
|
|
334
|
+
f"stop_when.green_command is not a valid regex: {e}") from e
|
|
335
|
+
return v
|
|
336
|
+
|
|
337
|
+
|
|
308
338
|
class StepConfig(BaseModel):
|
|
309
339
|
"""One pipeline step. Its type IS the key it carries: exactly one of
|
|
310
340
|
`agent`, `action`, or `command` - empty values do not count, anything
|
|
@@ -322,6 +352,8 @@ class StepConfig(BaseModel):
|
|
|
322
352
|
verdicts: VerdictSpec | None = None
|
|
323
353
|
max_iterations: int | None = None
|
|
324
354
|
timeout: float | None = Field(default=None, gt=0) # seconds, command steps only
|
|
355
|
+
terminal_timeout: float | None = Field(default=None, gt=0) # seconds, agent steps only (032)
|
|
356
|
+
stop_when: StopWhenSpec | None = None # agent steps only (032)
|
|
325
357
|
env: dict[str, str | None] | None = None # NFR-1: null = inherit host value
|
|
326
358
|
setup: str | None = None # BR-13: command steps only
|
|
327
359
|
network: bool = False # per-stage egress (command/agent + sandbox actions; host-side actions reject at registry time)
|
|
@@ -386,9 +418,17 @@ class StepConfig(BaseModel):
|
|
|
386
418
|
raise ValueError(
|
|
387
419
|
f"step {self.name!r}: 'timeout' requires 'command'"
|
|
388
420
|
)
|
|
389
|
-
if self.
|
|
421
|
+
if self.terminal_timeout is not None and self.agent is None:
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"step {self.name!r}: 'terminal_timeout' requires 'agent'"
|
|
424
|
+
)
|
|
425
|
+
if self.stop_when is not None and self.agent is None:
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"step {self.name!r}: 'stop_when' requires 'agent'"
|
|
428
|
+
)
|
|
429
|
+
if self.setup is not None and self.command is None and self.agent is None:
|
|
390
430
|
raise ValueError(
|
|
391
|
-
f"step {self.name!r}: 'setup' is only allowed on 'command' steps"
|
|
431
|
+
f"step {self.name!r}: 'setup' is only allowed on 'command' or 'agent' steps"
|
|
392
432
|
)
|
|
393
433
|
# `network` is NOT rejected here: the schema can't tell a host-side
|
|
394
434
|
# built-in action (e.g. `commit`, which must never take egress) from
|
{agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/agents.py
RENAMED
|
@@ -66,7 +66,9 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
66
66
|
def __init__(self, sandbox: "Sandbox", *, workspace: str = "",
|
|
67
67
|
env: dict[str, str] | None = None,
|
|
68
68
|
pipeline: str | None = None, step: str | None = None,
|
|
69
|
-
network: bool = False, mount_set=None, egress_policy=None
|
|
69
|
+
network: bool = False, mount_set=None, egress_policy=None,
|
|
70
|
+
terminal_timeout: float | None = None,
|
|
71
|
+
on_terminal_result=None):
|
|
70
72
|
self._sandbox = sandbox
|
|
71
73
|
self._workspace = workspace
|
|
72
74
|
self._env = env or {}
|
|
@@ -75,6 +77,8 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
75
77
|
self._network = network
|
|
76
78
|
self._mount_set = mount_set
|
|
77
79
|
self._egress_policy = egress_policy
|
|
80
|
+
self._terminal_timeout = terminal_timeout
|
|
81
|
+
self._on_terminal_result = on_terminal_result
|
|
78
82
|
|
|
79
83
|
def __call__(self, action, conversation=None):
|
|
80
84
|
from openhands.tools.terminal import TerminalObservation
|
|
@@ -92,9 +96,12 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
92
96
|
command=action.command, exit_code=None, timeout=False,
|
|
93
97
|
metadata=CmdOutputMetadata(exit_code=-1))
|
|
94
98
|
|
|
99
|
+
timeout = action.timeout
|
|
100
|
+
if timeout is None and self._terminal_timeout is not None:
|
|
101
|
+
timeout = self._terminal_timeout
|
|
95
102
|
request = SandboxExecutionRequest(
|
|
96
103
|
argv=command_argv(action.command), cwd=self._workspace,
|
|
97
|
-
env=self._env, timeout=
|
|
104
|
+
env=self._env, timeout=timeout,
|
|
98
105
|
pipeline=self._pipeline, step=self._step, network=self._network,
|
|
99
106
|
mount_set=self._mount_set, egress_policy=self._egress_policy)
|
|
100
107
|
try:
|
|
@@ -112,6 +119,9 @@ class SandboxTerminalExecutor(ToolExecutor):
|
|
|
112
119
|
ConversationExecutionStatus.STUCK
|
|
113
120
|
)
|
|
114
121
|
raise
|
|
122
|
+
if self._on_terminal_result is not None:
|
|
123
|
+
self._on_terminal_result(
|
|
124
|
+
action.command, result.exit_code, result.wrote_workspace)
|
|
115
125
|
output = result.stdout + result.stderr
|
|
116
126
|
return TerminalObservation.from_text(
|
|
117
127
|
output, is_error=not result.succeeded and not result.is_timeout,
|
|
@@ -130,6 +140,8 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
130
140
|
step: str | None = None,
|
|
131
141
|
network: bool = False,
|
|
132
142
|
mount_set=None, egress_policy=None,
|
|
143
|
+
terminal_timeout: float | None = None,
|
|
144
|
+
on_terminal_result=None,
|
|
133
145
|
secrets: dict[str, str] | None = None) -> None:
|
|
134
146
|
"""Per-run re-registration of the `terminal` name (KD-4): a callable
|
|
135
147
|
factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
|
|
@@ -147,6 +159,8 @@ def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None]
|
|
|
147
159
|
executor=SandboxTerminalExecutor(
|
|
148
160
|
sandbox, workspace=workspace, env=resolved_env,
|
|
149
161
|
pipeline=pipeline, step=step, network=network,
|
|
150
|
-
mount_set=mount_set, egress_policy=egress_policy
|
|
162
|
+
mount_set=mount_set, egress_policy=egress_policy,
|
|
163
|
+
terminal_timeout=terminal_timeout,
|
|
164
|
+
on_terminal_result=on_terminal_result))
|
|
151
165
|
|
|
152
166
|
register_tool("terminal", factory)
|
{agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/isolation.py
RENAMED
|
@@ -144,6 +144,7 @@ class SandboxExecutionResult:
|
|
|
144
144
|
duration_seconds: float
|
|
145
145
|
backend: str # "docker"
|
|
146
146
|
timed_out: bool = False
|
|
147
|
+
wrote_workspace: bool = False # declared-rw paths changed (032)
|
|
147
148
|
|
|
148
149
|
@property
|
|
149
150
|
def succeeded(self) -> bool:
|
|
@@ -699,11 +700,14 @@ def docker_create_payload(*, run_id: str, image: str, profile: EffectiveProfile,
|
|
|
699
700
|
|
|
700
701
|
def docker_provision_payload(*, run_id: str, image: str, workspace: str,
|
|
701
702
|
setup_cmd: str,
|
|
702
|
-
sandbox_venv_dir: str | None = None
|
|
703
|
+
sandbox_venv_dir: str | None = None,
|
|
704
|
+
cache_mounts: list[tuple[str, str, str]]
|
|
705
|
+
| None = None) -> list[str]:
|
|
703
706
|
"""Exact ``docker run --rm`` argv for the provision container (§3.4).
|
|
704
707
|
Network ON (default bridge), rw rootfs, --cap-drop ALL, mount set
|
|
705
|
-
{workspace} + host-only masks (022)
|
|
706
|
-
|
|
708
|
+
{workspace} + host-only masks (022) + the declared cache paths (028, rw
|
|
709
|
+
bind mounts so a ``setup:`` hook can warm them); --user host uid:gid
|
|
710
|
+
(AE-SBX-009) with HOME=/tmp."""
|
|
707
711
|
payload = [
|
|
708
712
|
"docker", "run", "--rm", "--name", f"{run_id}-provision",
|
|
709
713
|
"--network", "bridge", "--cap-drop", "ALL",
|
|
@@ -713,6 +717,8 @@ def docker_provision_payload(*, run_id: str, image: str, workspace: str,
|
|
|
713
717
|
if user is not None:
|
|
714
718
|
payload.extend(["--user", user, "--env", "HOME=/tmp"])
|
|
715
719
|
payload.extend(_sandbox_mounts(workspace, sandbox_venv_dir))
|
|
720
|
+
for source, target, _mode in (cache_mounts or []):
|
|
721
|
+
payload += ["-v", f"{source}:{target}"]
|
|
716
722
|
payload.extend([
|
|
717
723
|
"--workdir", workspace,
|
|
718
724
|
image, "sh", "-c", setup_cmd,
|
|
@@ -995,7 +1001,8 @@ class DockerBackend(Sandbox):
|
|
|
995
1001
|
return
|
|
996
1002
|
payload = docker_provision_payload(
|
|
997
1003
|
run_id=self._run_id, image=self._image, workspace=self._workspace,
|
|
998
|
-
setup_cmd=setup_cmd, sandbox_venv_dir=self._sandbox_venv_dir
|
|
1004
|
+
setup_cmd=setup_cmd, sandbox_venv_dir=self._sandbox_venv_dir,
|
|
1005
|
+
cache_mounts=self._cache_mounts)
|
|
999
1006
|
res = self._runner.run(payload, timeout=PROVISION_TIMEOUT_SECONDS)
|
|
1000
1007
|
if res.exit_code != 0:
|
|
1001
1008
|
tail = (res.stdout or res.stderr or "")[-4000:]
|
|
@@ -1062,8 +1069,15 @@ class DockerBackend(Sandbox):
|
|
|
1062
1069
|
"failing closed (BR-11.3): no networked execution runs "
|
|
1063
1070
|
"without egress enforcement")
|
|
1064
1071
|
result = self._exec(request, prof.name)
|
|
1072
|
+
report: SyncBackReport | None = None
|
|
1065
1073
|
if mount_set.rw_paths() and self._export_root is not None:
|
|
1066
|
-
self._sync_back(mount_set)
|
|
1074
|
+
report = self._sync_back(mount_set)
|
|
1075
|
+
if report is not None and (report.synced_paths or report.merged_paths):
|
|
1076
|
+
result = SandboxExecutionResult(
|
|
1077
|
+
exit_code=result.exit_code, stdout=result.stdout,
|
|
1078
|
+
stderr=result.stderr, duration_seconds=result.duration_seconds,
|
|
1079
|
+
backend=result.backend, timed_out=result.timed_out,
|
|
1080
|
+
wrote_workspace=True)
|
|
1067
1081
|
return result
|
|
1068
1082
|
|
|
1069
1083
|
def _exec(self, request: SandboxExecutionRequest,
|
|
@@ -1155,7 +1169,7 @@ class DockerBackend(Sandbox):
|
|
|
1155
1169
|
export_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1156
1170
|
shutil.copy2(host_path, export_path, follow_symlinks=False)
|
|
1157
1171
|
|
|
1158
|
-
def _sync_back(self, mount_set: MountSet) -> None:
|
|
1172
|
+
def _sync_back(self, mount_set: MountSet) -> SyncBackReport | None:
|
|
1159
1173
|
"""Sync declared-rw export changes back to the real repo (BR-4/F1),
|
|
1160
1174
|
git-aware, host-side, with the sandbox step already complete.
|
|
1161
1175
|
|
|
@@ -1221,6 +1235,7 @@ class DockerBackend(Sandbox):
|
|
|
1221
1235
|
"synced_paths": list(report.synced_paths),
|
|
1222
1236
|
"merged_paths": list(report.merged_paths),
|
|
1223
1237
|
})
|
|
1238
|
+
return report
|
|
1224
1239
|
|
|
1225
1240
|
def close(self) -> None:
|
|
1226
1241
|
"""Best-effort stop + rm; idempotent (BR-8.3). With ``--rm`` the
|
|
@@ -1541,7 +1556,9 @@ def _file_digest(p: Path) -> str:
|
|
|
1541
1556
|
def _declared_setups(cfg: PipelineConfig, workspace: Path):
|
|
1542
1557
|
hashes = _lockfile_hashes(workspace)
|
|
1543
1558
|
for step in cfg.steps:
|
|
1544
|
-
if step.
|
|
1559
|
+
if step.setup is not None and (
|
|
1560
|
+
step.command is not None or step.agent is not None
|
|
1561
|
+
):
|
|
1545
1562
|
yield step.setup, setup_stamp_key(step.setup, hashes)
|
|
1546
1563
|
|
|
1547
1564
|
|
{agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29}/src/agentic_dev_loop/engine/stages.py
RENAMED
|
@@ -20,6 +20,7 @@ pipeline config; the fixed policies stay here, not in config:
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import os
|
|
23
|
+
import re
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
25
26
|
import traceback
|
|
@@ -54,6 +55,74 @@ LAST_ERROR = "last-error.md"
|
|
|
54
55
|
ARTIFACT_CONTEXT_CAP = 24_000 # chars per artifact injected into a step's context
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
class _StopWhenController:
|
|
59
|
+
"""Turn-level stop-when-green observation (032).
|
|
60
|
+
|
|
61
|
+
Pure per-event state machine - no knowledge of how the conversation is
|
|
62
|
+
driven. ``on_event`` is a SDK conversation callback: it records the most
|
|
63
|
+
recent matching terminal observation and any subsequent file-editor
|
|
64
|
+
write, and pauses the conversation after every agent-sourced event so the
|
|
65
|
+
caller can drive one turn at a time.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
NUDGE = "Step complete: produce your final response now"
|
|
69
|
+
_WRITE_COMMANDS = frozenset({"create", "str_replace", "insert", "undo_edit"})
|
|
70
|
+
|
|
71
|
+
def __init__(self, spec) -> None:
|
|
72
|
+
self._pattern = re.compile(spec.green_command)
|
|
73
|
+
self.post_green_turns = spec.post_green_turns
|
|
74
|
+
self.green_hit = False
|
|
75
|
+
self.writes_since_green = False
|
|
76
|
+
self.nudged = False
|
|
77
|
+
self.post_nudge_turns = 0
|
|
78
|
+
self.conversation = None
|
|
79
|
+
|
|
80
|
+
def attach(self, conversation) -> None:
|
|
81
|
+
self.conversation = conversation
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def green_signal(self) -> bool:
|
|
85
|
+
return self.green_hit and not self.writes_since_green
|
|
86
|
+
|
|
87
|
+
def on_event(self, event) -> None:
|
|
88
|
+
if self.conversation is None:
|
|
89
|
+
return
|
|
90
|
+
from openhands.sdk.event import ObservationEvent
|
|
91
|
+
|
|
92
|
+
if isinstance(event, ObservationEvent):
|
|
93
|
+
self._record_observation(event)
|
|
94
|
+
if getattr(event, "source", None) == "agent":
|
|
95
|
+
from openhands.sdk.conversation.state import (
|
|
96
|
+
ConversationExecutionStatus,
|
|
97
|
+
)
|
|
98
|
+
self.conversation.state.execution_status = (
|
|
99
|
+
ConversationExecutionStatus.PAUSED
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def on_terminal_result(self, command: str | None, exit_code: int | None,
|
|
103
|
+
wrote_workspace: bool) -> None:
|
|
104
|
+
"""Feed the sandbox terminal result channel (032): a matching green
|
|
105
|
+
command is the signal; any other terminal write resets it."""
|
|
106
|
+
if command is not None and self._pattern.search(command):
|
|
107
|
+
self.green_hit = exit_code == 0
|
|
108
|
+
self.writes_since_green = False
|
|
109
|
+
elif wrote_workspace:
|
|
110
|
+
self.writes_since_green = True
|
|
111
|
+
|
|
112
|
+
def _record_observation(self, event) -> None:
|
|
113
|
+
observation = event.observation
|
|
114
|
+
tool_name = event.tool_name
|
|
115
|
+
if tool_name == "terminal":
|
|
116
|
+
command = getattr(observation, "command", None)
|
|
117
|
+
if command is not None and self._pattern.search(command):
|
|
118
|
+
self.green_hit = getattr(observation, "exit_code", None) == 0
|
|
119
|
+
self.writes_since_green = False
|
|
120
|
+
elif tool_name == "file_editor":
|
|
121
|
+
command = getattr(observation, "command", None)
|
|
122
|
+
if command in self._WRITE_COMMANDS and not observation.is_error:
|
|
123
|
+
self.writes_since_green = True
|
|
124
|
+
|
|
125
|
+
|
|
57
126
|
@dataclass
|
|
58
127
|
class Runtime:
|
|
59
128
|
"""Everything stage nodes need that isn't in LoopState: pipeline config,
|
|
@@ -85,6 +154,10 @@ class Runtime:
|
|
|
85
154
|
try:
|
|
86
155
|
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
87
156
|
|
|
157
|
+
controller = None
|
|
158
|
+
if step is not None and step.stop_when is not None:
|
|
159
|
+
controller = _StopWhenController(step.stop_when)
|
|
160
|
+
|
|
88
161
|
if self.sandbox is not None:
|
|
89
162
|
if step is not None:
|
|
90
163
|
cwd, mount_set, egress_policy = _step_sandbox(self, step)
|
|
@@ -99,13 +172,24 @@ class Runtime:
|
|
|
99
172
|
network=step.network if step is not None else False,
|
|
100
173
|
mount_set=mount_set,
|
|
101
174
|
egress_policy=egress_policy,
|
|
175
|
+
terminal_timeout=(step.terminal_timeout
|
|
176
|
+
if step is not None else None),
|
|
177
|
+
on_terminal_result=(controller.on_terminal_result
|
|
178
|
+
if controller is not None else None),
|
|
102
179
|
secrets=(step_secret_env(self.cfg, step, self.secrets)
|
|
103
180
|
if step is not None else None))
|
|
104
181
|
agent = load_agent(self.agents_dir, agent_name, self.llm)
|
|
182
|
+
callbacks = [controller.on_event] if controller is not None else None
|
|
105
183
|
conv = Conversation(agent=agent, workspace=str(self.workspace),
|
|
106
|
-
max_iteration_per_run=max_iterations
|
|
184
|
+
max_iteration_per_run=max_iterations,
|
|
185
|
+
callbacks=callbacks)
|
|
186
|
+
if controller is not None:
|
|
187
|
+
controller.attach(conv)
|
|
107
188
|
conv.send_message(message)
|
|
108
|
-
|
|
189
|
+
if controller is not None:
|
|
190
|
+
self._run_stop_when_loop(conv, controller, max_iterations)
|
|
191
|
+
else:
|
|
192
|
+
conv.run()
|
|
109
193
|
|
|
110
194
|
# 030, D1/D2: a sticky infra failure must fail the run with no
|
|
111
195
|
# retry - check before any capture/verdict path would schedule an
|
|
@@ -144,6 +228,40 @@ class Runtime:
|
|
|
144
228
|
if conv is not None:
|
|
145
229
|
conv.close()
|
|
146
230
|
|
|
231
|
+
def _run_stop_when_loop(self, conv, controller: _StopWhenController,
|
|
232
|
+
max_iterations: int) -> None:
|
|
233
|
+
"""Drive one turn at a time so a green signal can inject the nudge
|
|
234
|
+
and enforce the post-nudge turn budget. The controller's callback
|
|
235
|
+
pauses the SDK run loop after every agent-sourced event, so each
|
|
236
|
+
``conv.run()`` here advances exactly one agent turn."""
|
|
237
|
+
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
238
|
+
|
|
239
|
+
turns = 0
|
|
240
|
+
while True:
|
|
241
|
+
conv.run()
|
|
242
|
+
status = conv.state.execution_status
|
|
243
|
+
if status in (ConversationExecutionStatus.FINISHED,
|
|
244
|
+
ConversationExecutionStatus.STUCK,
|
|
245
|
+
ConversationExecutionStatus.ERROR):
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
turns += 1
|
|
249
|
+
if turns >= max_iterations:
|
|
250
|
+
conv.state.execution_status = ConversationExecutionStatus.ERROR
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
if not controller.nudged and controller.green_signal:
|
|
254
|
+
conv.send_message(controller.NUDGE)
|
|
255
|
+
controller.nudged = True
|
|
256
|
+
if controller.post_green_turns == 0:
|
|
257
|
+
break
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
if controller.nudged:
|
|
261
|
+
controller.post_nudge_turns += 1
|
|
262
|
+
if controller.post_nudge_turns >= controller.post_green_turns:
|
|
263
|
+
break
|
|
264
|
+
|
|
147
265
|
|
|
148
266
|
def prompt_dir(runtime: Runtime) -> str:
|
|
149
267
|
"""state_dir as agents must address it. Agents resolve prompt paths
|
|
@@ -409,7 +527,7 @@ def make_agent_node(runtime: Runtime, step: StepConfig):
|
|
|
409
527
|
update["artifacts"] = {step.name: result.response}
|
|
410
528
|
return update
|
|
411
529
|
if verdict == "unverifiable":
|
|
412
|
-
print(f"[{subject}] {step.name}
|
|
530
|
+
print(f"[{subject}] {step.name} verdict: UNVERIFIABLE "
|
|
413
531
|
f"(moves on).", flush=True)
|
|
414
532
|
update = {"step_verdicts": merged, "failed": False}
|
|
415
533
|
if captures and result.response is not None:
|
{agentic_dev_loop-1.0.0a27 → agentic_dev_loop-1.0.0a29/src/agentic_dev_loop.egg-info}/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.0a29
|
|
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
|
|
@@ -195,6 +195,8 @@ steps:
|
|
|
195
195
|
- name: dev
|
|
196
196
|
agent: dev
|
|
197
197
|
produces: implementation-summary.md
|
|
198
|
+
terminal_timeout: 900
|
|
199
|
+
stop_when: { green_command: ".venv-sandbox/bin/python -m pytest tests/ -q", post_green_turns: 2 }
|
|
198
200
|
on_pass: test
|
|
199
201
|
|
|
200
202
|
- name: test
|
|
@@ -227,8 +229,10 @@ steps:
|
|
|
227
229
|
| --- | --- |
|
|
228
230
|
| `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)). |
|
|
229
231
|
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
232
|
+
| `stop_when:` | Optional agent-step termination: `{ green_command: <argv regex>, post_green_turns: <int, default 2> }`. When the agent's most recent terminal observation matching `green_command` exits 0 and no workspace writes have happened since, the engine injects one nudge turn, allows `post_green_turns` further turns, then closes the conversation. Absent = run to finish. |
|
|
233
|
+
| `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. |
|
|
230
234
|
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
231
|
-
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox (command steps
|
|
235
|
+
| `env:` / `setup:` / `network:` | `env:` declares step vars (fixed safe base + declared vars only); `setup:` provisions the sandbox before the step runs (command or agent steps; network ON; stamp-cached, re-runs when the pipeline's lockfiles change); `network: true` lets a command/agent step reach the hosts its `egress:` allowlist names (default off) - see [Per-stage networking](#per-stage-networking). |
|
|
232
236
|
| `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). |
|
|
233
237
|
| `secrets:` (top-level) / step `secrets:` | Declares secret **refs only - never values**; each step opts in to the refs it needs. Resolved host-side and injected into that step's exec env only. See [Secrets](#secrets--refs-only-never-values). |
|
|
234
238
|
| `isolation:` (optional) | Configures the sandbox - image override, resource caps, `allow_git_mutations`, `seccomp_profile`. It can never disable it. See [browser sandbox](docs/browser-sandbox.md). |
|
|
@@ -236,7 +240,7 @@ steps:
|
|
|
236
240
|
| `extensions_dir` | Where consumer `extensions:` modules resolve - defaults to `~/.agentic-dev-loop/extensions`, **outside** the workspace. Workspace-resident extension/tool modules are refused at load. |
|
|
237
241
|
| `deps_policy` | `locked` (default) or `open`. `locked` = the human-owned lockfile is the source of truth and the agent may not add dependencies; `open` = the bootstrap protocol (the agent may add exact-pinned manifest entries). See [Dependency policy](#dependency-policy-locked-by-default). |
|
|
238
242
|
| `lockfiles` | The pipeline's declared lockfile names (workspace-relative), injected into agent context and named by the missing-dependency fail-fast. Required (non-empty) when `deps_policy: locked` - a `locked` pipeline with no lockfiles fails fast at load. |
|
|
239
|
-
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
243
|
+
| `caches:` / `sync_excludes:` | Top-level run-wide lists. `caches:` names repo-relative toolchain cache dirs bind-mounted rw from the real workspace (auto-created if missing); a `setup:` hook can warm them (the provision container carries the same cache mounts); `sync_excludes:` names scratch paths skipped from both the export copy and sync-back (wildcard `*` spans separators, so `**/bin/**` matches a `/bin/` segment at any depth). Both default to `[]`. See [docs/setup.md](docs/setup.md). |
|
|
240
244
|
| `merge_on_conflict:` | Top-level run-wide list of append-style logs (same path vocabulary as `sync_excludes:`, default `[]`). A concurrent host edit to a listed path is union-merged instead of raising: the host file wins and the sandbox's export-only lines are appended in order; both sides converge and the run continues. See [docs/setup.md](docs/setup.md). |
|
|
241
245
|
|
|
242
246
|
### Secrets (`secrets:`) - refs only, never values
|
|
@@ -432,7 +436,8 @@ Adaptation checklist for a new repo:
|
|
|
432
436
|
only if a stage runs Chromium.
|
|
433
437
|
3. `caches:` - toolchain cache dirs as rw bind-mounts shared across runs
|
|
434
438
|
(`.nuget-packages`, `qa/node_modules`, `qa/.browsers`,
|
|
435
|
-
`frontend-editor/node_modules`); caches are never synced back
|
|
439
|
+
`frontend-editor/node_modules`); caches are never synced back, and a
|
|
440
|
+
step's `setup:` hook runs with them mounted so it can warm them.
|
|
436
441
|
4. `sync_excludes:` - build output trees (`**/bin/**`, `**/obj/**`).
|
|
437
442
|
5. `secrets:` - refs only, resolved from host env; each step opts in to the
|
|
438
443
|
refs it needs.
|