@stigmer/runner 3.14.0 → 3.15.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__/execution-record-fixture.d.ts +47 -0
- package/dist/__test-utils__/execution-record-fixture.js +68 -0
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -0
- package/dist/__test-utils__/harness-contract/types.d.ts +166 -0
- package/dist/__test-utils__/harness-contract/types.js +49 -0
- package/dist/__test-utils__/harness-contract/types.js.map +1 -0
- package/dist/__test-utils__/hermetic-activity.d.ts +268 -0
- package/dist/__test-utils__/hermetic-activity.js +385 -0
- package/dist/__test-utils__/hermetic-activity.js.map +1 -0
- package/dist/__test-utils__/mock-client.d.ts +13 -0
- package/dist/__test-utils__/mock-client.js +45 -0
- package/dist/__test-utils__/mock-client.js.map +1 -0
- package/dist/__test-utils__/mock-workspace.d.ts +15 -0
- package/dist/__test-utils__/mock-workspace.js +21 -0
- package/dist/__test-utils__/mock-workspace.js.map +1 -0
- package/dist/__test-utils__/model-registry-fixture.d.ts +56 -0
- package/dist/__test-utils__/model-registry-fixture.js +66 -0
- package/dist/__test-utils__/model-registry-fixture.js.map +1 -0
- package/dist/__test-utils__/turn-input-fixture.d.ts +60 -0
- package/dist/__test-utils__/turn-input-fixture.js +118 -0
- package/dist/__test-utils__/turn-input-fixture.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.d.ts +107 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js +344 -0
- package/dist/activities/execute-cursor/__test-utils__/contract-subject.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.d.ts +20 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js +58 -0
- package/dist/activities/execute-cursor/__test-utils__/cursor-hook-harness.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js +2 -32
- package/dist/activities/execute-cursor/__test-utils__/gateway-substrate.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +148 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +257 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +183 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +262 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +138 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +191 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
- package/dist/activities/execute-cursor/adapter.d.ts +55 -0
- package/dist/activities/execute-cursor/adapter.js +119 -0
- package/dist/activities/execute-cursor/adapter.js.map +1 -0
- package/dist/activities/execute-cursor/approval-state.d.ts +2 -2
- package/dist/activities/execute-cursor/approval-state.js.map +1 -1
- package/dist/activities/execute-cursor/capture-flow.d.ts +15 -30
- package/dist/activities/execute-cursor/capture-flow.js +17 -26
- package/dist/activities/execute-cursor/capture-flow.js.map +1 -1
- package/dist/activities/execute-cursor/cursor-capabilities.d.ts +23 -0
- package/dist/activities/execute-cursor/cursor-capabilities.js +33 -0
- package/dist/activities/execute-cursor/cursor-capabilities.js.map +1 -0
- package/dist/activities/execute-cursor/cursor-mcp-config.d.ts +2 -2
- package/dist/activities/execute-cursor/cursor-mcp-config.js.map +1 -1
- package/dist/activities/execute-cursor/fetch-interceptor.d.ts +15 -25
- package/dist/activities/execute-cursor/fetch-interceptor.js +39 -33
- package/dist/activities/execute-cursor/fetch-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/http2-interceptor.d.ts +21 -17
- package/dist/activities/execute-cursor/http2-interceptor.js +38 -31
- package/dist/activities/execute-cursor/http2-interceptor.js.map +1 -1
- package/dist/activities/execute-cursor/message-translator.d.ts +28 -19
- package/dist/activities/execute-cursor/message-translator.js +76 -27
- package/dist/activities/execute-cursor/message-translator.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +180 -11
- package/dist/activities/execute-cursor/prompt-builder.js +171 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/rejection-capture.d.ts +3 -6
- package/dist/activities/execute-cursor/rejection-capture.js +5 -7
- package/dist/activities/execute-cursor/rejection-capture.js.map +1 -1
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +1 -4
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/turn-boundary.d.ts +1 -1
- package/dist/activities/execute-cursor/turn-settle.d.ts +49 -0
- package/dist/activities/execute-cursor/turn-settle.js +507 -0
- package/dist/activities/execute-cursor/turn-settle.js.map +1 -0
- package/dist/activities/execute-cursor/turn-setup.d.ts +173 -0
- package/dist/activities/execute-cursor/turn-setup.js +496 -0
- package/dist/activities/execute-cursor/turn-setup.js.map +1 -0
- package/dist/activities/execute-cursor/turn-stream.d.ts +41 -71
- package/dist/activities/execute-cursor/turn-stream.js +70 -135
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-cursor/turn.d.ts +43 -0
- package/dist/activities/execute-cursor/turn.js +222 -0
- package/dist/activities/execute-cursor/turn.js.map +1 -0
- package/dist/activities/execute-cursor/usage-pricing.d.ts +38 -0
- package/dist/activities/execute-cursor/usage-pricing.js +50 -0
- package/dist/activities/execute-cursor/usage-pricing.js.map +1 -0
- package/dist/activities/execute-deep-agent/approval-file-change.d.ts +1 -1
- package/dist/activities/execute-deep-agent/approval-file-change.js +1 -1
- package/dist/activities/execute-deep-agent/environment.js +1 -1
- package/dist/activities/execute-deep-agent/environment.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +3 -2
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/activities/hydrate-workflow-execution.js +1 -1
- package/dist/activities/hydrate-workflow-execution.js.map +1 -1
- package/dist/config.d.ts +15 -0
- package/dist/config.js.map +1 -1
- package/dist/harness/capabilities.d.ts +87 -0
- package/dist/harness/capabilities.js +37 -0
- package/dist/harness/capabilities.js.map +1 -0
- package/dist/harness/persist-chokepoint.d.ts +74 -0
- package/dist/harness/persist-chokepoint.js +88 -0
- package/dist/harness/persist-chokepoint.js.map +1 -0
- package/dist/harness/registry.d.ts +106 -0
- package/dist/harness/registry.js +168 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/run-turn.d.ts +53 -0
- package/dist/harness/run-turn.js +528 -0
- package/dist/harness/run-turn.js.map +1 -0
- package/dist/harness/stop-controller.d.ts +50 -0
- package/dist/harness/stop-controller.js +62 -0
- package/dist/harness/stop-controller.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +141 -0
- package/dist/harness/terminal-table.js +239 -0
- package/dist/harness/terminal-table.js.map +1 -0
- package/dist/harness/turn-context.d.ts +523 -0
- package/dist/harness/turn-context.js +739 -0
- package/dist/harness/turn-context.js.map +1 -0
- package/dist/harness/types.d.ts +448 -0
- package/dist/harness/types.js +64 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/usage-accumulator.d.ts +68 -0
- package/dist/harness/usage-accumulator.js +101 -0
- package/dist/harness/usage-accumulator.js.map +1 -0
- package/dist/harness-adapters.d.ts +27 -0
- package/dist/harness-adapters.js +28 -0
- package/dist/harness-adapters.js.map +1 -0
- package/dist/main.js.map +1 -1
- package/dist/runner-manager.js +64 -48
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +5 -4
- package/dist/runner.js +65 -64
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +1 -1
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/attachment-resolver.d.ts +2 -2
- package/dist/{activities/execute-cursor → shared}/attachment-resolver.js +5 -5
- package/dist/shared/attachment-resolver.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/blueprint-resolver.d.ts +7 -5
- package/dist/{activities/execute-cursor → shared}/blueprint-resolver.js +3 -63
- package/dist/shared/blueprint-resolver.js.map +1 -0
- package/dist/shared/channel-attachment.d.ts +1 -1
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/cost-guard.d.ts +5 -1
- package/dist/{activities/execute-cursor → shared}/cost-guard.js +5 -1
- package/dist/shared/cost-guard.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/env-resolver.d.ts +1 -1
- package/dist/shared/env-resolver.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/exact-apply.d.ts +1 -1
- package/dist/{activities/execute-cursor → shared}/exact-apply.js +6 -6
- package/dist/shared/exact-apply.js.map +1 -0
- package/dist/shared/execution-context.d.ts +35 -0
- package/dist/shared/execution-context.js +38 -0
- package/dist/shared/execution-context.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/extract-structured-output.d.ts +5 -2
- package/dist/{activities/execute-cursor → shared}/extract-structured-output.js +9 -6
- package/dist/shared/extract-structured-output.js.map +1 -0
- package/dist/shared/filereview/capture.d.ts +1 -1
- package/dist/shared/filereview/capture.js +1 -1
- package/dist/shared/heartbeat.d.ts +13 -11
- package/dist/shared/heartbeat.js +14 -29
- package/dist/shared/heartbeat.js.map +1 -1
- package/dist/{activities/execute-cursor → shared}/persist-decision.d.ts +5 -2
- package/dist/{activities/execute-cursor → shared}/persist-decision.js +4 -1
- package/dist/shared/persist-decision.js.map +1 -0
- package/dist/{activities/execute-cursor → shared}/skill-resolver.d.ts +16 -3
- package/dist/{activities/execute-cursor → shared}/skill-resolver.js +12 -4
- package/dist/shared/skill-resolver.js.map +1 -0
- package/dist/shared/subagent-rows.d.ts +21 -0
- package/dist/shared/subagent-rows.js +34 -0
- package/dist/shared/subagent-rows.js.map +1 -0
- package/dist/shared/worker-shutdown.d.ts +43 -16
- package/dist/shared/worker-shutdown.js +28 -13
- package/dist/shared/worker-shutdown.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +1 -1
- package/dist/shared/workspace/platform-dir.js +1 -1
- package/dist/shared/workspace/session-provision.d.ts +54 -0
- package/dist/{activities/execute-cursor/workspace-provision.js → shared/workspace/session-provision.js} +15 -12
- package/dist/shared/workspace/session-provision.js.map +1 -0
- package/package.json +4 -4
- package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +325 -0
- package/src/__test-utils__/config-fixture.ts +63 -0
- package/src/__test-utils__/execution-record-fixture.ts +104 -0
- package/src/__test-utils__/harness-boot-order-child.ts +58 -0
- package/src/__test-utils__/harness-contract/contract.ts +581 -0
- package/src/__test-utils__/harness-contract/recording-sink.ts +107 -0
- package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +349 -0
- package/src/__test-utils__/harness-contract/types.ts +176 -0
- package/src/__test-utils__/hermetic-activity.ts +502 -0
- package/src/__test-utils__/model-registry-fixture.ts +72 -0
- package/src/__test-utils__/module-specifiers.ts +82 -0
- package/src/__test-utils__/proto-helpers.ts +25 -0
- package/src/__test-utils__/turn-input-fixture.ts +155 -0
- package/src/__tests__/harness-boot-order.test.ts +77 -0
- package/src/__tests__/harness-contract.test.ts +27 -0
- package/src/__tests__/runner-manager.test.ts +18 -0
- package/src/activities/execute-cursor/__test-utils__/__tests__/scripted-double.test.ts +102 -0
- package/src/activities/execute-cursor/__test-utils__/contract-subject.ts +396 -0
- package/src/activities/execute-cursor/__test-utils__/cursor-hook-harness.ts +66 -0
- package/src/activities/execute-cursor/__test-utils__/gateway-substrate.ts +2 -43
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +342 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +368 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +254 -0
- package/src/activities/execute-cursor/__tests__/adapter-is-temporal-free.test.ts +45 -0
- package/src/activities/execute-cursor/__tests__/approval-decisions-agree.test.ts +90 -0
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +7 -8
- package/src/activities/execute-cursor/__tests__/capture-flow.test.ts +28 -1
- package/src/activities/execute-cursor/__tests__/deny-gate-exact-apply.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/fetch-interceptor.test.ts +64 -6
- package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +40 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-transport-timeout.status.json +48 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +18 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-cancelled.status.json +36 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/run-error-non-retryable.status.json +37 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/sdk-error-at-create.status.json +38 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +59 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +79 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +42 -0
- package/src/activities/execute-cursor/__tests__/hermetic/harness-contract.test.ts +170 -0
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +205 -0
- package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
- package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
- package/src/activities/execute-cursor/__tests__/hermetic/run-wait-arms.test.ts +286 -0
- package/src/activities/execute-cursor/__tests__/hermetic/stream-self-stop-arms.test.ts +332 -0
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +193 -0
- package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
- package/src/activities/execute-cursor/__tests__/hermetic/unattributed-hook-block.test.ts +185 -0
- package/src/activities/execute-cursor/__tests__/hermetic/workspace-lock-timeout.test.ts +151 -0
- package/src/activities/execute-cursor/__tests__/http2-interceptor.test.ts +65 -35
- package/src/activities/execute-cursor/__tests__/message-translator.test.ts +0 -40
- package/src/activities/execute-cursor/__tests__/same-identity-reproposal.test.ts +171 -0
- package/src/activities/execute-cursor/__tests__/turn-stream.test.ts +118 -243
- package/src/activities/execute-cursor/__tests__/usage-pricing.test.ts +84 -0
- package/src/activities/execute-cursor/adapter.ts +138 -0
- package/src/activities/execute-cursor/approval-state.ts +2 -2
- package/src/activities/execute-cursor/capture-flow.ts +18 -41
- package/src/activities/execute-cursor/cursor-capabilities.ts +35 -0
- package/src/activities/execute-cursor/cursor-mcp-config.ts +2 -2
- package/src/activities/execute-cursor/fetch-interceptor.ts +48 -45
- package/src/activities/execute-cursor/http2-interceptor.ts +47 -34
- package/src/activities/execute-cursor/message-translator.ts +90 -45
- package/src/activities/execute-cursor/prompt-builder.ts +327 -12
- package/src/activities/execute-cursor/rejection-capture.ts +5 -12
- package/src/activities/execute-cursor/session-lifecycle.ts +1 -5
- package/src/activities/execute-cursor/turn-boundary.ts +1 -1
- package/src/activities/execute-cursor/turn-settle.ts +601 -0
- package/src/activities/execute-cursor/turn-setup.ts +645 -0
- package/src/activities/execute-cursor/turn-stream.ts +84 -210
- package/src/activities/execute-cursor/turn.ts +240 -0
- package/src/activities/execute-cursor/usage-pricing.ts +67 -0
- package/src/activities/execute-deep-agent/approval-file-change.ts +1 -1
- package/src/activities/execute-deep-agent/environment.ts +1 -1
- package/src/activities/execute-deep-agent/setup.ts +3 -2
- package/src/activities/hydrate-workflow-execution.ts +1 -1
- package/src/config.ts +13 -0
- package/src/harness/__tests__/import-direction.test.ts +168 -0
- package/src/harness/__tests__/persist-chokepoint.test.ts +146 -0
- package/src/harness/__tests__/registry.test.ts +168 -0
- package/src/harness/__tests__/run-turn.test.ts +165 -0
- package/src/harness/__tests__/stop-controller.test.ts +49 -0
- package/src/harness/__tests__/turn-context.test.ts +178 -0
- package/src/harness/__tests__/usage-accumulator.test.ts +83 -0
- package/src/harness/capabilities.ts +92 -0
- package/src/harness/persist-chokepoint.ts +112 -0
- package/src/harness/registry.ts +197 -0
- package/src/harness/run-turn.ts +618 -0
- package/src/harness/stop-controller.ts +83 -0
- package/src/harness/terminal-table.ts +276 -0
- package/src/harness/turn-context.ts +1028 -0
- package/src/harness/types.ts +473 -0
- package/src/harness/usage-accumulator.ts +122 -0
- package/src/harness-adapters.ts +30 -0
- package/src/main.ts +1 -1
- package/src/runner-manager.ts +68 -61
- package/src/runner.ts +67 -78
- package/src/shared/__tests__/attachment-naming.test.ts +1 -1
- package/src/{activities/execute-cursor → shared}/__tests__/attachment-resolver.test.ts +6 -4
- package/src/{activities/execute-cursor → shared}/__tests__/cost-guard.test.ts +2 -1
- package/src/{activities/execute-cursor → shared}/__tests__/exact-apply.test.ts +3 -3
- package/src/{activities/execute-cursor → shared}/__tests__/extract-structured-output.test.ts +8 -8
- package/src/{activities/execute-cursor → shared}/__tests__/persist-decision.test.ts +4 -2
- package/src/{activities/execute-cursor → shared}/__tests__/skill-resolver.test.ts +1 -1
- package/src/shared/__tests__/subagent-rows.test.ts +34 -0
- package/src/shared/__tests__/worker-shutdown.test.ts +44 -39
- package/src/shared/approval-policy.ts +1 -1
- package/src/{activities/execute-cursor → shared}/attachment-resolver.ts +6 -6
- package/src/{activities/execute-cursor → shared}/blueprint-resolver.ts +7 -78
- package/src/shared/channel-attachment.ts +1 -1
- package/src/{activities/execute-cursor → shared}/cost-guard.ts +5 -1
- package/src/{activities/execute-cursor → shared}/env-resolver.ts +1 -1
- package/src/{activities/execute-cursor → shared}/exact-apply.ts +7 -7
- package/src/shared/execution-context.ts +45 -0
- package/src/{activities/execute-cursor → shared}/extract-structured-output.ts +10 -7
- package/src/shared/filereview/capture.ts +1 -1
- package/src/shared/heartbeat.ts +13 -33
- package/src/{activities/execute-cursor → shared}/persist-decision.ts +5 -2
- package/src/{activities/execute-cursor → shared}/skill-resolver.ts +20 -6
- package/src/shared/subagent-rows.ts +39 -0
- package/src/shared/worker-shutdown.ts +55 -25
- package/src/{activities/execute-cursor/__tests__/workspace-provision.test.ts → shared/workspace/__tests__/session-provision.test.ts} +25 -28
- package/src/shared/workspace/platform-dir.ts +1 -1
- package/src/{activities/execute-cursor/workspace-provision.ts → shared/workspace/session-provision.ts} +29 -16
- package/dist/activities/execute-cursor/attachment-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/blueprint-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/cost-guard.js.map +0 -1
- package/dist/activities/execute-cursor/env-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/exact-apply.js.map +0 -1
- package/dist/activities/execute-cursor/extract-structured-output.js.map +0 -1
- package/dist/activities/execute-cursor/index.d.ts +0 -209
- package/dist/activities/execute-cursor/index.js +0 -2345
- package/dist/activities/execute-cursor/index.js.map +0 -1
- package/dist/activities/execute-cursor/persist-decision.js.map +0 -1
- package/dist/activities/execute-cursor/skill-resolver.js.map +0 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +0 -89
- package/dist/activities/execute-cursor/usage-accumulator.js +0 -121
- package/dist/activities/execute-cursor/usage-accumulator.js.map +0 -1
- package/dist/activities/execute-cursor/workspace-provision.d.ts +0 -42
- package/dist/activities/execute-cursor/workspace-provision.js.map +0 -1
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +0 -164
- package/src/activities/execute-cursor/index.ts +0 -2876
- package/src/activities/execute-cursor/usage-accumulator.ts +0 -169
- /package/dist/{activities/execute-cursor → shared}/env-resolver.js +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic goldens: the `run.wait()` ARMS — every case of the terminal
|
|
3
|
+
* `switch (result.status)` other than `finished`, on a CREATED agent.
|
|
4
|
+
*
|
|
5
|
+
* Phase 13 of the activity maps what the SDK's `run.wait()` resolves into a
|
|
6
|
+
* terminal phase, and the `error` case is where the error classifier decides
|
|
7
|
+
* whether the runner tries again on its own. The three scenarios here pin the
|
|
8
|
+
* three outcomes of that decision table that the S0 net did not cover (the
|
|
9
|
+
* poisoned-handle recovery on a RESUMED agent is `recovery-fresh-agent.test.ts`):
|
|
10
|
+
*
|
|
11
|
+
* 1. A NON-RETRYABLE error (auth class): the classifier files it as
|
|
12
|
+
* `auth`, neither recovery spine fires, the turn RETURNS EXECUTION_FAILED
|
|
13
|
+
* with the classified error verbatim, and the agent is PARKED (Phase 13's
|
|
14
|
+
* terminal parks the handle whatever the phase — a bad key is not a bad
|
|
15
|
+
* handle). Golden `goldens/run-error-non-retryable.status.json`.
|
|
16
|
+
*
|
|
17
|
+
* 2. A NETWORK error on a created agent — the TRANSPORT-TIMEOUT recovery:
|
|
18
|
+
* the classifier files it as `network` + retryable, the activity closes
|
|
19
|
+
* the handle, resets the proxy sessions, `Agent.create`s a fresh agent,
|
|
20
|
+
* re-binds `harness_state_id`, RE-SENDS THE SAME PROMPT (not a rebuilt
|
|
21
|
+
* one — the created agent had no conversation to recover) through the
|
|
22
|
+
* identical stream loop, and ends COMPLETED on the replacement. At most
|
|
23
|
+
* once (`alreadyRetriedWithFreshAgent`). Golden
|
|
24
|
+
* `goldens/recovery-transport-timeout.status.json`.
|
|
25
|
+
*
|
|
26
|
+
* 3. An SDK-SIDE CANCEL: the run is cancelled from outside the runner (no
|
|
27
|
+
* runner flag set — not a pause, stall, cost cap or denial), the stream
|
|
28
|
+
* ends, the boundary passes, and `run.wait()` answers `cancelled`. The turn
|
|
29
|
+
* RETURNS EXECUTION_CANCELLED with no error and no system message, and the
|
|
30
|
+
* agent is parked. Golden `goldens/run-cancelled.status.json`.
|
|
31
|
+
*
|
|
32
|
+
* All three are RETURN arms: a Temporal retry would re-run the identical
|
|
33
|
+
* prompt against the same key, the same network, or the same cancelled run.
|
|
34
|
+
*
|
|
35
|
+
* Regenerate ONLY after a deliberate behavior change:
|
|
36
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
40
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
41
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
42
|
+
import { ExecutionPhase, MessageType } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
43
|
+
|
|
44
|
+
vi.mock("@cursor/sdk", async () =>
|
|
45
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
46
|
+
);
|
|
47
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
48
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
import {
|
|
52
|
+
ScriptedClock,
|
|
53
|
+
createHermeticEnvironment,
|
|
54
|
+
type HermeticEnvironment,
|
|
55
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
56
|
+
import { _parkedAgentCountForTests } from "../../agent-session-cache.js";
|
|
57
|
+
import { ScriptedCursorAgent, sdkEvents, step } from "../../__test-utils__/scripted-agent.js";
|
|
58
|
+
import {
|
|
59
|
+
FIXTURE,
|
|
60
|
+
SDK_CATALOG,
|
|
61
|
+
beginCursorScenario,
|
|
62
|
+
cursorExecutionRecord,
|
|
63
|
+
runCursorTurn,
|
|
64
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
65
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
66
|
+
|
|
67
|
+
const USER_MESSAGE = "List the open pull requests.";
|
|
68
|
+
const NEVER_SEEN = "This text must never reach the transcript.";
|
|
69
|
+
// An auth-class failure shape (error-classifier AUTH_PATTERNS: "unauthenticated",
|
|
70
|
+
// "invalid api key"). Terminal: retrying cannot mint a valid key.
|
|
71
|
+
const AUTH_ERROR = "UNAUTHENTICATED: invalid api key";
|
|
72
|
+
// A network-class failure shape (NETWORK_PATTERNS: "unavailable", "fetch failed",
|
|
73
|
+
// "econnreset"). Retryable: a fresh connection may succeed.
|
|
74
|
+
const TRANSPORT_ERROR = "UNAVAILABLE: fetch failed (ECONNRESET) while streaming the run";
|
|
75
|
+
const FINAL_TEXT = "There are three open pull requests: #12, #15 and #18.";
|
|
76
|
+
|
|
77
|
+
function systemMessages(status: { messages: { type: MessageType; content: string }[] }): string[] {
|
|
78
|
+
return status.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
describe("ExecuteCursor hermetic — the run.wait() arms on a created agent", () => {
|
|
82
|
+
let env: HermeticEnvironment;
|
|
83
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
84
|
+
const clock = new ScriptedClock();
|
|
85
|
+
|
|
86
|
+
beforeAll(() => {
|
|
87
|
+
env = createHermeticEnvironment();
|
|
88
|
+
registry = stubRegistryFetch();
|
|
89
|
+
clock.install();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterAll(() => {
|
|
93
|
+
clock.uninstall();
|
|
94
|
+
registry.restore();
|
|
95
|
+
env.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("a non-retryable error fails the turn with the classified error and parks the agent", async () => {
|
|
99
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
100
|
+
clock.reset(); // every golden in this file reads from :00
|
|
101
|
+
const agentId = "agent-hermetic-autherr-0001";
|
|
102
|
+
const runId = "run-hermetic-autherr-0001";
|
|
103
|
+
const ev = sdkEvents(agentId, runId);
|
|
104
|
+
const agent = new ScriptedCursorAgent({
|
|
105
|
+
agentId,
|
|
106
|
+
runIds: [runId],
|
|
107
|
+
observeStep: () => clock.tick(),
|
|
108
|
+
turns: [
|
|
109
|
+
[
|
|
110
|
+
step.event(ev.init()),
|
|
111
|
+
step.event(ev.assistant("Checking the repository.")),
|
|
112
|
+
step.errored({ result: AUTH_ERROR }),
|
|
113
|
+
],
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
117
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
118
|
+
|
|
119
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
120
|
+
const invocation = await runCursorTurn(scenario);
|
|
121
|
+
|
|
122
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
123
|
+
expect(invocation.outcome.kind, "a classified failure RETURNS").toBe("returned");
|
|
124
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
125
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
126
|
+
expect(record.persistedPhases).toEqual([
|
|
127
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
128
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
129
|
+
]);
|
|
130
|
+
const final = record.lastFullStatus!;
|
|
131
|
+
expect(final.error).toBe(`${AUTH_ERROR} [category=auth, source=sdk, retryable=false]`);
|
|
132
|
+
expect(final.completedAt).not.toBe("");
|
|
133
|
+
expect(systemMessages(final), "the classified arm appends no system message").toEqual([]);
|
|
134
|
+
|
|
135
|
+
// ── Assert: no recovery fired ────────────────────────────────────────────
|
|
136
|
+
expect(scenario.sdk.resolutions.map((r) => r.kind), "one create, no fresh agent").toEqual(["create"]);
|
|
137
|
+
expect(agent.sends).toHaveLength(1);
|
|
138
|
+
expect(agent.closeCalls, "a bad key is not a bad handle").toBe(0);
|
|
139
|
+
expect(_parkedAgentCountForTests(), "Phase 13 parks on every phase").toBe(1);
|
|
140
|
+
expect(record.sessionUpdates, "the one harness_state_id bind").toHaveLength(1);
|
|
141
|
+
|
|
142
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
143
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
144
|
+
expect(invocation.heartbeats.length).toBeGreaterThan(0);
|
|
145
|
+
|
|
146
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
147
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
148
|
+
await expect(json).toMatchFileSnapshot("./goldens/run-error-non-retryable.status.json");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("a network error on a created agent retries once on a fresh agent with the same prompt and completes", async () => {
|
|
152
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
153
|
+
clock.reset();
|
|
154
|
+
const staleId = "agent-hermetic-transport-stale-0001";
|
|
155
|
+
const freshId = "agent-hermetic-transport-fresh-0002";
|
|
156
|
+
const evStale = sdkEvents(staleId, "run-hermetic-transport-stale-0001");
|
|
157
|
+
const evFresh = sdkEvents(freshId, "run-hermetic-transport-fresh-0001");
|
|
158
|
+
const stale = new ScriptedCursorAgent({
|
|
159
|
+
agentId: staleId,
|
|
160
|
+
runIds: ["run-hermetic-transport-stale-0001"],
|
|
161
|
+
observeStep: () => clock.tick(),
|
|
162
|
+
turns: [
|
|
163
|
+
[
|
|
164
|
+
step.event(evStale.init()),
|
|
165
|
+
step.event(evStale.status("ERROR", TRANSPORT_ERROR)),
|
|
166
|
+
step.errored({ result: TRANSPORT_ERROR }),
|
|
167
|
+
],
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
const fresh = new ScriptedCursorAgent({
|
|
171
|
+
agentId: freshId,
|
|
172
|
+
runIds: ["run-hermetic-transport-fresh-0001"],
|
|
173
|
+
observeStep: () => clock.tick(),
|
|
174
|
+
turns: [
|
|
175
|
+
[
|
|
176
|
+
step.event(evFresh.init()),
|
|
177
|
+
step.event(evFresh.assistant(FINAL_TEXT)),
|
|
178
|
+
step.turnEnded({ inputTokens: 2_200, outputTokens: 30 }),
|
|
179
|
+
step.finished({ result: FINAL_TEXT, model: { id: FIXTURE.model, params: [] } }),
|
|
180
|
+
],
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
184
|
+
// Both agents are CREATED: the first by the turn, the second by the recovery.
|
|
185
|
+
const scenario = beginCursorScenario({
|
|
186
|
+
env,
|
|
187
|
+
clock,
|
|
188
|
+
record,
|
|
189
|
+
sdk: { agents: [stale, fresh], catalog: SDK_CATALOG },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
193
|
+
const invocation = await runCursorTurn(scenario);
|
|
194
|
+
|
|
195
|
+
// ── Assert: outcome ──────────────────────────────────────────────────────
|
|
196
|
+
expect(invocation.outcome.kind, "a recovered turn RETURNS like any completion").toBe("returned");
|
|
197
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
198
|
+
expect(slim.phase).toBe("EXECUTION_COMPLETED");
|
|
199
|
+
expect(slim.final_text).toBe(FINAL_TEXT);
|
|
200
|
+
expect(record.persistedPhases).toEqual([
|
|
201
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
202
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
203
|
+
]);
|
|
204
|
+
const final = record.lastFullStatus!;
|
|
205
|
+
expect(final.error, "the primary failure is superseded by the recovery").toBe("");
|
|
206
|
+
|
|
207
|
+
// ── Assert: the transport-recovery spine ─────────────────────────────────
|
|
208
|
+
expect(scenario.sdk.resolutions.map((r) => [r.kind, r.agentId])).toEqual([
|
|
209
|
+
["create", staleId],
|
|
210
|
+
["create", freshId],
|
|
211
|
+
]);
|
|
212
|
+
expect(stale.sends, "the first handle ran exactly once").toHaveLength(1);
|
|
213
|
+
expect(stale.closeCalls, "the first handle is disposed").toBe(1);
|
|
214
|
+
expect(fresh.sends, "the fresh agent ran the turn exactly once — no second retry").toHaveLength(1);
|
|
215
|
+
expect(fresh.closeCalls, "the fresh handle is parked, not closed").toBe(0);
|
|
216
|
+
expect(_parkedAgentCountForTests()).toBe(1);
|
|
217
|
+
// The SAME prompt is re-sent: a created agent had no conversation to
|
|
218
|
+
// rebuild from, unlike the poisoned-handle recovery's rebuilt prompt.
|
|
219
|
+
expect(String(fresh.sends[0].message)).toBe(String(stale.sends[0].message));
|
|
220
|
+
// Two harness_state_id binds: the primary agent, then the fresh one.
|
|
221
|
+
expect(record.sessionUpdates.map((s) => s.spec?.harnessStateId)).toEqual([staleId, freshId]);
|
|
222
|
+
|
|
223
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
224
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
225
|
+
|
|
226
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
227
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
228
|
+
await expect(json).toMatchFileSnapshot("./goldens/recovery-transport-timeout.status.json");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("an SDK-side cancel ends the turn CANCELLED with no error and parks the agent", async () => {
|
|
232
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
233
|
+
clock.reset();
|
|
234
|
+
const agentId = "agent-hermetic-sdkcancel-0001";
|
|
235
|
+
const runId = "run-hermetic-sdkcancel-0001";
|
|
236
|
+
const ev = sdkEvents(agentId, runId);
|
|
237
|
+
const agent = new ScriptedCursorAgent({
|
|
238
|
+
agentId,
|
|
239
|
+
runIds: [runId],
|
|
240
|
+
observeStep: () => clock.tick(),
|
|
241
|
+
turns: [
|
|
242
|
+
[
|
|
243
|
+
step.event(ev.init()),
|
|
244
|
+
step.event(ev.assistant("Looking at the pull requests.")),
|
|
245
|
+
// The run is cancelled from OUTSIDE the runner (Cursor's side): no
|
|
246
|
+
// runner flag is set, the stream ends, wait() answers "cancelled".
|
|
247
|
+
step.effect("the SDK cancels the run", ({ run }) => run.cancel()),
|
|
248
|
+
step.event(ev.assistant(NEVER_SEEN)),
|
|
249
|
+
step.finished({ result: NEVER_SEEN }),
|
|
250
|
+
],
|
|
251
|
+
],
|
|
252
|
+
});
|
|
253
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
254
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
255
|
+
|
|
256
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
257
|
+
const invocation = await runCursorTurn(scenario);
|
|
258
|
+
|
|
259
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
260
|
+
expect(invocation.outcome.kind, "a cancelled run RETURNS").toBe("returned");
|
|
261
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
262
|
+
expect(slim.phase).toBe("EXECUTION_CANCELLED");
|
|
263
|
+
expect(record.persistedPhases).toEqual([
|
|
264
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
265
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
266
|
+
]);
|
|
267
|
+
const final = record.lastFullStatus!;
|
|
268
|
+
expect(final.error, "a cancel is not an error").toBe("");
|
|
269
|
+
expect(final.completedAt, "the turn is complete").not.toBe("");
|
|
270
|
+
expect(systemMessages(final), "no copy is written for an SDK-side cancel").toEqual([]);
|
|
271
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN), "nothing after the cancel is processed").toBe(false);
|
|
272
|
+
|
|
273
|
+
// ── Assert: engine disposition (today's) ─────────────────────────────────
|
|
274
|
+
expect(agent.runs[0].cancelCalls, "the one cancel is the SDK's own").toHaveLength(1);
|
|
275
|
+
expect(agent.closeCalls).toBe(0);
|
|
276
|
+
expect(_parkedAgentCountForTests(), "Phase 13 parks on every phase").toBe(1);
|
|
277
|
+
expect(record.sessionUpdates).toHaveLength(1);
|
|
278
|
+
|
|
279
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
280
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
281
|
+
|
|
282
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
283
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
284
|
+
await expect(json).toMatchFileSnapshot("./goldens/run-cancelled.status.json");
|
|
285
|
+
});
|
|
286
|
+
});
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hermetic goldens: the three arms where the STREAM LOOP STOPS ITSELF and the
|
|
3
|
+
* activity RETURNS a terminal status — a stall, a cost cap, a platform stop.
|
|
4
|
+
*
|
|
5
|
+
* `resolvePreBoundaryTerminal` (index.ts) reads the flags the stream loop
|
|
6
|
+
* raised and maps each to a phase, a copy, an engine disposition and a
|
|
7
|
+
* throw-vs-return decision. The two THROW arms (user pause, worker shutdown)
|
|
8
|
+
* are `pause-vs-shutdown.test.ts`; these are the three RETURN arms, side by
|
|
9
|
+
* side because they differ in every column of that table:
|
|
10
|
+
*
|
|
11
|
+
* | arm | phase | run.cancel() | agent handle |
|
|
12
|
+
* |---------------|------------|--------------|--------------|
|
|
13
|
+
* | stall | FAILED | yes | parked |
|
|
14
|
+
* | cost cap | TERMINATED | yes | parked |
|
|
15
|
+
* | platform stop | COMPLETED | yes | parked |
|
|
16
|
+
*
|
|
17
|
+
* All three RETURN because a Temporal retry would re-run the identical prompt:
|
|
18
|
+
* a wedged agent would wedge again, an exhausted budget would be burnt again,
|
|
19
|
+
* a platform stop is the platform's decision.
|
|
20
|
+
*
|
|
21
|
+
* How each is triggered without a timer or a sleep:
|
|
22
|
+
* - STALL: the stall watchdog measures idle time with `Date.now()`, which the
|
|
23
|
+
* scripted clock fakes and only the script advances. An `effect` step ticks
|
|
24
|
+
* the clock past the stall window, then awaits the watchdog's own
|
|
25
|
+
* `run.cancel()` (the run's status flips to `cancelled`). The watchdog's
|
|
26
|
+
* poll interval is real (stallMs / 4), so the wait is sub-second and the
|
|
27
|
+
* reported idle seconds are exact.
|
|
28
|
+
* - COST CAP: a `turn-ended` usage delta prices above `max_cost_usd` on the
|
|
29
|
+
* fixture registry's round numbers; the loop breaks at the NEXT event.
|
|
30
|
+
* - PLATFORM STOP: the control plane answers STOP (`ExecutionRecord.controlSignal`)
|
|
31
|
+
* to the mid-stream persist that carries a COMPLETED tool call. That persist
|
|
32
|
+
* is the one point a scenario can rely on: the streaming scheduler paces
|
|
33
|
+
* persists on `performance.now()` — REAL time, not the scripted clock — so
|
|
34
|
+
* after its first-event update a text-only event persists only if 500 ms
|
|
35
|
+
* of wall time have passed, while a tool-call transition force-flushes
|
|
36
|
+
* (`contentDirty`) whatever the clock says.
|
|
37
|
+
*
|
|
38
|
+
* Engine disposition and cancellation since S2 M3 (entry 20260911.03): the
|
|
39
|
+
* adapter cancels the SDK run on EVERY stop (Q-M3-4; before, a platform stop
|
|
40
|
+
* broke the loop and left the run executing) and parks the handle on every
|
|
41
|
+
* non-failed exit (Q-S2-6; before, a stall dropped it — the adapter cannot
|
|
42
|
+
* tell a stall from a cost cap, and a wedged parked handle is caught by the
|
|
43
|
+
* poisoned-handle recovery on the next turn). The two assertions that moved
|
|
44
|
+
* say so on their line. The cost-cap golden was regenerated at M3b with a
|
|
45
|
+
* timestamp-only diff (every terminal stamp one scripted second earlier):
|
|
46
|
+
* the run is now cancelled the instant the signal aborts, before the SDK
|
|
47
|
+
* double pulls — and the clock ticks on — one more step.
|
|
48
|
+
*
|
|
49
|
+
* Regenerate ONLY after a deliberate behavior change:
|
|
50
|
+
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
54
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
55
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
56
|
+
import {
|
|
57
|
+
ExecutionControlSignal,
|
|
58
|
+
ExecutionPhase,
|
|
59
|
+
MessageType,
|
|
60
|
+
ToolCallStatus,
|
|
61
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
62
|
+
|
|
63
|
+
vi.mock("@cursor/sdk", async () =>
|
|
64
|
+
(await import("../../__test-utils__/scripted-sdk.js")).scriptedCursorSdkModule(),
|
|
65
|
+
);
|
|
66
|
+
vi.mock("../../../../client/stigmer-client.js", async () =>
|
|
67
|
+
(await import("../../../../__test-utils__/hermetic-activity.js")).hermeticStigmerClientModule(),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
import {
|
|
71
|
+
ScriptedClock,
|
|
72
|
+
createHermeticEnvironment,
|
|
73
|
+
type HermeticEnvironment,
|
|
74
|
+
} from "../../../../__test-utils__/hermetic-activity.js";
|
|
75
|
+
import { _parkedAgentCountForTests } from "../../agent-session-cache.js";
|
|
76
|
+
import { ScriptedCursorAgent, sdkEvents, step, type ScriptedRun } from "../../__test-utils__/scripted-agent.js";
|
|
77
|
+
import {
|
|
78
|
+
SDK_CATALOG,
|
|
79
|
+
beginCursorScenario,
|
|
80
|
+
cursorExecutionRecord,
|
|
81
|
+
runCursorTurn,
|
|
82
|
+
} from "../../__test-utils__/hermetic-cursor.js";
|
|
83
|
+
import { stubRegistryFetch } from "../../../../__test-utils__/model-registry-fixture.js";
|
|
84
|
+
|
|
85
|
+
const USER_MESSAGE = "Run the full test suite and report.";
|
|
86
|
+
const NEVER_SEEN = "This text must never reach the transcript.";
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The stall window for the stall scenario. Above the clock's 1 s step tick
|
|
90
|
+
* (so no ordinary step can read as idle) and small enough that the watchdog's
|
|
91
|
+
* real poll (stallMs / 4 = 500 ms) fires promptly once the script ticks past it.
|
|
92
|
+
*/
|
|
93
|
+
const STALL_TIMEOUT_MS = 2_000;
|
|
94
|
+
|
|
95
|
+
/** Resolves when the run's status flips to `cancelled` — the watchdog's act. */
|
|
96
|
+
function untilCancelled(run: ScriptedRun): Promise<void> {
|
|
97
|
+
return new Promise((resolve) => {
|
|
98
|
+
run.onDidChangeStatus((status) => {
|
|
99
|
+
if (status === "cancelled") resolve();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function systemMessages(status: { messages: { type: MessageType; content: string }[] }): string[] {
|
|
105
|
+
return status.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
describe("ExecuteCursor hermetic — the stream loop's self-stop arms", () => {
|
|
109
|
+
let env: HermeticEnvironment;
|
|
110
|
+
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
111
|
+
const clock = new ScriptedClock();
|
|
112
|
+
|
|
113
|
+
beforeAll(() => {
|
|
114
|
+
env = createHermeticEnvironment();
|
|
115
|
+
registry = stubRegistryFetch();
|
|
116
|
+
clock.install();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
afterAll(() => {
|
|
120
|
+
clock.uninstall();
|
|
121
|
+
registry.restore();
|
|
122
|
+
env.dispose();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("a stall fails the turn with the watchdog's idle time and drops the handle", async () => {
|
|
126
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
127
|
+
clock.reset(); // every golden in this file reads from :00
|
|
128
|
+
const agentId = "agent-hermetic-stall-0001";
|
|
129
|
+
const runId = "run-hermetic-stall-0001";
|
|
130
|
+
const ev = sdkEvents(agentId, runId);
|
|
131
|
+
const agent = new ScriptedCursorAgent({
|
|
132
|
+
agentId,
|
|
133
|
+
runIds: [runId],
|
|
134
|
+
observeStep: () => clock.tick(),
|
|
135
|
+
turns: [
|
|
136
|
+
[
|
|
137
|
+
step.event(ev.init()),
|
|
138
|
+
step.event(ev.assistant("Running the suite.")),
|
|
139
|
+
// A tool call that never returns: the shape of a real wedge, and the
|
|
140
|
+
// detail the stall copy carries (`last tool: shell`).
|
|
141
|
+
step.event(ev.toolCall("call-hermetic-stall-0001", "shell", "running", { command: "npm test" })),
|
|
142
|
+
step.effect("the stream goes silent past the stall window", async ({ run }) => {
|
|
143
|
+
const cancelled = untilCancelled(run);
|
|
144
|
+
clock.tick(STALL_TIMEOUT_MS);
|
|
145
|
+
await cancelled;
|
|
146
|
+
}),
|
|
147
|
+
step.event(ev.assistant(NEVER_SEEN)),
|
|
148
|
+
step.finished({ result: NEVER_SEEN }),
|
|
149
|
+
],
|
|
150
|
+
],
|
|
151
|
+
});
|
|
152
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE });
|
|
153
|
+
const scenario = beginCursorScenario({
|
|
154
|
+
env,
|
|
155
|
+
clock,
|
|
156
|
+
record,
|
|
157
|
+
sdk: { agents: [agent], catalog: SDK_CATALOG },
|
|
158
|
+
config: { cursorStreamStallTimeoutMs: STALL_TIMEOUT_MS },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
162
|
+
const invocation = await runCursorTurn(scenario);
|
|
163
|
+
|
|
164
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
165
|
+
expect(invocation.outcome.kind, "a stall RETURNS, never throws — a retry would wedge again").toBe("returned");
|
|
166
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
167
|
+
expect(slim.phase).toBe("EXECUTION_FAILED");
|
|
168
|
+
expect(record.persistedPhases).toEqual([
|
|
169
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
170
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
171
|
+
]);
|
|
172
|
+
const final = record.lastFullStatus!;
|
|
173
|
+
// Idle = the 1 s step tick before the effect + the effect's own tick: 3 s.
|
|
174
|
+
expect(final.error).toBe(
|
|
175
|
+
"[StallTimeoutError] Agent stream stalled: no activity for 3s (last tool: shell). Retry or resume.",
|
|
176
|
+
);
|
|
177
|
+
expect(final.completedAt).not.toBe("");
|
|
178
|
+
expect(systemMessages(final)).toEqual([
|
|
179
|
+
"Execution failed: the agent made no progress for too long and was stopped " +
|
|
180
|
+
"(Agent stream stalled: no activity for 3s (last tool: shell)). You can retry or resume.",
|
|
181
|
+
]);
|
|
182
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN), "nothing after the stall is processed").toBe(false);
|
|
183
|
+
|
|
184
|
+
// ── Assert: engine disposition ───────────────────────────────────────────
|
|
185
|
+
expect(agent.runs[0].cancelCalls, "the runtime's stop cancelled the run once").toHaveLength(1);
|
|
186
|
+
expect(agent.closeCalls, "the handle is not closed").toBe(0);
|
|
187
|
+
// Q-S2-6 (S2 M3): parked on every non-failed exit; before M3 a stall dropped it.
|
|
188
|
+
expect(_parkedAgentCountForTests(), "parked for the session's next turn").toBe(1);
|
|
189
|
+
expect(record.sessionUpdates).toHaveLength(1);
|
|
190
|
+
|
|
191
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
192
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
193
|
+
expect(invocation.heartbeats.length).toBeGreaterThan(0);
|
|
194
|
+
|
|
195
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
196
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
197
|
+
await expect(json).toMatchFileSnapshot("./goldens/stall.status.json");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("a cost cap terminates the turn with the budget copy and parks the agent", async () => {
|
|
201
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
202
|
+
clock.reset();
|
|
203
|
+
const agentId = "agent-hermetic-costcap-0001";
|
|
204
|
+
const runId = "run-hermetic-costcap-0001";
|
|
205
|
+
const ev = sdkEvents(agentId, runId);
|
|
206
|
+
const agent = new ScriptedCursorAgent({
|
|
207
|
+
agentId,
|
|
208
|
+
runIds: [runId],
|
|
209
|
+
observeStep: () => clock.tick(),
|
|
210
|
+
turns: [
|
|
211
|
+
[
|
|
212
|
+
step.event(ev.init()),
|
|
213
|
+
step.event(ev.assistant("Reading the whole repository first.")),
|
|
214
|
+
// 600 000 input tokens at the fixture's $1/M base rate = $0.60 > $0.50.
|
|
215
|
+
step.turnEnded({ inputTokens: 600_000, outputTokens: 0 }),
|
|
216
|
+
step.event(ev.assistant(NEVER_SEEN)),
|
|
217
|
+
step.finished({ result: NEVER_SEEN }),
|
|
218
|
+
],
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
const record = cursorExecutionRecord({ message: USER_MESSAGE, maxCostUsd: 0.5 });
|
|
222
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
223
|
+
|
|
224
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
225
|
+
const invocation = await runCursorTurn(scenario);
|
|
226
|
+
|
|
227
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
228
|
+
expect(invocation.outcome.kind, "a cost cap RETURNS — a retry would burn the budget again").toBe("returned");
|
|
229
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
230
|
+
expect(slim.phase).toBe("EXECUTION_TERMINATED");
|
|
231
|
+
expect(record.persistedPhases).toEqual([
|
|
232
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
233
|
+
ExecutionPhase.EXECUTION_TERMINATED,
|
|
234
|
+
]);
|
|
235
|
+
const final = record.lastFullStatus!;
|
|
236
|
+
expect(final.error).toBe(
|
|
237
|
+
"Agent reached the cost limit for this message (~$0.6000 of the $0.50 budget). Send another message to continue.",
|
|
238
|
+
);
|
|
239
|
+
expect(final.completedAt).not.toBe("");
|
|
240
|
+
expect(systemMessages(final)).toEqual([
|
|
241
|
+
"The agent reached the cost limit for this message. Work completed so far has been saved. " +
|
|
242
|
+
"Send another message to continue where the agent left off.",
|
|
243
|
+
]);
|
|
244
|
+
expect(final.streamingUsage?.estimatedCostUsd, "the figure the copy quotes is the usage summary's").toBe(0.6);
|
|
245
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN), "the event after the overrun is never processed").toBe(false);
|
|
246
|
+
|
|
247
|
+
// ── Assert: engine disposition ───────────────────────────────────────────
|
|
248
|
+
expect(agent.runs[0].cancelCalls, "the runtime's stop cancelled the run once").toHaveLength(1);
|
|
249
|
+
expect(agent.closeCalls).toBe(0);
|
|
250
|
+
expect(_parkedAgentCountForTests(), "a clean terminal parks for the next message").toBe(1);
|
|
251
|
+
expect(record.sessionUpdates).toHaveLength(1);
|
|
252
|
+
|
|
253
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
254
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
255
|
+
|
|
256
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
257
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
258
|
+
await expect(json).toMatchFileSnapshot("./goldens/cost-cap.status.json");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("a platform stop completes the turn early with the platform copy and parks the agent", async () => {
|
|
262
|
+
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
263
|
+
clock.reset();
|
|
264
|
+
const agentId = "agent-hermetic-platformstop-0001";
|
|
265
|
+
const runId = "run-hermetic-platformstop-0001";
|
|
266
|
+
const ev = sdkEvents(agentId, runId);
|
|
267
|
+
const agent = new ScriptedCursorAgent({
|
|
268
|
+
agentId,
|
|
269
|
+
runIds: [runId],
|
|
270
|
+
observeStep: () => clock.tick(),
|
|
271
|
+
turns: [
|
|
272
|
+
[
|
|
273
|
+
step.event(ev.init()),
|
|
274
|
+
step.event(ev.assistant("Starting the suite.")),
|
|
275
|
+
// A completed tool call force-flushes a persist (content dirty); the
|
|
276
|
+
// control plane answers THAT persist with STOP.
|
|
277
|
+
step.event(ev.toolCall("call-hermetic-platformstop-0001", "read", "running", { path: "package.json" })),
|
|
278
|
+
step.event(
|
|
279
|
+
ev.toolCall("call-hermetic-platformstop-0001", "read", "completed", { path: "package.json" }, '{"name":"demo"}'),
|
|
280
|
+
),
|
|
281
|
+
step.event(ev.assistant(NEVER_SEEN)),
|
|
282
|
+
step.finished({ result: NEVER_SEEN }),
|
|
283
|
+
],
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
const record = cursorExecutionRecord({
|
|
287
|
+
message: USER_MESSAGE,
|
|
288
|
+
// The platform's decision: stop once the tool call has completed.
|
|
289
|
+
controlSignal: (status) =>
|
|
290
|
+
status.messages.some((m) => m.toolCalls.some((tc) => tc.status === ToolCallStatus.TOOL_CALL_COMPLETED))
|
|
291
|
+
? ExecutionControlSignal.STOP
|
|
292
|
+
: ExecutionControlSignal.UNSPECIFIED,
|
|
293
|
+
});
|
|
294
|
+
const scenario = beginCursorScenario({ env, clock, record, sdk: { agents: [agent], catalog: SDK_CATALOG } });
|
|
295
|
+
|
|
296
|
+
// ── Act ──────────────────────────────────────────────────────────────────
|
|
297
|
+
const invocation = await runCursorTurn(scenario);
|
|
298
|
+
|
|
299
|
+
// ── Assert: outcome and phases ───────────────────────────────────────────
|
|
300
|
+
expect(invocation.outcome.kind, "a platform stop RETURNS a clean completion").toBe("returned");
|
|
301
|
+
const slim = (invocation.outcome as { value: Record<string, unknown> }).value;
|
|
302
|
+
expect(slim.phase).toBe("EXECUTION_COMPLETED");
|
|
303
|
+
expect(record.persistedPhases).toEqual([
|
|
304
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
305
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
306
|
+
]);
|
|
307
|
+
const final = record.lastFullStatus!;
|
|
308
|
+
expect(final.error, "a platform stop is not an error").toBe("");
|
|
309
|
+
expect(final.completedAt).not.toBe("");
|
|
310
|
+
expect(systemMessages(final)).toEqual(["Execution stopped by the platform."]);
|
|
311
|
+
expect(record.toolCalls().map((tc) => tc.status), "the persist that drew the STOP is on the transcript").toEqual([
|
|
312
|
+
ToolCallStatus.TOOL_CALL_COMPLETED,
|
|
313
|
+
]);
|
|
314
|
+
expect(final.messages.some((m) => m.content === NEVER_SEEN), "the event after the STOP is never processed").toBe(false);
|
|
315
|
+
|
|
316
|
+
// ── Assert: engine disposition ───────────────────────────────────────────
|
|
317
|
+
// Q-M3-4 (S2 M3): the SDK run is cancelled on every stop; before M3 a
|
|
318
|
+
// platform stop broke the loop and left the run executing behind the
|
|
319
|
+
// parked handle.
|
|
320
|
+
expect(agent.runs[0].cancelCalls, "the runtime's stop cancelled the run once").toHaveLength(1);
|
|
321
|
+
expect(agent.closeCalls).toBe(0);
|
|
322
|
+
expect(_parkedAgentCountForTests(), "a clean terminal parks for the next message").toBe(1);
|
|
323
|
+
expect(record.sessionUpdates).toHaveLength(1);
|
|
324
|
+
|
|
325
|
+
// ── Assert: hermeticity ──────────────────────────────────────────────────
|
|
326
|
+
expect(registry.urls.every((u) => u.includes("/model-registry"))).toBe(true);
|
|
327
|
+
|
|
328
|
+
// ── Assert: the golden ───────────────────────────────────────────────────
|
|
329
|
+
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
330
|
+
await expect(json).toMatchFileSnapshot("./goldens/platform-stop.status.json");
|
|
331
|
+
});
|
|
332
|
+
});
|