@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
|
@@ -2,25 +2,42 @@ import { describe, it, expect, vi } from "vitest";
|
|
|
2
2
|
import { create, toBinary } from "@bufbuild/protobuf";
|
|
3
3
|
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
4
4
|
import { AgentMessageSchema, ToolCallSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
5
|
+
import { SubAgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
|
|
5
6
|
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
6
7
|
import type { ToolCall } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
7
|
-
import
|
|
8
|
+
import { makeInMemoryArtifactStorage } from "../../__test-utils__/fake-artifact-storage.js";
|
|
9
|
+
import {
|
|
10
|
+
DiffCompleteness,
|
|
11
|
+
FileCaptureClass,
|
|
12
|
+
FileChangeKind,
|
|
13
|
+
FileChangeType,
|
|
14
|
+
FileChangeCaptureLevel,
|
|
15
|
+
FileReviewBlockReason,
|
|
16
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
17
|
import {
|
|
9
18
|
offloadOversizedToolOutputs,
|
|
19
|
+
offloadCandidateChangesToFit,
|
|
10
20
|
enforceStatusSizeLimit,
|
|
11
21
|
detectImagePayload,
|
|
12
22
|
} from "../status-offload.js";
|
|
23
|
+
import {
|
|
24
|
+
appendFileReviewEvents,
|
|
25
|
+
buildCandidateCapturedEvent,
|
|
26
|
+
buildCapturedFileChange,
|
|
27
|
+
type ChangeSetContext,
|
|
28
|
+
} from "../filereview/events.js";
|
|
29
|
+
import type { CapturedFileChange } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/filereview_pb";
|
|
13
30
|
|
|
14
31
|
function makeFakeStorage() {
|
|
32
|
+
// Canonical double, with the metadata-tracking `uploads` shim these tests assert
|
|
33
|
+
// on and the serve-URL base they expect. `download` reads back what was uploaded.
|
|
34
|
+
const { storage, blobs } = makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" });
|
|
15
35
|
const uploads: { key: string; size: number; contentType?: string }[] = [];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
getDownloadUrl: vi.fn(async (key: string) => `https://artifacts.local/${key}`),
|
|
22
|
-
exists: vi.fn(async () => true),
|
|
23
|
-
};
|
|
36
|
+
storage.upload.mockImplementation(async (key: string, content: Buffer, contentType?: string) => {
|
|
37
|
+
uploads.push({ key, size: content.length, contentType });
|
|
38
|
+
blobs.set(key, Buffer.from(content));
|
|
39
|
+
return key;
|
|
40
|
+
});
|
|
24
41
|
return { storage, uploads };
|
|
25
42
|
}
|
|
26
43
|
|
|
@@ -72,6 +89,87 @@ describe("detectImagePayload", () => {
|
|
|
72
89
|
expect(img?.mimeType).toBe("image/png");
|
|
73
90
|
expect(img?.base64).toBe(BIG_BASE64_IMAGE);
|
|
74
91
|
});
|
|
92
|
+
|
|
93
|
+
it("extracts the Cursor SDK MCP block shape ({ image: { data, mimeType } })", () => {
|
|
94
|
+
// The shape @cursor/sdk uses for MCP image content. When the result reaches
|
|
95
|
+
// the offloader as a pre-serialized string it bypasses canonicalizeImageResult,
|
|
96
|
+
// so detection must recognize this shape directly.
|
|
97
|
+
const result = JSON.stringify([
|
|
98
|
+
{ text: { text: "Screen captured. App state: ready." } },
|
|
99
|
+
{ image: { data: BIG_BASE64_IMAGE, mimeType: "image/png" } },
|
|
100
|
+
]);
|
|
101
|
+
const img = detectImagePayload(result);
|
|
102
|
+
expect(img?.mimeType).toBe("image/png");
|
|
103
|
+
expect(img?.base64).toBe(BIG_BASE64_IMAGE);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("extracts an image nested under a status/value/content envelope", () => {
|
|
107
|
+
// The full Cursor MCP result envelope, serialized to a string. The image is
|
|
108
|
+
// two levels deep — the recursive walk must still find it.
|
|
109
|
+
const result = JSON.stringify({
|
|
110
|
+
status: "success",
|
|
111
|
+
value: {
|
|
112
|
+
isError: false,
|
|
113
|
+
content: [
|
|
114
|
+
{ text: { text: "accessibility tree…" } },
|
|
115
|
+
{ image: { data: BIG_BASE64_IMAGE, mimeType: "image/jpeg" } },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
const img = detectImagePayload(result);
|
|
120
|
+
expect(img?.mimeType).toBe("image/jpeg");
|
|
121
|
+
expect(img?.base64).toBe(BIG_BASE64_IMAGE);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("extracts a data URL embedded in a JSON field (not a content block)", () => {
|
|
125
|
+
const result = JSON.stringify({
|
|
126
|
+
accessibilityTree: "Window > Button(OK)",
|
|
127
|
+
screenshot: `data:image/png;base64,${BIG_BASE64_IMAGE}`,
|
|
128
|
+
});
|
|
129
|
+
const img = detectImagePayload(result);
|
|
130
|
+
expect(img?.mimeType).toBe("image/png");
|
|
131
|
+
expect(img?.base64).toBe(BIG_BASE64_IMAGE);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("extracts a data URL embedded in surrounding prose", () => {
|
|
135
|
+
const img = detectImagePayload(`Here is the screenshot: data:image/gif;base64,${BIG_BASE64_IMAGE}`);
|
|
136
|
+
expect(img?.mimeType).toBe("image/gif");
|
|
137
|
+
expect(img?.base64).toBe(BIG_BASE64_IMAGE);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does NOT treat a bare base64 string with no image marker as an image", () => {
|
|
141
|
+
// A base64-encoded file or a long log must remain text — never a false image.
|
|
142
|
+
const result = JSON.stringify({ file: BIG_BASE64_IMAGE, encoding: "base64" });
|
|
143
|
+
expect(detectImagePayload(result)).toBeNull();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("does NOT treat an image field that is a file path as an image", () => {
|
|
147
|
+
const result = JSON.stringify({ image: "assets/capture.png" });
|
|
148
|
+
expect(detectImagePayload(result)).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("extracts the REAL cursor-harness get_app_state shape (value.content + Buffer-JSON, no mimeType)", () => {
|
|
152
|
+
// Captured verbatim from a production cursor-harness get_app_state result:
|
|
153
|
+
// the multimodal envelope arrives as a serialized string, blocks live under
|
|
154
|
+
// value.content, the image data is Node Buffer-JSON, and there is NO
|
|
155
|
+
// mimeType. The old top-level-only detector missed this (offloaded as text);
|
|
156
|
+
// this is the exact regression that made screenshots render as raw JSON.
|
|
157
|
+
const PNG_BYTES = [137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82];
|
|
158
|
+
const result = JSON.stringify({
|
|
159
|
+
status: "success",
|
|
160
|
+
value: {
|
|
161
|
+
content: [
|
|
162
|
+
{ text: { text: "App=com.google.Chrome … 65 button Done." } },
|
|
163
|
+
{ image: { data: { type: "Buffer", data: PNG_BYTES } } },
|
|
164
|
+
],
|
|
165
|
+
isError: false,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const img = detectImagePayload(result);
|
|
169
|
+
expect(img).not.toBeNull();
|
|
170
|
+
expect(img?.mimeType).toBe("image/png");
|
|
171
|
+
expect(img?.base64).toBe(Buffer.from(PNG_BYTES).toString("base64"));
|
|
172
|
+
});
|
|
75
173
|
});
|
|
76
174
|
|
|
77
175
|
describe("offloadOversizedToolOutputs", () => {
|
|
@@ -93,13 +191,83 @@ describe("offloadOversizedToolOutputs", () => {
|
|
|
93
191
|
expect(out.outputRef).toBeDefined();
|
|
94
192
|
expect(out.outputRef?.isImage).toBe(true);
|
|
95
193
|
expect(out.outputRef?.mimeType).toBe("image/png");
|
|
96
|
-
expect(out.outputRef?.
|
|
194
|
+
expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-1.png");
|
|
97
195
|
// result is collapsed to a short label, no longer the giant blob.
|
|
98
196
|
expect(out.result.length).toBeLessThan(200);
|
|
99
197
|
expect(uploads).toHaveLength(1);
|
|
100
198
|
expect(uploads[0].contentType).toBe("image/png");
|
|
101
199
|
});
|
|
102
200
|
|
|
201
|
+
it("offloads a Cursor-SDK-shape multimodal result as a renderable image (not text)", async () => {
|
|
202
|
+
// Reproduces the production symptom class: a get_app_state-style result
|
|
203
|
+
// (status line text + screenshot) delivered as a pre-serialized string in
|
|
204
|
+
// the @cursor/sdk MCP block shape. Before the recursive detector this fell
|
|
205
|
+
// through to a text offload (is_image=false → "view full output" instead of
|
|
206
|
+
// the picture); it must now offload as an image.
|
|
207
|
+
const { storage, uploads } = makeFakeStorage();
|
|
208
|
+
const result = JSON.stringify({
|
|
209
|
+
status: "success",
|
|
210
|
+
value: {
|
|
211
|
+
isError: false,
|
|
212
|
+
content: [
|
|
213
|
+
{ text: { text: "Screen captured. App state: ready." } },
|
|
214
|
+
{ image: { data: BIG_BASE64_IMAGE, mimeType: "image/png" } },
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
const tc = create(ToolCallSchema, { id: "tc-app", name: "get_app_state", result });
|
|
219
|
+
const status = statusWithToolCall(tc);
|
|
220
|
+
|
|
221
|
+
await offloadOversizedToolOutputs(status, {
|
|
222
|
+
artifactStorage: storage,
|
|
223
|
+
executionId: "exec-1",
|
|
224
|
+
maxInlineBytes: 256,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const out = status.messages[0].toolCalls[0];
|
|
228
|
+
expect(out.outputRef?.isImage).toBe(true);
|
|
229
|
+
expect(out.outputRef?.mimeType).toBe("image/png");
|
|
230
|
+
expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-app.png");
|
|
231
|
+
expect(out.result).not.toContain(BIG_BASE64_IMAGE);
|
|
232
|
+
expect(uploads).toHaveLength(1);
|
|
233
|
+
expect(uploads[0].contentType).toBe("image/png");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("offloads the REAL get_app_state shape (Buffer-JSON, no mimeType) as a renderable image", async () => {
|
|
237
|
+
// End-to-end guard for the production regression: a serialized multimodal
|
|
238
|
+
// envelope with the image as Buffer-JSON under value.content and no mimeType
|
|
239
|
+
// must offload as an image ref (is_image=true), not text.
|
|
240
|
+
const { storage, uploads } = makeFakeStorage();
|
|
241
|
+
const pngBytes = [137, 80, 78, 71, 13, 10, 26, 10, ...Array(4096).fill(65)];
|
|
242
|
+
const result = JSON.stringify({
|
|
243
|
+
status: "success",
|
|
244
|
+
value: {
|
|
245
|
+
content: [
|
|
246
|
+
{ text: { text: "accessibility tree…" } },
|
|
247
|
+
{ image: { data: { type: "Buffer", data: pngBytes } } },
|
|
248
|
+
],
|
|
249
|
+
isError: false,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
const tc = create(ToolCallSchema, { id: "tc-gas", name: "get_app_state", result });
|
|
253
|
+
const status = statusWithToolCall(tc);
|
|
254
|
+
|
|
255
|
+
await offloadOversizedToolOutputs(status, {
|
|
256
|
+
artifactStorage: storage,
|
|
257
|
+
executionId: "exec-1",
|
|
258
|
+
maxInlineBytes: 256,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const out = status.messages[0].toolCalls[0];
|
|
262
|
+
expect(out.outputRef?.isImage).toBe(true);
|
|
263
|
+
expect(out.outputRef?.mimeType).toBe("image/png");
|
|
264
|
+
expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-gas.png");
|
|
265
|
+
expect(uploads).toHaveLength(1);
|
|
266
|
+
expect(uploads[0].contentType).toBe("image/png");
|
|
267
|
+
// The decoded image bytes are what gets uploaded, not the Buffer-JSON array.
|
|
268
|
+
expect(out.result).not.toContain('"Buffer"');
|
|
269
|
+
});
|
|
270
|
+
|
|
103
271
|
it("offloads oversized text with a preview head and text/plain upload", async () => {
|
|
104
272
|
const { storage, uploads } = makeFakeStorage();
|
|
105
273
|
const result = "LOG ".repeat(2000); // ~8 KB
|
|
@@ -141,7 +309,7 @@ describe("offloadOversizedToolOutputs", () => {
|
|
|
141
309
|
|
|
142
310
|
const out = status.messages[0].toolCalls[0];
|
|
143
311
|
expect(out.outputRef?.isImage).toBe(true);
|
|
144
|
-
expect(out.outputRef?.
|
|
312
|
+
expect(out.outputRef?.storageKey).toBe("artifacts/exec-1/toolcalls/tc-img.png");
|
|
145
313
|
expect(out.result).not.toContain(smallImage);
|
|
146
314
|
expect(uploads).toHaveLength(1);
|
|
147
315
|
expect(uploads[0].contentType).toBe("image/png");
|
|
@@ -201,11 +369,8 @@ describe("offloadOversizedToolOutputs", () => {
|
|
|
201
369
|
});
|
|
202
370
|
|
|
203
371
|
it("falls back to inline truncation when the upload fails (never throws)", async () => {
|
|
204
|
-
const storage
|
|
205
|
-
|
|
206
|
-
getDownloadUrl: vi.fn(async (k: string) => k),
|
|
207
|
-
exists: vi.fn(async () => false),
|
|
208
|
-
};
|
|
372
|
+
const { storage } = makeInMemoryArtifactStorage();
|
|
373
|
+
storage.upload.mockImplementation(async () => { throw new Error("storage down"); });
|
|
209
374
|
const result = "LOG ".repeat(2000);
|
|
210
375
|
const tc = create(ToolCallSchema, { id: "tc-6", name: "Shell", result });
|
|
211
376
|
const status = statusWithToolCall(tc);
|
|
@@ -226,6 +391,97 @@ describe("offloadOversizedToolOutputs", () => {
|
|
|
226
391
|
});
|
|
227
392
|
});
|
|
228
393
|
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Sub-agent coverage — the size-bounding walks must include
|
|
396
|
+
// sub_agent_executions[].messages, not just the parent transcript. A delegated
|
|
397
|
+
// read/grep/screenshot result is as unbounded as a top-level one; missing this
|
|
398
|
+
// location was a hole in the persist boundary's bounded-payload guarantee.
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
function statusWithSubAgentToolCall(tc: ToolCall): AgentExecutionStatus {
|
|
402
|
+
return create(AgentExecutionStatusSchema, {
|
|
403
|
+
subAgentExecutions: [
|
|
404
|
+
create(SubAgentExecutionSchema, {
|
|
405
|
+
id: "sa-1",
|
|
406
|
+
name: "researcher",
|
|
407
|
+
messages: [create(AgentMessageSchema, { toolCalls: [tc] })],
|
|
408
|
+
}),
|
|
409
|
+
],
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
describe("sub-agent message size bounding", () => {
|
|
414
|
+
it("offloads an oversized sub-agent tool result exactly like a parent one", async () => {
|
|
415
|
+
const { storage, uploads } = makeFakeStorage();
|
|
416
|
+
const result = "LOG ".repeat(2000); // ~8 KB
|
|
417
|
+
const tc = create(ToolCallSchema, { id: "tc-sub", name: "Shell", result });
|
|
418
|
+
const status = statusWithSubAgentToolCall(tc);
|
|
419
|
+
|
|
420
|
+
await offloadOversizedToolOutputs(status, {
|
|
421
|
+
artifactStorage: storage,
|
|
422
|
+
executionId: "exec-1",
|
|
423
|
+
maxInlineBytes: 256,
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
const out = status.subAgentExecutions[0].messages[0].toolCalls[0];
|
|
427
|
+
expect(out.outputRef?.mimeType).toBe("text/plain");
|
|
428
|
+
expect(out.result).toContain("view full output");
|
|
429
|
+
expect(uploads).toHaveLength(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("offloads a sub-agent image result into a renderable ref", async () => {
|
|
433
|
+
const { storage, uploads } = makeFakeStorage();
|
|
434
|
+
const result = JSON.stringify([
|
|
435
|
+
{ type: "image", data: BIG_BASE64_IMAGE, mimeType: "image/png" },
|
|
436
|
+
]);
|
|
437
|
+
const tc = create(ToolCallSchema, { id: "tc-sub-img", name: "screenshot", result });
|
|
438
|
+
const status = statusWithSubAgentToolCall(tc);
|
|
439
|
+
|
|
440
|
+
await offloadOversizedToolOutputs(status, {
|
|
441
|
+
artifactStorage: storage,
|
|
442
|
+
executionId: "exec-1",
|
|
443
|
+
maxInlineBytes: 256,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const out = status.subAgentExecutions[0].messages[0].toolCalls[0];
|
|
447
|
+
expect(out.outputRef?.isImage).toBe(true);
|
|
448
|
+
expect(out.result).not.toContain(BIG_BASE64_IMAGE);
|
|
449
|
+
expect(uploads).toHaveLength(1);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it("enforceStatusSizeLimit elides oversized sub-agent tool results", () => {
|
|
453
|
+
const big = "Z".repeat(50_000);
|
|
454
|
+
const tc = create(ToolCallSchema, { id: "tc-sub-big", name: "Shell", result: big });
|
|
455
|
+
const status = statusWithSubAgentToolCall(tc);
|
|
456
|
+
expect(encodedSize(status)).toBeGreaterThan(40_000);
|
|
457
|
+
|
|
458
|
+
const elided = enforceStatusSizeLimit(status, 4_000);
|
|
459
|
+
|
|
460
|
+
expect(elided).toBe(true);
|
|
461
|
+
expect(encodedSize(status)).toBeLessThanOrEqual(4_000);
|
|
462
|
+
expect(status.subAgentExecutions[0].messages[0].toolCalls[0].result)
|
|
463
|
+
.not.toBe(big);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it("enforceStatusSizeLimit last-resort elides oversized sub-agent message content", () => {
|
|
467
|
+
const status = create(AgentExecutionStatusSchema, {
|
|
468
|
+
subAgentExecutions: [
|
|
469
|
+
create(SubAgentExecutionSchema, {
|
|
470
|
+
id: "sa-2",
|
|
471
|
+
messages: [create(AgentMessageSchema, { content: "W".repeat(50_000) })],
|
|
472
|
+
}),
|
|
473
|
+
],
|
|
474
|
+
});
|
|
475
|
+
expect(encodedSize(status)).toBeGreaterThan(40_000);
|
|
476
|
+
|
|
477
|
+
const elided = enforceStatusSizeLimit(status, 8_000);
|
|
478
|
+
|
|
479
|
+
expect(elided).toBe(true);
|
|
480
|
+
expect(encodedSize(status)).toBeLessThanOrEqual(8_000);
|
|
481
|
+
expect(status.subAgentExecutions[0].messages[0].content).toContain("elided");
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
|
|
229
485
|
describe("enforceStatusSizeLimit", () => {
|
|
230
486
|
it("returns false and changes nothing when under the limit", () => {
|
|
231
487
|
const tc = create(ToolCallSchema, { id: "tc", name: "Read", result: "tiny" });
|
|
@@ -253,4 +509,305 @@ describe("enforceStatusSizeLimit", () => {
|
|
|
253
509
|
expect(elided).toBe(true);
|
|
254
510
|
expect(encodedSize(status)).toBeLessThanOrEqual(4_000);
|
|
255
511
|
});
|
|
512
|
+
|
|
513
|
+
it("marks a size-elided file-review change SIZE_ELIDED and PARTIAL_BLOCKED (doc 15)", () => {
|
|
514
|
+
const big = "Z".repeat(50_000);
|
|
515
|
+
const ctx: ChangeSetContext = {
|
|
516
|
+
changeSetId: "exec-1:0",
|
|
517
|
+
turnId: "turn-0",
|
|
518
|
+
harnessId: "deep-agent",
|
|
519
|
+
timestamp: "2026-07-01T00:00:00Z",
|
|
520
|
+
};
|
|
521
|
+
const change = buildCapturedFileChange({
|
|
522
|
+
id: "fc-big",
|
|
523
|
+
pathBefore: "src/big.ts",
|
|
524
|
+
pathAfter: "src/big.ts",
|
|
525
|
+
kind: FileChangeKind.MODIFY,
|
|
526
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
527
|
+
before: big,
|
|
528
|
+
after: big,
|
|
529
|
+
});
|
|
530
|
+
// Reviewable at capture time: no reason, complete.
|
|
531
|
+
expect(change.blockedReason).toBe(FileReviewBlockReason.UNSPECIFIED);
|
|
532
|
+
expect(change.diffComplete).toBe(true);
|
|
533
|
+
|
|
534
|
+
const status = create(AgentExecutionStatusSchema, {});
|
|
535
|
+
appendFileReviewEvents(status, "exec-1", [
|
|
536
|
+
buildCandidateCapturedEvent(ctx, undefined, [change]),
|
|
537
|
+
]);
|
|
538
|
+
expect(encodedSize(status)).toBeGreaterThan(90_000);
|
|
539
|
+
|
|
540
|
+
const elided = enforceStatusSizeLimit(status, 4_000);
|
|
541
|
+
expect(elided).toBe(true);
|
|
542
|
+
|
|
543
|
+
const ev = status.fileReviewEventStream?.events.find(
|
|
544
|
+
(e) => e.payload.case === "candidateCaptured",
|
|
545
|
+
);
|
|
546
|
+
expect(ev?.payload.case).toBe("candidateCaptured");
|
|
547
|
+
if (ev?.payload.case === "candidateCaptured") {
|
|
548
|
+
const dropped = ev.payload.value.changes[0];
|
|
549
|
+
// Bodies dropped, marked incomplete, and the honest cause recorded.
|
|
550
|
+
expect(dropped.before).toBeUndefined();
|
|
551
|
+
expect(dropped.after).toBeUndefined();
|
|
552
|
+
expect(dropped.diffComplete).toBe(false);
|
|
553
|
+
expect(dropped.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
|
|
554
|
+
expect(ev.payload.value.diffCompleteness).toBe(DiffCompleteness.PARTIAL_BLOCKED);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it("downgrades a BINARY_SUMMARY_ONLY set to PARTIAL_BLOCKED when size-elision drops a text body", () => {
|
|
559
|
+
// A set whose only blocker is a binary file (BINARY_SUMMARY_ONLY) plus a
|
|
560
|
+
// large-but-reviewable text file. When the text body is elided it becomes a
|
|
561
|
+
// non-binary incompleteness, so the shared re-derivation must downgrade the
|
|
562
|
+
// whole set to PARTIAL_BLOCKED (it is no longer binary-only).
|
|
563
|
+
const big = "Z".repeat(50_000);
|
|
564
|
+
const ctx: ChangeSetContext = {
|
|
565
|
+
changeSetId: "exec-3:0",
|
|
566
|
+
turnId: "turn-0",
|
|
567
|
+
harnessId: "deep-agent",
|
|
568
|
+
timestamp: "2026-07-01T00:00:00Z",
|
|
569
|
+
};
|
|
570
|
+
const binary = buildCapturedFileChange({
|
|
571
|
+
id: "fc-bin",
|
|
572
|
+
pathBefore: "",
|
|
573
|
+
pathAfter: "assets/logo.png",
|
|
574
|
+
kind: FileChangeKind.ADD,
|
|
575
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
576
|
+
after: { kind: "binary", sha256: "sha-bin" },
|
|
577
|
+
diffComplete: false,
|
|
578
|
+
});
|
|
579
|
+
const bigText = buildCapturedFileChange({
|
|
580
|
+
id: "fc-text",
|
|
581
|
+
pathBefore: "src/big.ts",
|
|
582
|
+
pathAfter: "src/big.ts",
|
|
583
|
+
kind: FileChangeKind.MODIFY,
|
|
584
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
585
|
+
before: big,
|
|
586
|
+
after: big,
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
const status = create(AgentExecutionStatusSchema, {});
|
|
590
|
+
appendFileReviewEvents(status, "exec-3", [
|
|
591
|
+
buildCandidateCapturedEvent(ctx, undefined, [binary, bigText]),
|
|
592
|
+
]);
|
|
593
|
+
// Before elision: binary is the only blocker.
|
|
594
|
+
const before = status.fileReviewEventStream?.events.find(
|
|
595
|
+
(e) => e.payload.case === "candidateCaptured",
|
|
596
|
+
);
|
|
597
|
+
if (before?.payload.case === "candidateCaptured") {
|
|
598
|
+
expect(before.payload.value.diffCompleteness).toBe(DiffCompleteness.BINARY_SUMMARY_ONLY);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
expect(enforceStatusSizeLimit(status, 4_000)).toBe(true);
|
|
602
|
+
|
|
603
|
+
const after = status.fileReviewEventStream?.events.find(
|
|
604
|
+
(e) => e.payload.case === "candidateCaptured",
|
|
605
|
+
);
|
|
606
|
+
if (after?.payload.case === "candidateCaptured") {
|
|
607
|
+
const cs = after.payload.value;
|
|
608
|
+
const text = cs.changes.find((c) => c.id === "fc-text");
|
|
609
|
+
const bin = cs.changes.find((c) => c.id === "fc-bin");
|
|
610
|
+
// The text body is dropped (now a non-binary incompleteness); the binary
|
|
611
|
+
// side is body-less already, so it is untouched and keeps is_binary.
|
|
612
|
+
expect(text?.diffComplete).toBe(false);
|
|
613
|
+
expect(text?.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
|
|
614
|
+
expect(bin?.after?.isBinary).toBe(true);
|
|
615
|
+
expect(cs.diffCompleteness).toBe(DiffCompleteness.PARTIAL_BLOCKED);
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it("does not overwrite a more specific reason (SECRET_WITHHELD survives size elision)", () => {
|
|
620
|
+
// A secret entry is content-less, so it can't be size-elided; but guard the
|
|
621
|
+
// precedence explicitly: if a reason is already set, the size backstop keeps it.
|
|
622
|
+
const big = "Z".repeat(50_000);
|
|
623
|
+
const ctx: ChangeSetContext = {
|
|
624
|
+
changeSetId: "exec-2:0",
|
|
625
|
+
turnId: "turn-0",
|
|
626
|
+
harnessId: "deep-agent",
|
|
627
|
+
timestamp: "2026-07-01T00:00:00Z",
|
|
628
|
+
};
|
|
629
|
+
const secretButLarge = buildCapturedFileChange({
|
|
630
|
+
id: "fc-x",
|
|
631
|
+
pathBefore: "big.env",
|
|
632
|
+
pathAfter: "big.env",
|
|
633
|
+
kind: FileChangeKind.MODIFY,
|
|
634
|
+
captureClass: FileCaptureClass.GIT_IGNORED_CAPTURED,
|
|
635
|
+
before: big,
|
|
636
|
+
after: big,
|
|
637
|
+
diffComplete: false,
|
|
638
|
+
blockedReason: FileReviewBlockReason.SECRET_WITHHELD,
|
|
639
|
+
});
|
|
640
|
+
const status = create(AgentExecutionStatusSchema, {});
|
|
641
|
+
appendFileReviewEvents(status, "exec-2", [
|
|
642
|
+
buildCandidateCapturedEvent(ctx, undefined, [secretButLarge]),
|
|
643
|
+
]);
|
|
644
|
+
enforceStatusSizeLimit(status, 4_000);
|
|
645
|
+
const ev = status.fileReviewEventStream?.events.find(
|
|
646
|
+
(e) => e.payload.case === "candidateCaptured",
|
|
647
|
+
);
|
|
648
|
+
if (ev?.payload.case === "candidateCaptured") {
|
|
649
|
+
expect(ev.payload.value.changes[0].blockedReason).toBe(
|
|
650
|
+
FileReviewBlockReason.SECRET_WITHHELD,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
});
|
|
256
654
|
});
|
|
655
|
+
|
|
656
|
+
describe("offloadCandidateChangesToFit", () => {
|
|
657
|
+
function statusWithCandidate(
|
|
658
|
+
changes: CapturedFileChange[],
|
|
659
|
+
executionId: string,
|
|
660
|
+
): AgentExecutionStatus {
|
|
661
|
+
const ctx: ChangeSetContext = {
|
|
662
|
+
changeSetId: `${executionId}:0`,
|
|
663
|
+
turnId: "turn-0",
|
|
664
|
+
harnessId: "deep-agent",
|
|
665
|
+
timestamp: "2026-07-01T00:00:00Z",
|
|
666
|
+
};
|
|
667
|
+
const status = create(AgentExecutionStatusSchema, {});
|
|
668
|
+
appendFileReviewEvents(status, executionId, [
|
|
669
|
+
buildCandidateCapturedEvent(ctx, undefined, changes),
|
|
670
|
+
]);
|
|
671
|
+
return status;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function candidateOf(status: AgentExecutionStatus) {
|
|
675
|
+
const ev = status.fileReviewEventStream?.events.find(
|
|
676
|
+
(e) => e.payload.case === "candidateCaptured",
|
|
677
|
+
);
|
|
678
|
+
if (ev?.payload.case !== "candidateCaptured") throw new Error("no candidate event");
|
|
679
|
+
return ev.payload.value;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function textChange(id: string, path: string, before: string, after: string): CapturedFileChange {
|
|
683
|
+
return buildCapturedFileChange({
|
|
684
|
+
id,
|
|
685
|
+
pathBefore: before === "" ? "" : path,
|
|
686
|
+
pathAfter: after === "" ? "" : path,
|
|
687
|
+
kind: FileChangeKind.MODIFY,
|
|
688
|
+
captureClass: FileCaptureClass.GIT_TRACKED,
|
|
689
|
+
...(before !== "" ? { before } : {}),
|
|
690
|
+
...(after !== "" ? { after } : {}),
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
it("offloads oversized captured bodies to refs so they stay reviewable (no SIZE_ELIDED)", async () => {
|
|
695
|
+
const { storage, uploads } = makeFakeStorage();
|
|
696
|
+
// Two mid-sized files, each under the 128 KiB per-file cap but summing well
|
|
697
|
+
// past the injected soft cap — the exact case the per-item pass misses. The
|
|
698
|
+
// cap sits above the ~4 KB-per-side preview floor a ref keeps, so all four
|
|
699
|
+
// oversized sides can be offloaded and the status still lands under it.
|
|
700
|
+
const a = textChange("exec-fit:0:a.ts", "a.ts", "A".repeat(30_000), "A".repeat(50_000));
|
|
701
|
+
const b = textChange("exec-fit:0:b.ts", "b.ts", "B".repeat(40_000), "B".repeat(60_000));
|
|
702
|
+
const status = statusWithCandidate([a, b], "exec-fit");
|
|
703
|
+
expect(candidateOf(status).diffCompleteness).toBe(DiffCompleteness.COMPLETE);
|
|
704
|
+
expect(encodedSize(status)).toBeGreaterThan(150_000);
|
|
705
|
+
|
|
706
|
+
const offloaded = await offloadCandidateChangesToFit(
|
|
707
|
+
status,
|
|
708
|
+
{ artifactStorage: storage, executionId: "exec-fit" },
|
|
709
|
+
20_000,
|
|
710
|
+
);
|
|
711
|
+
expect(offloaded).toBe(true);
|
|
712
|
+
expect(encodedSize(status)).toBeLessThanOrEqual(20_000);
|
|
713
|
+
|
|
714
|
+
const cs = candidateOf(status);
|
|
715
|
+
for (const c of cs.changes) {
|
|
716
|
+
// Every remaining side is a retrievable ref (never dropped, never left large-inline).
|
|
717
|
+
if (c.before) expect(c.before.body.case).toBe("ref");
|
|
718
|
+
if (c.after) expect(c.after.body.case).toBe("ref");
|
|
719
|
+
// Reviewability preserved: complete, no block reason.
|
|
720
|
+
expect(c.diffComplete).toBe(true);
|
|
721
|
+
expect(c.blockedReason).toBe(FileReviewBlockReason.UNSPECIFIED);
|
|
722
|
+
}
|
|
723
|
+
// The set stays COMPLETE (approvable), never PARTIAL_BLOCKED.
|
|
724
|
+
expect(cs.diffCompleteness).toBe(DiffCompleteness.COMPLETE);
|
|
725
|
+
// Keys follow the canonical maybeOffloadCandidateChanges convention.
|
|
726
|
+
expect(uploads.some((u) => u.key === "artifacts/exec-fit/filereview/exec-fit:0:a.ts.after.txt")).toBe(true);
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
it("offloads biggest-first and stops as soon as the status fits", async () => {
|
|
730
|
+
const { storage, uploads } = makeFakeStorage();
|
|
731
|
+
const big = textChange("exec-bf:0:big.ts", "big.ts", "", "B".repeat(60_000));
|
|
732
|
+
const small = textChange("exec-bf:0:small.ts", "small.ts", "", "s".repeat(2_000));
|
|
733
|
+
const status = statusWithCandidate([small, big], "exec-bf");
|
|
734
|
+
// Cap chosen so offloading only the big side gets under it.
|
|
735
|
+
const cap = encodedSize(status) - 40_000;
|
|
736
|
+
|
|
737
|
+
await offloadCandidateChangesToFit(
|
|
738
|
+
status,
|
|
739
|
+
{ artifactStorage: storage, executionId: "exec-bf" },
|
|
740
|
+
cap,
|
|
741
|
+
);
|
|
742
|
+
|
|
743
|
+
const cs = candidateOf(status);
|
|
744
|
+
const bigC = cs.changes.find((c) => c.id === "exec-bf:0:big.ts");
|
|
745
|
+
const smallC = cs.changes.find((c) => c.id === "exec-bf:0:small.ts");
|
|
746
|
+
// The big body was offloaded; the small one was never touched (still inline).
|
|
747
|
+
expect(bigC?.after?.body.case).toBe("ref");
|
|
748
|
+
expect(smallC?.after?.body.case).toBe("inline");
|
|
749
|
+
expect(uploads).toHaveLength(1);
|
|
750
|
+
expect(uploads[0].key).toBe("artifacts/exec-bf/filereview/exec-bf:0:big.ts.after.txt");
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it("is idempotent: an already-offloaded ref side is skipped, never re-uploaded", async () => {
|
|
754
|
+
const { storage, uploads } = makeFakeStorage();
|
|
755
|
+
const c = textChange("exec-idem:0:x.ts", "x.ts", "", "X".repeat(40_000));
|
|
756
|
+
const status = statusWithCandidate([c], "exec-idem");
|
|
757
|
+
const ctx = { artifactStorage: storage, executionId: "exec-idem" };
|
|
758
|
+
|
|
759
|
+
// A tiny cap the ref can never satisfy forces full iteration on both passes.
|
|
760
|
+
const first = await offloadCandidateChangesToFit(status, ctx, 100);
|
|
761
|
+
expect(first).toBe(true);
|
|
762
|
+
expect(uploads).toHaveLength(1);
|
|
763
|
+
expect(candidateOf(status).changes[0].after?.body.case).toBe("ref");
|
|
764
|
+
|
|
765
|
+
// Second pass over the same status: the side is a ref, so it is not collected
|
|
766
|
+
// and nothing is re-uploaded.
|
|
767
|
+
const second = await offloadCandidateChangesToFit(status, ctx, 100);
|
|
768
|
+
expect(second).toBe(false);
|
|
769
|
+
expect(uploads).toHaveLength(1);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
it("falls through to the SIZE_ELIDED backstop when the upload fails", async () => {
|
|
773
|
+
const { storage } = makeFakeStorage();
|
|
774
|
+
storage.upload.mockRejectedValue(new Error("storage down"));
|
|
775
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
776
|
+
const c = textChange("exec-fail:0:y.ts", "y.ts", "", "Y".repeat(50_000));
|
|
777
|
+
const status = statusWithCandidate([c], "exec-fail");
|
|
778
|
+
|
|
779
|
+
const offloaded = await offloadCandidateChangesToFit(
|
|
780
|
+
status,
|
|
781
|
+
{ artifactStorage: storage, executionId: "exec-fail" },
|
|
782
|
+
4_000,
|
|
783
|
+
);
|
|
784
|
+
// Nothing offloaded; the body is left inline for the backstop, not dropped here.
|
|
785
|
+
expect(offloaded).toBe(false);
|
|
786
|
+
expect(candidateOf(status).changes[0].after?.body.case).toBe("inline");
|
|
787
|
+
|
|
788
|
+
// The storage-less backstop then drops it and records the honest cause.
|
|
789
|
+
expect(enforceStatusSizeLimit(status, 4_000)).toBe(true);
|
|
790
|
+
const dropped = candidateOf(status).changes[0];
|
|
791
|
+
expect(dropped.after).toBeUndefined();
|
|
792
|
+
expect(dropped.diffComplete).toBe(false);
|
|
793
|
+
expect(dropped.blockedReason).toBe(FileReviewBlockReason.SIZE_ELIDED);
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
it("only offloads file-review bodies, never tool-call content", async () => {
|
|
797
|
+
const { storage, uploads } = makeFakeStorage();
|
|
798
|
+
const tc = create(ToolCallSchema, { id: "tc-1", name: "Read", result: "R".repeat(50_000) });
|
|
799
|
+
const status = statusWithToolCall(tc);
|
|
800
|
+
expect(encodedSize(status)).toBeGreaterThan(40_000);
|
|
801
|
+
|
|
802
|
+
const offloaded = await offloadCandidateChangesToFit(
|
|
803
|
+
status,
|
|
804
|
+
{ artifactStorage: storage, executionId: "exec-tc" },
|
|
805
|
+
4_000,
|
|
806
|
+
);
|
|
807
|
+
expect(offloaded).toBe(false);
|
|
808
|
+
expect(uploads).toHaveLength(0);
|
|
809
|
+
// The tool-call result is untouched (that is offloadOversizedToolOutputs' job).
|
|
810
|
+
expect(status.messages[0].toolCalls[0].result).toHaveLength(50_000);
|
|
811
|
+
});
|
|
812
|
+
});
|
|
813
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { makeInMemoryArtifactStorage } from "../../__test-utils__/fake-artifact-storage.js";
|
|
2
3
|
import { ConnectError, Code } from "@connectrpc/connect";
|
|
3
4
|
import { create } from "@bufbuild/protobuf";
|
|
4
5
|
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
@@ -144,13 +145,11 @@ describe("persistStatus", () => {
|
|
|
144
145
|
|
|
145
146
|
it("offloads oversized tool outputs before persisting when given an offload context", async () => {
|
|
146
147
|
const uploaded: string[] = [];
|
|
148
|
+
const { storage: offloadStorage } = makeInMemoryArtifactStorage({ urlBase: "https://artifacts.local/" });
|
|
149
|
+
offloadStorage.upload.mockImplementation(async (key: string) => { uploaded.push(key); return key; });
|
|
147
150
|
const offload = {
|
|
148
151
|
executionId: "exec-off",
|
|
149
|
-
artifactStorage:
|
|
150
|
-
upload: vi.fn(async (key: string) => { uploaded.push(key); return key; }),
|
|
151
|
-
getDownloadUrl: vi.fn(async (key: string) => `https://artifacts.local/${key}`),
|
|
152
|
-
exists: vi.fn(async () => true),
|
|
153
|
-
},
|
|
152
|
+
artifactStorage: offloadStorage,
|
|
154
153
|
maxInlineBytes: 256,
|
|
155
154
|
};
|
|
156
155
|
const mockClient = {
|