@stigmer/runner 3.0.9-dev.20260616060535 → 3.1.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/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/approval-contract/types.d.ts +174 -0
- package/dist/__test-utils__/approval-contract/types.js +24 -0
- package/dist/__test-utils__/approval-contract/types.js.map +1 -0
- package/dist/activities/call-agent-status.d.ts +19 -1
- package/dist/activities/call-agent-status.js +64 -5
- package/dist/activities/call-agent-status.js.map +1 -1
- package/dist/activities/call-llm.js +19 -53
- package/dist/activities/call-llm.js.map +1 -1
- package/dist/activities/classify-tool-approvals.d.ts +24 -0
- package/dist/activities/classify-tool-approvals.js +69 -17
- package/dist/activities/classify-tool-approvals.js.map +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -0
- package/dist/activities/discover-mcp-server.js +11 -1
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +140 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +130 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.d.ts +18 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +123 -0
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-cursor/approval-policy.d.ts +17 -61
- package/dist/activities/execute-cursor/approval-policy.js +8 -120
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/approval-state.d.ts +219 -9
- package/dist/activities/execute-cursor/approval-state.js +233 -17
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +127 -0
- package/dist/activities/execute-cursor/capture-flow.js +234 -0
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -0
- package/dist/activities/execute-cursor/cas-observations.d.ts +96 -0
- package/dist/activities/execute-cursor/cas-observations.js +184 -0
- package/dist/activities/execute-cursor/cas-observations.js.map +1 -0
- package/dist/activities/execute-cursor/command-provenance.d.ts +62 -0
- package/dist/activities/execute-cursor/command-provenance.js +128 -0
- package/dist/activities/execute-cursor/command-provenance.js.map +1 -0
- package/dist/activities/execute-cursor/exact-apply.d.ts +110 -0
- package/dist/activities/execute-cursor/exact-apply.js +204 -0
- package/dist/activities/execute-cursor/exact-apply.js.map +1 -0
- package/dist/activities/execute-cursor/hook-script.d.ts +53 -24
- package/dist/activities/execute-cursor/hook-script.js +310 -47
- package/dist/activities/execute-cursor/hook-script.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +8 -1
- package/dist/activities/execute-cursor/index.js +500 -64
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +258 -11
- package/dist/activities/execute-cursor/message-translator.js +836 -90
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +20 -23
- package/dist/activities/execute-cursor/prompt-builder.js +74 -15
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/workspace-setup.d.ts +17 -0
- package/dist/activities/execute-cursor/workspace-setup.js +212 -33
- package/dist/activities/execute-cursor/workspace-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.d.ts +19 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js +143 -0
- package/dist/activities/execute-deep-agent/__test-utils__/gateway-substrate.js.map +1 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.d.ts +88 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js +81 -0
- package/dist/activities/execute-deep-agent/__test-utils__/scripted-model.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +47 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js +68 -0
- package/dist/activities/execute-deep-agent/approval-file-change.js.map +1 -0
- package/dist/activities/execute-deep-agent/attachment-injector.d.ts +8 -1
- package/dist/activities/execute-deep-agent/attachment-injector.js +7 -7
- package/dist/activities/execute-deep-agent/attachment-injector.js.map +1 -1
- package/dist/activities/execute-deep-agent/cas-capture-backend.d.ts +42 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js +47 -0
- package/dist/activities/execute-deep-agent/cas-capture-backend.js.map +1 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.d.ts +79 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js +112 -0
- package/dist/activities/execute-deep-agent/cas-capture-observer.js.map +1 -0
- package/dist/activities/execute-deep-agent/hitl.d.ts +10 -0
- package/dist/activities/execute-deep-agent/hitl.js +5 -1
- package/dist/activities/execute-deep-agent/hitl.js.map +1 -1
- package/dist/activities/execute-deep-agent/index.d.ts +2 -1
- package/dist/activities/execute-deep-agent/index.js +370 -56
- package/dist/activities/execute-deep-agent/index.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +7 -1
- package/dist/activities/execute-deep-agent/inline-publisher.js +23 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.d.ts +53 -2
- package/dist/activities/execute-deep-agent/setup.js +149 -92
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.d.ts +36 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js +56 -0
- package/dist/activities/execute-deep-agent/stamp-flowed-rows.js.map +1 -0
- package/dist/activities/execute-deep-agent/status-builder-shared.d.ts +34 -1
- package/dist/activities/execute-deep-agent/status-builder-shared.js +26 -25
- package/dist/activities/execute-deep-agent/status-builder-shared.js.map +1 -1
- package/dist/activities/execute-deep-agent/status-builder.d.ts +11 -5
- package/dist/activities/execute-deep-agent/status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/status-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +2 -19
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming.js +3 -15
- package/dist/activities/execute-deep-agent/streaming.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-transformer.d.ts +25 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js +23 -7
- package/dist/activities/execute-deep-agent/subagent-transformer.js.map +1 -1
- package/dist/activities/execute-deep-agent/subagent-wiring.d.ts +30 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js +29 -3
- package/dist/activities/execute-deep-agent/subagent-wiring.js.map +1 -1
- package/dist/activities/execute-deep-agent/v3-status-builder.js +6 -2
- package/dist/activities/execute-deep-agent/v3-status-builder.js.map +1 -1
- package/dist/claimcheck/payload-codec.js +9 -5
- package/dist/claimcheck/payload-codec.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +2 -0
- package/dist/client/stigmer-client.js +2 -0
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/middleware/approval-gate.d.ts +85 -4
- package/dist/middleware/approval-gate.js +165 -38
- package/dist/middleware/approval-gate.js.map +1 -1
- package/dist/middleware/types.d.ts +2 -5
- package/dist/shared/activity-input.d.ts +43 -0
- package/dist/shared/activity-input.js +17 -0
- package/dist/shared/activity-input.js.map +1 -0
- package/dist/shared/approval-canonicalize.d.ts +19 -0
- package/dist/shared/approval-canonicalize.js +119 -0
- package/dist/shared/approval-canonicalize.js.map +1 -0
- package/dist/shared/approval-fingerprint.d.ts +106 -0
- package/dist/shared/approval-fingerprint.js +113 -0
- package/dist/shared/approval-fingerprint.js.map +1 -0
- package/dist/shared/approval-policy.d.ts +182 -12
- package/dist/shared/approval-policy.js +213 -27
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/args-preview.d.ts +52 -0
- package/dist/shared/args-preview.js +93 -0
- package/dist/shared/args-preview.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +19 -1
- package/dist/shared/artifact-storage.js +48 -11
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/file-change.d.ts +44 -0
- package/dist/shared/file-change.js +57 -0
- package/dist/shared/file-change.js.map +1 -0
- package/dist/shared/file-tools.d.ts +107 -0
- package/dist/shared/file-tools.js +168 -0
- package/dist/shared/file-tools.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +202 -0
- package/dist/shared/filereview/capture.js +498 -0
- package/dist/shared/filereview/capture.js.map +1 -0
- package/dist/shared/filereview/cas-substrate.d.ts +190 -0
- package/dist/shared/filereview/cas-substrate.js +284 -0
- package/dist/shared/filereview/cas-substrate.js.map +1 -0
- package/dist/shared/filereview/digest.d.ts +40 -0
- package/dist/shared/filereview/digest.js +66 -0
- package/dist/shared/filereview/digest.js.map +1 -0
- package/dist/shared/filereview/events.d.ts +170 -0
- package/dist/shared/filereview/events.js +298 -0
- package/dist/shared/filereview/events.js.map +1 -0
- package/dist/shared/filereview/git-substrate.d.ts +175 -0
- package/dist/shared/filereview/git-substrate.js +439 -0
- package/dist/shared/filereview/git-substrate.js.map +1 -0
- package/dist/shared/filereview/index.d.ts +11 -0
- package/dist/shared/filereview/index.js +12 -0
- package/dist/shared/filereview/index.js.map +1 -0
- package/dist/shared/filereview/secret-paths.d.ts +63 -0
- package/dist/shared/filereview/secret-paths.js +105 -0
- package/dist/shared/filereview/secret-paths.js.map +1 -0
- package/dist/shared/fingerprint-secret.d.ts +26 -0
- package/dist/shared/fingerprint-secret.js +47 -0
- package/dist/shared/fingerprint-secret.js.map +1 -0
- package/dist/shared/model-client.d.ts +51 -0
- package/dist/shared/model-client.js +77 -0
- package/dist/shared/model-client.js.map +1 -0
- package/dist/shared/plan-artifact.js +0 -2
- package/dist/shared/plan-artifact.js.map +1 -1
- package/dist/shared/status-offload.d.ts +83 -9
- package/dist/shared/status-offload.js +399 -79
- package/dist/shared/status-offload.js.map +1 -1
- package/dist/shared/status.js +14 -1
- package/dist/shared/status.js.map +1 -1
- package/dist/shared/tool-kind.d.ts +19 -0
- package/dist/shared/tool-kind.js +13 -0
- package/dist/shared/tool-kind.js.map +1 -1
- package/dist/shared/tool-row.d.ts +88 -0
- package/dist/shared/tool-row.js +127 -0
- package/dist/shared/tool-row.js.map +1 -0
- package/dist/shared/workspace/platform-dir.d.ts +25 -0
- package/dist/shared/workspace/platform-dir.js +38 -2
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.js +56 -7
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/connect-mcp-server.d.ts +50 -0
- package/dist/workflows/connect-mcp-server.js +136 -15
- package/dist/workflows/connect-mcp-server.js.map +1 -1
- package/dist/workflows/types.d.ts +8 -0
- package/package.json +2 -2
- package/src/__test-utils__/approval-contract/contract.ts +224 -0
- package/src/__test-utils__/approval-contract/types.ts +179 -0
- package/src/__test-utils__/fake-artifact-storage.ts +72 -0
- package/src/__tests__/approval-gateway-contract.test.ts +29 -0
- package/src/__tests__/claimcheck-codec.test.ts +16 -53
- package/src/__tests__/golden-e2e.test.ts +2 -0
- package/src/__tests__/runner-token-coordinator.test.ts +3 -3
- package/src/activities/__tests__/call-agent-status.test.ts +135 -0
- package/src/activities/__tests__/call-llm.test.ts +1 -1
- package/src/activities/__tests__/classify-tool-approvals.test.ts +208 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +30 -0
- package/src/activities/__tests__/workflow-event-activities.test.ts +2 -1
- package/src/activities/call-agent-status.ts +74 -4
- package/src/activities/call-llm.ts +18 -63
- package/src/activities/classify-tool-approvals.ts +101 -19
- package/src/activities/discover-mcp-server.ts +29 -1
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +216 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +148 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +41 -9
- package/src/activities/execute-cursor/__tests__/approval-state.test.ts +292 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +68 -1
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +1005 -0
- package/src/activities/execute-cursor/__tests__/cas-observations.test.ts +187 -0
- package/src/activities/execute-cursor/__tests__/coarse-fingerprint.test.ts +97 -0
- package/src/activities/execute-cursor/__tests__/command-provenance.test.ts +240 -0
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +203 -0
- package/src/activities/execute-cursor/__tests__/exact-apply.test.ts +375 -0
- package/src/activities/execute-cursor/__tests__/hitl-ledger.test.ts +1294 -24
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +446 -0
- package/src/activities/execute-cursor/__tests__/hook-script.test.ts +384 -110
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +171 -25
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +189 -0
- package/src/activities/execute-cursor/__tests__/tool-result-image.test.ts +44 -23
- package/src/activities/execute-cursor/__tests__/workspace-setup.test.ts +190 -10
- package/src/activities/execute-cursor/approval-policy.ts +28 -159
- package/src/activities/execute-cursor/approval-state.ts +366 -18
- package/src/activities/execute-cursor/capture-flow.ts +323 -0
- package/src/activities/execute-cursor/cas-observations.ts +204 -0
- package/src/activities/execute-cursor/command-provenance.ts +168 -0
- package/src/activities/execute-cursor/exact-apply.ts +253 -0
- package/src/activities/execute-cursor/hook-script.ts +317 -51
- package/src/activities/execute-cursor/index.ts +575 -67
- package/src/activities/execute-cursor/message-translator.ts +963 -89
- package/src/activities/execute-cursor/prompt-builder.ts +80 -14
- package/src/activities/execute-cursor/workspace-setup.ts +257 -42
- package/src/activities/execute-deep-agent/__test-utils__/gateway-substrate.ts +180 -0
- package/src/activities/execute-deep-agent/__test-utils__/scripted-model.ts +134 -0
- package/src/activities/execute-deep-agent/__tests__/approval-file-change.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/attachment-injector.test.ts +11 -24
- package/src/activities/execute-deep-agent/__tests__/cas-capture-backend.test.ts +64 -0
- package/src/activities/execute-deep-agent/__tests__/cas-capture-observer.test.ts +163 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-integration.test.ts +8 -5
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-approve-all.test.ts +342 -0
- package/src/activities/execute-deep-agent/__tests__/hitl-resume-history.test.ts +2 -5
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +31 -13
- package/src/activities/execute-deep-agent/__tests__/sequential-gate-resume.test.ts +349 -0
- package/src/activities/execute-deep-agent/__tests__/stamp-flowed-rows.test.ts +119 -0
- package/src/activities/execute-deep-agent/__tests__/status-builder.test.ts +12 -11
- package/src/activities/execute-deep-agent/__tests__/streaming-v3.test.ts +9 -9
- package/src/activities/execute-deep-agent/__tests__/subagent-approval-propagation.test.ts +160 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-gitignored-capture.test.ts +213 -0
- package/src/activities/execute-deep-agent/__tests__/subagent-transformer.test.ts +3 -6
- package/src/activities/execute-deep-agent/__tests__/subagent-wiring.test.ts +84 -1
- package/src/activities/execute-deep-agent/__tests__/v3-status-builder.test.ts +4 -1
- package/src/activities/execute-deep-agent/approval-file-change.ts +80 -0
- package/src/activities/execute-deep-agent/attachment-injector.ts +20 -11
- package/src/activities/execute-deep-agent/cas-capture-backend.ts +66 -0
- package/src/activities/execute-deep-agent/cas-capture-observer.ts +125 -0
- package/src/activities/execute-deep-agent/hitl.ts +15 -1
- package/src/activities/execute-deep-agent/index.ts +434 -64
- package/src/activities/execute-deep-agent/inline-publisher.ts +27 -4
- package/src/activities/execute-deep-agent/setup.ts +223 -125
- package/src/activities/execute-deep-agent/stamp-flowed-rows.ts +64 -0
- package/src/activities/execute-deep-agent/status-builder-shared.ts +62 -23
- package/src/activities/execute-deep-agent/status-builder.ts +19 -7
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +2 -16
- package/src/activities/execute-deep-agent/streaming.ts +3 -13
- package/src/activities/execute-deep-agent/subagent-transformer.ts +53 -13
- package/src/activities/execute-deep-agent/subagent-wiring.ts +50 -3
- package/src/activities/execute-deep-agent/v3-status-builder.ts +8 -2
- package/src/claimcheck/payload-codec.ts +8 -8
- package/src/client/stigmer-client.ts +9 -1
- package/src/middleware/__tests__/approval-gate.test.ts +488 -4
- package/src/middleware/approval-gate.ts +247 -38
- package/src/middleware/types.ts +5 -5
- package/src/shared/__tests__/activity-input.test.ts +78 -0
- package/src/shared/__tests__/approval-canonicalize.test.ts +106 -0
- package/src/shared/__tests__/approval-fingerprint.test.ts +115 -0
- package/src/shared/__tests__/approval-policy.test.ts +274 -40
- package/src/shared/__tests__/args-preview.test.ts +78 -0
- package/src/shared/__tests__/artifact-storage-extended.test.ts +62 -10
- package/src/shared/__tests__/artifact-storage.test.ts +123 -11
- package/src/shared/__tests__/file-change.test.ts +85 -0
- package/src/shared/__tests__/file-tools.test.ts +90 -0
- package/src/shared/__tests__/fingerprint-secret.test.ts +51 -0
- package/src/shared/__tests__/lease-scope-corpus.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +162 -0
- package/src/shared/__tests__/plan-artifact.test.ts +11 -26
- package/src/shared/__tests__/policy-source-corpus.test.ts +58 -0
- package/src/shared/__tests__/status-offload.test.ts +573 -16
- package/src/shared/__tests__/status.test.ts +4 -5
- package/src/shared/__tests__/tool-kind.test.ts +24 -1
- package/src/shared/__tests__/tool-row.test.ts +221 -0
- package/src/shared/activity-input.ts +57 -0
- package/src/shared/approval-canonicalize.ts +159 -0
- package/src/shared/approval-fingerprint.ts +148 -0
- package/src/shared/approval-policy.ts +303 -27
- package/src/shared/args-preview.ts +98 -0
- package/src/shared/artifact-storage.ts +62 -11
- package/src/shared/checkpointer/__tests__/http-saver.test.ts +1 -2
- package/src/shared/file-change.ts +64 -0
- package/src/shared/file-tools.ts +169 -0
- package/src/shared/filereview/__tests__/capture.test.ts +856 -0
- package/src/shared/filereview/__tests__/cas-substrate.test.ts +404 -0
- package/src/shared/filereview/__tests__/digest.test.ts +100 -0
- package/src/shared/filereview/__tests__/events.test.ts +245 -0
- package/src/shared/filereview/__tests__/git-substrate.test.ts +362 -0
- package/src/shared/filereview/__tests__/proxy-reconcile.test.ts +286 -0
- package/src/shared/filereview/__tests__/secret-paths.test.ts +121 -0
- package/src/shared/filereview/capture.ts +727 -0
- package/src/shared/filereview/cas-substrate.ts +401 -0
- package/src/shared/filereview/digest.ts +83 -0
- package/src/shared/filereview/events.ts +449 -0
- package/src/shared/filereview/git-substrate.ts +555 -0
- package/src/shared/filereview/index.ts +60 -0
- package/src/shared/filereview/secret-paths.ts +121 -0
- package/src/shared/fingerprint-secret.ts +53 -0
- package/src/shared/model-client.ts +122 -0
- package/src/shared/plan-artifact.ts +0 -2
- package/src/shared/status-offload.ts +433 -77
- package/src/shared/status.ts +13 -0
- package/src/shared/tool-kind.ts +33 -0
- package/src/shared/tool-row.ts +135 -0
- package/src/shared/workspace/platform-dir.ts +41 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +35 -18
- package/src/workflow-engine/__tests__/tasks/try.test.ts +1 -1
- package/src/workflows/__tests__/connect-mcp-server.test.ts +304 -29
- package/src/workflows/call-agent-orchestrator.ts +53 -6
- package/src/workflows/connect-mcp-server.ts +179 -24
- package/src/workflows/types.ts +8 -0
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Tool outputs (an MCP screenshot's base64 image, a giant accessibility-tree
|
|
5
5
|
* dump, a multi-MB shell log, a huge file write) are stored inline in
|
|
6
|
-
* `ToolCall.result`/`args_preview`, which live in `status.messages`
|
|
7
|
-
*
|
|
6
|
+
* `ToolCall.result`/`args_preview`, which live in `status.messages` AND in
|
|
7
|
+
* each `status.sub_agent_executions[].messages` (a delegated tool call's
|
|
8
|
+
* result is just as unbounded), and are re-serialized whole on every
|
|
9
|
+
* `persistStatus` -> `updateStatus` gRPC call.
|
|
8
10
|
* Left unchecked, a single large result pushes the message past the server's
|
|
9
11
|
* 4 MiB gRPC receive cap; the call fails with `resource_exhausted`, progress
|
|
10
12
|
* stops persisting, and the live UI freezes mid-execution.
|
|
@@ -20,12 +22,22 @@
|
|
|
20
22
|
* Idempotent and content-hash-deduped so the throttled, repeated persists
|
|
21
23
|
* (and result re-inflation by mergeToolCallEvent) upload each blob once.
|
|
22
24
|
*
|
|
23
|
-
* 2.
|
|
25
|
+
* 2. offloadCandidateChangesToFit — an aggregate, storage-backed step for the
|
|
26
|
+
* file-review ledger: if the status still exceeds the soft cap after (1),
|
|
27
|
+
* it offloads the largest still-inline captured before/after bodies to
|
|
28
|
+
* retrievable refs (biggest-first) until it fits, so a captured file stays
|
|
29
|
+
* REVIEWABLE (the UI lazily fetches the ref) instead of being dropped. The
|
|
30
|
+
* persisted body is a display projection — reconcile sources bytes from the
|
|
31
|
+
* git refs / CAS manifest, never this body — so this is correctness-neutral.
|
|
32
|
+
*
|
|
33
|
+
* 3. enforceStatusSizeLimit — an aggregate, type-agnostic backstop that runs
|
|
24
34
|
* even when no artifact storage is available: if the encoded status still
|
|
25
35
|
* exceeds a soft cap (comfortably under 4 MiB), it elides the largest
|
|
26
|
-
* remaining inline fields in place until the payload fits.
|
|
36
|
+
* remaining inline fields in place until the payload fits. For file-review
|
|
37
|
+
* bodies this is the LAST resort (no storage, or (2) could not free enough):
|
|
38
|
+
* the body is dropped and the file marked SIZE_ELIDED / incomplete.
|
|
27
39
|
*
|
|
28
|
-
*
|
|
40
|
+
* All operate ONLY on what is persisted/streamed; the agent's working context
|
|
29
41
|
* is managed by the harness/SDK separately, so reasoning is unaffected.
|
|
30
42
|
*/
|
|
31
43
|
|
|
@@ -36,8 +48,11 @@ import {
|
|
|
36
48
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
37
49
|
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
38
50
|
import { ToolCallOutputRefSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
39
|
-
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
51
|
+
import type { AgentMessage, FileContent, ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
52
|
+
import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
53
|
+
import { FileReviewBlockReason } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
40
54
|
import type { ArtifactStorage } from "./artifact-storage.js";
|
|
55
|
+
import { deriveDiffCompleteness } from "./filereview/events.js";
|
|
41
56
|
|
|
42
57
|
/**
|
|
43
58
|
* A single tool output (result or args_preview) larger than this many bytes is
|
|
@@ -47,6 +62,14 @@ import type { ArtifactStorage } from "./artifact-storage.js";
|
|
|
47
62
|
*/
|
|
48
63
|
export const INLINE_TOOL_OUTPUT_MAX_BYTES = 256 * 1024;
|
|
49
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A single FileChange before/after body larger than this is offloaded to
|
|
67
|
+
* artifact storage. Smaller than the 256 KiB tool-output cap because a file
|
|
68
|
+
* change can carry two bodies (before + after) per change and several changes
|
|
69
|
+
* per tool call, so the aggregate would otherwise climb quickly.
|
|
70
|
+
*/
|
|
71
|
+
export const INLINE_FILE_CONTENT_MAX_BYTES = 128 * 1024;
|
|
72
|
+
|
|
50
73
|
/** Head of an offloaded text result kept inline for an at-a-glance preview. */
|
|
51
74
|
export const TEXT_PREVIEW_HEAD_CHARS = 4_000;
|
|
52
75
|
|
|
@@ -62,8 +85,14 @@ export const STATUS_PAYLOAD_SOFT_LIMIT_BYTES = 3 * 1024 * 1024;
|
|
|
62
85
|
*/
|
|
63
86
|
export const STATUS_PAYLOAD_HARD_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
64
87
|
|
|
65
|
-
/**
|
|
66
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Marker left in place of an aggregate-elided inline field. Exported so the
|
|
90
|
+
* resume-time exact-apply resolver can recognize (and refuse to write) an elided
|
|
91
|
+
* body rather than corrupting a file with the marker text — the lossy elision is
|
|
92
|
+
* the one case where the exact approved bytes are unrecoverable and exact-apply
|
|
93
|
+
* must fall back. The two sides cannot drift because they share this constant.
|
|
94
|
+
*/
|
|
95
|
+
export const ELISION_MARKER = "[output elided to keep status under the size limit]";
|
|
67
96
|
|
|
68
97
|
/** Below this size an inline field is not worth eliding (the marker is ~50B). */
|
|
69
98
|
const ELISION_MIN_BYTES = 1_024;
|
|
@@ -73,6 +102,8 @@ export interface ToolOutputOffloadContext {
|
|
|
73
102
|
readonly executionId: string;
|
|
74
103
|
/** Override the per-result byte threshold (tests use a small value). */
|
|
75
104
|
readonly maxInlineBytes?: number;
|
|
105
|
+
/** Override the per-file-content-body byte threshold (tests use a small value). */
|
|
106
|
+
readonly maxInlineFileBytes?: number;
|
|
76
107
|
}
|
|
77
108
|
|
|
78
109
|
interface ImagePayload {
|
|
@@ -98,6 +129,29 @@ function formatBytes(n: number): string {
|
|
|
98
129
|
return `${n} B`;
|
|
99
130
|
}
|
|
100
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Every message list carried on the status: the parent transcript plus each
|
|
134
|
+
* sub-agent's nested transcript. Sub-agent tool calls hold real results (a
|
|
135
|
+
* delegated read/grep/screenshot can be as large as a top-level one), so every
|
|
136
|
+
* size-bounding pass in this module walks THIS set — a location covered by
|
|
137
|
+
* offload but not elision (or vice versa) would be a hole in the persist
|
|
138
|
+
* boundary's bounded-payload guarantee.
|
|
139
|
+
*/
|
|
140
|
+
function allMessageLists(status: AgentExecutionStatus): readonly (readonly AgentMessage[])[] {
|
|
141
|
+
return [status.messages, ...status.subAgentExecutions.map((sa) => sa.messages)];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every tool call in the status, across the parent and all sub-agents. */
|
|
145
|
+
function allToolCalls(status: AgentExecutionStatus): ToolCall[] {
|
|
146
|
+
const out: ToolCall[] = [];
|
|
147
|
+
for (const messages of allMessageLists(status)) {
|
|
148
|
+
for (const msg of messages) {
|
|
149
|
+
for (const tc of msg.toolCalls) out.push(tc);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
101
155
|
function extFromMime(mimeType: string): string {
|
|
102
156
|
switch (mimeType) {
|
|
103
157
|
case "image/png": return "png";
|
|
@@ -109,65 +163,94 @@ function extFromMime(mimeType: string): string {
|
|
|
109
163
|
}
|
|
110
164
|
}
|
|
111
165
|
|
|
166
|
+
/** Match an exact `data:image/...;base64,...` URL (the whole string). */
|
|
112
167
|
function matchDataUrl(s: string): ImagePayload | null {
|
|
113
168
|
const m = s.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,([\s\S]+)$/);
|
|
114
169
|
if (!m) return null;
|
|
115
170
|
return { mimeType: m[1], base64: m[2].replace(/\s+/g, "") };
|
|
116
171
|
}
|
|
117
172
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return (kwargs as Record<string, unknown>).content as unknown[];
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return [];
|
|
173
|
+
/**
|
|
174
|
+
* Find a `data:image/...;base64,...` URL anywhere in a string — whether it IS
|
|
175
|
+
* the whole result or is embedded in surrounding text/JSON. A data URL is an
|
|
176
|
+
* unambiguous image signal, so scanning is safe (no false positives on ordinary
|
|
177
|
+
* text). The base64 run is bounded by the first non-base64 character (e.g. a
|
|
178
|
+
* closing JSON quote), so an embedded URL is extracted cleanly.
|
|
179
|
+
*/
|
|
180
|
+
function findDataUrlInString(s: string): ImagePayload | null {
|
|
181
|
+
const m = s.match(/data:(image\/[a-zA-Z0-9.+-]+);base64,([A-Za-z0-9+/=\s]+)/);
|
|
182
|
+
if (!m) return null;
|
|
183
|
+
return { mimeType: m[1], base64: m[2].replace(/\s+/g, "") };
|
|
133
184
|
}
|
|
134
185
|
|
|
135
186
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
187
|
+
* Build an ImagePayload from a block's `data` + optional mime hint. Accepts the
|
|
188
|
+
* shapes the Cursor SDK actually delivers for an image block's `data`:
|
|
189
|
+
* - Node Buffer-JSON ({ type:"Buffer", data:number[] }) — the runtime shape
|
|
190
|
+
* confirmed from a real cursor-harness get_app_state result. (The SDK's .d.ts
|
|
191
|
+
* types `data` as a string, but at runtime image bytes serialize as
|
|
192
|
+
* Buffer-JSON, so both must be handled.)
|
|
193
|
+
* - a `data:` URL string, or
|
|
194
|
+
* - a bare base64 string.
|
|
195
|
+
* Anything else (a file path, a number) yields null, so only an explicit image
|
|
196
|
+
* signal ever matches. The mime hint defaults to image/png when absent — Cursor
|
|
197
|
+
* MCP image blocks frequently omit mimeType.
|
|
140
198
|
*/
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
if (direct) return direct;
|
|
199
|
+
function imageFromData(data: unknown, mime: unknown): ImagePayload | null {
|
|
200
|
+
const mimeType = typeof mime === "string" && mime.length > 0 ? mime : "image/png";
|
|
144
201
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
202
|
+
if (data && typeof data === "object") {
|
|
203
|
+
const d = data as Record<string, unknown>;
|
|
204
|
+
if (d.type === "Buffer" && Array.isArray(d.data)) {
|
|
205
|
+
try {
|
|
206
|
+
const base64 = Buffer.from(d.data as number[]).toString("base64");
|
|
207
|
+
return base64.length > 0 ? { mimeType, base64 } : null;
|
|
208
|
+
} catch {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
149
212
|
return null;
|
|
150
213
|
}
|
|
151
214
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
215
|
+
if (typeof data !== "string" || data.length === 0) return null;
|
|
216
|
+
const asUrl = matchDataUrl(data);
|
|
217
|
+
if (asUrl) return asUrl;
|
|
218
|
+
return { mimeType, base64: data.replace(/\s+/g, "") };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Extract an image from a single object IF it is a recognized image block.
|
|
223
|
+
* Recognized shapes, all of which carry an explicit image marker (so an
|
|
224
|
+
* arbitrary object never matches):
|
|
225
|
+
* - Cursor SDK MCP block: { image: { data: <base64|dataUrl>, mimeType? } }
|
|
226
|
+
* - Anthropic/MCP-style block: { type: "image", data: <base64>, mimeType? }
|
|
227
|
+
* - OpenAI-style block: { type: "image_url", image_url: { url: <dataUrl> } }
|
|
228
|
+
* (also tolerates { type: "image", image: <base64> })
|
|
229
|
+
*
|
|
230
|
+
* Returns null when no image marker is present, leaving the recursive walk to
|
|
231
|
+
* keep searching siblings/children.
|
|
232
|
+
*/
|
|
233
|
+
function imageFromBlock(obj: Record<string, unknown>): ImagePayload | null {
|
|
234
|
+
// Cursor SDK shape: the image rides under a nested `image` object. This is the
|
|
235
|
+
// exact shape @cursor/sdk uses for MCP image content (see conversation-types),
|
|
236
|
+
// which canonicalizeImageResult normalizes — but only when the result reaches
|
|
237
|
+
// it as an object. A result delivered already-serialized (a string) bypasses
|
|
238
|
+
// that, so detection must recognize this shape directly.
|
|
239
|
+
if (obj.image && typeof obj.image === "object") {
|
|
240
|
+
const img = obj.image as Record<string, unknown>;
|
|
241
|
+
const payload = imageFromData(img.data, img.mimeType ?? img.mime_type);
|
|
242
|
+
if (payload) return payload;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const type = typeof obj.type === "string" ? obj.type : "";
|
|
246
|
+
if (type === "image" || type === "image_url") {
|
|
247
|
+
const inline = imageFromData(
|
|
248
|
+
typeof obj.data === "string" ? obj.data
|
|
249
|
+
: typeof obj.image === "string" ? obj.image
|
|
250
|
+
: undefined,
|
|
251
|
+
obj.mimeType ?? obj.mime_type,
|
|
252
|
+
);
|
|
253
|
+
if (inline) return inline;
|
|
171
254
|
|
|
172
255
|
const imageUrl = obj.image_url;
|
|
173
256
|
if (imageUrl && typeof imageUrl === "object") {
|
|
@@ -181,6 +264,67 @@ export function detectImagePayload(result: string): ImagePayload | null {
|
|
|
181
264
|
return null;
|
|
182
265
|
}
|
|
183
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Walk a parsed JSON value depth-first, returning the first recognized image
|
|
269
|
+
* block. Recursing (rather than only checking the top level or a `content`
|
|
270
|
+
* array) is what makes detection robust to HOW a harness wraps the image: a
|
|
271
|
+
* multimodal MCP result may arrive as a top-level array, under `value.content`,
|
|
272
|
+
* under `kwargs.content`, or nested deeper still. Because imageFromBlock
|
|
273
|
+
* requires an explicit image marker, the walk never misclassifies ordinary
|
|
274
|
+
* nested data as an image.
|
|
275
|
+
*/
|
|
276
|
+
function findImageInValue(value: unknown): ImagePayload | null {
|
|
277
|
+
if (Array.isArray(value)) {
|
|
278
|
+
for (const item of value) {
|
|
279
|
+
const found = findImageInValue(item);
|
|
280
|
+
if (found) return found;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
if (value && typeof value === "object") {
|
|
285
|
+
const obj = value as Record<string, unknown>;
|
|
286
|
+
const direct = imageFromBlock(obj);
|
|
287
|
+
if (direct) return direct;
|
|
288
|
+
for (const child of Object.values(obj)) {
|
|
289
|
+
const found = findImageInValue(child);
|
|
290
|
+
if (found) return found;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Best-effort extraction of an inline image from a tool result string.
|
|
298
|
+
*
|
|
299
|
+
* An MCP tool that returns an image (e.g. a computer-use screenshot) must be
|
|
300
|
+
* lifted into a renderable `ToolCallOutputRef`; otherwise it persists as text
|
|
301
|
+
* and the UI shows raw JSON / "view full output" instead of the picture. The
|
|
302
|
+
* image can arrive in many wrappers depending on the harness and whether the
|
|
303
|
+
* result was pre-serialized, so detection looks for an UNAMBIGUOUS image signal
|
|
304
|
+
* rather than a fixed envelope position:
|
|
305
|
+
*
|
|
306
|
+
* 1. a `data:image/*;base64,...` URL anywhere in the string, then
|
|
307
|
+
* 2. a recognized image block at any depth of a JSON result
|
|
308
|
+
* (see {@link imageFromBlock}).
|
|
309
|
+
*
|
|
310
|
+
* Returns null for non-image or unparseable results so the caller falls back to
|
|
311
|
+
* text offload. It deliberately does NOT treat a bare base64 string with no
|
|
312
|
+
* image marker as an image — that would misclassify legitimate large text
|
|
313
|
+
* (logs, base64-encoded files) as pictures.
|
|
314
|
+
*/
|
|
315
|
+
export function detectImagePayload(result: string): ImagePayload | null {
|
|
316
|
+
const embeddedUrl = findDataUrlInString(result);
|
|
317
|
+
if (embeddedUrl) return embeddedUrl;
|
|
318
|
+
|
|
319
|
+
let parsed: unknown;
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(result);
|
|
322
|
+
} catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
return findImageInValue(parsed);
|
|
326
|
+
}
|
|
327
|
+
|
|
184
328
|
function collapsedResultFor(ref: { isImage: boolean; sizeBytes: bigint; truncatedPreview: string }): string {
|
|
185
329
|
if (ref.isImage) {
|
|
186
330
|
return `[image output — ${formatBytes(Number(ref.sizeBytes))}, view inline]`;
|
|
@@ -216,10 +360,8 @@ async function maybeOffloadToolCall(
|
|
|
216
360
|
const bytes = Buffer.from(image.base64, "base64");
|
|
217
361
|
const key = `artifacts/${ctx.executionId}/toolcalls/${tc.id}.${extFromMime(image.mimeType)}`;
|
|
218
362
|
await ctx.artifactStorage.upload(key, bytes, image.mimeType);
|
|
219
|
-
const downloadUrl = await ctx.artifactStorage.getDownloadUrl(key);
|
|
220
363
|
tc.outputRef = create(ToolCallOutputRefSchema, {
|
|
221
364
|
storageKey: key,
|
|
222
|
-
downloadUrl,
|
|
223
365
|
sizeBytes: BigInt(bytes.length),
|
|
224
366
|
contentHash: hash,
|
|
225
367
|
mimeType: image.mimeType,
|
|
@@ -237,10 +379,8 @@ async function maybeOffloadToolCall(
|
|
|
237
379
|
const content = Buffer.from(result, "utf8");
|
|
238
380
|
const key = `artifacts/${ctx.executionId}/toolcalls/${tc.id}.txt`;
|
|
239
381
|
await ctx.artifactStorage.upload(key, content, "text/plain");
|
|
240
|
-
const downloadUrl = await ctx.artifactStorage.getDownloadUrl(key);
|
|
241
382
|
tc.outputRef = create(ToolCallOutputRefSchema, {
|
|
242
383
|
storageKey: key,
|
|
243
|
-
downloadUrl,
|
|
244
384
|
sizeBytes: BigInt(content.length),
|
|
245
385
|
contentHash: hash,
|
|
246
386
|
mimeType: "text/plain",
|
|
@@ -250,39 +390,216 @@ async function maybeOffloadToolCall(
|
|
|
250
390
|
tc.result = collapsedResultFor(tc.outputRef);
|
|
251
391
|
}
|
|
252
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Spill one side (before/after) of a file change to artifact storage when its
|
|
395
|
+
* inline body exceeds the cap, replacing the inline body with a ref carrying a
|
|
396
|
+
* head preview. A side that is absent, already a ref (offloaded on a prior
|
|
397
|
+
* persist), or under the cap is left untouched — the `case === "ref"` check
|
|
398
|
+
* makes this idempotent across the throttled, repeated persists.
|
|
399
|
+
*/
|
|
400
|
+
async function maybeOffloadFileContent(
|
|
401
|
+
content: FileContent | undefined,
|
|
402
|
+
key: string,
|
|
403
|
+
ctx: ToolOutputOffloadContext,
|
|
404
|
+
maxBytes: number,
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
if (!content || content.body.case !== "inline") return;
|
|
407
|
+
const text = content.body.value;
|
|
408
|
+
if (byteLen(text) <= maxBytes) return;
|
|
409
|
+
|
|
410
|
+
const bytes = Buffer.from(text, "utf8");
|
|
411
|
+
await ctx.artifactStorage.upload(key, bytes, "text/plain");
|
|
412
|
+
content.body = {
|
|
413
|
+
case: "ref",
|
|
414
|
+
value: create(ToolCallOutputRefSchema, {
|
|
415
|
+
storageKey: key,
|
|
416
|
+
sizeBytes: BigInt(bytes.length),
|
|
417
|
+
contentHash: sha256(text),
|
|
418
|
+
mimeType: "text/plain",
|
|
419
|
+
isImage: false,
|
|
420
|
+
truncatedPreview: headChars(text, TEXT_PREVIEW_HEAD_CHARS),
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Every captured file change carried on a CANDIDATE_CAPTURED event in the
|
|
427
|
+
* file_review ledger. The before/after bodies live HERE (not on tool calls)
|
|
428
|
+
* under the apply-then-review model, so the persist-boundary size guards must
|
|
429
|
+
* cover this location too — otherwise a large captured file silently pushes the
|
|
430
|
+
* status past the gRPC cap (the freeze this module exists to prevent).
|
|
431
|
+
*/
|
|
432
|
+
function candidateChanges(status: AgentExecutionStatus): CapturedFileChange[] {
|
|
433
|
+
const out: CapturedFileChange[] = [];
|
|
434
|
+
for (const ev of status.fileReviewEventStream?.events ?? []) {
|
|
435
|
+
if (ev.payload.case === "candidateCaptured") {
|
|
436
|
+
out.push(...ev.payload.value.changes);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return out;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Offload oversized before/after bodies of every captured file-review change. */
|
|
443
|
+
async function maybeOffloadCandidateChanges(
|
|
444
|
+
status: AgentExecutionStatus,
|
|
445
|
+
ctx: ToolOutputOffloadContext,
|
|
446
|
+
maxBytes: number,
|
|
447
|
+
): Promise<void> {
|
|
448
|
+
const changes = candidateChanges(status);
|
|
449
|
+
for (const change of changes) {
|
|
450
|
+
const base = `artifacts/${ctx.executionId}/filereview/${change.id}`;
|
|
451
|
+
await maybeOffloadFileContent(change.before, `${base}.before.txt`, ctx, maxBytes);
|
|
452
|
+
await maybeOffloadFileContent(change.after, `${base}.after.txt`, ctx, maxBytes);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
253
456
|
/**
|
|
254
457
|
* Offload every oversized tool result in the status to artifact storage,
|
|
255
458
|
* replacing the inline value with a short head + ToolCallOutputRef. Per-tool
|
|
256
459
|
* failures fall back to an inline truncation (a bounded result beats a failed
|
|
257
460
|
* persist) and never throw, so a storage hiccup cannot fail the execution.
|
|
461
|
+
*
|
|
462
|
+
* File-review before/after bodies (on the CANDIDATE_CAPTURED ledger events) are
|
|
463
|
+
* offloaded in the same pass — a tool can produce a small result yet a large
|
|
464
|
+
* captured diff. A file-review offload failure is non-fatal — the body stays
|
|
465
|
+
* inline and the aggregate backstop (enforceStatusSizeLimit) drops it if needed.
|
|
258
466
|
*/
|
|
259
467
|
export async function offloadOversizedToolOutputs(
|
|
260
468
|
status: AgentExecutionStatus,
|
|
261
469
|
ctx: ToolOutputOffloadContext,
|
|
262
470
|
): Promise<void> {
|
|
263
471
|
const maxBytes = ctx.maxInlineBytes ?? INLINE_TOOL_OUTPUT_MAX_BYTES;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
}
|
|
472
|
+
const maxFileBytes = ctx.maxInlineFileBytes ?? INLINE_FILE_CONTENT_MAX_BYTES;
|
|
473
|
+
for (const tc of allToolCalls(status)) {
|
|
474
|
+
try {
|
|
475
|
+
await maybeOffloadToolCall(tc, ctx, maxBytes);
|
|
476
|
+
} catch (err) {
|
|
477
|
+
const original = tc.result ?? "";
|
|
478
|
+
tc.result =
|
|
479
|
+
headChars(original, TEXT_PREVIEW_HEAD_CHARS) +
|
|
480
|
+
`\n\n[output truncated — offload failed: ${err instanceof Error ? err.message : String(err)}]`;
|
|
481
|
+
console.warn(
|
|
482
|
+
`[status-offload] execution=${ctx.executionId} tool=${tc.name} ` +
|
|
483
|
+
`offload failed (non-fatal); truncated inline`,
|
|
484
|
+
);
|
|
278
485
|
}
|
|
279
486
|
}
|
|
487
|
+
|
|
488
|
+
// File-review ledger: offload oversized captured before/after bodies the same
|
|
489
|
+
// way (the proto's contract is "offloaded before the candidate event is
|
|
490
|
+
// persisted"). A failure is non-fatal — the body stays inline and the
|
|
491
|
+
// mark-incomplete backstop handles it without corrupting the ledger.
|
|
492
|
+
try {
|
|
493
|
+
await maybeOffloadCandidateChanges(status, ctx, maxFileBytes);
|
|
494
|
+
} catch {
|
|
495
|
+
console.warn(
|
|
496
|
+
`[status-offload] execution=${ctx.executionId} ` +
|
|
497
|
+
`file-review change offload failed (non-fatal); left inline for the size backstop`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Aggregate-budget offload of file-review candidate bodies (async, storage-backed).
|
|
504
|
+
*
|
|
505
|
+
* The per-item pass ({@link offloadOversizedToolOutputs}) only offloads a body
|
|
506
|
+
* over {@link INLINE_FILE_CONTENT_MAX_BYTES}; many mid-sized captured files (each
|
|
507
|
+
* under that per-file cap) can still sum the whole status past the soft limit.
|
|
508
|
+
* Rather than let the storage-less backstop ({@link enforceStatusSizeLimit}) DROP
|
|
509
|
+
* those bodies — which sets `diff_complete=false` and blocks approval, turning a
|
|
510
|
+
* reviewable change discard-only — this step offloads the largest still-inline
|
|
511
|
+
* captured bodies to retrievable refs (biggest-first) until the status fits. The
|
|
512
|
+
* review UI then lazily fetches each ref via getArtifactContent exactly as it
|
|
513
|
+
* already does for a >128 KiB file, and the change stays reviewable
|
|
514
|
+
* (`diff_complete` is untouched, so the set rollup is unchanged).
|
|
515
|
+
*
|
|
516
|
+
* Correctness: the persisted before/after body is a DISPLAY projection —
|
|
517
|
+
* reconcile sources the approved bytes from the pinned git refs / CAS manifest
|
|
518
|
+
* and verifies them against the enforcement digests (`before_sha256`/
|
|
519
|
+
* `after_sha256`), never this body (see {@link ../filereview/capture.js}). So
|
|
520
|
+
* converting a body to a ref (or, in the backstop, dropping it) cannot change
|
|
521
|
+
* what is applied on approval.
|
|
522
|
+
*
|
|
523
|
+
* Non-fatal per side: a storage failure leaves that body inline for the backstop
|
|
524
|
+
* to drop. Returns true if any body was offloaded. Called only for a status that
|
|
525
|
+
* actually carries file-review events (the caller guards on that), and a no-op
|
|
526
|
+
* (single encode) when the status already fits.
|
|
527
|
+
*/
|
|
528
|
+
export async function offloadCandidateChangesToFit(
|
|
529
|
+
status: AgentExecutionStatus,
|
|
530
|
+
ctx: ToolOutputOffloadContext,
|
|
531
|
+
softLimitBytes: number = STATUS_PAYLOAD_SOFT_LIMIT_BYTES,
|
|
532
|
+
): Promise<boolean> {
|
|
533
|
+
if (encodedSize(status) <= softLimitBytes) return false;
|
|
534
|
+
|
|
535
|
+
// Every still-inline captured side worth offloading, paired with its stable
|
|
536
|
+
// artifact key (identical to maybeOffloadCandidateChanges so a later persist is
|
|
537
|
+
// idempotent) and its byte size. ELISION_MIN_BYTES is the same "worth it"
|
|
538
|
+
// threshold the drop backstop uses, so the two agree on what is large enough.
|
|
539
|
+
interface InlineCandidateSide {
|
|
540
|
+
readonly content: FileContent;
|
|
541
|
+
readonly key: string;
|
|
542
|
+
readonly bytes: number;
|
|
543
|
+
}
|
|
544
|
+
const sides: InlineCandidateSide[] = [];
|
|
545
|
+
for (const change of candidateChanges(status)) {
|
|
546
|
+
const base = `artifacts/${ctx.executionId}/filereview/${change.id}`;
|
|
547
|
+
if (change.before?.body.case === "inline" && byteLen(change.before.body.value) > ELISION_MIN_BYTES) {
|
|
548
|
+
sides.push({ content: change.before, key: `${base}.before.txt`, bytes: byteLen(change.before.body.value) });
|
|
549
|
+
}
|
|
550
|
+
if (change.after?.body.case === "inline" && byteLen(change.after.body.value) > ELISION_MIN_BYTES) {
|
|
551
|
+
sides.push({ content: change.after, key: `${base}.after.txt`, bytes: byteLen(change.after.body.value) });
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Largest first: shed the most bytes per upload and offload the fewest bodies.
|
|
556
|
+
sides.sort((a, b) => b.bytes - a.bytes);
|
|
557
|
+
|
|
558
|
+
let offloadedAny = false;
|
|
559
|
+
for (const side of sides) {
|
|
560
|
+
if (encodedSize(status) <= softLimitBytes) break;
|
|
561
|
+
try {
|
|
562
|
+
// Pre-filtered to inline & over the threshold, so this always offloads;
|
|
563
|
+
// the shared helper keeps the ref shape + the `case === "ref"` idempotency.
|
|
564
|
+
await maybeOffloadFileContent(side.content, side.key, ctx, ELISION_MIN_BYTES);
|
|
565
|
+
offloadedAny = true;
|
|
566
|
+
} catch {
|
|
567
|
+
// Non-fatal: leave this body inline for enforceStatusSizeLimit to drop.
|
|
568
|
+
console.warn(
|
|
569
|
+
`[status-offload] execution=${ctx.executionId} ` +
|
|
570
|
+
`aggregate file-review offload failed for ${side.key} (non-fatal); ` +
|
|
571
|
+
`left inline for the size backstop`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return offloadedAny;
|
|
280
577
|
}
|
|
281
578
|
|
|
282
579
|
function encodedSize(status: AgentExecutionStatus): number {
|
|
283
580
|
return toBinary(AgentExecutionStatusSchema, status).length;
|
|
284
581
|
}
|
|
285
582
|
|
|
583
|
+
/**
|
|
584
|
+
* Drop a captured change's oversized inline before/after bodies (replacing them
|
|
585
|
+
* with nothing), returning true if either side was large enough to drop. Used by
|
|
586
|
+
* the backstop for file-review bodies, where overwriting with the elision marker
|
|
587
|
+
* would corrupt the authoritative content — dropping + marking incomplete is the
|
|
588
|
+
* safe alternative (bytes are re-sourced from refs/re-capture on reconcile).
|
|
589
|
+
*/
|
|
590
|
+
function dropInlineBodiesIfLarge(change: CapturedFileChange): boolean {
|
|
591
|
+
let dropped = false;
|
|
592
|
+
if (change.before?.body.case === "inline" && byteLen(change.before.body.value) > ELISION_MIN_BYTES) {
|
|
593
|
+
change.before = undefined;
|
|
594
|
+
dropped = true;
|
|
595
|
+
}
|
|
596
|
+
if (change.after?.body.case === "inline" && byteLen(change.after.body.value) > ELISION_MIN_BYTES) {
|
|
597
|
+
change.after = undefined;
|
|
598
|
+
dropped = true;
|
|
599
|
+
}
|
|
600
|
+
return dropped;
|
|
601
|
+
}
|
|
602
|
+
|
|
286
603
|
/**
|
|
287
604
|
* Aggregate, type-agnostic backstop. If the encoded status exceeds
|
|
288
605
|
* `softLimitBytes`, elide the largest inline tool fields (and, as a last
|
|
@@ -296,10 +613,7 @@ export function enforceStatusSizeLimit(
|
|
|
296
613
|
): boolean {
|
|
297
614
|
if (encodedSize(status) <= softLimitBytes) return false;
|
|
298
615
|
|
|
299
|
-
const toolCalls
|
|
300
|
-
for (const msg of status.messages) {
|
|
301
|
-
for (const tc of msg.toolCalls) toolCalls.push(tc);
|
|
302
|
-
}
|
|
616
|
+
const toolCalls = allToolCalls(status);
|
|
303
617
|
// Largest inline footprint first so we shed the most bytes per elision.
|
|
304
618
|
toolCalls.sort(
|
|
305
619
|
(a, b) =>
|
|
@@ -324,9 +638,51 @@ export function enforceStatusSizeLimit(
|
|
|
324
638
|
}
|
|
325
639
|
}
|
|
326
640
|
|
|
327
|
-
//
|
|
641
|
+
// File-review ledger bodies — the storage-less LAST resort. When storage is
|
|
642
|
+
// available, offloadCandidateChangesToFit has already turned oversized captured
|
|
643
|
+
// bodies into retrievable refs (kept reviewable); a body still inline here means
|
|
644
|
+
// there was no storage, or offloading everything still did not free enough.
|
|
645
|
+
// Unlike a tool output, we never overwrite the captured body with the elision
|
|
646
|
+
// marker — the review renders this body, so a marker would show corrupt content;
|
|
647
|
+
// instead we DROP it and mark the file incomplete (SIZE_ELIDED). Reconcile
|
|
648
|
+
// sources bytes from the git refs / CAS manifest (never this display body), and
|
|
649
|
+
// the review surface blocks approval of an incomplete diff. This trades
|
|
650
|
+
// reviewability for a bounded payload, never correctness.
|
|
651
|
+
if (encodedSize(status) > softLimitBytes) {
|
|
652
|
+
for (const ev of status.fileReviewEventStream?.events ?? []) {
|
|
653
|
+
if (encodedSize(status) <= softLimitBytes) break;
|
|
654
|
+
if (ev.payload.case !== "candidateCaptured") continue;
|
|
655
|
+
const candidate = ev.payload.value;
|
|
656
|
+
let markedAny = false;
|
|
657
|
+
for (const change of candidate.changes) {
|
|
658
|
+
if (encodedSize(status) <= softLimitBytes) break;
|
|
659
|
+
if (dropInlineBodiesIfLarge(change)) {
|
|
660
|
+
change.diffComplete = false;
|
|
661
|
+
// Record the honest cause so the review UI distinguishes a size-elided
|
|
662
|
+
// diff from a secret-withheld one (doc 15). Don't overwrite a reason a
|
|
663
|
+
// more specific producer already set (e.g. SECRET_WITHHELD).
|
|
664
|
+
if (change.blockedReason === FileReviewBlockReason.UNSPECIFIED) {
|
|
665
|
+
change.blockedReason = FileReviewBlockReason.SIZE_ELIDED;
|
|
666
|
+
}
|
|
667
|
+
markedAny = true;
|
|
668
|
+
elidedAny = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (markedAny) {
|
|
672
|
+
// Re-derive the rollup from the now-elided changes via the single shared
|
|
673
|
+
// rule. A dropped inline body is non-binary incomplete, so the set
|
|
674
|
+
// downgrades to PARTIAL_BLOCKED (a BINARY_SUMMARY_ONLY set that loses a
|
|
675
|
+
// text body is no longer binary-only); computing it here keeps the rule
|
|
676
|
+
// in one place instead of hardcoding the outcome.
|
|
677
|
+
candidate.diffCompleteness = deriveDiffCompleteness(candidate.changes);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Last resort: oversized message content (e.g. a huge AI response),
|
|
683
|
+
// parent and sub-agent alike.
|
|
328
684
|
if (encodedSize(status) > softLimitBytes) {
|
|
329
|
-
const byContent =
|
|
685
|
+
const byContent = allMessageLists(status).flat().sort(
|
|
330
686
|
(a, b) => byteLen(b.content) - byteLen(a.content),
|
|
331
687
|
);
|
|
332
688
|
for (const msg of byContent) {
|