agentic-dev-loop 0.3.0__tar.gz → 1.0.0a2__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.
- agentic_dev_loop-1.0.0a2/PKG-INFO +239 -0
- agentic_dev_loop-1.0.0a2/README.md +201 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/pyproject.toml +6 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/actions.py +45 -2
- agentic_dev_loop-1.0.0a2/src/agentic_dev_loop/agents.py +125 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/cli.py +23 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/config.py +56 -2
- agentic_dev_loop-1.0.0a2/src/agentic_dev_loop/dryrun.py +122 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/extensions/aidlc.py +1 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/graph.py +24 -27
- agentic_dev_loop-1.0.0a2/src/agentic_dev_loop/isolation.py +723 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/loaders.py +25 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/outputs.py +1 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/proc.py +46 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/stages.py +45 -19
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/viz.py +10 -3
- agentic_dev_loop-1.0.0a2/src/agentic_dev_loop.egg-info/PKG-INFO +239 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop.egg-info/SOURCES.txt +6 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_actions.py +117 -6
- agentic_dev_loop-1.0.0a2/tests/test_agents.py +167 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_aidlc.py +5 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_cli.py +47 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_config.py +90 -2
- agentic_dev_loop-1.0.0a2/tests/test_dry_run.py +137 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_e2e_fixtures.py +14 -4
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_graph.py +33 -2
- agentic_dev_loop-1.0.0a2/tests/test_isolation.py +677 -0
- agentic_dev_loop-1.0.0a2/tests/test_isolation_integration.py +128 -0
- agentic_dev_loop-1.0.0a2/tests/test_isolation_pbt.py +140 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_outputs_pbt.py +3 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_proc.py +72 -1
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_sdk_compat.py +22 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_source_resolution.py +1 -3
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_stages.py +75 -8
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_viz.py +33 -2
- agentic_dev_loop-0.3.0/PKG-INFO +0 -318
- agentic_dev_loop-0.3.0/README.md +0 -280
- agentic_dev_loop-0.3.0/src/agentic_dev_loop/agents.py +0 -49
- agentic_dev_loop-0.3.0/src/agentic_dev_loop.egg-info/PKG-INFO +0 -318
- agentic_dev_loop-0.3.0/tests/test_agents.py +0 -88
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/LICENSE +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/setup.cfg +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/__init__.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/extensions/__init__.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/llm.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/py.typed +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/registry.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop/results.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop.egg-info/dependency_links.txt +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop.egg-info/entry_points.txt +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop.egg-info/requires.txt +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/src/agentic_dev_loop.egg-info/top_level.txt +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_llm.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_loaders.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_outputs.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_publish.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_registry.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_results.py +0 -0
- {agentic_dev_loop-0.3.0 → agentic_dev_loop-1.0.0a2}/tests/test_verdict.py +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-dev-loop
|
|
3
|
+
Version: 1.0.0a2
|
|
4
|
+
Summary: Opinionated LangGraph engine for OpenHands agent pipelines
|
|
5
|
+
Author: Luke Wilson
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/lukedawilson/agentic-dev-loop
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: MacOS
|
|
12
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
23
|
+
Classifier: Topic :: Security
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: openhands-sdk~=1.17.0
|
|
29
|
+
Requires-Dist: openhands-tools~=1.17.0
|
|
30
|
+
Requires-Dist: langgraph>=1.2
|
|
31
|
+
Requires-Dist: langgraph-checkpoint-sqlite>=3.0
|
|
32
|
+
Requires-Dist: pydantic>=2.0
|
|
33
|
+
Requires-Dist: PyYAML>=6.0
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: hypothesis>=6.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# agentic-dev-loop
|
|
40
|
+
|
|
41
|
+
> **Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.**
|
|
42
|
+
|
|
43
|
+
Describe your pipeline in one YAML file — agent steps, mechanical test gates, and
|
|
44
|
+
trusted actions — and `agentic-dev-loop` runs it end to end: agent and command steps get
|
|
45
|
+
kernel-level sandboxing via Docker, verdicts route the graph, every boundary is
|
|
46
|
+
checkpointed, and nothing gets committed until every gate passes.
|
|
47
|
+
|
|
48
|
+
```mermaid
|
|
49
|
+
flowchart LR
|
|
50
|
+
dev["agent: dev"] --> test{{"test gate"}} --> review["agent: review"] --> qa["agent: qa"] --> commit["action: commit"]
|
|
51
|
+
test -- fail --> dev
|
|
52
|
+
review -- fail --> dev
|
|
53
|
+
qa -- fail --> dev
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **A tool, not a wiring project.** One CLI entry point, opinionated defaults,
|
|
57
|
+
fixed safety policies. You write the pipeline and the agent definitions —
|
|
58
|
+
nothing else.
|
|
59
|
+
- **Deterministic.** Same pipeline + same state → same routing. Verdicts are
|
|
60
|
+
parsed from files, retries are bounded, nothing is best-effort.
|
|
61
|
+
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
62
|
+
an always-on Docker sandbox. There is no opt-out.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Core features
|
|
67
|
+
|
|
68
|
+
| Feature | What it gives you |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| **Stage-gated agent loop** | Agents do the work, test stages verify it, and the pipeline only advances when each stage passes its gates — looping back with full context when one fails. |
|
|
71
|
+
| **Kernel-level sandboxing** | Agent work and command stages run isolated from your machine: no network access, no writes outside your workspace. No sandbox available → the run stops instead of degrading. |
|
|
72
|
+
| **Crash-safe resume** | Interrupt a run at any time and pick it up later — finished stages stay finished, and you resume from exactly where it stopped. |
|
|
73
|
+
| **Live graph visualisation** | Follow every run in the browser: stage status, pass/fail verdicts, retry cycles, and live console output as it happens. |
|
|
74
|
+
| **Extensible** | Plug in your own actions, tools, and document loaders to adapt the loop to your projects. |
|
|
75
|
+
|
|
76
|
+
The fixed policies *are* the product: fail fast on missing config/secrets, no
|
|
77
|
+
silent fallbacks, verdicts only from artifacts, bounded retries, per-attempt
|
|
78
|
+
artifact hygiene, a single clean-worktree-gated commit point, and no default
|
|
79
|
+
LLM timeout (agent runs are long).
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
**Prerequisites**
|
|
84
|
+
|
|
85
|
+
| Requirement | Detail |
|
|
86
|
+
| --- | --- |
|
|
87
|
+
| Python | ≥ 3.12 |
|
|
88
|
+
| Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works — Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, Linux-containers mode is required. |
|
|
89
|
+
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install agentic-dev-loop==1.0.0a1 # current release (alpha — pin it explicitly)
|
|
93
|
+
# or: pip install --pre agentic-dev-loop
|
|
94
|
+
agentic-dev-loop --version
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
On the first run the engine pulls the **digest-pinned** dev image
|
|
98
|
+
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
|
|
99
|
+
supply-chain check refuses to continue on a mismatch. If you're running
|
|
100
|
+
pre-publish tooling, point `AGENTIC_DEV_LOOP_DEV_IMAGE` at your locally built
|
|
101
|
+
image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
102
|
+
troubleshooting for every fail-fast code live in
|
|
103
|
+
[docs/setup.md](docs/setup.md).
|
|
104
|
+
|
|
105
|
+
## Usage
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
export OPENAI_API_KEY=... # whatever api_key_env names
|
|
109
|
+
agentic-dev-loop pipeline.yaml # run it
|
|
110
|
+
agentic-dev-loop pipeline.yaml --dry-run # validate first — no LLM, no Docker
|
|
111
|
+
agentic-dev-loop pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
| Flag | Effect |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `--dry-run` | Full load-and-validate pass, then print the resolved plan. No LLM, no Docker, no state writes, no API key. Exit `0` on a clean load. |
|
|
117
|
+
| `--resume <thread-id>` | Continue the latest checkpoint of a previous run (crash, Ctrl-C, exhaustion) with all accumulated notes intact. |
|
|
118
|
+
| `--max-attempts <n>` | Override the pipeline's `max_attempts`. |
|
|
119
|
+
| `--refresh` | Force re-provision of every `setup:` hook (bypasses the stamp cache). |
|
|
120
|
+
| `--clean-stamps` | Delete cached provision stamps and exit. |
|
|
121
|
+
| `--no-viz` / `--viz-port <port>` | Suppress the live graph view / set a preferred port (default 8321; falls back to a free port when busy). |
|
|
122
|
+
| extension flags | Document-loader flags (e.g. `--plan`, `--aidlc-unit`) appear once the pipeline's extensions are loaded — `agentic-dev-loop PIPELINE --help` shows them. |
|
|
123
|
+
|
|
124
|
+
Exit code is `0` on pipeline success, `1` otherwise.
|
|
125
|
+
|
|
126
|
+
**What a run looks like** (the shipped self-loop example):
|
|
127
|
+
|
|
128
|
+

|
|
129
|
+
|
|
130
|
+
1. **dev** — the dev agent works with full context (loaded docs + shared files).
|
|
131
|
+
Its edits are untracked; only declared `produces:` files are hygiene-tracked.
|
|
132
|
+
2. **test** — a mechanical command step runs the suite inside the sandbox.
|
|
133
|
+
Exit 0 moves on; anything else rewinds to `dev` with the failing output.
|
|
134
|
+
3. **review / qa** — verdict agents write artifacts whose `VERDICT:` lines route
|
|
135
|
+
the graph. `fail` rewinds to `dev` and consumes an attempt.
|
|
136
|
+
4. **commit** — the single commit point fires only when all gates passed and
|
|
137
|
+
the worktree was clean at start.
|
|
138
|
+
5. **success** — committed; or **failure** when attempts are exhausted.
|
|
139
|
+
|
|
140
|
+
### pipeline.yaml reference
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
name: my-construction-loop
|
|
144
|
+
extensions:
|
|
145
|
+
- agentic_dev_loop.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
146
|
+
additional_files:
|
|
147
|
+
- ../README.md # shared context for every agent step
|
|
148
|
+
|
|
149
|
+
llm:
|
|
150
|
+
model: openai/deepseek-v4-pro
|
|
151
|
+
base_url: https://api.deepseek.com
|
|
152
|
+
api_key_env: OPENAI_API_KEY
|
|
153
|
+
|
|
154
|
+
max_attempts: 10 # CLI --max-attempts overrides
|
|
155
|
+
agents_dir: sdk_agents # your <name>.agent.md files
|
|
156
|
+
|
|
157
|
+
steps:
|
|
158
|
+
- name: dev
|
|
159
|
+
agent: dev
|
|
160
|
+
produces: implementation-summary.md
|
|
161
|
+
on_pass: test
|
|
162
|
+
|
|
163
|
+
- name: test
|
|
164
|
+
command: .venv-sandbox/bin/python -m pytest tests/ -q -m "not docker"
|
|
165
|
+
setup: python -m venv .venv-sandbox && .venv-sandbox/bin/pip install -e ".[dev]"
|
|
166
|
+
timeout: 600
|
|
167
|
+
on_pass: review
|
|
168
|
+
on_fail: { goto: dev, retry: true }
|
|
169
|
+
|
|
170
|
+
- name: review
|
|
171
|
+
agent: review
|
|
172
|
+
artifact: review-findings.md
|
|
173
|
+
verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
|
|
174
|
+
on_pass: qa
|
|
175
|
+
on_fail: { goto: dev, retry: true }
|
|
176
|
+
|
|
177
|
+
- name: qa
|
|
178
|
+
agent: qa
|
|
179
|
+
artifact: qa-report.md
|
|
180
|
+
verdicts: { pass: PASS, fail: FAIL }
|
|
181
|
+
on_pass: commit
|
|
182
|
+
on_fail: { goto: dev, retry: true }
|
|
183
|
+
|
|
184
|
+
- name: commit
|
|
185
|
+
action: commit
|
|
186
|
+
params: { message: "feat: construct {subject} (agent dev loop)" }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
| Key | Notes |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are registered host-side functions. |
|
|
192
|
+
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
193
|
+
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
194
|
+
| `env:` / `setup:` (command steps) | Declared env inside the sandbox (fixed safe base + declared vars only); `setup:` provisions the sandbox (network ON, stamp-cached, `--refresh` forces). |
|
|
195
|
+
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
196
|
+
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
197
|
+
|
|
198
|
+
## Development environment
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
202
|
+
cd agentic-dev-loop
|
|
203
|
+
./setup-venv.sh # idempotent .venv setup (never touches bare pip)
|
|
204
|
+
# create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
|
|
205
|
+
# echo 'OPENAI_API_KEY=...' > .env
|
|
206
|
+
python -m pytest tests/ -q # fast suite
|
|
207
|
+
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
208
|
+
./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
|
|
209
|
+
./run-demo.sh # viz demo — scripted run replay, no pipeline needed
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- `setup-venv.sh` is safe to re-run and picks the right interpreter even on
|
|
213
|
+
machines with exotic Python installations.
|
|
214
|
+
- Docker-marked integration tests need a local daemon; CI runs them on Linux.
|
|
215
|
+
- The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
|
|
216
|
+
its own repo** — dev → test → review → qa → commit, with the engine's own
|
|
217
|
+
three agent definitions in `sdk_agents/`.
|
|
218
|
+
- CI: `ci.yml` (unit + integration suite, ubuntu + windows),
|
|
219
|
+
`publish-image.yml` (multi-arch dev image, amd64 + arm64), `release.yml`
|
|
220
|
+
(PyPI publishing).
|
|
221
|
+
|
|
222
|
+
## Contributing
|
|
223
|
+
|
|
224
|
+
- **Read [AGENTS.md](AGENTS.md) first** — it is the instruction set for both
|
|
225
|
+
human and agent contributors (default mode: the user drives, ask before
|
|
226
|
+
coding, TDD, no silent fallbacks).
|
|
227
|
+
- Development is structured by **AI-DLC** — the lifecycle workflow in
|
|
228
|
+
`.aidlc/aidlc-rules/aws-aidlc-rules/` with working docs (plans, units, state)
|
|
229
|
+
in `aidlc-docs/`. The bundled document loader autodetects the current unit
|
|
230
|
+
from `aidlc-state.md` via `--aidlc-unit`, or load any plan with `--plan`.
|
|
231
|
+
- OpenCode commands over the workflow: `/unit-status`, `/epic-status`,
|
|
232
|
+
`/ticket-details`, `/update-subtask`, `/apply-aidlc-patches`, and more.
|
|
233
|
+
- Working extension samples (actions, tools, document loaders) ship as
|
|
234
|
+
`tests/sample_ext.py`.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
Built on [OpenHands SDK](https://github.com/OpenHands/software-agent-sdk) and
|
|
239
|
+
[LangGraph](https://github.com/langchain-ai/langgraph).
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# agentic-dev-loop
|
|
2
|
+
|
|
3
|
+
> **Turn an LLM into a gated, sandboxed, resumable construction loop over your codebase.**
|
|
4
|
+
|
|
5
|
+
Describe your pipeline in one YAML file — agent steps, mechanical test gates, and
|
|
6
|
+
trusted actions — and `agentic-dev-loop` runs it end to end: agent and command steps get
|
|
7
|
+
kernel-level sandboxing via Docker, verdicts route the graph, every boundary is
|
|
8
|
+
checkpointed, and nothing gets committed until every gate passes.
|
|
9
|
+
|
|
10
|
+
```mermaid
|
|
11
|
+
flowchart LR
|
|
12
|
+
dev["agent: dev"] --> test{{"test gate"}} --> review["agent: review"] --> qa["agent: qa"] --> commit["action: commit"]
|
|
13
|
+
test -- fail --> dev
|
|
14
|
+
review -- fail --> dev
|
|
15
|
+
qa -- fail --> dev
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- **A tool, not a wiring project.** One CLI entry point, opinionated defaults,
|
|
19
|
+
fixed safety policies. You write the pipeline and the agent definitions —
|
|
20
|
+
nothing else.
|
|
21
|
+
- **Deterministic.** Same pipeline + same state → same routing. Verdicts are
|
|
22
|
+
parsed from files, retries are bounded, nothing is best-effort.
|
|
23
|
+
- **Sandboxed by default.** The agent's shell and every command step run inside
|
|
24
|
+
an always-on Docker sandbox. There is no opt-out.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Core features
|
|
29
|
+
|
|
30
|
+
| Feature | What it gives you |
|
|
31
|
+
| --- | --- |
|
|
32
|
+
| **Stage-gated agent loop** | Agents do the work, test stages verify it, and the pipeline only advances when each stage passes its gates — looping back with full context when one fails. |
|
|
33
|
+
| **Kernel-level sandboxing** | Agent work and command stages run isolated from your machine: no network access, no writes outside your workspace. No sandbox available → the run stops instead of degrading. |
|
|
34
|
+
| **Crash-safe resume** | Interrupt a run at any time and pick it up later — finished stages stay finished, and you resume from exactly where it stopped. |
|
|
35
|
+
| **Live graph visualisation** | Follow every run in the browser: stage status, pass/fail verdicts, retry cycles, and live console output as it happens. |
|
|
36
|
+
| **Extensible** | Plug in your own actions, tools, and document loaders to adapt the loop to your projects. |
|
|
37
|
+
|
|
38
|
+
The fixed policies *are* the product: fail fast on missing config/secrets, no
|
|
39
|
+
silent fallbacks, verdicts only from artifacts, bounded retries, per-attempt
|
|
40
|
+
artifact hygiene, a single clean-worktree-gated commit point, and no default
|
|
41
|
+
LLM timeout (agent runs are long).
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
**Prerequisites**
|
|
46
|
+
|
|
47
|
+
| Requirement | Detail |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| Python | ≥ 3.12 |
|
|
50
|
+
| Docker engine | Engine API ≥ 1.43 (Engine 24). Any compliant engine works — Docker Desktop, OrbStack, Colima, Rancher Desktop, podman docker-compat. On Windows, Linux-containers mode is required. |
|
|
51
|
+
| LLM API key | The env var named by your pipeline's `api_key_env` (e.g. `OPENAI_API_KEY`) must be set — a missing key fails fast, naming it. |
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install agentic-dev-loop==1.0.0a1 # current release (alpha — pin it explicitly)
|
|
55
|
+
# or: pip install --pre agentic-dev-loop
|
|
56
|
+
agentic-dev-loop --version
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On the first run the engine pulls the **digest-pinned** dev image
|
|
60
|
+
(`ghcr.io/lukedawilson/agentic-dev-loop/dev`) and verifies its digest — the
|
|
61
|
+
supply-chain check refuses to continue on a mismatch. If you're running
|
|
62
|
+
pre-publish tooling, point `AGENTIC_DEV_LOOP_DEV_IMAGE` at your locally built
|
|
63
|
+
image. Engine setup, first-run details, the full `pipeline.yaml` contract, and
|
|
64
|
+
troubleshooting for every fail-fast code live in
|
|
65
|
+
[docs/setup.md](docs/setup.md).
|
|
66
|
+
|
|
67
|
+
## Usage
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
export OPENAI_API_KEY=... # whatever api_key_env names
|
|
71
|
+
agentic-dev-loop pipeline.yaml # run it
|
|
72
|
+
agentic-dev-loop pipeline.yaml --dry-run # validate first — no LLM, no Docker
|
|
73
|
+
agentic-dev-loop pipeline.yaml --resume <thread-id> --max-attempts 5
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| Flag | Effect |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `--dry-run` | Full load-and-validate pass, then print the resolved plan. No LLM, no Docker, no state writes, no API key. Exit `0` on a clean load. |
|
|
79
|
+
| `--resume <thread-id>` | Continue the latest checkpoint of a previous run (crash, Ctrl-C, exhaustion) with all accumulated notes intact. |
|
|
80
|
+
| `--max-attempts <n>` | Override the pipeline's `max_attempts`. |
|
|
81
|
+
| `--refresh` | Force re-provision of every `setup:` hook (bypasses the stamp cache). |
|
|
82
|
+
| `--clean-stamps` | Delete cached provision stamps and exit. |
|
|
83
|
+
| `--no-viz` / `--viz-port <port>` | Suppress the live graph view / set a preferred port (default 8321; falls back to a free port when busy). |
|
|
84
|
+
| extension flags | Document-loader flags (e.g. `--plan`, `--aidlc-unit`) appear once the pipeline's extensions are loaded — `agentic-dev-loop PIPELINE --help` shows them. |
|
|
85
|
+
|
|
86
|
+
Exit code is `0` on pipeline success, `1` otherwise.
|
|
87
|
+
|
|
88
|
+
**What a run looks like** (the shipped self-loop example):
|
|
89
|
+
|
|
90
|
+

