@stigmer/runner 3.14.1 → 3.15.1
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 +28 -5
- package/dist/__test-utils__/hermetic-activity.js +23 -7
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- 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 +38 -62
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +47 -121
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +17 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +27 -4
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +52 -11
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +66 -7
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -1
- 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 +16 -0
- package/dist/harness/capabilities.js +1 -0
- package/dist/harness/capabilities.js.map +1 -1
- 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 +44 -5
- package/dist/harness/registry.js +61 -5
- package/dist/harness/registry.js.map +1 -1
- 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 +232 -52
- package/dist/harness/types.js +17 -8
- package/dist/harness/types.js.map +1 -1
- 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 +110 -14
- 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 +128 -83
- package/src/__test-utils__/harness-contract/recording-sink.ts +15 -4
- package/src/__test-utils__/harness-contract/runtime-contract.ts +666 -0
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +92 -32
- package/src/__test-utils__/harness-contract/types.ts +85 -9
- package/src/__test-utils__/hermetic-activity.ts +33 -8
- package/src/__test-utils__/model-registry-fixture.ts +72 -0
- package/src/__test-utils__/module-specifiers.ts +82 -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 +7 -5
- 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 +65 -145
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +30 -4
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +96 -8
- 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 +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/cost-cap.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +1 -6
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/platform-stop.status.json +56 -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/unattributed-hook-block.status.json +79 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +2 -7
- 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 +22 -18
- package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +1 -1
- 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 +1 -1
- 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 +1 -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 +17 -0
- package/src/harness/persist-chokepoint.ts +112 -0
- package/src/harness/registry.ts +79 -5
- 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 +248 -53
- 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
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
* Unit tests for the Cursor harness's shared stream seam (turn-stream.ts).
|
|
3
3
|
*
|
|
4
4
|
* These lock the behaviors the old bare retry loops used to DROP — live persist,
|
|
5
|
-
* DD-32/DD-33 mid-run progress, sub-agent tracking, the first-denial early stop
|
|
6
|
-
* and
|
|
7
|
-
*
|
|
5
|
+
* DD-32/DD-33 mid-run progress, sub-agent tracking, the first-denial early stop
|
|
6
|
+
* — and the loop's side of the adapter contract: progress reported per event
|
|
7
|
+
* and per delta, usage reported priced, the persist awaited, and the runtime's
|
|
8
|
+
* stop signal honoured at every event boundary and inside a blocked pull by
|
|
9
|
+
* cancelling the SDK run. Since S2 M3 the loop no longer decides WHY it
|
|
10
|
+
* stopped (stall, cost cap, pause, platform stop are the runtime's evidence;
|
|
11
|
+
* `harness/__tests__/run-turn.test.ts` proves that table); it reports
|
|
12
|
+
* `completed`, `first-denial` or `interrupted`.
|
|
8
13
|
*
|
|
9
14
|
* The loop is driven with a structural `mockRun` (an async-iterable `.stream()`)
|
|
10
|
-
* and
|
|
11
|
-
*
|
|
15
|
+
* and the kit's `RecordingTurnSink`, so it runs without the live Cursor SDK or
|
|
16
|
+
* Temporal — mirroring the deep-agent `streaming.test.ts` pattern.
|
|
12
17
|
*/
|
|
13
18
|
|
|
14
19
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
15
20
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
16
21
|
import { tmpdir } from "node:os";
|
|
17
22
|
import { join } from "node:path";
|
|
18
|
-
import { create } from "@bufbuild/protobuf";
|
|
19
|
-
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
20
|
-
import { ExecutionControlSignal } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
21
|
-
import { CancelledFailure } from "@temporalio/activity";
|
|
22
23
|
import type { SDKMessage } from "@cursor/sdk";
|
|
24
|
+
import { RecordingTurnSink } from "../../../__test-utils__/harness-contract/recording-sink.js";
|
|
23
25
|
import {
|
|
24
26
|
consumeCursorTurnStream,
|
|
25
27
|
makeCursorTurnOnDelta,
|
|
@@ -75,45 +77,32 @@ function stubEnricher() {
|
|
|
75
77
|
};
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// hasTurns:false keeps the loop from building a StreamingUsageSummary (which
|
|
82
|
-
// would need a real snapshot); usage plumbing is covered by usage-accumulator's
|
|
83
|
-
// own tests.
|
|
84
|
-
hasTurns: false,
|
|
85
|
-
snapshot: vi.fn(() => ({ inputTokens: 0n, outputTokens: 0n })),
|
|
86
|
-
};
|
|
80
|
+
/** Prices nothing: hands the counts back as a delta with a stated cost, so the sink sees a priced delta. */
|
|
81
|
+
function stubUsagePricer() {
|
|
82
|
+
return { price: vi.fn((usage: Record<string, number>) => ({ ...usage, estimatedCostUsd: 0.01, model: "m" })) };
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
interface BuiltDeps {
|
|
90
86
|
deps: CursorTurnStreamDeps;
|
|
91
87
|
state: TurnStreamState;
|
|
92
|
-
|
|
88
|
+
sink: RecordingTurnSink;
|
|
93
89
|
accumulator: ReturnType<typeof stubAccumulator>;
|
|
94
|
-
status: ReturnType<typeof create<typeof AgentExecutionStatusSchema>>;
|
|
95
90
|
}
|
|
96
91
|
|
|
97
92
|
function buildDeps(overrides: Partial<CursorTurnStreamDeps> = {}): BuiltDeps {
|
|
98
93
|
const state = overrides.state ?? newTurnStreamState();
|
|
99
|
-
const
|
|
100
|
-
const accumulator = (overrides.accumulator as unknown as ReturnType<typeof stubAccumulator>) ??
|
|
101
|
-
stubAccumulator();
|
|
102
|
-
const persist =
|
|
103
|
-
(overrides.persist as ReturnType<typeof vi.fn>) ??
|
|
104
|
-
vi.fn(async () => ExecutionControlSignal.UNSPECIFIED);
|
|
94
|
+
const sink = (overrides.sink as RecordingTurnSink | undefined) ?? new RecordingTurnSink();
|
|
95
|
+
const accumulator = (overrides.accumulator as unknown as ReturnType<typeof stubAccumulator>) ?? stubAccumulator();
|
|
105
96
|
|
|
106
97
|
const deps = {
|
|
107
98
|
// TurnOnDeltaDeps
|
|
108
|
-
|
|
99
|
+
sink,
|
|
100
|
+
usagePricer: stubUsagePricer(),
|
|
109
101
|
deltaEnricher: stubEnricher(),
|
|
110
|
-
heartbeat: vi.fn(),
|
|
111
102
|
promptEstimatedTokens: 100,
|
|
112
103
|
executionId: "exec-test",
|
|
113
104
|
state,
|
|
114
|
-
maxCostUsd: 0,
|
|
115
105
|
// CursorTurnStreamDeps
|
|
116
|
-
status,
|
|
117
106
|
accumulator,
|
|
118
107
|
todoTracker: { processEvent: vi.fn(), markPersisted: vi.fn(), isDirty: false },
|
|
119
108
|
eventRecorder: undefined,
|
|
@@ -124,43 +113,39 @@ function buildDeps(overrides: Partial<CursorTurnStreamDeps> = {}): BuiltDeps {
|
|
|
124
113
|
progressState: { lastAtMs: 0 },
|
|
125
114
|
changeSetId: "exec-test:0",
|
|
126
115
|
hitlDir: undefined,
|
|
127
|
-
stallTimeoutMs: 120_000,
|
|
128
|
-
persist,
|
|
129
|
-
isCancelled: () => false,
|
|
130
116
|
...overrides,
|
|
131
117
|
} as unknown as CursorTurnStreamDeps;
|
|
132
118
|
|
|
133
|
-
return { deps, state,
|
|
119
|
+
return { deps, state, sink, accumulator };
|
|
134
120
|
}
|
|
135
121
|
|
|
136
122
|
describe("consumeCursorTurnStream", () => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
123
|
+
it("processes every event, persists live through the sink, and returns 'completed' on a natural end", async () => {
|
|
124
|
+
const { deps, state, sink, accumulator } = buildDeps();
|
|
140
125
|
|
|
141
|
-
|
|
142
|
-
const { deps, state, persist, accumulator } = buildDeps();
|
|
143
|
-
|
|
144
|
-
const reason = await consumeCursorTurnStream(
|
|
145
|
-
mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]),
|
|
146
|
-
deps,
|
|
147
|
-
);
|
|
126
|
+
const reason = await consumeCursorTurnStream(mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]), deps);
|
|
148
127
|
|
|
149
128
|
expect(reason).toBe("completed");
|
|
150
129
|
expect(accumulator.processEvent).toHaveBeenCalledTimes(2);
|
|
151
130
|
// The #1 retry gap: the bare loop never persisted mid-stream. This proves the
|
|
152
|
-
// shared loop persists live.
|
|
153
|
-
expect(
|
|
131
|
+
// shared loop persists live — through the runtime's chokepoint.
|
|
132
|
+
expect(sink.persistRequests).toBeGreaterThan(0);
|
|
154
133
|
expect(state.eventCount).toBe(2);
|
|
155
134
|
});
|
|
156
135
|
|
|
136
|
+
it("reports progress per event, naming the tool of a tool_call event (the stall copy's `last tool`)", async () => {
|
|
137
|
+
const { deps, sink } = buildDeps();
|
|
138
|
+
|
|
139
|
+
await consumeCursorTurnStream(mockRun([ev({ type: "assistant" }), ev({ type: "tool_call", name: "shell" })]), deps);
|
|
140
|
+
|
|
141
|
+
const marks = sink.events.filter((e) => e.kind === "activity");
|
|
142
|
+
expect(marks.map((m) => (m.kind === "activity" ? m.detail : undefined))).toEqual([undefined, "shell"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
157
145
|
it("tracks a sub-agent delegation (the 'task' tool call) — dropped by the old bare retry loop", async () => {
|
|
158
146
|
const { deps, accumulator } = buildDeps();
|
|
159
147
|
|
|
160
|
-
await consumeCursorTurnStream(
|
|
161
|
-
mockRun([ev({ type: "tool_call", name: "task" })]),
|
|
162
|
-
deps,
|
|
163
|
-
);
|
|
148
|
+
await consumeCursorTurnStream(mockRun([ev({ type: "tool_call", name: "task" })]), deps);
|
|
164
149
|
|
|
165
150
|
expect(accumulator.trackSubAgentExecution).toHaveBeenCalledTimes(1);
|
|
166
151
|
});
|
|
@@ -168,56 +153,27 @@ describe("consumeCursorTurnStream", () => {
|
|
|
168
153
|
it("attaches DD-32/DD-33 mid-run progress when a substrate is present", async () => {
|
|
169
154
|
const capture = vi.fn(async () => ({ delta: { entries: [] }, changed: true }));
|
|
170
155
|
const progressSubstrate = { capture } as unknown as ProgressSubstrate;
|
|
171
|
-
const { deps,
|
|
156
|
+
const { deps, sink } = buildDeps({ progressSubstrate });
|
|
172
157
|
|
|
173
158
|
await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
|
|
174
159
|
|
|
175
160
|
expect(capture).toHaveBeenCalled();
|
|
176
161
|
// changed:true → the transient snapshot is attached to status.
|
|
177
|
-
expect(status.fileChangeProgress).toBeDefined();
|
|
162
|
+
expect(sink.status.fileChangeProgress).toBeDefined();
|
|
178
163
|
});
|
|
179
164
|
|
|
180
165
|
it("does not touch progress when no substrate is configured", async () => {
|
|
181
|
-
const { deps,
|
|
166
|
+
const { deps, sink } = buildDeps({ progressSubstrate: undefined });
|
|
182
167
|
|
|
183
168
|
await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
|
|
184
169
|
|
|
185
|
-
expect(status.fileChangeProgress).toBeUndefined();
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it("returns 'paused' and sets pauseDetected when the activity is cancelled", async () => {
|
|
189
|
-
const { deps, state, accumulator } = buildDeps({ isCancelled: () => true });
|
|
190
|
-
|
|
191
|
-
const reason = await consumeCursorTurnStream(mockRun([ev({ type: "assistant" })]), deps);
|
|
192
|
-
|
|
193
|
-
expect(reason).toBe("paused");
|
|
194
|
-
expect(state.pauseDetected).toBe(true);
|
|
195
|
-
// Cancellation is checked before the event is processed.
|
|
196
|
-
expect(accumulator.processEvent).not.toHaveBeenCalled();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("returns 'platform-stop' when a persist reports the STOP control signal", async () => {
|
|
200
|
-
const persist = vi.fn(async () => ExecutionControlSignal.STOP);
|
|
201
|
-
const { deps, state } = buildDeps({ persist });
|
|
202
|
-
|
|
203
|
-
const reason = await consumeCursorTurnStream(
|
|
204
|
-
mockRun([ev({ type: "assistant" }), ev({ type: "assistant" })]),
|
|
205
|
-
deps,
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
expect(reason).toBe("platform-stop");
|
|
209
|
-
expect(state.platformStopSignaled).toBe(true);
|
|
210
|
-
// Broke after the first persist — the second event never persisted.
|
|
211
|
-
expect(persist).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(sink.status.fileChangeProgress).toBeUndefined();
|
|
212
171
|
});
|
|
213
172
|
|
|
214
173
|
it("captures a stream ERROR status message onto state.streamErrorMessage", async () => {
|
|
215
174
|
const { deps, state } = buildDeps();
|
|
216
175
|
|
|
217
|
-
await consumeCursorTurnStream(
|
|
218
|
-
mockRun([ev({ type: "status", status: "ERROR", message: "boom" })]),
|
|
219
|
-
deps,
|
|
220
|
-
);
|
|
176
|
+
await consumeCursorTurnStream(mockRun([ev({ type: "status", status: "ERROR", message: "boom" })]), deps);
|
|
221
177
|
|
|
222
178
|
expect(state.streamErrorMessage).toBe("boom");
|
|
223
179
|
});
|
|
@@ -227,10 +183,7 @@ describe("consumeCursorTurnStream", () => {
|
|
|
227
183
|
// crash classifyText downstream (.toLowerCase() on a non-string).
|
|
228
184
|
const { deps, state } = buildDeps();
|
|
229
185
|
|
|
230
|
-
await consumeCursorTurnStream(
|
|
231
|
-
mockRun([ev({ type: "status", status: "ERROR", message: { code: 14 } })]),
|
|
232
|
-
deps,
|
|
233
|
-
);
|
|
186
|
+
await consumeCursorTurnStream(mockRun([ev({ type: "status", status: "ERROR", message: { code: 14 } })]), deps);
|
|
234
187
|
|
|
235
188
|
expect(state.streamErrorMessage).toBeUndefined();
|
|
236
189
|
});
|
|
@@ -244,11 +197,7 @@ describe("consumeCursorTurnStream", () => {
|
|
|
244
197
|
|
|
245
198
|
it("stops the turn, cancels the run, and arms denialCancelSettled on a ledger entry", async () => {
|
|
246
199
|
hitlDir = await mkdtemp(join(tmpdir(), "turn-stream-denial-"));
|
|
247
|
-
await writeFile(
|
|
248
|
-
join(hitlDir, "denials.jsonl"),
|
|
249
|
-
JSON.stringify({ toolName: "shell", token: "tok-1" }) + "\n",
|
|
250
|
-
"utf-8",
|
|
251
|
-
);
|
|
200
|
+
await writeFile(join(hitlDir, "denials.jsonl"), JSON.stringify({ toolName: "shell", token: "tok-1" }) + "\n", "utf-8");
|
|
252
201
|
const { deps, state } = buildDeps({ hitlDir });
|
|
253
202
|
const run = mockRun([ev({ type: "tool_call", name: "shell" })]);
|
|
254
203
|
|
|
@@ -265,72 +214,92 @@ describe("consumeCursorTurnStream", () => {
|
|
|
265
214
|
await writeFile(join(hitlDir, "denials.jsonl"), "", "utf-8");
|
|
266
215
|
const { deps, state } = buildDeps({ hitlDir });
|
|
267
216
|
|
|
268
|
-
const reason = await consumeCursorTurnStream(
|
|
269
|
-
mockRun([ev({ type: "tool_call", name: "shell" })]),
|
|
270
|
-
deps,
|
|
271
|
-
);
|
|
217
|
+
const reason = await consumeCursorTurnStream(mockRun([ev({ type: "tool_call", name: "shell" })]), deps);
|
|
272
218
|
|
|
273
219
|
expect(reason).toBe("completed");
|
|
274
220
|
expect(state.firstDenialDetected).toBe(false);
|
|
275
221
|
});
|
|
276
222
|
});
|
|
277
223
|
|
|
278
|
-
describe("
|
|
279
|
-
it("
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
const { deps, accumulator } = buildDeps({
|
|
283
|
-
const run = mockRun([ev({ type: "assistant" })
|
|
224
|
+
describe("the runtime's stop signal", () => {
|
|
225
|
+
it("a signal aborted before the stream starts: the run is cancelled and nothing is processed", async () => {
|
|
226
|
+
const sink = new RecordingTurnSink();
|
|
227
|
+
sink.abort("cost-cap");
|
|
228
|
+
const { deps, accumulator } = buildDeps({ sink });
|
|
229
|
+
const run = mockRun([ev({ type: "assistant" })]);
|
|
284
230
|
|
|
285
231
|
const reason = await consumeCursorTurnStream(run, deps);
|
|
286
232
|
|
|
287
|
-
expect(reason).toBe("
|
|
288
|
-
expect(run.cancel).
|
|
289
|
-
// The break happens before the event is processed — no post-cap content.
|
|
233
|
+
expect(reason).toBe("interrupted");
|
|
234
|
+
expect(run.cancel).toHaveBeenCalledTimes(1);
|
|
290
235
|
expect(accumulator.processEvent).not.toHaveBeenCalled();
|
|
291
236
|
});
|
|
292
237
|
|
|
293
|
-
it("
|
|
294
|
-
const
|
|
295
|
-
const { deps, accumulator } = buildDeps({
|
|
296
|
-
//
|
|
297
|
-
//
|
|
298
|
-
// loop is between stream events.
|
|
238
|
+
it("an abort between events: the run is cancelled at once and the next event is never processed", async () => {
|
|
239
|
+
const sink = new RecordingTurnSink();
|
|
240
|
+
const { deps, accumulator } = buildDeps({ sink });
|
|
241
|
+
// The realistic shape: the runtime aborts (a turn-ended delta crossed the
|
|
242
|
+
// cap, a STOP came back from a persist) while the loop is between events.
|
|
299
243
|
async function* gen(): AsyncIterable<SDKMessage> {
|
|
300
244
|
yield ev({ type: "assistant" });
|
|
301
|
-
|
|
245
|
+
sink.abort("platform-stop");
|
|
302
246
|
yield ev({ type: "assistant" });
|
|
303
247
|
}
|
|
248
|
+
const run: MockRun = { stream: () => gen(), supports: () => true, cancel: vi.fn(async () => {}) };
|
|
249
|
+
|
|
250
|
+
const reason = await consumeCursorTurnStream(run, deps);
|
|
251
|
+
|
|
252
|
+
expect(reason).toBe("interrupted");
|
|
253
|
+
expect(run.cancel).toHaveBeenCalledTimes(1);
|
|
254
|
+
// Event 1 was processed; event 2 hit the boundary check.
|
|
255
|
+
expect(accumulator.processEvent).toHaveBeenCalledTimes(1);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("an abort while the pull is blocked (a wedged stream): the cancel is what unblocks it", async () => {
|
|
259
|
+
const sink = new RecordingTurnSink();
|
|
260
|
+
const { deps } = buildDeps({ sink });
|
|
261
|
+
// The generator yields one event, then awaits a promise resolved only by
|
|
262
|
+
// run.cancel() — the loop is suspended inside the pull when the runtime's
|
|
263
|
+
// watchdog aborts.
|
|
264
|
+
let unblock: (() => void) | undefined;
|
|
265
|
+
async function* gen(): AsyncIterable<SDKMessage> {
|
|
266
|
+
yield ev({ type: "assistant" });
|
|
267
|
+
await new Promise<void>((resolve) => {
|
|
268
|
+
unblock = resolve;
|
|
269
|
+
});
|
|
270
|
+
}
|
|
304
271
|
const run: MockRun = {
|
|
305
272
|
stream: () => gen(),
|
|
306
273
|
supports: () => true,
|
|
307
|
-
cancel: vi.fn(async () => {
|
|
274
|
+
cancel: vi.fn(async () => {
|
|
275
|
+
unblock?.();
|
|
276
|
+
}),
|
|
308
277
|
};
|
|
309
278
|
|
|
310
|
-
const
|
|
279
|
+
const pending = consumeCursorTurnStream(run, deps);
|
|
280
|
+
await vi.waitFor(() => expect(unblock).toBeDefined());
|
|
281
|
+
sink.abort("stall");
|
|
282
|
+
const reason = await pending;
|
|
311
283
|
|
|
312
|
-
expect(reason).toBe("
|
|
313
|
-
expect(run.cancel).
|
|
314
|
-
// Event 1 was processed; event 2 hit the break before processing.
|
|
315
|
-
expect(accumulator.processEvent).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(reason).toBe("interrupted");
|
|
285
|
+
expect(run.cancel).toHaveBeenCalledTimes(1);
|
|
316
286
|
});
|
|
317
287
|
|
|
318
288
|
it("swallows the cancel-induced teardown rejection (expected, not a failure)", async () => {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
const { deps } = buildDeps({ state });
|
|
289
|
+
const sink = new RecordingTurnSink();
|
|
290
|
+
const { deps } = buildDeps({ sink });
|
|
322
291
|
// The break exits the for-await, which invokes the iterator's return();
|
|
323
|
-
// a cancelled SDK run can reject there. The catch must exempt
|
|
324
|
-
// teardown exactly like
|
|
292
|
+
// a cancelled SDK run can reject there. The catch must exempt a stopped
|
|
293
|
+
// turn's teardown exactly like the first-denial teardown.
|
|
325
294
|
const events = [ev({ type: "assistant" })];
|
|
326
295
|
let i = 0;
|
|
327
296
|
const run: MockRun = {
|
|
328
297
|
stream: () => ({
|
|
329
298
|
[Symbol.asyncIterator]: () => ({
|
|
330
|
-
next: async () =>
|
|
331
|
-
i
|
|
332
|
-
|
|
333
|
-
|
|
299
|
+
next: async () => {
|
|
300
|
+
if (i === 1) sink.abort("cost-cap");
|
|
301
|
+
return i < events.length ? { value: events[i++], done: false as const } : { value: undefined, done: true as const };
|
|
302
|
+
},
|
|
334
303
|
return: async () => {
|
|
335
304
|
throw new Error("run cancelled");
|
|
336
305
|
},
|
|
@@ -342,139 +311,45 @@ describe("consumeCursorTurnStream", () => {
|
|
|
342
311
|
|
|
343
312
|
const reason = await consumeCursorTurnStream(run, deps);
|
|
344
313
|
|
|
345
|
-
expect(reason).toBe("
|
|
314
|
+
expect(reason).toBe("interrupted");
|
|
346
315
|
expect(run.cancel).toHaveBeenCalled();
|
|
347
316
|
});
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it("returns 'stalled' and cancels the run when the stall watchdog fires", async () => {
|
|
351
|
-
vi.useFakeTimers();
|
|
352
|
-
// The generator yields one event, then awaits a promise resolved only by
|
|
353
|
-
// run.cancel() — so the loop is suspended when the watchdog fires.
|
|
354
|
-
let unblock: (() => void) | undefined;
|
|
355
|
-
async function* gen(): AsyncIterable<SDKMessage> {
|
|
356
|
-
yield ev({ type: "assistant" });
|
|
357
|
-
await new Promise<void>((resolve) => {
|
|
358
|
-
unblock = resolve;
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
const run: MockRun = {
|
|
362
|
-
stream: () => gen(),
|
|
363
|
-
supports: () => true,
|
|
364
|
-
cancel: vi.fn(async () => {
|
|
365
|
-
unblock?.();
|
|
366
|
-
}),
|
|
367
|
-
};
|
|
368
|
-
const { deps, state } = buildDeps({ stallTimeoutMs: 40 });
|
|
369
|
-
|
|
370
|
-
const pending = consumeCursorTurnStream(run, deps);
|
|
371
|
-
// Advance past the stall window (tick = stallMs/4 = 10ms), flushing the
|
|
372
|
-
// microtasks between ticks so the loop reaches its await first.
|
|
373
|
-
await vi.advanceTimersByTimeAsync(80);
|
|
374
|
-
const reason = await pending;
|
|
375
|
-
|
|
376
|
-
expect(reason).toBe("stalled");
|
|
377
|
-
expect(state.stallDetected).toBe(true);
|
|
378
|
-
expect(run.cancel).toHaveBeenCalled();
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe("makeCursorTurnOnDelta", () => {
|
|
383
|
-
it("flags a pause (not a throw) when the heartbeat reports CancelledFailure", () => {
|
|
384
|
-
const state = newTurnStreamState();
|
|
385
|
-
const onDelta = makeCursorTurnOnDelta({
|
|
386
|
-
usageAccumulator: stubUsageAccumulator() as never,
|
|
387
|
-
deltaEnricher: stubEnricher() as never,
|
|
388
|
-
heartbeat: () => {
|
|
389
|
-
throw new CancelledFailure("paused");
|
|
390
|
-
},
|
|
391
|
-
promptEstimatedTokens: 10,
|
|
392
|
-
executionId: "e",
|
|
393
|
-
state,
|
|
394
|
-
maxCostUsd: 0,
|
|
395
|
-
});
|
|
396
317
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
318
|
+
it("rethrows a genuine stream failure when nothing stopped the turn", async () => {
|
|
319
|
+
const { deps } = buildDeps();
|
|
320
|
+
async function* gen(): AsyncIterable<SDKMessage> {
|
|
321
|
+
yield ev({ type: "assistant" });
|
|
322
|
+
throw new Error("transport reset");
|
|
323
|
+
}
|
|
324
|
+
const run: MockRun = { stream: () => gen(), supports: () => true, cancel: vi.fn(async () => {}) };
|
|
400
325
|
|
|
401
|
-
|
|
402
|
-
const state = newTurnStreamState();
|
|
403
|
-
const onDelta = makeCursorTurnOnDelta({
|
|
404
|
-
usageAccumulator: stubUsageAccumulator() as never,
|
|
405
|
-
deltaEnricher: stubEnricher() as never,
|
|
406
|
-
heartbeat: () => {
|
|
407
|
-
throw new Error("boom");
|
|
408
|
-
},
|
|
409
|
-
promptEstimatedTokens: 10,
|
|
410
|
-
executionId: "e",
|
|
411
|
-
state,
|
|
412
|
-
maxCostUsd: 0,
|
|
326
|
+
await expect(consumeCursorTurnStream(run, deps)).rejects.toThrow("transport reset");
|
|
413
327
|
});
|
|
414
|
-
|
|
415
|
-
expect(() => onDelta({ update: { type: "text" } as never })).toThrow("boom");
|
|
416
|
-
expect(state.pauseDetected).toBe(false);
|
|
417
328
|
});
|
|
329
|
+
});
|
|
418
330
|
|
|
419
|
-
|
|
331
|
+
describe("makeCursorTurnOnDelta", () => {
|
|
332
|
+
it("reports every delta as progress and every turn-ended usage priced, logging first-turn attribution exactly once", () => {
|
|
420
333
|
const state = newTurnStreamState();
|
|
421
|
-
const
|
|
334
|
+
const sink = new RecordingTurnSink();
|
|
335
|
+
const usagePricer = stubUsagePricer();
|
|
422
336
|
const onDelta = makeCursorTurnOnDelta({
|
|
423
|
-
|
|
337
|
+
sink,
|
|
338
|
+
usagePricer: usagePricer as never,
|
|
424
339
|
deltaEnricher: stubEnricher() as never,
|
|
425
|
-
heartbeat: vi.fn(),
|
|
426
340
|
promptEstimatedTokens: 10,
|
|
427
341
|
executionId: "e",
|
|
428
342
|
state,
|
|
429
|
-
maxCostUsd: 0,
|
|
430
343
|
});
|
|
431
344
|
|
|
345
|
+
onDelta({ update: { type: "text" } as never });
|
|
432
346
|
onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
|
|
433
347
|
onDelta({ update: { type: "turn-ended", usage: { inputTokens: 50 } } as never });
|
|
434
348
|
|
|
435
|
-
expect(
|
|
349
|
+
expect(sink.activityMarks).toBe(3);
|
|
350
|
+
expect(usagePricer.price).toHaveBeenCalledTimes(2);
|
|
351
|
+
expect(sink.usageDeltas.map((d) => d.inputTokens)).toEqual([100, 50]);
|
|
352
|
+
expect(sink.usageDeltas.every((d) => d.estimatedCostUsd === 0.01), "the delta reaches the sink priced").toBe(true);
|
|
436
353
|
expect(state.firstTurnAttributionLogged).toBe(true);
|
|
437
354
|
});
|
|
438
|
-
|
|
439
|
-
it("flags costCapExceeded when a turn-ended usage delta pushes the estimate past the cap", () => {
|
|
440
|
-
const state = newTurnStreamState();
|
|
441
|
-
const usageAccumulator = {
|
|
442
|
-
...stubUsageAccumulator(),
|
|
443
|
-
snapshot: vi.fn(() => ({ estimatedCostUsd: 0.51 })),
|
|
444
|
-
};
|
|
445
|
-
const onDelta = makeCursorTurnOnDelta({
|
|
446
|
-
usageAccumulator: usageAccumulator as never,
|
|
447
|
-
deltaEnricher: stubEnricher() as never,
|
|
448
|
-
heartbeat: vi.fn(),
|
|
449
|
-
promptEstimatedTokens: 10,
|
|
450
|
-
executionId: "e",
|
|
451
|
-
state,
|
|
452
|
-
maxCostUsd: 0.5,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
|
|
456
|
-
|
|
457
|
-
expect(state.costCapExceeded).toBe(true);
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
it("never flags costCapExceeded when no cap is configured", () => {
|
|
461
|
-
const state = newTurnStreamState();
|
|
462
|
-
const usageAccumulator = {
|
|
463
|
-
...stubUsageAccumulator(),
|
|
464
|
-
snapshot: vi.fn(() => ({ estimatedCostUsd: 999 })),
|
|
465
|
-
};
|
|
466
|
-
const onDelta = makeCursorTurnOnDelta({
|
|
467
|
-
usageAccumulator: usageAccumulator as never,
|
|
468
|
-
deltaEnricher: stubEnricher() as never,
|
|
469
|
-
heartbeat: vi.fn(),
|
|
470
|
-
promptEstimatedTokens: 10,
|
|
471
|
-
executionId: "e",
|
|
472
|
-
state,
|
|
473
|
-
maxCostUsd: 0,
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
onDelta({ update: { type: "turn-ended", usage: { inputTokens: 100 } } as never });
|
|
477
|
-
|
|
478
|
-
expect(state.costCapExceeded).toBe(false);
|
|
479
|
-
});
|
|
480
355
|
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Cursor pricer: one `turn-ended` delta priced at the requested
|
|
3
|
+
* variant's rates and attributed with the basis the runtime records. The
|
|
4
|
+
* accounting half is `harness/__tests__/usage-accumulator.test.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeAll } from "vitest";
|
|
8
|
+
import { ServiceTier } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
9
|
+
|
|
10
|
+
import { CursorUsagePricer } from "../usage-pricing.js";
|
|
11
|
+
|
|
12
|
+
// The pricer reads the worker's pricing table (model-pricing.ts); load it
|
|
13
|
+
// from a stubbed registry so the fast-rate assertions run against known
|
|
14
|
+
// prices (the model-pricing.test.ts pattern).
|
|
15
|
+
beforeAll(async () => {
|
|
16
|
+
const registry = {
|
|
17
|
+
models: [
|
|
18
|
+
{
|
|
19
|
+
id: "composer-2.5",
|
|
20
|
+
displayName: "Composer 2.5",
|
|
21
|
+
provider: "cursor",
|
|
22
|
+
harness: "cursor",
|
|
23
|
+
costTier: "economy",
|
|
24
|
+
pricing: {
|
|
25
|
+
inputPricePerMillion: 0.5,
|
|
26
|
+
outputPricePerMillion: 2.5,
|
|
27
|
+
cacheWritePricePerMillion: 0,
|
|
28
|
+
cacheReadPricePerMillion: 0.2,
|
|
29
|
+
},
|
|
30
|
+
pricingVariants: {
|
|
31
|
+
fast: {
|
|
32
|
+
inputPricePerMillion: 3.0,
|
|
33
|
+
outputPricePerMillion: 15.0,
|
|
34
|
+
cacheWritePricePerMillion: 0,
|
|
35
|
+
cacheReadPricePerMillion: 0.2,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
vi.stubGlobal(
|
|
42
|
+
"fetch",
|
|
43
|
+
vi.fn(async () => ({ ok: true, status: 200, json: async () => registry })),
|
|
44
|
+
);
|
|
45
|
+
process.env.STIGMER_TOKEN = "test-token";
|
|
46
|
+
await (await import("../model-pricing.js")).ensureLoaded();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const MILLION_EACH = { inputTokens: 1_000_000, outputTokens: 1_000_000 };
|
|
50
|
+
|
|
51
|
+
describe("CursorUsagePricer", () => {
|
|
52
|
+
it("prices FAST runs at fast-variant rates, not base rates (#357)", () => {
|
|
53
|
+
// Revert guard for the tier→pricing wiring: a FAST run priced at base
|
|
54
|
+
// rates would understate the display estimate ~6x relative to the
|
|
55
|
+
// authoritative bill. Rates from the stubbed registry: base $0.5/$2.5
|
|
56
|
+
// per M, fast $3/$15 per M.
|
|
57
|
+
const standard = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD).price(MILLION_EACH);
|
|
58
|
+
const fast = new CursorUsagePricer("composer-2.5", ServiceTier.FAST).price(MILLION_EACH);
|
|
59
|
+
expect(standard.estimatedCostUsd).toBeCloseTo(3.0, 6);
|
|
60
|
+
expect(fast.estimatedCostUsd).toBeCloseTo(18.0, 6);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("prices UNSPECIFIED at base rates (resolves to standard)", () => {
|
|
64
|
+
const unspecified = new CursorUsagePricer("composer-2.5", ServiceTier.UNSPECIFIED).price(MILLION_EACH);
|
|
65
|
+
const standard = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD).price(MILLION_EACH);
|
|
66
|
+
expect(unspecified.estimatedCostUsd).toBe(standard.estimatedCostUsd);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("names the basis on every priced delta: the model id and the JSON of the params sent", () => {
|
|
70
|
+
const delta = new CursorUsagePricer("composer-2.5", ServiceTier.FAST, [{ id: "fast", value: "true" }]).price({
|
|
71
|
+
inputTokens: 10,
|
|
72
|
+
outputTokens: 5,
|
|
73
|
+
});
|
|
74
|
+
expect(delta.model).toBe("composer-2.5");
|
|
75
|
+
expect(delta.requestedModelParams).toBe('[{"id":"fast","value":"true"}]');
|
|
76
|
+
expect(delta).toMatchObject({ inputTokens: 10, outputTokens: 5, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("names an empty params string when the runner sent none, and treats missing counts as zero", () => {
|
|
80
|
+
const delta = new CursorUsagePricer("composer-2.5", ServiceTier.STANDARD, []).price({ inputTokens: 1 });
|
|
81
|
+
expect(delta.requestedModelParams).toBe("");
|
|
82
|
+
expect(delta.outputTokens).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|