@stigmer/runner 3.40.0 → 3.41.0
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/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/sources/git.ts +14 -8
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
* a status the host could reach), and `goldens/file-review-capture.status.json`
|
|
28
28
|
* pins the whole ledger beside Cursor's.
|
|
29
29
|
*
|
|
30
|
+
* Golden hunk since #1443: the inline artifact's `createdAt` is t=1, the
|
|
31
|
+
* edit's finish, where it was t=2, the moment the publisher's upload
|
|
32
|
+
* returned (it reads the clock before its first await now).
|
|
33
|
+
*
|
|
30
34
|
* Parent phase rows exercised beyond the plain arms: a `local_path` workspace
|
|
31
35
|
* entry (mounted as-is in local mode, the agent rooted at the entry per
|
|
32
36
|
* `provisionWorkspace`); capture-mode derivation on a git tree; the baseline
|
package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
41
41
|
"sizeBytes": "22",
|
|
42
42
|
"storageKey": "artifacts/aex_hermetic_0001/notes.md",
|
|
43
|
-
"createdAt": "2026-01-01T00:00:
|
|
43
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
44
44
|
"contentHash": "e4cdd09e8ca79e376a6ec5766d190754bc8ee4a2d6c5c7f4d86a9dc7fdf25a0f"
|
|
45
45
|
}
|
|
46
46
|
],
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
58
58
|
"sizeBytes": "32",
|
|
59
59
|
"storageKey": "artifacts/aex_hermetic_0001/report.md",
|
|
60
|
-
"createdAt": "2026-01-01T00:00:
|
|
60
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
61
61
|
"contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
|
|
62
62
|
}
|
|
63
63
|
],
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"kind": "EXECUTION_ARTIFACT_KIND_FILE",
|
|
68
68
|
"sizeBytes": "32",
|
|
69
69
|
"storageKey": "artifacts/aex_hermetic_0001/report.md",
|
|
70
|
-
"createdAt": "2026-01-01T00:00:
|
|
70
|
+
"createdAt": "2026-01-01T00:00:01.000Z",
|
|
71
71
|
"contentHash": "62eae4d09b2b051cf4ac5aa1fd70847b34c47994a1732ff999f47749e71b0441"
|
|
72
72
|
}
|
|
73
73
|
],
|
|
@@ -200,89 +200,24 @@
|
|
|
200
200
|
}
|
|
201
201
|
]
|
|
202
202
|
},
|
|
203
|
-
{
|
|
204
|
-
"type": "MESSAGE_AI",
|
|
205
|
-
"content": "Reading file 10.",
|
|
206
|
-
"timestamp": "2026-01-01T00:00:11.000Z",
|
|
207
|
-
"toolCalls": [
|
|
208
|
-
{
|
|
209
|
-
"id": "call-hermetic-loop-0010",
|
|
210
|
-
"name": "read_file",
|
|
211
|
-
"args": {
|
|
212
|
-
"file_path": "/f10.md"
|
|
213
|
-
},
|
|
214
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f10.md' not found\"}]",
|
|
215
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
216
|
-
"startedAt": "2026-01-01T00:00:11.000Z",
|
|
217
|
-
"completedAt": "2026-01-01T00:00:11.000Z",
|
|
218
|
-
"argsPreview": "{\"file_path\":\"/f10.md\"}",
|
|
219
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
220
|
-
}
|
|
221
|
-
]
|
|
222
|
-
},
|
|
223
|
-
{
|
|
224
|
-
"type": "MESSAGE_AI",
|
|
225
|
-
"content": "Reading file 11.",
|
|
226
|
-
"timestamp": "2026-01-01T00:00:12.000Z",
|
|
227
|
-
"toolCalls": [
|
|
228
|
-
{
|
|
229
|
-
"id": "call-hermetic-loop-0011",
|
|
230
|
-
"name": "read_file",
|
|
231
|
-
"args": {
|
|
232
|
-
"file_path": "/f11.md"
|
|
233
|
-
},
|
|
234
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f11.md' not found\"}]",
|
|
235
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
236
|
-
"startedAt": "2026-01-01T00:00:12.000Z",
|
|
237
|
-
"completedAt": "2026-01-01T00:00:12.000Z",
|
|
238
|
-
"argsPreview": "{\"file_path\":\"/f11.md\"}",
|
|
239
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
240
|
-
}
|
|
241
|
-
]
|
|
242
|
-
},
|
|
243
|
-
{
|
|
244
|
-
"type": "MESSAGE_AI",
|
|
245
|
-
"content": "Reading file 12.",
|
|
246
|
-
"timestamp": "2026-01-01T00:00:13.000Z",
|
|
247
|
-
"toolCalls": [
|
|
248
|
-
{
|
|
249
|
-
"id": "call-hermetic-loop-0012",
|
|
250
|
-
"name": "read_file",
|
|
251
|
-
"args": {
|
|
252
|
-
"file_path": "/f12.md"
|
|
253
|
-
},
|
|
254
|
-
"result": "[{\"type\":\"text\",\"text\":\"Error: File '/f12.md' not found\"}]",
|
|
255
|
-
"status": "TOOL_CALL_COMPLETED",
|
|
256
|
-
"startedAt": "2026-01-01T00:00:13.000Z",
|
|
257
|
-
"completedAt": "2026-01-01T00:00:13.000Z",
|
|
258
|
-
"argsPreview": "{\"file_path\":\"/f12.md\"}",
|
|
259
|
-
"toolKind": "TOOL_KIND_FILE_READ"
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
"type": "MESSAGE_AI",
|
|
265
|
-
"content": "Reading file 13.",
|
|
266
|
-
"timestamp": "2026-01-01T00:00:14.000Z"
|
|
267
|
-
},
|
|
268
203
|
{
|
|
269
204
|
"type": "MESSAGE_SYSTEM",
|
|
270
205
|
"content": "The agent reached the tool-call limit for this message. Work completed so far has been saved. Send another message to continue where the agent left off.",
|
|
271
|
-
"timestamp": "2026-01-01T00:00:
|
|
206
|
+
"timestamp": "2026-01-01T00:00:10.000Z"
|
|
272
207
|
}
|
|
273
208
|
],
|
|
274
209
|
"phase": "EXECUTION_TERMINATED",
|
|
275
210
|
"error": "Agent reached the tool-call limit for this message. Send another message to continue.",
|
|
276
211
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
277
|
-
"completedAt": "2026-01-01T00:00:
|
|
212
|
+
"completedAt": "2026-01-01T00:00:10.000Z",
|
|
278
213
|
"streamingUsage": {
|
|
279
|
-
"inputTokens": "
|
|
280
|
-
"outputTokens": "
|
|
281
|
-
"totalTokens": "
|
|
282
|
-
"turnCount":
|
|
283
|
-
"estimatedCostUsd": 0.
|
|
214
|
+
"inputTokens": "10000",
|
|
215
|
+
"outputTokens": "200",
|
|
216
|
+
"totalTokens": "10200",
|
|
217
|
+
"turnCount": 10,
|
|
218
|
+
"estimatedCostUsd": 0.011000000000000001,
|
|
284
219
|
"model": "claude-haiku-4.5",
|
|
285
|
-
"observedAt": "2026-01-01T00:00:
|
|
220
|
+
"observedAt": "2026-01-01T00:00:10.000Z",
|
|
286
221
|
"requestedServiceTier": "SERVICE_TIER_STANDARD",
|
|
287
222
|
"requestedThinkingMode": "THINKING_MODE_DISABLED"
|
|
288
223
|
},
|
package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
14
|
"phase": "EXECUTION_FAILED",
|
|
15
|
-
"error": "
|
|
15
|
+
"error": "[Error] hermetic fault injected at getAgent",
|
|
16
16
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
17
17
|
"completedAt": "2026-01-01T00:00:00.000Z"
|
|
18
18
|
}
|
package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts
CHANGED
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
* this pair of goldens (`inline-artifact.turn1`, `inline-artifact.turn2`) is
|
|
25
25
|
* what proves it did.
|
|
26
26
|
*
|
|
27
|
+
* Turn 2 completes, so its post-stream safety net walks the seeded history
|
|
28
|
+
* and asks for `report.md` again. The publisher re-reads it, finds the bytes
|
|
29
|
+
* the seeded row lists, and uploads nothing: the store sees turn 1's write
|
|
30
|
+
* only (stigmer/stigmer#1448).
|
|
31
|
+
*
|
|
27
32
|
* Also recorded (a shape fact, as found): the write's tool row
|
|
28
33
|
* shows `requiresApproval: true` with `approvalPolicySource` BUILTIN_CATEGORY
|
|
29
34
|
* — the category policy's verdict — while the row is COMPLETED because
|
|
@@ -50,6 +55,12 @@
|
|
|
50
55
|
* to persist "0 files" beside a one-file change set. The runtime now
|
|
51
56
|
* refreshes the strip on every write, the terminal one included.
|
|
52
57
|
*
|
|
58
|
+
* Golden hunk since #1443, in both goldens: the artifact's `createdAt` is
|
|
59
|
+
* t=1, the write's finish, where it was t=2. The publisher read the clock
|
|
60
|
+
* after its upload, which runs beside the stream, so the stamp landed on
|
|
61
|
+
* either side of the next model turn's tick and flaked under the full
|
|
62
|
+
* suite; it reads the clock before its first await now.
|
|
63
|
+
*
|
|
53
64
|
* Regenerate ONLY after a deliberate behavior change:
|
|
54
65
|
* npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
|
|
55
66
|
*/
|
|
@@ -88,6 +99,7 @@ import {
|
|
|
88
99
|
runDeepAgentTurn,
|
|
89
100
|
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
90
101
|
import { CLOSING_TURN, EXECUTE_CALL_A } from "../../__test-utils__/hitl-script.js";
|
|
102
|
+
import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
|
|
91
103
|
|
|
92
104
|
const REPORT = "report.md";
|
|
93
105
|
const REPORT_BODY = "# Report\n\nAll fixtures nominal.\n";
|
|
@@ -148,6 +160,7 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
|
|
|
148
160
|
expect(existsSync(join(env.artifactPath, run1.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
|
|
149
161
|
const writeRow = record.toolCalls().find((tc) => tc.id === WRITE_CALL_ID)!;
|
|
150
162
|
expect(writeRow.status, "capture mode let the write flow").toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
163
|
+
expect(run1.artifacts[0].createdAt, "the artifact is stamped at its write's finish").toBe(writeRow.completedAt);
|
|
151
164
|
expect(record.waitingToolCalls().map((tc) => tc.id)).toEqual([EXECUTE_CALL_A.id]);
|
|
152
165
|
await expect(statusJson(run1)).toMatchFileSnapshot("./goldens/inline-artifact.turn1.status.json");
|
|
153
166
|
|
|
@@ -155,13 +168,17 @@ describe("ExecuteDeepAgent hermetic — inline artifact across a gate (sqlite)",
|
|
|
155
168
|
expect(record.decideWaitingToolCalls(ApprovalAction.APPROVE, DECIDED_AT)).toBe(1);
|
|
156
169
|
|
|
157
170
|
// ── Turn 2 ───────────────────────────────────────────────────────────────
|
|
171
|
+
const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload");
|
|
158
172
|
const turn2 = await runDeepAgentTurn(scenario, { turnSeq: 1 });
|
|
173
|
+
const turn2Keys = uploads.mock.calls.map(([key]) => key);
|
|
174
|
+
uploads.mockRestore();
|
|
159
175
|
expect(turn2.outcome.kind).toBe("returned");
|
|
160
176
|
expect((turn2.outcome as { value: Record<string, unknown> }).value.phase).toBe("EXECUTION_COMPLETED");
|
|
161
177
|
const final = record.lastFullStatus!;
|
|
162
178
|
expect(final.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]), "the seed carried the artifact").toEqual(
|
|
163
179
|
run1.artifacts.map((a) => [a.name, a.storageKey, a.contentHash]),
|
|
164
180
|
);
|
|
181
|
+
expect(turn2Keys, "turn 2 does not upload the unchanged report again").not.toContain(run1.artifacts[0].storageKey);
|
|
165
182
|
expect(final.messages.flatMap((m) => m.toolCalls).map((tc) => [tc.id, tc.status])).toEqual([
|
|
166
183
|
[WRITE_CALL_ID, ToolCallStatus.TOOL_CALL_COMPLETED],
|
|
167
184
|
[EXECUTE_CALL_A.id, ToolCallStatus.TOOL_CALL_COMPLETED],
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
* model that keeps calling tools.
|
|
4
4
|
*
|
|
5
5
|
* What the arm does: `max_tool_rounds = 10` (the clamp floor,
|
|
6
|
-
* `shared/tool-rounds.ts` MIN_TOOL_ROUNDS)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
6
|
+
* `shared/tool-rounds.ts` MIN_TOOL_ROUNDS) is the execution budget
|
|
7
|
+
* middleware's round limit; the script proposes a DISTINCT `read_file` every
|
|
8
|
+
* round (distinct ids and paths, so the loop-detection middleware's
|
|
9
|
+
* "repetitive pattern" warning does not end the run first); after round 10's
|
|
10
|
+
* results the middleware throws `ToolRoundLimitError` in place of round 11's
|
|
11
|
+
* model call; the adapter's turn classifies it, through LangChain's
|
|
12
|
+
* `MiddlewareError` wrapping, as the `tool_call_limit` outcome
|
|
13
|
+
* (`shared/tool-rounds.ts` `isToolCallBudgetStop`), and the runtime's
|
|
14
|
+
* `toolCallLimitArm` writes
|
|
13
15
|
* TERMINATED with the cross-repo prefix `TOOL_CALL_LIMIT_ERROR_PREFIX`
|
|
14
16
|
* (matched with `startsWith` by stigmer-cloud's `reply-extractor.ts`) and
|
|
15
17
|
* its row, PERSISTS it, and returns the slim.
|
|
@@ -29,16 +31,26 @@
|
|
|
29
31
|
* empty message once and its `currentAiMessage[""]` then caught every later
|
|
30
32
|
* row.
|
|
31
33
|
*
|
|
32
|
-
* How many rounds
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
34
|
+
* How many rounds run is the knob's, exactly (#1113): TEN tool rows, on any
|
|
35
|
+
* middleware stack. Until #1113 the knob was LangGraph's recursion limit at
|
|
36
|
+
* 10 × 6 super-steps, and since LangChain makes every middleware hook its own
|
|
37
|
+
* graph node, the rows moved with the stack: 11 until #1096 deleted
|
|
38
|
+
* `graceful-stop.ts` (an `afterModel` node), 13 after it. #1113 moved this
|
|
39
|
+
* golden from 13 rows to 10, the copy unchanged. It also dropped the golden's
|
|
40
|
+
* last AI message, "Reading file 13.": the step limit had cut that fourteenth
|
|
41
|
+
* model call's proposal before its tool ran, so the transcript ended on a
|
|
42
|
+
* call that never executed. The round stop replaces the model call instead,
|
|
43
|
+
* so the transcript ends on round 10's results, and usage counts ten model
|
|
44
|
+
* calls, not fourteen.
|
|
45
|
+
*
|
|
46
|
+
* The second arm is the copy's promise, "send another message to continue":
|
|
47
|
+
* on the durable (`sqlite`) checkpointer, a follow-up message on the same
|
|
48
|
+
* thread starts from the checkpoint the stop left. The stop is a throw inside
|
|
49
|
+
* the model node rather than a limit between super-steps, so this is proven,
|
|
50
|
+
* not assumed: the follow-up's first model turn sees all ten rounds in its
|
|
51
|
+
* transcript (the scripted model indexes its script by the rounds the
|
|
52
|
+
* transcript holds), the run completes, and the budget is the new message's
|
|
53
|
+
* own.
|
|
42
54
|
*
|
|
43
55
|
* Regenerate ONLY after a deliberate behavior change:
|
|
44
56
|
* npx vitest run src/activities/execute-deep-agent/__tests__/hermetic -u
|
|
@@ -65,14 +77,25 @@ import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fix
|
|
|
65
77
|
import { MIN_TOOL_ROUNDS } from "../../../../shared/tool-rounds.js";
|
|
66
78
|
import {
|
|
67
79
|
beginDeepAgentScenario,
|
|
80
|
+
continueDeepAgentScenario,
|
|
68
81
|
deepAgentExecutionRecord,
|
|
69
82
|
runDeepAgentTurn,
|
|
70
83
|
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
84
|
+
import type { ScriptedTurn } from "../../__test-utils__/scripted-model.js";
|
|
71
85
|
import { TOOL_CALL_LIMIT_ERROR_PREFIX } from "../../../../shared/tool-rounds.js";
|
|
72
86
|
|
|
73
87
|
/** More rounds than the budget allows; the graph stops the script, not the other way round. */
|
|
74
88
|
const SCRIPTED_ROUNDS = 40;
|
|
75
89
|
|
|
90
|
+
/** The model that keeps calling tools: a distinct `read_file` per round. */
|
|
91
|
+
function readingRounds(): ScriptedTurn[] {
|
|
92
|
+
return Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
|
|
93
|
+
text: `Reading file ${i}.`,
|
|
94
|
+
toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
|
|
95
|
+
usage: { inputTokens: 1_000, outputTokens: 20 },
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
76
99
|
describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
77
100
|
let env: HermeticEnvironment;
|
|
78
101
|
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
@@ -97,13 +120,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
97
120
|
env,
|
|
98
121
|
clock,
|
|
99
122
|
record,
|
|
100
|
-
script: () => ({
|
|
101
|
-
turns: Array.from({ length: SCRIPTED_ROUNDS }, (_, i) => ({
|
|
102
|
-
text: `Reading file ${i}.`,
|
|
103
|
-
toolCalls: [{ id: `call-hermetic-loop-${String(i).padStart(4, "0")}`, name: "read_file", args: { file_path: `/f${i}.md` } }],
|
|
104
|
-
usage: { inputTokens: 1_000, outputTokens: 20 },
|
|
105
|
-
})),
|
|
106
|
-
}),
|
|
123
|
+
script: () => ({ turns: readingRounds() }),
|
|
107
124
|
});
|
|
108
125
|
|
|
109
126
|
// ── Act ──────────────────────────────────────────────────────────────────
|
|
@@ -125,8 +142,7 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
125
142
|
expect(persisted.error, "the event count left the copy").not.toMatch(/\d+ events?/);
|
|
126
143
|
expect(persisted.completedAt).not.toBe("");
|
|
127
144
|
const rows = record.toolCalls();
|
|
128
|
-
expect(rows.length, "
|
|
129
|
-
expect(rows.length).toBeGreaterThanOrEqual(MIN_TOOL_ROUNDS);
|
|
145
|
+
expect(rows.length, "exactly max_tool_rounds rounds ran: the budget counts rounds, not graph steps").toBe(MIN_TOOL_ROUNDS);
|
|
130
146
|
// The message boundary: every row sits on the message whose text proposed it
|
|
131
147
|
// — one "Reading file N." message per row, no empty host (before #1097 all
|
|
132
148
|
// thirteen rows sat on ONE empty message).
|
|
@@ -144,4 +160,48 @@ describe("ExecuteDeepAgent hermetic — tool-call budget exhausted", () => {
|
|
|
144
160
|
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, persisted), null, 2) + "\n";
|
|
145
161
|
await expect(json).toMatchFileSnapshot("./goldens/recursion-limit.persisted.status.json");
|
|
146
162
|
});
|
|
163
|
+
|
|
164
|
+
it("continues on the next message from the checkpoint the stop left (sqlite)", async () => {
|
|
165
|
+
// ── Arrange: the first message exhausts its budget ───────────────────────
|
|
166
|
+
const first = beginDeepAgentScenario({
|
|
167
|
+
env,
|
|
168
|
+
clock,
|
|
169
|
+
record: deepAgentExecutionRecord({ message: "Keep reading files.", maxToolRounds: MIN_TOOL_ROUNDS }),
|
|
170
|
+
checkpointer: "sqlite",
|
|
171
|
+
script: () => ({ turns: readingRounds() }),
|
|
172
|
+
});
|
|
173
|
+
await runDeepAgentTurn(first);
|
|
174
|
+
expect(first.record.lastFullStatus!.phase).toBe(ExecutionPhase.EXECUTION_TERMINATED);
|
|
175
|
+
expect(first.record.toolCalls()).toHaveLength(MIN_TOOL_ROUNDS);
|
|
176
|
+
|
|
177
|
+
// The follow-up's script is indexed by the rounds its transcript holds: a
|
|
178
|
+
// model that saw the ten rounds answers in text; one that lost them would
|
|
179
|
+
// play turn 0 and read a file.
|
|
180
|
+
const LOST_THREAD: ScriptedTurn = {
|
|
181
|
+
text: "Starting over.",
|
|
182
|
+
toolCalls: [{ id: "call-hermetic-lost-thread", name: "read_file", args: { file_path: "/lost.md" } }],
|
|
183
|
+
};
|
|
184
|
+
const PICKED_UP: ScriptedTurn = { text: "Picking up where I left off: all ten files are read." };
|
|
185
|
+
const roundsSeen: number[] = [];
|
|
186
|
+
const record = deepAgentExecutionRecord({ message: "Continue.", maxToolRounds: MIN_TOOL_ROUNDS });
|
|
187
|
+
const next = continueDeepAgentScenario(first, {
|
|
188
|
+
record,
|
|
189
|
+
script: () => ({ turns: [...Array.from({ length: MIN_TOOL_ROUNDS }, () => LOST_THREAD), PICKED_UP] }),
|
|
190
|
+
onTurn: (info) => {
|
|
191
|
+
roundsSeen.push(info.round);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
196
|
+
const invocation = await runDeepAgentTurn(next);
|
|
197
|
+
|
|
198
|
+
// ── Assert ───────────────────────────────────────────────────────────────
|
|
199
|
+
expect(invocation.outcome.kind).toBe("returned");
|
|
200
|
+
expect(roundsSeen, "the follow-up's one model turn saw the ten rounds the stop checkpointed").toEqual([MIN_TOOL_ROUNDS]);
|
|
201
|
+
expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS, ExecutionPhase.EXECUTION_COMPLETED]);
|
|
202
|
+
const persisted = record.lastFullStatus!;
|
|
203
|
+
expect(persisted.error).toBe("");
|
|
204
|
+
const answer = persisted.messages.filter((m) => m.type === MessageType.MESSAGE_AI).at(-1);
|
|
205
|
+
expect(answer?.content).toBe(PICKED_UP.text);
|
|
206
|
+
});
|
|
147
207
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Invariant pinned: the runtime's blueprint phase throws, the runtime's
|
|
6
6
|
* catch takes its generic-error arm (`terminal-table.ts` `unexpectedErrorArm`),
|
|
7
7
|
* persists ONE EXECUTION_FAILED status carrying
|
|
8
|
-
* `status.error = "
|
|
8
|
+
* `status.error = "[Error] <message>"` and two system rows
|
|
9
9
|
* — the boilerplate "Internal system error occurred. Please contact support
|
|
10
10
|
* if this issue persists." and `"Error details: [Error] <message>"` — and
|
|
11
11
|
* RETURNS the slim status (the workflow reads the phase; a throw would make
|
|
@@ -14,10 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* The row shape is one of the alignments #1096 made on purpose: the
|
|
16
16
|
* orchestrator wrote one row `"Error: [Error] <message>"`; the runtime's arm
|
|
17
|
-
* writes the boilerplate plus the details row.
|
|
18
|
-
* not move (the conformance suite pins it). The setup labels are the
|
|
17
|
+
* writes the boilerplate plus the details row. The setup labels are the
|
|
19
18
|
* runtime's resolution phases'.
|
|
20
19
|
*
|
|
20
|
+
* Golden hunk since stigmer#1122: `status.error` is `[Error] <message>`, the
|
|
21
|
+
* details row's text, where it was `Execution failed: [Error] <message>`.
|
|
22
|
+
* This arm alone framed the error; every other FAILED arm, and this
|
|
23
|
+
* harness's own thrown-turn classification, writes the failure unframed.
|
|
24
|
+
*
|
|
21
25
|
* Carried from `index.test.ts` ("returns EXECUTION_FAILED status when setup
|
|
22
26
|
* fails", "includes error message in failed status", "always returns a
|
|
23
27
|
* serializable result").
|
|
@@ -70,7 +74,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
|
|
|
70
74
|
env.dispose();
|
|
71
75
|
});
|
|
72
76
|
|
|
73
|
-
it("fails with the fault's message in status.error and
|
|
77
|
+
it("fails with the fault's message in status.error and two system rows, and returns", async () => {
|
|
74
78
|
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
75
79
|
const record = deepAgentExecutionRecord({ message: "Never reaches a model." });
|
|
76
80
|
const scenario = beginDeepAgentScenario({
|
|
@@ -101,7 +105,7 @@ describe("ExecuteDeepAgent hermetic — resolution error", () => {
|
|
|
101
105
|
|
|
102
106
|
// ── Assert: the copy ─────────────────────────────────────────────────────
|
|
103
107
|
const final = record.lastFullStatus!;
|
|
104
|
-
expect(final.error).toBe(`
|
|
108
|
+
expect(final.error).toBe(`[Error] ${CONTROL_PLANE_FAULT}`);
|
|
105
109
|
expect(final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content)).toEqual([
|
|
106
110
|
"Internal system error occurred. Please contact support if this issue persists.",
|
|
107
111
|
`Error details: [Error] ${CONTROL_PLANE_FAULT}`,
|
package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic: a THROWN TURN still carries every artifact it published — the
|
|
3
|
+
* native harness drains its in-flight inline publishes before the runtime
|
|
4
|
+
* writes the terminal status (stigmer#1116).
|
|
5
|
+
*
|
|
6
|
+
* The model writes a report in round one (`write_file`), and
|
|
7
|
+
* `streaming-side-effects.ts` hands the path to `InlinePublisher`, whose
|
|
8
|
+
* upload runs beside the stream. Round two's model call fails mid-stream
|
|
9
|
+
* (the scripted model's `fail` ending: a provider error), so the turn never
|
|
10
|
+
* reaches its settle — the one place that used to drain the publishes. The
|
|
11
|
+
* adapter classifies the throw as `failed`, and the runtime writes the
|
|
12
|
+
* turn's terminal status as soon as the adapter returns. The report is an
|
|
13
|
+
* edit on the tree, and the hermetic environment runs in capture mode (it
|
|
14
|
+
* carries artifact storage, as every hosted runner does), so that status is
|
|
15
|
+
* the file review's WAITING_FOR_APPROVAL with the failure's rows riding it
|
|
16
|
+
* and its phase deferred (`terminal-table.ts` `awaitingReviewArm`): the
|
|
17
|
+
* production shape of this fault.
|
|
18
|
+
*
|
|
19
|
+
* The order is made deterministic, not left to I/O timing: the upload is
|
|
20
|
+
* held until the turn asks for its publishes to drain, or until the
|
|
21
|
+
* activity has returned, whichever comes first. With the drain on the
|
|
22
|
+
* turn's every exit, the drain asks first and the artifact is on the
|
|
23
|
+
* persisted status. Without it the activity returns first, the upload
|
|
24
|
+
* lands afterwards on a status nobody persists again, and the assertion on
|
|
25
|
+
* the persisted artifacts fails — the defect this pins.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { existsSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
|
31
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
32
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
33
|
+
|
|
34
|
+
vi.mock("../../../../shared/model-client.js", async () =>
|
|
35
|
+
(await import("../../__test-utils__/scripted-model-module.js")).scriptedModelClientModule(),
|
|
36
|
+
);
|
|
37
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
38
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
import {
|
|
42
|
+
ScriptedClock,
|
|
43
|
+
createHermeticEnvironment,
|
|
44
|
+
type HermeticEnvironment,
|
|
45
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
46
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
47
|
+
import {
|
|
48
|
+
FIXTURE,
|
|
49
|
+
beginDeepAgentScenario,
|
|
50
|
+
deepAgentExecutionRecord,
|
|
51
|
+
runDeepAgentTurn,
|
|
52
|
+
} from "../../__test-utils__/hermetic-deep-agent.js";
|
|
53
|
+
import { LocalArtifactStorage } from "../../../../shared/artifact-storage.js";
|
|
54
|
+
import { StreamingSideEffects } from "../../streaming-side-effects.js";
|
|
55
|
+
import { TERMINAL_COPY } from "../../../../harness/terminal-table.js";
|
|
56
|
+
|
|
57
|
+
const REPORT = "report.md";
|
|
58
|
+
const REPORT_BODY = "# Report\n\nWritten before the provider failed.\n";
|
|
59
|
+
const WRITE_CALL_ID = "call-hermetic-write-0001";
|
|
60
|
+
const PROVIDER_FAULT = "provider stream failed mid-turn";
|
|
61
|
+
|
|
62
|
+
function systemMessages(status: AgentExecutionStatus): string[] {
|
|
63
|
+
return status.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("ExecuteDeepAgent hermetic — a thrown turn drains its inline publishes", () => {
|
|
67
|
+
let env: HermeticEnvironment;
|
|
68
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
69
|
+
const clock = new ScriptedClock();
|
|
70
|
+
|
|
71
|
+
beforeAll(() => {
|
|
72
|
+
env = createHermeticEnvironment();
|
|
73
|
+
registry = stubRegistryFetch();
|
|
74
|
+
clock.install();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
vi.restoreAllMocks();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterAll(() => {
|
|
82
|
+
clock.uninstall();
|
|
83
|
+
registry.restore();
|
|
84
|
+
env.dispose();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("persists the turn's end with the artifact it published before the model call threw", async () => {
|
|
88
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
89
|
+
const record = deepAgentExecutionRecord({ message: "Write the report." });
|
|
90
|
+
const scenario = beginDeepAgentScenario({
|
|
91
|
+
env,
|
|
92
|
+
clock,
|
|
93
|
+
record,
|
|
94
|
+
script: () => ({
|
|
95
|
+
turns: [
|
|
96
|
+
{
|
|
97
|
+
text: "Writing the report.",
|
|
98
|
+
toolCalls: [{ id: WRITE_CALL_ID, name: "write_file", args: { file_path: `/${REPORT}`, content: REPORT_BODY } }],
|
|
99
|
+
usage: { inputTokens: 1_300, outputTokens: 60 },
|
|
100
|
+
},
|
|
101
|
+
{ text: "Now summarising", ends: { kind: "fail", error: new Error(PROVIDER_FAULT) } },
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// The artifact's upload waits on `held`; the first of "the turn drains"
|
|
107
|
+
// and "the activity returned" releases it (the header's deterministic
|
|
108
|
+
// order). Capture mode's own blobs share the store and are never held:
|
|
109
|
+
// the runtime writes them after the adapter returns.
|
|
110
|
+
const artifactKey = `artifacts/${FIXTURE.executionId}/${REPORT}`;
|
|
111
|
+
let release = (): void => undefined;
|
|
112
|
+
const held = new Promise<void>((resolve) => {
|
|
113
|
+
release = resolve;
|
|
114
|
+
});
|
|
115
|
+
const upload = LocalArtifactStorage.prototype.upload;
|
|
116
|
+
const uploads = vi.spyOn(LocalArtifactStorage.prototype, "upload").mockImplementation(async function (
|
|
117
|
+
this: LocalArtifactStorage,
|
|
118
|
+
...args: Parameters<LocalArtifactStorage["upload"]>
|
|
119
|
+
) {
|
|
120
|
+
if (args[0] === artifactKey) await held;
|
|
121
|
+
return upload.apply(this, args);
|
|
122
|
+
});
|
|
123
|
+
const drain = StreamingSideEffects.prototype.drainPublishes;
|
|
124
|
+
const drains = vi.spyOn(StreamingSideEffects.prototype, "drainPublishes").mockImplementation(async function (
|
|
125
|
+
this: StreamingSideEffects,
|
|
126
|
+
) {
|
|
127
|
+
release();
|
|
128
|
+
return drain.apply(this);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
132
|
+
const invocation = await runDeepAgentTurn(scenario);
|
|
133
|
+
release();
|
|
134
|
+
|
|
135
|
+
// ── Assert: the turn failed, deferred behind the review, and returned ────
|
|
136
|
+
expect(invocation.outcome.kind, "a provider failure RETURNS the status").toBe("returned");
|
|
137
|
+
const final = record.lastFullStatus!;
|
|
138
|
+
expect(final.phase, "the edit reaches review before the failure settles").toBe(
|
|
139
|
+
ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
140
|
+
);
|
|
141
|
+
expect(final.error, "a deferred terminal writes no error yet").toBe("");
|
|
142
|
+
expect(systemMessages(final), "the failure's rows ride the review").toEqual([
|
|
143
|
+
TERMINAL_COPY.internalFailure.row,
|
|
144
|
+
`Error details: [Error] ${PROVIDER_FAULT}`,
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
// ── Assert: the artifact reached the terminal status ─────────────────────
|
|
148
|
+
expect(
|
|
149
|
+
uploads.mock.calls.map(([key]) => key).filter((key) => key === artifactKey),
|
|
150
|
+
"the write was published inline, once",
|
|
151
|
+
).toHaveLength(1);
|
|
152
|
+
expect(final.artifacts.map((a) => [a.name, a.storageKey])).toEqual([[REPORT, artifactKey]]);
|
|
153
|
+
expect(drains, "the thrown turn drained its publishes").toHaveBeenCalled();
|
|
154
|
+
expect(existsSync(join(env.artifactPath, final.artifacts[0].storageKey)), "the bytes reached the store").toBe(true);
|
|
155
|
+
|
|
156
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
157
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
});
|