@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,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child entry `__tests__/harness-boot-order.test.ts` spawns: one FRESH
|
|
3
|
+
* Node process that does exactly what the composition roots do before
|
|
4
|
+
* bootstrap — import `harness-adapters.js` and `harness/registry.js`, then
|
|
5
|
+
* `bootHarnesses` on a proxy-mode config — and reports how it went.
|
|
6
|
+
*
|
|
7
|
+
* Why a child process: the `node:http2` ESM facade is snapshotted once per
|
|
8
|
+
* process at its first import, so the positive load-order contract ("the
|
|
9
|
+
* patch reached the facade") cannot be observed inside a shared vitest
|
|
10
|
+
* worker that has already imported half the runner. A fresh process is the
|
|
11
|
+
* only honest instrument (`http2-interceptor.test.ts` says as much).
|
|
12
|
+
*
|
|
13
|
+
* Arms (argv[2]):
|
|
14
|
+
* - `boot`: import the two pre-boot modules, boot with a proxy endpoint and
|
|
15
|
+
* a bound token ref. Success means the pre-boot graph was connect-free,
|
|
16
|
+
* both interceptors installed, `assertHttp2ConnectPatched` passed, and the
|
|
17
|
+
* SDK slice loaded from inside `boot`.
|
|
18
|
+
* - `boot-after-connect-node`: the same, after importing the Stigmer client
|
|
19
|
+
* (connect-node) FIRST. Must fail with the facade sentence: proves the
|
|
20
|
+
* guard is live inside `boot`, so a future regression is loud.
|
|
21
|
+
*
|
|
22
|
+
* Output: one line, `harness-boot-order: ok` or `harness-boot-order: <message>`,
|
|
23
|
+
* then exit 0 on success and 1 on a rejection. The proxy endpoint is an inert
|
|
24
|
+
* loopback nothing listens on; boot dials nothing.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { testConfig } from "./config-fixture.js";
|
|
28
|
+
|
|
29
|
+
const arm = process.argv[2];
|
|
30
|
+
|
|
31
|
+
async function main(): Promise<void> {
|
|
32
|
+
if (arm === "boot-after-connect-node") {
|
|
33
|
+
await import("../client/stigmer-client.js");
|
|
34
|
+
} else if (arm !== "boot") {
|
|
35
|
+
throw new Error(`harness-boot-order-child: unknown arm '${String(arm)}' (expected 'boot' or 'boot-after-connect-node')`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const [{ HARNESS_ADAPTERS }, { adaptersOf, bootHarnesses }] = await Promise.all([
|
|
39
|
+
import("../harness-adapters.js"),
|
|
40
|
+
import("../harness/registry.js"),
|
|
41
|
+
]);
|
|
42
|
+
const config = testConfig({
|
|
43
|
+
proxyEndpoint: "http://127.0.0.1:1",
|
|
44
|
+
proxyTokenRef: { current: "boot-order-test-token" },
|
|
45
|
+
});
|
|
46
|
+
await bootHarnesses(adaptersOf(HARNESS_ADAPTERS), config);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().then(
|
|
50
|
+
() => {
|
|
51
|
+
process.stdout.write("harness-boot-order: ok\n");
|
|
52
|
+
process.exit(0);
|
|
53
|
+
},
|
|
54
|
+
(err: unknown) => {
|
|
55
|
+
process.stdout.write(`harness-boot-order: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness adapter contract kit — the single authoritative statement of
|
|
3
|
+
* "these are THE things every `HarnessAdapter` must do," runnable against any
|
|
4
|
+
* {@link HarnessContractSubject}.
|
|
5
|
+
*
|
|
6
|
+
* Two halves exist in the program's design; this file is the ADAPTER-SIDE
|
|
7
|
+
* half: what an adapter owes the runtime through `runTurn`, `boot`,
|
|
8
|
+
* `shutdown` and `releaseSession`. The runtime-side half (the throw-vs-return
|
|
9
|
+
* table end to end, the whole-activity heartbeat, the single persist
|
|
10
|
+
* chokepoint, the byte-pinned copy) is `runtime-contract.ts`, proven against
|
|
11
|
+
* the real runtime under `MockActivityEnvironment` with the same subjects.
|
|
12
|
+
*
|
|
13
|
+
* The kit IS the runtime stand-in. {@link ExecutionDriver} does what the
|
|
14
|
+
* runtime and the server do around a turn: builds the `TurnInput`, threads
|
|
15
|
+
* the engine's state id (empty on an engine-minted harness's first turn, then
|
|
16
|
+
* the id the adapter bound; one fixed id for a deterministic harness),
|
|
17
|
+
* advances `turnSeq`, records a user's decision the way `SubmitApproval` does
|
|
18
|
+
* (on the persisted row's `approvalAction`, the one copy) and derives the
|
|
19
|
+
* decisions map from those rows on the next invocation with the runtime's
|
|
20
|
+
* own reader, hands each reinvocation a CLONE of the previous status on BOTH
|
|
21
|
+
* the sink and the record (`execution.status` — the runtime seeds the one
|
|
22
|
+
* from the other, and an adapter may read the record's copy for its facts;
|
|
23
|
+
* nothing survives by object identity), and owns the sink. The subject owns
|
|
24
|
+
* the engine, and tells the kit when that engine is parked on a hang, which
|
|
25
|
+
* is the only moment the kit stops a turn from the outside: a real engine
|
|
26
|
+
* reaches its hang after real setup, and a stop delivered earlier lands in
|
|
27
|
+
* the setup and proves nothing. The kit also owns the adapter's lifetime —
|
|
28
|
+
* booted before the first turn, shut down after the last — because the
|
|
29
|
+
* registry does, and a real adapter refuses a turn before boot.
|
|
30
|
+
*
|
|
31
|
+
* ── Invariant catalog ───────────────────────────────────────────────────────
|
|
32
|
+
* 1. Every exit is a `TurnOutcome`. `runTurn` resolves, never rejects, under
|
|
33
|
+
* every scenario kind and under a pre-aborted signal; a `CancelledFailure`
|
|
34
|
+
* never escapes (the runtime, not the adapter, throws it).
|
|
35
|
+
* 2. A proposal is a WAITING_APPROVAL row on `sink.status` when
|
|
36
|
+
* `awaiting_approval` resolves, and never executes in that turn.
|
|
37
|
+
* 3. Reinvoked with APPROVE for that id → executes exactly once and the row
|
|
38
|
+
* is carried to COMPLETED; with REJECT or SKIP → never executes and the
|
|
39
|
+
* turn continues (the row's terminal status is the runtime's to write,
|
|
40
|
+
* not the adapter's — Q-M4-1); reinvoked again after the approval → still
|
|
41
|
+
* once, never re-gated.
|
|
42
|
+
* 4. Aborting `stopSignal` mid-hang settles `runTurn` as `interrupted` within
|
|
43
|
+
* {@link INTERRUPT_SETTLE_BOUND_MS} and nothing after the stop executes; a
|
|
44
|
+
* signal aborted BEFORE `runTurn` yields `interrupted` with nothing done.
|
|
45
|
+
* 5. Usage reaches the sink as non-negative deltas summing to what the engine
|
|
46
|
+
* emitted.
|
|
47
|
+
* 6. Capability and behaviour agree on the state id: an `engine-minted`
|
|
48
|
+
* adapter binds before its first persist and resumes by the bound id, and
|
|
49
|
+
* surfaces a rejected bind as `failed` with nothing done after it; a
|
|
50
|
+
* `deterministic` adapter never binds.
|
|
51
|
+
* 7. Lifetimes: `releaseSession` for a served session and for an unknown one
|
|
52
|
+
* both resolve; `shutdown` after a release resolves; `boot` after
|
|
53
|
+
* `shutdown` resolves (a worker restart re-boots the same module state).
|
|
54
|
+
* 8. One adapter object serves concurrent turns independently: a hanging turn
|
|
55
|
+
* and a completing turn on the same adapter never cross outcomes or
|
|
56
|
+
* execution counts.
|
|
57
|
+
*
|
|
58
|
+
* Every invariant is an exported plain async function first and an `it`
|
|
59
|
+
* block second, so `__tests__/harness-contract-self-check.test.ts` can run
|
|
60
|
+
* each one against a deliberately broken adapter and prove it fires — a kit
|
|
61
|
+
* that cannot fail proves nothing. Every message names the subject. Each
|
|
62
|
+
* function requires a booted adapter; {@link describeHarnessContract} boots.
|
|
63
|
+
*
|
|
64
|
+
* Above the contract line the pause primitives are indistinguishable: both
|
|
65
|
+
* end a turn `awaiting_approval`, both take the decisions on reinvocation.
|
|
66
|
+
* The kit therefore never branches on `capabilities.pausePrimitive`, and the
|
|
67
|
+
* runner test runs the fake under both to prove it.
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
import { afterAll, beforeAll, describe, it, expect } from "vitest";
|
|
71
|
+
import { CancelledFailure } from "@temporalio/activity";
|
|
72
|
+
import { clone } from "@bufbuild/protobuf";
|
|
73
|
+
import { AgentExecutionStatusSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
74
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
75
|
+
import { ApprovalAction, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
76
|
+
|
|
77
|
+
import { approvalDecisionsOf } from "../../harness/turn-context.js";
|
|
78
|
+
import type { TurnInput, TurnOutcome, UsageDelta } from "../../harness/types.js";
|
|
79
|
+
import type { ProposedAction } from "../approval-contract/types.js";
|
|
80
|
+
import { emptyStatus, findToolCallRow } from "../proto-helpers.js";
|
|
81
|
+
import { turnInputFixture } from "../turn-input-fixture.js";
|
|
82
|
+
import { RecordingTurnSink } from "./recording-sink.js";
|
|
83
|
+
import { scenario } from "./types.js";
|
|
84
|
+
import type { EngineView, HarnessContractSubject, TurnScenario } from "./types.js";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* How long a turn may take to settle `interrupted` after its signal aborts.
|
|
88
|
+
* Generous against a saturated CI runner (the fake settles in microtasks; a
|
|
89
|
+
* real adapter cancels an SDK run), tight enough that an adapter parked on
|
|
90
|
+
* anything but the signal fails here rather than at Temporal's heartbeat
|
|
91
|
+
* timeout in production. A timeout race, never a sleep.
|
|
92
|
+
*/
|
|
93
|
+
export const INTERRUPT_SETTLE_BOUND_MS = 5_000;
|
|
94
|
+
|
|
95
|
+
const OUTCOME_KINDS = ["completed", "cancelled", "awaiting_approval", "failed", "interrupted"] as const satisfies readonly TurnOutcome["kind"][];
|
|
96
|
+
|
|
97
|
+
/** Representative gated action shared by the invariants. */
|
|
98
|
+
const WRITE_ALPHA: ProposedAction = { kind: "write", resource: "/work/alpha.txt" };
|
|
99
|
+
|
|
100
|
+
// ── The runtime (and server) stand-in ───────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
export interface TurnRun {
|
|
103
|
+
readonly input: TurnInput;
|
|
104
|
+
readonly sink: RecordingTurnSink;
|
|
105
|
+
readonly outcome: TurnOutcome;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface TurnOptions {
|
|
109
|
+
/** A prepared sink (e.g. one whose bind rejects); defaults to a fresh one over the threaded status. */
|
|
110
|
+
readonly sink?: RecordingTurnSink;
|
|
111
|
+
/**
|
|
112
|
+
* Abort the sink's signal with this reason the moment the engine reports
|
|
113
|
+
* it is parked on a `hang` — the one way the kit stops a turn from the
|
|
114
|
+
* outside (see the header on why not sooner).
|
|
115
|
+
*/
|
|
116
|
+
readonly stopWhenHanging?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface TurnInFlight {
|
|
120
|
+
readonly input: TurnInput;
|
|
121
|
+
readonly sink: RecordingTurnSink;
|
|
122
|
+
readonly settled: Promise<TurnOutcome>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* One execution's worth of turns against one subject, doing the runtime's
|
|
127
|
+
* and the server's bookkeeping between them. A new driver per test: it
|
|
128
|
+
* carries the threaded state id and the persisted status across
|
|
129
|
+
* reinvocations.
|
|
130
|
+
*/
|
|
131
|
+
export class ExecutionDriver {
|
|
132
|
+
readonly executionId: string;
|
|
133
|
+
readonly sessionId: string;
|
|
134
|
+
private threadId: string;
|
|
135
|
+
private turnSeq = 0;
|
|
136
|
+
private persisted: AgentExecutionStatus | undefined;
|
|
137
|
+
|
|
138
|
+
constructor(private readonly subject: HarnessContractSubject, label: string) {
|
|
139
|
+
this.executionId = `exec-${label}`;
|
|
140
|
+
this.sessionId = `ses-${label}`;
|
|
141
|
+
// A deterministic harness's id is the runtime's, known before any engine
|
|
142
|
+
// exists (native's `thread-{sessionId}`); an engine-minted one has nothing
|
|
143
|
+
// until the adapter binds.
|
|
144
|
+
this.threadId = subject.adapter.capabilities.stateIdSource === "deterministic" ? `thread-${this.sessionId}` : "";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Start a turn without awaiting it, for invariants about turns in flight. */
|
|
148
|
+
begin(turn: TurnScenario, options: TurnOptions = {}): TurnInFlight {
|
|
149
|
+
const sink = options.sink ?? new RecordingTurnSink({ status: this.seedStatus() });
|
|
150
|
+
// The engine's view and the record's copy both derive from the status
|
|
151
|
+
// this turn folds into, so a prepared sink's status is honoured too.
|
|
152
|
+
const view: EngineView = {
|
|
153
|
+
executionId: this.executionId,
|
|
154
|
+
sessionId: this.sessionId,
|
|
155
|
+
approvalDecisions: approvalDecisionsOf(sink.status),
|
|
156
|
+
};
|
|
157
|
+
this.subject.arrange(turn, view);
|
|
158
|
+
// Registered BEFORE the turn is entered so a hang reached synchronously
|
|
159
|
+
// is still observed; the abort itself lands when the engine reports.
|
|
160
|
+
const parked = options.stopWhenHanging !== undefined ? this.subject.whenHanging() : undefined;
|
|
161
|
+
// The whole resolved record: the subject's needs under the driver's five
|
|
162
|
+
// facts and the persisted status (`turn-input-fixture.ts`).
|
|
163
|
+
const input: TurnInput = turnInputFixture({
|
|
164
|
+
...this.subject.inputOverrides?.(view),
|
|
165
|
+
executionId: this.executionId,
|
|
166
|
+
threadId: this.threadId,
|
|
167
|
+
turnSeq: this.turnSeq,
|
|
168
|
+
sessionId: this.sessionId,
|
|
169
|
+
approvalDecisions: view.approvalDecisions,
|
|
170
|
+
persistedStatus: clone(AgentExecutionStatusSchema, sink.status),
|
|
171
|
+
});
|
|
172
|
+
const settled = this.subject.adapter.runTurn(input, sink);
|
|
173
|
+
if (parked && options.stopWhenHanging !== undefined) {
|
|
174
|
+
const reason = options.stopWhenHanging;
|
|
175
|
+
void parked.then(() => sink.abort(reason));
|
|
176
|
+
}
|
|
177
|
+
return { input, sink, settled };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Run a turn to its outcome and do the runtime's bookkeeping after it. */
|
|
181
|
+
async turn(turn: TurnScenario, options: TurnOptions = {}): Promise<TurnRun> {
|
|
182
|
+
const { input, sink, settled } = this.begin(turn, options);
|
|
183
|
+
const outcome = await settled;
|
|
184
|
+
this.recordTurn(sink);
|
|
185
|
+
return { input, sink, outcome };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The runtime's post-turn bookkeeping: persist the status, adopt a bound id, advance the cycle. */
|
|
189
|
+
recordTurn(sink: RecordingTurnSink): void {
|
|
190
|
+
this.persisted = sink.status;
|
|
191
|
+
const bound = sink.boundStateIds.at(-1);
|
|
192
|
+
if (bound !== undefined) this.threadId = bound;
|
|
193
|
+
this.turnSeq += 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* The server's act between invocations: `SubmitApproval` writes the user's
|
|
198
|
+
* verdict onto the WAITING row it owns. A decision on a row that does not
|
|
199
|
+
* exist or is not waiting is a test bug, not a contract case.
|
|
200
|
+
*/
|
|
201
|
+
decide(toolCallId: string, action: ApprovalAction): void {
|
|
202
|
+
const row = this.persisted ? findToolCallRow(this.persisted, toolCallId) : undefined;
|
|
203
|
+
if (!row || row.status !== ToolCallStatus.TOOL_CALL_WAITING_APPROVAL) {
|
|
204
|
+
const rows = (this.persisted?.messages ?? [])
|
|
205
|
+
.flatMap((m) => m.toolCalls)
|
|
206
|
+
.map((tc) => `${tc.id}=${ToolCallStatus[tc.status]}`)
|
|
207
|
+
.join(", ");
|
|
208
|
+
throw new Error(`${this.subject.name}: kit bug — decide('${toolCallId}') but no WAITING row was persisted (rows: ${rows || "none"})`);
|
|
209
|
+
}
|
|
210
|
+
row.approvalAction = action;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** The status a reinvocation is seeded with: a clone of what was persisted. */
|
|
214
|
+
seedStatus(): AgentExecutionStatus {
|
|
215
|
+
return this.persisted ? clone(AgentExecutionStatusSchema, this.persisted) : emptyStatus();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Shared assertion helpers ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Await a turn, translating a rejection into a contract violation that names
|
|
223
|
+
* the subject and the kind of throw. A `CancelledFailure` gets named
|
|
224
|
+
* specially because throwing it is the one thing an adapter is most tempted
|
|
225
|
+
* to do and must never do.
|
|
226
|
+
*/
|
|
227
|
+
async function settleAsOutcome(subject: HarnessContractSubject, settled: Promise<TurnOutcome>, when: string): Promise<TurnOutcome> {
|
|
228
|
+
let outcome: TurnOutcome;
|
|
229
|
+
try {
|
|
230
|
+
outcome = await settled;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const kind = err instanceof CancelledFailure ? "a CancelledFailure" : `an exception (${err instanceof Error ? err.message : String(err)})`;
|
|
233
|
+
throw new Error(`${subject.name}: runTurn rejected with ${kind} ${when}; every exit must be a TurnOutcome`);
|
|
234
|
+
}
|
|
235
|
+
expect(OUTCOME_KINDS, `${subject.name}: runTurn settled with an unknown outcome kind ${when}`).toContain(outcome.kind);
|
|
236
|
+
return outcome;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Race a settlement against the interrupt bound; the timer is cleared on settle so a passing test holds nothing. */
|
|
240
|
+
async function settleWithinBound<T>(subject: HarnessContractSubject, settled: Promise<T>, when: string, boundMs: number): Promise<T> {
|
|
241
|
+
let timer: NodeJS.Timeout | undefined;
|
|
242
|
+
const bound = new Promise<never>((_, reject) => {
|
|
243
|
+
timer = setTimeout(
|
|
244
|
+
() => reject(new Error(`${subject.name}: runTurn did not settle within ${boundMs}ms ${when}; every adapter call must be bounded by stopSignal`)),
|
|
245
|
+
boundMs,
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
return await Promise.race([settled, bound]);
|
|
250
|
+
} finally {
|
|
251
|
+
if (timer) clearTimeout(timer);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function expectResolves(subject: HarnessContractSubject, call: Promise<void>, what: string): Promise<void> {
|
|
256
|
+
try {
|
|
257
|
+
await call;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
throw new Error(`${subject.name}: ${what} rejected (${err instanceof Error ? err.message : String(err)}); it must resolve`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Invariant 1 ─────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Invariant 1: every exit is a `TurnOutcome`. Each scenario kind is played
|
|
267
|
+
* once, the hanging one is stopped from the outside, and one turn is entered
|
|
268
|
+
* with an already-aborted signal; none may reject, none may settle with an
|
|
269
|
+
* unknown kind.
|
|
270
|
+
*/
|
|
271
|
+
export async function assertEveryExitIsAnOutcome(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
|
|
272
|
+
const driver = new ExecutionDriver(subject, "inv1");
|
|
273
|
+
const plays: ReadonlyArray<{ readonly when: string; readonly turn: TurnScenario; readonly options?: TurnOptions }> = [
|
|
274
|
+
{ when: "on a plain text turn", turn: [scenario.say("hello")] },
|
|
275
|
+
{ when: "on a usage-only turn", turn: [scenario.usage({ inputTokens: 1, outputTokens: 1 })] },
|
|
276
|
+
{ when: "on an undecided proposal", turn: [scenario.propose("inv1-call", WRITE_ALPHA)] },
|
|
277
|
+
{ when: "on an engine failure", turn: [scenario.fail("engine exploded")] },
|
|
278
|
+
{ when: "on a hang stopped from the outside", turn: [scenario.hang()], options: { stopWhenHanging: "kit: stop" } },
|
|
279
|
+
];
|
|
280
|
+
for (const play of plays) {
|
|
281
|
+
const { sink, settled } = driver.begin(play.turn, play.options);
|
|
282
|
+
await settleAsOutcome(subject, settleWithinBound(subject, settled, play.when, boundMs), play.when);
|
|
283
|
+
driver.recordTurn(sink);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const preAborted = new RecordingTurnSink({ status: driver.seedStatus() });
|
|
287
|
+
preAborted.abort("kit: aborted before the turn");
|
|
288
|
+
const { settled } = driver.begin([scenario.say("never")], { sink: preAborted });
|
|
289
|
+
await settleAsOutcome(subject, settleWithinBound(subject, settled, "under a pre-aborted signal", boundMs), "under a pre-aborted signal");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Invariant 2 ─────────────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Invariant 2: a proposal is a WAITING_APPROVAL row and never executes in its
|
|
296
|
+
* own turn. The row must carry `requiresApproval` and an UNSPECIFIED
|
|
297
|
+
* `approvalAction` (the server's field, untouched by the adapter), and the
|
|
298
|
+
* turn must end `awaiting_approval` — not `completed`, which would tell the
|
|
299
|
+
* workflow nothing is pending.
|
|
300
|
+
*/
|
|
301
|
+
export async function assertProposalIsWaitingAndUnexecuted(subject: HarnessContractSubject): Promise<void> {
|
|
302
|
+
const driver = new ExecutionDriver(subject, "inv2");
|
|
303
|
+
const id = "inv2-write";
|
|
304
|
+
const { outcome, sink } = await driver.turn([scenario.say("about to write"), scenario.propose(id, WRITE_ALPHA), scenario.say("after")]);
|
|
305
|
+
|
|
306
|
+
expect(outcome.kind, `${subject.name}: a turn that proposes an undecided action must end awaiting_approval`).toBe("awaiting_approval");
|
|
307
|
+
const row = findToolCallRow(sink.status, id);
|
|
308
|
+
expect(row, `${subject.name}: the proposal must be a tool-call row on sink.status when awaiting_approval resolves`).toBeDefined();
|
|
309
|
+
expect(row?.status, `${subject.name}: the proposal's row must be WAITING_APPROVAL`).toBe(ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
310
|
+
expect(row?.requiresApproval, `${subject.name}: the proposal's row must carry requiresApproval`).toBe(true);
|
|
311
|
+
expect(row?.approvalAction, `${subject.name}: the adapter must never write the server's approvalAction`).toBe(ApprovalAction.UNSPECIFIED);
|
|
312
|
+
expect(subject.executionCount(id), `${subject.name}: an undecided proposal must not execute in its own turn`).toBe(0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Invariant 3 ─────────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Invariant 3: the decision is honoured exactly. APPROVE executes once and
|
|
319
|
+
* only once, however many times the same decided row is seen again, and the
|
|
320
|
+
* engine's completion reaches the row; REJECT and SKIP never execute and the
|
|
321
|
+
* turn continues — the row's terminal status is NOT asserted here because
|
|
322
|
+
* it is not the adapter's to write (the decision is the server's field and
|
|
323
|
+
* the WAITING → SKIPPED transition follows from it with no engine knowledge,
|
|
324
|
+
* so it is the runtime's; S2 M4 Q-M4-1, the runtime's arm lands in S3). The
|
|
325
|
+
* reinvocation sees a CLONE of the persisted status, as it would from the
|
|
326
|
+
* server.
|
|
327
|
+
*/
|
|
328
|
+
export async function assertDecisionsExecuteExactlyOnce(subject: HarnessContractSubject): Promise<void> {
|
|
329
|
+
const driver = new ExecutionDriver(subject, "inv3");
|
|
330
|
+
|
|
331
|
+
const approved = "inv3-approve";
|
|
332
|
+
const first = await driver.turn([scenario.propose(approved, WRITE_ALPHA)]);
|
|
333
|
+
expect(first.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
|
|
334
|
+
driver.decide(approved, ApprovalAction.APPROVE);
|
|
335
|
+
|
|
336
|
+
const resumed = await driver.turn([scenario.propose(approved, WRITE_ALPHA), scenario.say("done")]);
|
|
337
|
+
expect(resumed.outcome.kind, `${subject.name}: after APPROVE the turn must run to completion`).toBe("completed");
|
|
338
|
+
expect(subject.executionCount(approved), `${subject.name}: an approved action must execute exactly once`).toBe(1);
|
|
339
|
+
expect(findToolCallRow(resumed.sink.status, approved)?.status, `${subject.name}: the approved row must be carried to COMPLETED`).toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
340
|
+
|
|
341
|
+
const again = await driver.turn([scenario.propose(approved, WRITE_ALPHA), scenario.say("again")]);
|
|
342
|
+
expect(again.outcome.kind, `${subject.name}: a settled proposal seen again must not re-gate`).toBe("completed");
|
|
343
|
+
expect(subject.executionCount(approved), `${subject.name}: reinvoked again after the approval, the action must still have executed exactly once`).toBe(1);
|
|
344
|
+
|
|
345
|
+
for (const [label, action] of [["REJECT", ApprovalAction.REJECT], ["SKIP", ApprovalAction.SKIP]] as const) {
|
|
346
|
+
const id = `inv3-${label.toLowerCase()}`;
|
|
347
|
+
const proposed = await driver.turn([scenario.propose(id, WRITE_ALPHA)]);
|
|
348
|
+
expect(proposed.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
|
|
349
|
+
driver.decide(id, action);
|
|
350
|
+
const decided = await driver.turn([scenario.propose(id, WRITE_ALPHA), scenario.say("moving on")]);
|
|
351
|
+
expect(decided.outcome.kind, `${subject.name}: after ${label} the turn must continue to completion`).toBe("completed");
|
|
352
|
+
expect(subject.executionCount(id), `${subject.name}: a ${label}-ed action must never execute`).toBe(0);
|
|
353
|
+
expect(findToolCallRow(decided.sink.status, id)?.status, `${subject.name}: a ${label}-ed action must never be reported as executed`).not.toBe(ToolCallStatus.TOOL_CALL_COMPLETED);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ── Invariant 4 ─────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Invariant 4: the stop signal is honoured promptly and completely. A turn
|
|
361
|
+
* stopped WHILE HANGING (the engine has reported it is parked, so the stop
|
|
362
|
+
* lands in the hang and not in the adapter's setup) settles `interrupted`
|
|
363
|
+
* within the bound and executes nothing that came after the hang, even an
|
|
364
|
+
* already-approved action; a turn entered under an aborted signal does
|
|
365
|
+
* nothing at all.
|
|
366
|
+
*/
|
|
367
|
+
export async function assertStopSignalInterrupts(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
|
|
368
|
+
const driver = new ExecutionDriver(subject, "inv4");
|
|
369
|
+
const id = "inv4-approved-after-hang";
|
|
370
|
+
|
|
371
|
+
const proposed = await driver.turn([scenario.propose(id, WRITE_ALPHA)]);
|
|
372
|
+
expect(proposed.outcome.kind, `${subject.name}: proposal must end awaiting_approval`).toBe("awaiting_approval");
|
|
373
|
+
driver.decide(id, ApprovalAction.APPROVE);
|
|
374
|
+
|
|
375
|
+
const hanging = driver.begin([scenario.say("working"), scenario.hang(), scenario.propose(id, WRITE_ALPHA)], { stopWhenHanging: "kit: user pause" });
|
|
376
|
+
const outcome = await settleWithinBound(subject, hanging.settled, "after stopSignal aborted mid-hang", boundMs);
|
|
377
|
+
expect(outcome.kind, `${subject.name}: a turn stopped mid-hang must settle interrupted`).toBe("interrupted");
|
|
378
|
+
expect(subject.executionCount(id), `${subject.name}: nothing after the stop may execute, even an approved action`).toBe(0);
|
|
379
|
+
driver.recordTurn(hanging.sink);
|
|
380
|
+
|
|
381
|
+
const preAborted = new RecordingTurnSink({ status: driver.seedStatus() });
|
|
382
|
+
preAborted.abort("kit: aborted before the turn");
|
|
383
|
+
const early = driver.begin([scenario.propose(id, WRITE_ALPHA)], { sink: preAborted });
|
|
384
|
+
const earlyOutcome = await settleWithinBound(subject, early.settled, "under a pre-aborted signal", boundMs);
|
|
385
|
+
expect(earlyOutcome.kind, `${subject.name}: a turn entered under an aborted signal must settle interrupted`).toBe("interrupted");
|
|
386
|
+
expect(subject.executionCount(id), `${subject.name}: a turn entered under an aborted signal must do no work`).toBe(0);
|
|
387
|
+
expect(preAborted.persistRequests, `${subject.name}: a turn entered under an aborted signal must not ask to persist`).toBe(0);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── Invariant 5 ─────────────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
/** The four counts summed; the price and its basis are the adapter's per delta and are not summed here. */
|
|
393
|
+
interface UsageTotals {
|
|
394
|
+
inputTokens: number;
|
|
395
|
+
outputTokens: number;
|
|
396
|
+
cacheReadTokens: number;
|
|
397
|
+
cacheWriteTokens: number;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const TOKEN_COUNT_FIELDS = ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"] as const satisfies readonly (keyof UsageTotals)[];
|
|
401
|
+
|
|
402
|
+
function sumUsage(deltas: readonly UsageDelta[]): UsageTotals {
|
|
403
|
+
const total: UsageTotals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 };
|
|
404
|
+
for (const d of deltas) {
|
|
405
|
+
for (const field of TOKEN_COUNT_FIELDS) total[field] += d[field] ?? 0;
|
|
406
|
+
}
|
|
407
|
+
return total;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Invariant 5: usage reaches the sink as deltas — the token counts and the
|
|
412
|
+
* price non-negative, and the counts summing to what the engine emitted. An
|
|
413
|
+
* adapter that reports cumulative totals instead of deltas would
|
|
414
|
+
* double-count in the runtime's accumulator and trip the cost cap early; one
|
|
415
|
+
* that reports a negative number would credit it. The basis a delta names
|
|
416
|
+
* (`model`, `requestedModelParams`) is the adapter's own and not judged here.
|
|
417
|
+
*/
|
|
418
|
+
export async function assertUsageReachesSinkAsDeltas(subject: HarnessContractSubject): Promise<void> {
|
|
419
|
+
const driver = new ExecutionDriver(subject, "inv5");
|
|
420
|
+
const emitted: readonly UsageDelta[] = [
|
|
421
|
+
{ inputTokens: 10, outputTokens: 5 },
|
|
422
|
+
{ inputTokens: 3, cacheReadTokens: 2 },
|
|
423
|
+
{ outputTokens: 4, cacheWriteTokens: 1 },
|
|
424
|
+
];
|
|
425
|
+
const { outcome, sink } = await driver.turn([...emitted.map((d) => scenario.usage(d)), scenario.say("done")]);
|
|
426
|
+
expect(outcome.kind, `${subject.name}: a usage-reporting turn must complete`).toBe("completed");
|
|
427
|
+
|
|
428
|
+
for (const delta of sink.usageDeltas) {
|
|
429
|
+
for (const field of [...TOKEN_COUNT_FIELDS, "estimatedCostUsd"] as const) {
|
|
430
|
+
const value = delta[field];
|
|
431
|
+
if (value === undefined) continue;
|
|
432
|
+
expect(value, `${subject.name}: usage delta field ${field} must be non-negative`).toBeGreaterThanOrEqual(0);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
expect(sumUsage(sink.usageDeltas), `${subject.name}: the usage deltas must sum to what the engine emitted`).toEqual(sumUsage(emitted));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── Invariant 6 ─────────────────────────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Invariant 6: capability and behaviour agree on the state id. Engine-minted:
|
|
442
|
+
* the bind precedes the first persist (so a crash mid-turn still resumes),
|
|
443
|
+
* the id threaded back on the next invocation is accepted, and a rejected
|
|
444
|
+
* bind ends the turn `failed` with nothing done after it. Deterministic: the
|
|
445
|
+
* adapter never binds and accepts the runtime's id on every turn.
|
|
446
|
+
*/
|
|
447
|
+
export async function assertStateIdCapabilityAgrees(subject: HarnessContractSubject): Promise<void> {
|
|
448
|
+
const driver = new ExecutionDriver(subject, "inv6");
|
|
449
|
+
const first = await driver.turn([scenario.say("first turn")]);
|
|
450
|
+
expect(first.outcome.kind, `${subject.name}: a plain turn must complete`).toBe("completed");
|
|
451
|
+
|
|
452
|
+
if (subject.adapter.capabilities.stateIdSource === "engine-minted") {
|
|
453
|
+
expect(first.input.threadId, `${subject.name}: an engine-minted harness has no state id before its first turn`).toBe("");
|
|
454
|
+
const events = first.sink.events;
|
|
455
|
+
const firstBind = events.findIndex((e) => e.kind === "bind");
|
|
456
|
+
const firstPersist = events.findIndex((e) => e.kind === "persist");
|
|
457
|
+
expect(firstBind, `${subject.name}: an engine-minted adapter must bind its state id on the first turn`).toBeGreaterThanOrEqual(0);
|
|
458
|
+
expect(first.sink.boundStateIds[0], `${subject.name}: the bound state id must be non-empty`).toBeTruthy();
|
|
459
|
+
if (firstPersist >= 0) {
|
|
460
|
+
expect(firstBind, `${subject.name}: the state id must be bound BEFORE the first persist request, or a crash mid-turn cannot resume`).toBeLessThan(firstPersist);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const second = await driver.turn([scenario.say("second turn")]);
|
|
464
|
+
expect(second.input.threadId, `${subject.name}: the kit threads the bound id back as threadId`).toBe(first.sink.boundStateIds[0]);
|
|
465
|
+
expect(second.outcome.kind, `${subject.name}: resuming by the id the adapter bound must complete`).toBe("completed");
|
|
466
|
+
expect(second.sink.boundStateIds, `${subject.name}: a resumed turn must not bind a new state id`).toHaveLength(0);
|
|
467
|
+
|
|
468
|
+
const failing = new ExecutionDriver(subject, "inv6-bind-rejects");
|
|
469
|
+
const rejecting = new RecordingTurnSink({ bindRejectsWith: new Error("session write failed") });
|
|
470
|
+
const rejected = await failing.turn([scenario.say("never persisted")], { sink: rejecting });
|
|
471
|
+
expect(rejected.outcome.kind, `${subject.name}: a rejected bind must end the turn failed, not reject runTurn`).toBe("failed");
|
|
472
|
+
expect(rejecting.persistRequests, `${subject.name}: nothing may be persisted after a rejected bind`).toBe(0);
|
|
473
|
+
expect(rejecting.status.messages, `${subject.name}: nothing may be folded into the status after a rejected bind`).toHaveLength(0);
|
|
474
|
+
} else {
|
|
475
|
+
expect(first.input.threadId, `${subject.name}: a deterministic harness is handed the runtime's id on its first turn`).not.toBe("");
|
|
476
|
+
expect(first.sink.boundStateIds, `${subject.name}: a deterministic adapter must never bind a state id`).toHaveLength(0);
|
|
477
|
+
const second = await driver.turn([scenario.say("second turn")]);
|
|
478
|
+
expect(second.input.threadId, `${subject.name}: a deterministic id is the same on every turn`).toBe(first.input.threadId);
|
|
479
|
+
expect(second.outcome.kind, `${subject.name}: a deterministic resume must complete`).toBe("completed");
|
|
480
|
+
expect(second.sink.boundStateIds, `${subject.name}: a deterministic adapter must never bind a state id`).toHaveLength(0);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Invariant 7 ─────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Invariant 7: the lifetimes resolve. On the booted adapter: serve a turn,
|
|
488
|
+
* release the served session and an unknown one, shut down, and boot again
|
|
489
|
+
* (a worker restart re-boots the same module state). Each must resolve — a
|
|
490
|
+
* rejection in any would leak a session's resources, hang a drain, or fail
|
|
491
|
+
* the next worker's boot. Boot itself is proven by every turn before this
|
|
492
|
+
* one having run.
|
|
493
|
+
*/
|
|
494
|
+
export async function assertLifetimesResolve(subject: HarnessContractSubject): Promise<void> {
|
|
495
|
+
const { adapter } = subject;
|
|
496
|
+
const driver = new ExecutionDriver(subject, "inv7");
|
|
497
|
+
const served = await driver.turn([scenario.say("served")]);
|
|
498
|
+
expect(served.outcome.kind, `${subject.name}: a plain turn must complete`).toBe("completed");
|
|
499
|
+
|
|
500
|
+
await expectResolves(subject, adapter.releaseSession(served.input.sessionId), `releaseSession('${served.input.sessionId}') for a served session`);
|
|
501
|
+
await expectResolves(subject, adapter.releaseSession("ses-never-served"), "releaseSession for an unknown session");
|
|
502
|
+
await expectResolves(subject, adapter.shutdown(), "shutdown() after release");
|
|
503
|
+
await expectResolves(subject, adapter.boot(subject.config), "boot(config) after shutdown (a worker restart)");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Invariant 8 ─────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Invariant 8: one adapter object, many turns. While one execution's turn
|
|
510
|
+
* hangs (reported parked before the other execution starts, so the two are
|
|
511
|
+
* truly concurrent and not merely queued), another execution's turns
|
|
512
|
+
* propose, get decided and execute on the SAME adapter; stopping the hanging
|
|
513
|
+
* turn settles only it; nothing crosses. This is "the adapter holds no
|
|
514
|
+
* per-turn state" made observable — the property `maxConcurrentActivities`
|
|
515
|
+
* relies on.
|
|
516
|
+
*/
|
|
517
|
+
export async function assertConcurrentTurnsAreIndependent(subject: HarnessContractSubject, boundMs = INTERRUPT_SETTLE_BOUND_MS): Promise<void> {
|
|
518
|
+
const hangingExecution = new ExecutionDriver(subject, "inv8-hanging");
|
|
519
|
+
const busyExecution = new ExecutionDriver(subject, "inv8-busy");
|
|
520
|
+
const id = "inv8-busy-write";
|
|
521
|
+
|
|
522
|
+
const parked = subject.whenHanging();
|
|
523
|
+
const hanging = hangingExecution.begin([scenario.say("parked"), scenario.hang()]);
|
|
524
|
+
await settleWithinBound(subject, parked, "waiting for the first execution to park on its hang", boundMs);
|
|
525
|
+
|
|
526
|
+
const proposed = await settleWithinBound(subject, busyExecution.turn([scenario.propose(id, WRITE_ALPHA)]), "on a second execution while another turn hangs", boundMs);
|
|
527
|
+
expect(proposed.outcome.kind, `${subject.name}: a second execution's proposal must settle while another turn hangs`).toBe("awaiting_approval");
|
|
528
|
+
busyExecution.decide(id, ApprovalAction.APPROVE);
|
|
529
|
+
const resumed = await settleWithinBound(subject, busyExecution.turn([scenario.propose(id, WRITE_ALPHA)]), "on a second execution's reinvocation while another turn hangs", boundMs);
|
|
530
|
+
expect(resumed.outcome.kind, `${subject.name}: a second execution's approved turn must complete while another turn hangs`).toBe("completed");
|
|
531
|
+
expect(subject.executionCount(id), `${subject.name}: the busy execution's action must execute exactly once`).toBe(1);
|
|
532
|
+
|
|
533
|
+
hanging.sink.abort("kit: stop the parked turn");
|
|
534
|
+
const stopped = await settleWithinBound(subject, hanging.settled, "after the hanging turn was stopped", boundMs);
|
|
535
|
+
expect(stopped.kind, `${subject.name}: the hanging turn must settle interrupted, and only it`).toBe("interrupted");
|
|
536
|
+
expect(subject.executionCount(id), `${subject.name}: stopping one execution must not touch another's execution count`).toBe(1);
|
|
537
|
+
expect(findToolCallRow(hanging.sink.status, id), `${subject.name}: one execution's rows must never appear on another's status`).toBeUndefined();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ── The runnable suite ──────────────────────────────────────────────────────
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Register the adapter-side contract against one subject. Boots the adapter
|
|
544
|
+
* once before the first invariant and shuts it down after the last, as the
|
|
545
|
+
* registry does around a worker's life (`bootHarnesses` / `shutdownHarnesses`).
|
|
546
|
+
*/
|
|
547
|
+
export function describeHarnessContract(subject: HarnessContractSubject): void {
|
|
548
|
+
describe(`harness contract — ${subject.name}`, () => {
|
|
549
|
+
beforeAll(async () => {
|
|
550
|
+
await subject.adapter.boot(subject.config);
|
|
551
|
+
});
|
|
552
|
+
afterAll(async () => {
|
|
553
|
+
await subject.adapter.shutdown();
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("settles every turn with a TurnOutcome and never rejects (invariant 1)", async () => {
|
|
557
|
+
await assertEveryExitIsAnOutcome(subject);
|
|
558
|
+
});
|
|
559
|
+
it("surfaces a proposal as a WAITING row and never executes it in its turn (invariant 2)", async () => {
|
|
560
|
+
await assertProposalIsWaitingAndUnexecuted(subject);
|
|
561
|
+
});
|
|
562
|
+
it("executes an approval exactly once and a rejection or skip never (invariant 3)", async () => {
|
|
563
|
+
await assertDecisionsExecuteExactlyOnce(subject);
|
|
564
|
+
});
|
|
565
|
+
it("settles interrupted promptly when stopSignal aborts and does nothing after it (invariant 4)", async () => {
|
|
566
|
+
await assertStopSignalInterrupts(subject);
|
|
567
|
+
});
|
|
568
|
+
it("reports usage as non-negative deltas that sum to what the engine emitted (invariant 5)", async () => {
|
|
569
|
+
await assertUsageReachesSinkAsDeltas(subject);
|
|
570
|
+
});
|
|
571
|
+
it(`binds its state id as its ${subject.adapter.capabilities.stateIdSource} capability declares (invariant 6)`, async () => {
|
|
572
|
+
await assertStateIdCapabilityAgrees(subject);
|
|
573
|
+
});
|
|
574
|
+
it("serves, releases, shuts down and re-boots without a rejection (invariant 7)", async () => {
|
|
575
|
+
await assertLifetimesResolve(subject);
|
|
576
|
+
});
|
|
577
|
+
it("serves concurrent turns on one adapter object independently (invariant 8)", async () => {
|
|
578
|
+
await assertConcurrentTurnsAreIndependent(subject);
|
|
579
|
+
});
|
|
580
|
+
});
|
|
581
|
+
}
|