|
|
91
|
+
|
|
92
|
+
1. **dev** — the dev agent works with full context (loaded docs + shared files).
|
|
93
|
+
Its edits are untracked; only declared `produces:` files are hygiene-tracked.
|
|
94
|
+
2. **test** — a mechanical command step runs the suite inside the sandbox.
|
|
95
|
+
Exit 0 moves on; anything else rewinds to `dev` with the failing output.
|
|
96
|
+
3. **review / qa** — verdict agents write artifacts whose `VERDICT:` lines route
|
|
97
|
+
the graph. `fail` rewinds to `dev` and consumes an attempt.
|
|
98
|
+
4. **commit** — the single commit point fires only when all gates passed and
|
|
99
|
+
the worktree was clean at start.
|
|
100
|
+
5. **success** — committed; or **failure** when attempts are exhausted.
|
|
101
|
+
|
|
102
|
+
### pipeline.yaml reference
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
name: my-construction-loop
|
|
106
|
+
extensions:
|
|
107
|
+
- agentic_dev_loop.extensions.aidlc:AidlcExtension # bundled AI-DLC doc loader
|
|
108
|
+
additional_files:
|
|
109
|
+
- ../README.md # shared context for every agent step
|
|
110
|
+
|
|
111
|
+
llm:
|
|
112
|
+
model: openai/deepseek-v4-pro
|
|
113
|
+
base_url: https://api.deepseek.com
|
|
114
|
+
api_key_env: OPENAI_API_KEY
|
|
115
|
+
|
|
116
|
+
max_attempts: 10 # CLI --max-attempts overrides
|
|
117
|
+
agents_dir: sdk_agents # your <name>.agent.md files
|
|
118
|
+
|
|
119
|
+
steps:
|
|
120
|
+
- name: dev
|
|
121
|
+
agent: dev
|
|
122
|
+
produces: implementation-summary.md
|
|
123
|
+
on_pass: test
|
|
124
|
+
|
|
125
|
+
- name: test
|
|
126
|
+
command: .venv-sandbox/bin/python -m pytest tests/ -q -m "not docker"
|
|
127
|
+
setup: python -m venv .venv-sandbox && .venv-sandbox/bin/pip install -e ".[dev]"
|
|
128
|
+
timeout: 600
|
|
129
|
+
on_pass: review
|
|
130
|
+
on_fail: { goto: dev, retry: true }
|
|
131
|
+
|
|
132
|
+
- name: review
|
|
133
|
+
agent: review
|
|
134
|
+
artifact: review-findings.md
|
|
135
|
+
verdicts: { pass: APPROVED, fail: NEEDS CHANGES }
|
|
136
|
+
on_pass: qa
|
|
137
|
+
on_fail: { goto: dev, retry: true }
|
|
138
|
+
|
|
139
|
+
- name: qa
|
|
140
|
+
agent: qa
|
|
141
|
+
artifact: qa-report.md
|
|
142
|
+
verdicts: { pass: PASS, fail: FAIL }
|
|
143
|
+
on_pass: commit
|
|
144
|
+
on_fail: { goto: dev, retry: true }
|
|
145
|
+
|
|
146
|
+
- name: commit
|
|
147
|
+
action: commit
|
|
148
|
+
params: { message: "feat: construct {subject} (agent dev loop)" }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Key | Notes |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `agent:` / `command:` / `action:` | Exactly one per step. Agents live in `agents_dir` as `<name>.agent.md`; commands run sandboxed (mechanical, never an LLM call); actions are registered host-side functions. |
|
|
154
|
+
| `produces:` / `artifact:` + `verdicts:` | Hygiene-tracked outputs vs verdict-parsed routing. The **last** `VERDICT:` line wins; stale artifacts are deleted at each attempt start. |
|
|
155
|
+
| `on_pass:` / `on_fail:` | A step name, terminal `success`/`failure`, or `{goto: X, retry: true}`. No loops happen unless you write them. |
|
|
156
|
+
| `env:` / `setup:` (command steps) | Declared env inside the sandbox (fixed safe base + declared vars only); `setup:` provisions the sandbox (network ON, stamp-cached, `--refresh` forces). |
|
|
157
|
+
| `isolation:` (optional) | Configures the sandbox — image override, resource caps, `allow_git_mutations`. It can never disable it. |
|
|
158
|
+
| `state_dir` | Defaults to `~/.agentic-dev-loop/runs/<pipeline-hash>/` — artifacts and checkpoints live outside the sandbox mount. |
|
|
159
|
+
|
|
160
|
+
## Development environment
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone git@github.com:lukedawilson/agentic-dev-loop.git
|
|
164
|
+
cd agentic-dev-loop
|
|
165
|
+
./setup-venv.sh # idempotent .venv setup (never touches bare pip)
|
|
166
|
+
# create .env with your LLM key (loaded by trigger-agentic-dev-loop.sh, never committed):
|
|
167
|
+
# echo 'OPENAI_API_KEY=...' > .env
|
|
168
|
+
python -m pytest tests/ -q # fast suite
|
|
169
|
+
python -m pytest tests/ -q -m "not docker" # skip engine-dependent tests
|
|
170
|
+
./trigger-agentic-dev-loop.sh --dry-run # run the shipped pipeline against this repo
|
|
171
|
+
./run-demo.sh # viz demo — scripted run replay, no pipeline needed
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
- `setup-venv.sh` is safe to re-run and picks the right interpreter even on
|
|
175
|
+
machines with exotic Python installations.
|
|
176
|
+
- Docker-marked integration tests need a local daemon; CI runs them on Linux.
|
|
177
|
+
- The shipped pipeline (`agentic-dev-loop/pipeline.yaml`) **dogfoods the tool on
|
|
178
|
+
its own repo** — dev → test → review → qa → commit, with the engine's own
|
|
179
|
+
three agent definitions in `sdk_agents/`.
|
|
180
|
+
- CI: `ci.yml` (unit + integration suite, ubuntu + windows),
|
|
181
|
+
`publish-image.yml` (multi-arch dev image, amd64 + arm64), `release.yml`
|
|
182
|
+
(PyPI publishing).
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
- **Read [AGENTS.md](AGENTS.md) first** — it is the instruction set for both
|
|
187
|
+
human and agent contributors (default mode: the user drives, ask before
|
|
188
|
+
coding, TDD, no silent fallbacks).
|
|
189
|
+
- Development is structured by **AI-DLC** — the lifecycle workflow in
|
|
190
|
+
`.aidlc/aidlc-rules/aws-aidlc-rules/` with working docs (plans, units, state)
|
|
191
|
+
in `aidlc-docs/`. The bundled document loader autodetects the current unit
|
|
192
|
+
from `aidlc-state.md` via `--aidlc-unit`, or load any plan with `--plan`.
|
|
193
|
+
- OpenCode commands over the workflow: `/unit-status`, `/epic-status`,
|
|
194
|
+
`/ticket-details`, `/update-subtask`, `/apply-aidlc-patches`, and more.
|
|
195
|
+
- Working extension samples (actions, tools, document loaders) ship as
|
|
196
|
+
`tests/sample_ext.py`.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
Built on [OpenHands SDK](https://github.com/OpenHands/software-agent-sdk) and
|
|
201
|
+
[LangGraph](https://github.com/langchain-ai/langgraph).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agentic-dev-loop"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0a2"
|
|
8
8
|
description = "Opinionated LangGraph engine for OpenHands agent pipelines"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -57,3 +57,8 @@ where = ["src"]
|
|
|
57
57
|
|
|
58
58
|
[tool.setuptools.package-data]
|
|
59
59
|
agentic_dev_loop = ["py.typed"]
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
markers = [
|
|
63
|
+
"docker: real-Docker integration tests (require a running Linux daemon)",
|
|
64
|
+
]
|
|
@@ -30,6 +30,15 @@ def _git(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
|
30
30
|
env=child_env())
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def _trusted_hooks_dir() -> str:
|
|
34
|
+
"""The agentic-dev-loop-managed hooks dir (Q3): repo hooks in ``.git/hooks``
|
|
35
|
+
never execute during the trusted commit action. Created empty; git runs
|
|
36
|
+
with ``-c core.hooksPath=<this dir>`` so there is no hook to run."""
|
|
37
|
+
d = Path("~/.agentic-dev-loop/state/hooks").expanduser()
|
|
38
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
return str(d)
|
|
40
|
+
|
|
41
|
+
|
|
33
42
|
def git_worktree_clean(path: Path) -> bool:
|
|
34
43
|
"""True only when `path` is a git repo with no uncommitted changes.
|
|
35
44
|
Outside a repo there is no clean baseline — False (fail-safe, keeps the
|
|
@@ -155,6 +164,28 @@ def kill_listeners_on_port(port: int, match: str | None = None,
|
|
|
155
164
|
shell=False, env=child_env())
|
|
156
165
|
|
|
157
166
|
|
|
167
|
+
def checkpoint_exclude_pathspec(state_dir: str | None, cwd: Path) -> str | None:
|
|
168
|
+
"""git pathspec excluding the live checkpoint DB from ``git add -A``.
|
|
169
|
+
|
|
170
|
+
The checkpointer keeps ``state_dir/loop-checkpoints.sqlite`` open for the
|
|
171
|
+
whole run; on Windows an open SQLite file blocks other readers, so
|
|
172
|
+
``git add`` fails to hash it (CI-red gate failure). When the state dir
|
|
173
|
+
sits inside the repo, exclude its sqlite files (db + wal/shm) from the
|
|
174
|
+
auto-commit — checkpoint state is runtime machinery, not constructed
|
|
175
|
+
work. Returns None when there is nothing in-repo to exclude (no state
|
|
176
|
+
dir known, or it lives outside the repo).
|
|
177
|
+
"""
|
|
178
|
+
if not state_dir:
|
|
179
|
+
return None
|
|
180
|
+
rel = os.path.relpath(state_dir, cwd)
|
|
181
|
+
if rel == ".." or rel.startswith(".." + os.sep):
|
|
182
|
+
return None
|
|
183
|
+
rel = rel.replace(os.sep, "/")
|
|
184
|
+
if rel == ".":
|
|
185
|
+
return ":(exclude)*.sqlite*"
|
|
186
|
+
return f":(exclude){rel}/*.sqlite*"
|
|
187
|
+
|
|
188
|
+
|
|
158
189
|
def commit(state: dict, params: dict) -> dict:
|
|
159
190
|
"""The pipeline's only commit point. Skipped loudly when the worktree was
|
|
160
191
|
dirty at run start, so the auto-commit can never swallow pre-existing
|
|
@@ -172,12 +203,24 @@ def commit(state: dict, params: dict) -> dict:
|
|
|
172
203
|
raise ValueError("commit action requires params.message "
|
|
173
204
|
"(a template rendered with {subject})")
|
|
174
205
|
cwd = Path.cwd()
|
|
175
|
-
|
|
206
|
+
hooks = _trusted_hooks_dir()
|
|
207
|
+
add_argv = ["-c", f"core.hooksPath={hooks}", "add", "-A"]
|
|
208
|
+
exclude = checkpoint_exclude_pathspec(state.get("state_dir"), cwd)
|
|
209
|
+
if exclude:
|
|
210
|
+
add_argv += ["--", exclude]
|
|
211
|
+
added = _git(add_argv, cwd=cwd)
|
|
212
|
+
if added.returncode != 0:
|
|
213
|
+
# The add is the step that touches the most files (a locked DB, a
|
|
214
|
+
# permission problem) — its error is the diagnostic that matters.
|
|
215
|
+
print(f"WARNING: [{subject}] auto-commit failed — git add: "
|
|
216
|
+
f"{added.stderr.strip()}", file=sys.stderr, flush=True)
|
|
217
|
+
return {}
|
|
176
218
|
if not _git(["status", "--porcelain"], cwd=cwd).stdout.strip():
|
|
177
219
|
print(f"[{subject}] Nothing to commit.", flush=True)
|
|
178
220
|
return {}
|
|
179
221
|
message = params["message"].format(subject=subject)
|
|
180
|
-
result = _git(["commit", "-m", message],
|
|
222
|
+
result = _git(["-c", f"core.hooksPath={hooks}", "commit", "-m", message],
|
|
223
|
+
cwd=cwd)
|
|
181
224
|
if result.returncode != 0:
|
|
182
225
|
# The work is built and verified — a git error is a loud warning,
|
|
183
226
|
# not a pipeline failure.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Agent file loading + SDK factory mechanics.
|
|
2
|
+
|
|
3
|
+
The SDK's built-in tools are registered at import (its registry is global);
|
|
4
|
+
consumer tools are registered by extensions via `Registry.add_tool`. Unit-3
|
|
5
|
+
adds the sandbox terminal seam: `SandboxTerminalExecutor` delegates each
|
|
6
|
+
terminal action to a `Sandbox.run`, and `register_sandbox_terminal` re-registers
|
|
7
|
+
the `terminal` tool name per run (KD-4 / §6)."""
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from openhands.sdk import LLM
|
|
13
|
+
from openhands.sdk.tool import ToolExecutor
|
|
14
|
+
from openhands.tools.file_editor import FileEditorTool # noqa: F401 — registers "file_editor"
|
|
15
|
+
from openhands.tools.task_tracker import TaskTrackerTool # noqa: F401 — registers "task_tracker"
|
|
16
|
+
from openhands.tools.terminal import TerminalTool # noqa: F401 — registers "terminal"
|
|
17
|
+
|
|
18
|
+
from .isolation import SandboxExecutionRequest, command_argv, resolve_sandbox_env
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from openhands.sdk.agent.agent import Agent
|
|
22
|
+
|
|
23
|
+
from .isolation import Sandbox
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_agent(agents_dir: Path | str, name: str, llm: LLM) -> "Agent":
|
|
27
|
+
"""Load `<agents_dir>/<name>.agent.md` and build the Agent via the SDK
|
|
28
|
+
factory (frontmatter tool names resolved against the registry, unknown →
|
|
29
|
+
ValueError; the markdown body is delivered as the system prompt suffix).
|
|
30
|
+
|
|
31
|
+
Tool surface (BR-1): the frontmatter `tools:` list is the exact
|
|
32
|
+
capability allowlist — every name must resolve to a registered SDK tool.
|
|
33
|
+
`FinishTool` (done signal) and `ThinkTool` (reasoning scaffold) are added
|
|
34
|
+
by the SDK as default infra, never part of the allowlist and never
|
|
35
|
+
removable via `tools:`. There is no regex or default-tools knob.
|
|
36
|
+
|
|
37
|
+
Two pipeline policies on top of the SDK's native AgentDefinition loader:
|
|
38
|
+
the file must exist (fail fast — the SDK's dir loader would silently
|
|
39
|
+
skip it) and its frontmatter name must match its filename. A file with
|
|
40
|
+
no frontmatter or no name gets the SDK's stem fallback ("<name>.agent"),
|
|
41
|
+
which can never match the stem — so both degenerate cases fail the name
|
|
42
|
+
check."""
|
|
43
|
+
from openhands.sdk.subagent.registry import agent_definition_to_factory
|
|
44
|
+
from openhands.sdk.subagent.schema import AgentDefinition
|
|
45
|
+
|
|
46
|
+
path = Path(agents_dir) / f"{name}.agent.md"
|
|
47
|
+
if not path.is_file():
|
|
48
|
+
raise FileNotFoundError(f"Agent file not found: {path}")
|
|
49
|
+
|
|
50
|
+
agent_def = AgentDefinition.load(path)
|
|
51
|
+
|
|
52
|
+
stem = path.name.removesuffix(".agent.md")
|
|
53
|
+
if agent_def.name != stem:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Agent file {path} name {agent_def.name!r} does not match "
|
|
56
|
+
f"filename {stem!r}")
|
|
57
|
+
return agent_definition_to_factory(agent_def)(llm)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SandboxTerminalExecutor(ToolExecutor):
|
|
61
|
+
"""A stateless terminal executor: each action is a fresh ``Sandbox.run``
|
|
62
|
+
(no persistent shell session). ``is_input`` degrades to an unsupported
|
|
63
|
+
observation; ``reset`` is a no-op (there is no session to reset)."""
|
|
64
|
+
|
|
65
|
+
def __init__(self, sandbox: "Sandbox", *, workspace: str = "",
|
|
66
|
+
env: dict[str, str] | None = None,
|
|
67
|
+
pipeline: str | None = None, step: str | None = None):
|
|
68
|
+
self._sandbox = sandbox
|
|
69
|
+
self._workspace = workspace
|
|
70
|
+
self._env = env or {}
|
|
71
|
+
self._pipeline = pipeline
|
|
72
|
+
self._step = step
|
|
73
|
+
|
|
74
|
+
def __call__(self, action, conversation=None):
|
|
75
|
+
from openhands.tools.terminal import TerminalObservation
|
|
76
|
+
from openhands.tools.terminal.metadata import CmdOutputMetadata
|
|
77
|
+
|
|
78
|
+
if action.reset:
|
|
79
|
+
return TerminalObservation.from_text(
|
|
80
|
+
"[reset unsupported — stateless sandbox]",
|
|
81
|
+
command=action.command, exit_code=None, timeout=False,
|
|
82
|
+
metadata=CmdOutputMetadata(exit_code=-1))
|
|
83
|
+
|
|
84
|
+
if action.is_input:
|
|
85
|
+
return TerminalObservation.from_text(
|
|
86
|
+
"[input unsupported — stateless sandbox]",
|
|
87
|
+
command=action.command, exit_code=None, timeout=False,
|
|
88
|
+
metadata=CmdOutputMetadata(exit_code=-1))
|
|
89
|
+
|
|
90
|
+
request = SandboxExecutionRequest(
|
|
91
|
+
argv=command_argv(action.command), cwd=self._workspace,
|
|
92
|
+
env=self._env, timeout=action.timeout,
|
|
93
|
+
pipeline=self._pipeline, step=self._step)
|
|
94
|
+
result = self._sandbox.run(request)
|
|
95
|
+
output = result.stdout + result.stderr
|
|
96
|
+
return TerminalObservation.from_text(
|
|
97
|
+
output, is_error=not result.succeeded and not result.is_timeout,
|
|
98
|
+
command=action.command, exit_code=result.exit_code,
|
|
99
|
+
timeout=result.timed_out,
|
|
100
|
+
metadata=CmdOutputMetadata(
|
|
101
|
+
exit_code=result.exit_code if result.exit_code is not None else -1,
|
|
102
|
+
working_dir=self._workspace))
|
|
103
|
+
|
|
104
|
+
def close(self) -> None:
|
|
105
|
+
pass # the sandbox is closed by Runtime; idempotent
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def register_sandbox_terminal(sandbox: "Sandbox", *, env: dict[str, str | None] | None = None,
|
|
109
|
+
workspace: str = "", pipeline: str | None = None,
|
|
110
|
+
step: str | None = None) -> None:
|
|
111
|
+
"""Per-run re-registration of the `terminal` name (KD-4): a callable
|
|
112
|
+
factory that builds the SDK terminal with a `SandboxTerminalExecutor`.
|
|
113
|
+
Pipelines are sequential, so re-registering per agent step is safe."""
|
|
114
|
+
from openhands.sdk.tool import register_tool
|
|
115
|
+
|
|
116
|
+
resolved_env = resolve_sandbox_env(env)
|
|
117
|
+
|
|
118
|
+
def factory(conv_state, **params):
|
|
119
|
+
return TerminalTool.create(
|
|
120
|
+
conv_state,
|
|
121
|
+
executor=SandboxTerminalExecutor(
|
|
122
|
+
sandbox, workspace=workspace, env=resolved_env,
|
|
123
|
+
pipeline=pipeline, step=step))
|
|
124
|
+
|
|
125
|
+
register_tool("terminal", factory)
|