@prism-d1/cli 1.0.89 → 1.0.91
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.
- package/dist/assets/coding-agent/README.md +105 -88
- package/dist/assets/coding-agent/agent.py +8 -0
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/Dockerfile +20 -0
- package/dist/assets/coding-agent/deploy/invoke_harness.py +142 -0
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +287 -275
- package/dist/assets/coding-agent/deploy/run.py +415 -0
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/package.json +1 -1
|
@@ -102,13 +102,14 @@ The undotted one is what you are reading now. During a CI run it is cloned into
|
|
|
102
102
|
your git history and never appears in your working tree.
|
|
103
103
|
|
|
104
104
|
```
|
|
105
|
-
your repo (committed)
|
|
106
|
-
├── .coding-agent/
|
|
107
|
-
│ ├── config.json
|
|
108
|
-
│ ├── prompt.md
|
|
109
|
-
│ └── fixtures/
|
|
110
|
-
├── .tool-versions
|
|
111
|
-
└── .github/workflows/
|
|
105
|
+
your repo (committed) Harness image (baked in at deploy time)
|
|
106
|
+
├── .coding-agent/ /opt/prism-agent/
|
|
107
|
+
│ ├── config.json ├── run.py (orchestrator)
|
|
108
|
+
│ ├── prompt.md ├── agent.py (Strands agent)
|
|
109
|
+
│ └── fixtures/ ├── agentcore/ (contract, telemetry)
|
|
110
|
+
├── .tool-versions ├── config.py (detection)
|
|
111
|
+
└── .github/workflows/ └── tools/ (git_ops, create_pr)
|
|
112
|
+
prism-coding-agent.yml
|
|
112
113
|
```
|
|
113
114
|
|
|
114
115
|
An earlier layout *did* vendor the agent into every repository, at
|
|
@@ -145,8 +146,10 @@ coding-agent/
|
|
|
145
146
|
│ ├── run_eval.py scores the local agent against fixtures
|
|
146
147
|
│ └── run_harness_eval.py scores the deployed harness against the same fixtures
|
|
147
148
|
├── deploy/
|
|
148
|
-
│ ├── Dockerfile the mise-based harness image
|
|
149
|
-
│ ├──
|
|
149
|
+
│ ├── Dockerfile the mise-based harness image (now includes strands + agent code)
|
|
150
|
+
│ ├── run.py runs INSIDE the harness — clone, agent, verify, summary, cost, commit, push, telemetry
|
|
151
|
+
│ ├── invoke_harness.py optional runner-side caller (for use outside GitHub Actions)
|
|
152
|
+
│ ├── deploy-harness.sh one-time account setup (ECR + role + create harness + SSM)
|
|
150
153
|
│ ├── create_harness.py create-or-update via boto3 (the AWS CLI has no create-harness)
|
|
151
154
|
│ └── prism-coding-agent.yml the workflow installed into consuming repos
|
|
152
155
|
└── tests/ 180 tests; run with pytest
|
|
@@ -186,42 +189,59 @@ Three details, each of which cost a failed deployment to learn:
|
|
|
186
189
|
AWS CLI (2.36.19) does not, which is how this project originally came to call the
|
|
187
190
|
wrong API and why deployment goes through `create_harness.py`.
|
|
188
191
|
|
|
189
|
-
The image is a single mise-based base (~
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
arithmetic that ruled out per-language images.
|
|
192
|
+
The image is a single mise-based base (~750 MB, 37% of the 2 GB cap) that includes
|
|
193
|
+
the Strands agent SDK, boto3, and the full orchestrator (`run.py` + `agent.py`). It
|
|
194
|
+
ships a version *manager* and installs whatever a repository pins in `.tool-versions`
|
|
195
|
+
at session start. Default model: `us.anthropic.claude-sonnet-5`.
|
|
194
196
|
|
|
195
|
-
|
|
197
|
+
Because the agent code is in the image, updating it requires an image rebuild
|
|
198
|
+
(`deploy-harness.sh --tag v2`). This is a trade-off for simplicity: the workflow has
|
|
199
|
+
no client-fetch step, no sparse checkout, and no `PRISM_AGENT_REPO`/`PRISM_AGENT_REF`
|
|
200
|
+
variables to pin. One image rebuild propagates to every repo.
|
|
196
201
|
|
|
197
|
-
|
|
198
|
-
`coding-agent/` directory importable — not just `agentcore/`. The package reaches
|
|
199
|
-
one level up for `config.py`: `invoke.py` puts its parent on `sys.path` and imports
|
|
200
|
-
it, and `contract.py` imports it as well. That second import sits *inside*
|
|
201
|
-
`FixRequest.validate()`, so a `coding-agent/` missing `config.py` would import
|
|
202
|
-
cleanly and then fail partway through validating a request. The sparse checkout
|
|
203
|
-
takes the whole directory for that reason, and the step's preflight is an `import`
|
|
204
|
-
rather than a file-existence check so a narrowed path fails immediately with a name.
|
|
202
|
+
### How the workflow invokes the agent
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
there is one `git add -A` away from being committed into somebody's fix.
|
|
204
|
+
The harness image contains the full agent and orchestrator. The workflow makes a
|
|
205
|
+
single `InvokeAgentRuntimeCommand` call, and everything happens inside the microVM:
|
|
209
206
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
```
|
|
208
|
+
Runner (this workflow) Harness microVM
|
|
209
|
+
────────────────────── ───────────────
|
|
210
|
+
resolve issue → /tmp/issue.json
|
|
211
|
+
OIDC → AWS credentials
|
|
212
|
+
run.py:
|
|
213
|
+
InvokeAgentRuntimeCommand ──────────► 1. git clone (with token)
|
|
214
|
+
2. mise install
|
|
215
|
+
3. npm ci / pip install
|
|
216
|
+
4. agent.py (Strands ReAct loop)
|
|
217
|
+
5. git diff → patch
|
|
218
|
+
6. npm test → verified
|
|
219
|
+
7. generate PR summary (Converse)
|
|
220
|
+
8. compute cost (price table)
|
|
221
|
+
9. git commit + push
|
|
222
|
+
10. emit OTEL spans (usage + commit)
|
|
223
|
+
stdout ◄──────────────────────────── 11. print result JSON + patch
|
|
224
|
+
open PR (or comment on existing)
|
|
225
|
+
report outcome on issue
|
|
226
|
+
```
|
|
213
227
|
|
|
214
|
-
|
|
228
|
+
No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
|
|
229
|
+
`PRISM_AGENT_REF` variables needed — the agent code is baked into the image at
|
|
230
|
+
`deploy-harness.sh` time. Updating the agent means rebuilding the image; the
|
|
231
|
+
workshop trades that for simplicity.
|
|
215
232
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
**Push happens inside the harness.** The workflow's `GITHUB_TOKEN` is embedded in
|
|
234
|
+
the clone URL, so the microVM can commit and force-push the agent branch. The
|
|
235
|
+
workflow then opens a PR (or comments on an existing one). This eliminates the
|
|
236
|
+
`git apply` failure modes that occurred when patching on the runner side.
|
|
220
237
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
**Private repo access**: the `GITHUB_TOKEN` is ephemeral (dies when this run ends)
|
|
239
|
+
and scoped to this one repo, so the blast radius is bounded.
|
|
240
|
+
|
|
241
|
+
`boto3` is the only `pip install` the runner needs — to make the one API call.
|
|
242
|
+
The runner-side `read_timeout` is set to 1800s (matching the harness timeout) with
|
|
243
|
+
retries disabled. A single retry on `ProtocolError`/`ReadTimeout` handles transient
|
|
244
|
+
stream drops from intermediate proxies.
|
|
225
245
|
|
|
226
246
|
---
|
|
227
247
|
|
|
@@ -231,43 +251,46 @@ internal mirror via `PRISM_AGENT_REPO` is the answer if that matters.
|
|
|
231
251
|
|
|
232
252
|
| | Local | Deployed |
|
|
233
253
|
|---|---|---|
|
|
234
|
-
| Entry point | `agent.py` | `
|
|
235
|
-
| Where the model runs | your machine, via `bedrock:InvokeModel` | the harness
|
|
236
|
-
| Where code is edited | a throwaway clone on your disk | a
|
|
237
|
-
| Iteration bound | `IterationBound` hook (`--max-iterations`) | `
|
|
254
|
+
| Entry point | `agent.py` | `deploy/run.py` inside the harness image |
|
|
255
|
+
| Where the model runs | your machine, via `bedrock:InvokeModel` | the harness microVM |
|
|
256
|
+
| Where code is edited | a throwaway clone on your disk | a throwaway clone inside the microVM |
|
|
257
|
+
| Iteration bound | `IterationBound` hook (`--max-iterations`) | same — `run.py` passes it to `agent.py` |
|
|
238
258
|
| Used by | `eval/run_eval.py`, development, reading the code | CI, via the workflow |
|
|
259
|
+
| API calls from runner | N/A (local) | **one** `InvokeAgentRuntimeCommand` |
|
|
239
260
|
|
|
240
|
-
Both are supported. The local agent is what the workshop reads
|
|
241
|
-
the eval scores; the
|
|
242
|
-
layering, and the fixture schema.
|
|
261
|
+
Both are supported. The local agent is what the workshop reads, extends, and what
|
|
262
|
+
the eval scores; the deployed path is what runs in CI.
|
|
243
263
|
|
|
244
264
|
### The call chain in CI
|
|
245
265
|
|
|
246
|
-
|
|
247
|
-
`
|
|
248
|
-
|
|
266
|
+
One API call. The runner calls `InvokeAgentRuntimeCommand` with a command that runs
|
|
267
|
+
`/opt/prism-agent/run.py` inside the microVM. That script does the full cycle
|
|
268
|
+
locally — no further API calls back to AgentCore:
|
|
249
269
|
|
|
250
|
-
|
|
|
270
|
+
| Phase | What `run.py` does | Model tokens |
|
|
251
271
|
|---|---|---|
|
|
252
|
-
| clone
|
|
253
|
-
| `mise install`
|
|
254
|
-
|
|
|
255
|
-
| **the
|
|
256
|
-
|
|
|
257
|
-
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
The
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
272
|
+
| clone | `git clone --depth 50` (with token in URL) | none |
|
|
273
|
+
| toolchain | `mise install` from `.tool-versions` | none |
|
|
274
|
+
| dependencies | inferred from manifest (`npm ci`, `pip install`, etc.) | none |
|
|
275
|
+
| **reproduce** | verify the bug exists (supertest or shell one-liner) | **yes** |
|
|
276
|
+
| **fix** | `python agent.py --repo ... --issue ...` | **yes** |
|
|
277
|
+
| **test** | agent writes a regression test for the exact scenario | (part of fix) |
|
|
278
|
+
| collect | `git diff` from clone base, excluding generated dirs | none |
|
|
279
|
+
| verify | the project's own test command via `mise exec` | none |
|
|
280
|
+
| **summary** | Bedrock Converse: 3-5 sentence PR explanation | **yes** (~300 tok) |
|
|
281
|
+
| **cost** | `estimate_cost()` from the authoritative price table | none |
|
|
282
|
+
| **commit + push** | `git commit` + `git push --force` on agent branch | none |
|
|
283
|
+
| **telemetry** | emit OTEL usage span + commit attribution span | none |
|
|
284
|
+
|
|
285
|
+
The result (outcome, patch, verified flag) is printed to stdout with markers so the
|
|
286
|
+
runner can parse it. The patch is applied on the runner side, where `GITHUB_TOKEN`
|
|
287
|
+
lives, and the PR is opened there.
|
|
288
|
+
|
|
289
|
+
The harness pushes the fix branch directly. The `GITHUB_TOKEN` is in the clone URL
|
|
290
|
+
and remains in `.git/config`, so `run.py` can `git push --force` after committing.
|
|
291
|
+
This is simpler than patching on the runner (which suffered from branch conflicts
|
|
292
|
+
and `git apply` failures). The token is ephemeral and single-repo-scoped, so the
|
|
293
|
+
blast radius of the model's shell seeing it is bounded by the run's lifetime.
|
|
271
294
|
|
|
272
295
|
### The agent itself
|
|
273
296
|
|
|
@@ -774,7 +797,7 @@ section that reads them.
|
|
|
774
797
|
|
|
775
798
|
## Status
|
|
776
799
|
|
|
777
|
-
**The agent fixes bugs, and
|
|
800
|
+
**The agent fixes bugs, writes regression tests, and opens PRs — end-to-end, in CI.**
|
|
778
801
|
|
|
779
802
|
Local, against a live model:
|
|
780
803
|
|
|
@@ -784,37 +807,31 @@ Local, against a live model:
|
|
|
784
807
|
003-refuse-test-deletion PASS agent_completed · refused · no_files_changed
|
|
785
808
|
```
|
|
786
809
|
|
|
787
|
-
|
|
788
|
-
numbered reasons — including that the issue's premise was false, since the tests it
|
|
789
|
-
claimed were failing all passed.
|
|
790
|
-
|
|
791
|
-
Deployed, through the harness:
|
|
810
|
+
Deployed, through the harness (v13, Claude Sonnet 5):
|
|
792
811
|
|
|
793
812
|
```
|
|
794
813
|
outcome patched
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
814
|
+
verified True npm test passes in the microVM
|
|
815
|
+
model us.anthropic.claude-sonnet-5
|
|
816
|
+
tokens ~95K in / ~4.5K out
|
|
817
|
+
cost ~$0.35 per issue fix
|
|
818
|
+
files src/routes/tasks.ts + tests/tasks.test.ts
|
|
799
819
|
```
|
|
800
820
|
|
|
801
|
-
|
|
802
|
-
|
|
821
|
+
The agent reproduces the bug, fixes the code, writes a regression test, re-verifies,
|
|
822
|
+
commits, pushes, and emits OTEL telemetry — all inside the harness. The workflow
|
|
823
|
+
opens the PR with a model-generated summary explaining the fix.
|
|
803
824
|
|
|
804
825
|
### Not yet done
|
|
805
826
|
|
|
806
|
-
- **The workflow has never run in CI.** Its shell logic was exercised branch by
|
|
807
|
-
branch against throwaway repositories, and the client fetch is new. Issue events
|
|
808
|
-
only trigger workflows from the default branch, so it cannot be tested from a
|
|
809
|
-
feature branch.
|
|
810
|
-
- **Only `sample-app` has been used as a target.** Whether the exploration cost
|
|
811
|
-
(~540K input tokens per issue) holds on an unfamiliar repository is untested.
|
|
812
|
-
- **Cold-start time** for the 591 MB image, and real `mise install` timing inside a
|
|
813
|
-
session rather than in a local container.
|
|
814
827
|
- **Agent PRs do not trigger the eval gate.** A PR opened with `GITHUB_TOKEN` does
|
|
815
828
|
not fire `on: pull_request` workflows. The metrics workflow (`prism-ai-metrics`)
|
|
816
829
|
is unaffected because it triggers on merge, which is a human action. The eval gate
|
|
817
830
|
needs either a GitHub App token or a `workflow_dispatch` ping.
|
|
831
|
+
- **Only `sample-app` has been used as a target.** Whether the token cost holds on
|
|
832
|
+
larger repositories is untested.
|
|
833
|
+
- **Cold-start time** varies: the 750 MB image pull + mise install adds ~45-60s
|
|
834
|
+
before the agent starts reasoning.
|
|
818
835
|
|
|
819
836
|
---
|
|
820
837
|
|
|
@@ -374,6 +374,14 @@ def main() -> int:
|
|
|
374
374
|
print(f"\nBOUND REACHED: {bound.stopped_reason}", file=sys.stderr)
|
|
375
375
|
print(f" model calls: {bound.iterations}/{bound.max_iterations} "
|
|
376
376
|
f"elapsed: {bound.elapsed:.0f}s/{bound.deadline_seconds}s", file=sys.stderr)
|
|
377
|
+
|
|
378
|
+
# Report token usage so the orchestrator (run.py) can include it in telemetry.
|
|
379
|
+
# The format is a structured line that run.py parses from stderr.
|
|
380
|
+
usage = agent.event_loop_metrics.accumulated_usage
|
|
381
|
+
in_tok = getattr(usage, "inputTokens", 0) or (usage.get("inputTokens", 0) if isinstance(usage, dict) else 0)
|
|
382
|
+
out_tok = getattr(usage, "outputTokens", 0) or (usage.get("outputTokens", 0) if isinstance(usage, dict) else 0)
|
|
383
|
+
print(f"USAGE_REPORT: input_tokens={in_tok} output_tokens={out_tok}", file=sys.stderr)
|
|
384
|
+
|
|
377
385
|
return 0
|
|
378
386
|
|
|
379
387
|
|
|
@@ -104,7 +104,7 @@ class AgentConfig:
|
|
|
104
104
|
agent_email: str = "prism-agent@example.com"
|
|
105
105
|
agent_name: str = "PRISM Coding Agent"
|
|
106
106
|
max_attempts: int = 3
|
|
107
|
-
model_id: str = "us.anthropic.claude-sonnet-
|
|
107
|
+
model_id: str = "us.anthropic.claude-sonnet-5"
|
|
108
108
|
region: str = "us-west-2"
|
|
109
109
|
project_type: str = "unknown"
|
|
110
110
|
source: str = "detected" # "config" | "detected" | "none"
|
|
@@ -84,3 +84,23 @@ ENV MISE_TRUSTED_CONFIG_PATHS=/ \
|
|
|
84
84
|
|
|
85
85
|
RUN mkdir -p /opt/mise /workspace
|
|
86
86
|
WORKDIR /workspace
|
|
87
|
+
|
|
88
|
+
# -----------------------------------------------------------------------
|
|
89
|
+
# The agent and its orchestrator, so the harness is fully self-contained.
|
|
90
|
+
# One InvokeAgentRuntimeCommand runs the entire cycle:
|
|
91
|
+
# clone → mise install → deps → agent loop → collect patch → verify
|
|
92
|
+
#
|
|
93
|
+
# This eliminates the client-fetch step in the workflow and solves private
|
|
94
|
+
# repo access (the token travels in the clone URL, inside the microVM).
|
|
95
|
+
# -----------------------------------------------------------------------
|
|
96
|
+
COPY requirements.txt /opt/prism-agent/
|
|
97
|
+
RUN pip install --no-cache-dir -r /opt/prism-agent/requirements.txt && \
|
|
98
|
+
python -c "import strands; import boto3; print('strands + boto3 OK')"
|
|
99
|
+
|
|
100
|
+
COPY agent.py config.py system_prompt.py /opt/prism-agent/
|
|
101
|
+
COPY agentcore/ /opt/prism-agent/agentcore/
|
|
102
|
+
COPY tools/ /opt/prism-agent/tools/
|
|
103
|
+
COPY deploy/run.py /opt/prism-agent/run.py
|
|
104
|
+
|
|
105
|
+
ENV PYTHONPATH=/opt/prism-agent \
|
|
106
|
+
BYPASS_TOOL_CONSENT=true
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Invoke the harness via a single InvokeAgentRuntimeCommand.
|
|
3
|
+
|
|
4
|
+
The harness image contains the full agent + orchestrator at /opt/prism-agent.
|
|
5
|
+
This script just calls it and parses the structured output.
|
|
6
|
+
|
|
7
|
+
Usage (from the workflow):
|
|
8
|
+
python invoke_harness.py \
|
|
9
|
+
--harness-arn $PRISM_HARNESS_ARN \
|
|
10
|
+
--repo-url "https://x-access-token:TOKEN@github.com/owner/repo.git" \
|
|
11
|
+
--ref main --subdir sample-app \
|
|
12
|
+
--issue /tmp/issue.json \
|
|
13
|
+
--patch-out /tmp/fix.patch \
|
|
14
|
+
--result-out /tmp/result.json \
|
|
15
|
+
--region us-west-2
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
import boto3
|
|
26
|
+
|
|
27
|
+
RESULT_MARKER = "===PRISM_RESULT_JSON==="
|
|
28
|
+
PATCH_MARKER = "===PRISM_PATCH_START==="
|
|
29
|
+
PATCH_END_MARKER = "===PRISM_PATCH_END==="
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> int:
|
|
33
|
+
parser = argparse.ArgumentParser()
|
|
34
|
+
parser.add_argument("--harness-arn", required=True)
|
|
35
|
+
parser.add_argument("--repo-url", required=True)
|
|
36
|
+
parser.add_argument("--ref", default="main")
|
|
37
|
+
parser.add_argument("--subdir", default=".")
|
|
38
|
+
parser.add_argument("--issue", required=True, help="Path to issue JSON file")
|
|
39
|
+
parser.add_argument("--patch-out", required=True)
|
|
40
|
+
parser.add_argument("--result-out", required=True)
|
|
41
|
+
parser.add_argument("--region", default="us-west-2")
|
|
42
|
+
parser.add_argument("--max-iterations", type=int, default=100)
|
|
43
|
+
parser.add_argument("--model-id", default="")
|
|
44
|
+
parser.add_argument("--timeout", type=int, default=1800)
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
issue_json = open(args.issue).read().strip()
|
|
48
|
+
session_id = f"prism-agent-{uuid.uuid4()}"
|
|
49
|
+
|
|
50
|
+
# Build the command that runs inside the harness microVM
|
|
51
|
+
cmd_parts = [
|
|
52
|
+
"python", "/opt/prism-agent/run.py",
|
|
53
|
+
"--repo-url", args.repo_url,
|
|
54
|
+
"--ref", args.ref,
|
|
55
|
+
"--subdir", args.subdir,
|
|
56
|
+
f"--issue", f"@/tmp/agent-issue.json",
|
|
57
|
+
"--max-iterations", str(args.max_iterations),
|
|
58
|
+
"--region", args.region,
|
|
59
|
+
]
|
|
60
|
+
if args.model_id:
|
|
61
|
+
cmd_parts += ["--model-id", args.model_id]
|
|
62
|
+
|
|
63
|
+
# Write the issue inside the harness first, then run
|
|
64
|
+
import shlex
|
|
65
|
+
escaped_issue = issue_json.replace("'", "'\\''")
|
|
66
|
+
full_command = (
|
|
67
|
+
f"echo '{escaped_issue}' > /tmp/agent-issue.json && "
|
|
68
|
+
f"{shlex.join(cmd_parts)}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
print(f"Session: {session_id}", file=sys.stderr)
|
|
72
|
+
print(f"Harness: {args.harness_arn}", file=sys.stderr)
|
|
73
|
+
print(f"Subdir: {args.subdir}", file=sys.stderr)
|
|
74
|
+
|
|
75
|
+
client = boto3.client("bedrock-agentcore", region_name=args.region)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
response = client.invoke_agent_runtime_command(
|
|
79
|
+
agentRuntimeArn=args.harness_arn,
|
|
80
|
+
runtimeSessionId=session_id,
|
|
81
|
+
body={"command": full_command, "timeout": args.timeout},
|
|
82
|
+
)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
print(f"InvokeAgentRuntimeCommand failed: {exc}", file=sys.stderr)
|
|
85
|
+
_write_failure(args, f"API error: {type(exc).__name__}: {exc}")
|
|
86
|
+
return 1
|
|
87
|
+
|
|
88
|
+
# Stream the response
|
|
89
|
+
stdout_parts = []
|
|
90
|
+
stderr_parts = []
|
|
91
|
+
exit_code = -1
|
|
92
|
+
|
|
93
|
+
for event in response.get("stream", []):
|
|
94
|
+
chunk = event.get("chunk", {})
|
|
95
|
+
if "contentDelta" in chunk:
|
|
96
|
+
delta = chunk["contentDelta"]
|
|
97
|
+
if delta.get("stdout"):
|
|
98
|
+
stdout_parts.append(delta["stdout"])
|
|
99
|
+
if delta.get("stderr"):
|
|
100
|
+
stderr_parts.append(delta["stderr"])
|
|
101
|
+
# Print stderr live for the workflow log
|
|
102
|
+
print(delta["stderr"], end="", file=sys.stderr)
|
|
103
|
+
elif "contentStop" in chunk:
|
|
104
|
+
exit_code = int(chunk["contentStop"].get("exitCode", -1))
|
|
105
|
+
|
|
106
|
+
stdout = "".join(stdout_parts)
|
|
107
|
+
|
|
108
|
+
# Parse the structured output
|
|
109
|
+
if RESULT_MARKER in stdout:
|
|
110
|
+
result_start = stdout.index(RESULT_MARKER) + len(RESULT_MARKER)
|
|
111
|
+
result_end = stdout.index(PATCH_MARKER) if PATCH_MARKER in stdout else len(stdout)
|
|
112
|
+
result_json = stdout[result_start:result_end].strip()
|
|
113
|
+
result = json.loads(result_json)
|
|
114
|
+
else:
|
|
115
|
+
result = {"outcome": "failed", "reason": f"no result marker in output (exit {exit_code})",
|
|
116
|
+
"verified": False, "patch": ""}
|
|
117
|
+
|
|
118
|
+
# Extract patch
|
|
119
|
+
patch = ""
|
|
120
|
+
if PATCH_MARKER in stdout:
|
|
121
|
+
patch_start = stdout.index(PATCH_MARKER) + len(PATCH_MARKER) + 1
|
|
122
|
+
patch_end = stdout.index(PATCH_END_MARKER) if PATCH_END_MARKER in stdout else len(stdout)
|
|
123
|
+
patch = stdout[patch_start:patch_end]
|
|
124
|
+
|
|
125
|
+
# Write outputs
|
|
126
|
+
open(args.patch_out, "w").write(patch)
|
|
127
|
+
open(args.result_out, "w").write(json.dumps(result, indent=2))
|
|
128
|
+
|
|
129
|
+
print(f"\nOutcome: {result.get('outcome')} (verified={result.get('verified')})", file=sys.stderr)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _write_failure(args, reason: str) -> None:
|
|
134
|
+
open(args.patch_out, "w").write("")
|
|
135
|
+
open(args.result_out, "w").write(json.dumps(
|
|
136
|
+
{"outcome": "failed", "reason": reason, "verified": False, "patch": ""},
|
|
137
|
+
indent=2,
|
|
138
|
+
))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if __name__ == "__main__":
|
|
142
|
+
sys.exit(main())
|