@stigmer/runner 3.14.1 → 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 +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
|
@@ -0,0 +1,1028 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The turn context — everything the runtime resolves for one agent turn
|
|
3
|
+
* before any harness runs, as small named phases producing one typed record.
|
|
4
|
+
*
|
|
5
|
+
* A turn begins with a dozen reads and provisions that have nothing to do
|
|
6
|
+
* with the engine that will run it: the execution and its session, the
|
|
7
|
+
* agent blueprint, the environment, the workspace and its lock, what the
|
|
8
|
+
* previous turn left waiting for approval, the MCP servers and their
|
|
9
|
+
* approval policies, the attachments, the standing context. Until S2 M2
|
|
10
|
+
* that sequence lived inline in the Cursor orchestrator
|
|
11
|
+
* (`activities/execute-cursor/index.ts` `executeCursorInner`, phases 1 to
|
|
12
|
+
* 9c); the native orchestrator carried a second copy in its `performSetup`.
|
|
13
|
+
* This module is the one copy. Each phase is a function that reads its
|
|
14
|
+
* inputs, reports its progress label, heartbeats once on entry, and returns
|
|
15
|
+
* a typed slice of the contract's `TurnInput`; {@link resolveTurnContext}
|
|
16
|
+
* sequences them, and the adapter runs its own harness-specific steps after
|
|
17
|
+
* the whole record exists.
|
|
18
|
+
*
|
|
19
|
+
* Shape, chosen against `execute-deep-agent/setup.ts`: a typed record built
|
|
20
|
+
* from small functions (the `SetupResult` mold), never one 775-line
|
|
21
|
+
* `performSetup`. The record is the adapter contract's `TurnInput`
|
|
22
|
+
* (`types.ts`): every phase returns a named slice of it, and
|
|
23
|
+
* {@link resolveTurnContext} composes the whole. There is no wider
|
|
24
|
+
* "runtime-private" record beside it — the lock release and the write-back
|
|
25
|
+
* coordinator a phase produces are the caller's `TurnFrame` resources, set
|
|
26
|
+
* the instant they exist so a `finally` sees them on every path (M2 had a
|
|
27
|
+
* `TurnContext extends TurnInput` for them; once the groups lifted, nothing
|
|
28
|
+
* read the extension after composition, so M3 deleted it).
|
|
29
|
+
*
|
|
30
|
+
* Three rules every phase obeys:
|
|
31
|
+
*
|
|
32
|
+
* - It never persists and never returns a slim status. A phase that can end
|
|
33
|
+
* the turn (the lock, the reinvocation reconcile) returns a
|
|
34
|
+
* {@link TurnSettlement} naming WHAT was decided; the caller writes the
|
|
35
|
+
* status, persists once and returns. The lock golden pins that persist as
|
|
36
|
+
* the turn's one and only, which is why the rule is structural.
|
|
37
|
+
* - It imports nothing from `src/activities/` (pinned by
|
|
38
|
+
* `__tests__/import-direction.test.ts`). A fact only the harness knows
|
|
39
|
+
* (its file-review identity, its vision profile) arrives through
|
|
40
|
+
* `HarnessCapabilities`.
|
|
41
|
+
* - Its heartbeat pulse and its cold-start timing marks are its own: the
|
|
42
|
+
* step name it enters under is the mark name it emits, one vocabulary for
|
|
43
|
+
* "where in setup are we" in Temporal heartbeat details and the
|
|
44
|
+
* `execution_setup` log (Q-S2-10).
|
|
45
|
+
*
|
|
46
|
+
* What is deliberately NOT here, and where it is instead:
|
|
47
|
+
*
|
|
48
|
+
* - Cursor's catalog validation of the requested model (`resolveModelId`),
|
|
49
|
+
* the pricing preload and the MCP env pre-flight warning stay in the
|
|
50
|
+
* Cursor adapter for S2: each reads a module in the adapter column of the
|
|
51
|
+
* entry's disposition table, and the runtime carries the REQUESTED model
|
|
52
|
+
* name so every harness validates against its own catalog (S2 M2 gate,
|
|
53
|
+
* Q-M2-1). The skill mount was in that list until M3b found it reads the
|
|
54
|
+
* control plane, which only the runtime holds; it is {@link mountSkills}.
|
|
55
|
+
* - The approval grants, the HITL gate, the capture baseline pin and the
|
|
56
|
+
* Cursor agent itself (phases 5c, 7, 8a, 9) are the adapter's.
|
|
57
|
+
* - The memory selection is a memoized thunk, not a value: whether a prompt
|
|
58
|
+
* carries standing context depends on how the ENGINE resolved (a resumed
|
|
59
|
+
* agent already holds it), which the runtime learns only after the
|
|
60
|
+
* adapter runs. The runtime prepares the selection once; the adapter
|
|
61
|
+
* pulls it when its prompt needs it.
|
|
62
|
+
*
|
|
63
|
+
* Every `index.ts` line reference below was read from `2c06bc7ea` on
|
|
64
|
+
* 2026-09-11, before the orchestrator moved onto this module.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
import { CancelledFailure } from "@temporalio/activity";
|
|
68
|
+
import { clone } from "@bufbuild/protobuf";
|
|
69
|
+
import type { AgentExecution, AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
70
|
+
import type { AgentExecutionSpec } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
71
|
+
import { AgentMessageSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
|
|
72
|
+
import { ApprovalAction, FileChangeSetStatus, ToolCallStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
73
|
+
import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
74
|
+
|
|
75
|
+
import type { Config } from "../config.js";
|
|
76
|
+
import type { StigmerClient } from "../client/stigmer-client.js";
|
|
77
|
+
import type { NormalizedActivityInput } from "../shared/activity-input.js";
|
|
78
|
+
import type { ArtifactStorage } from "../shared/artifact-storage.js";
|
|
79
|
+
import type { TimingRecorder } from "../shared/cold-start-timing.js";
|
|
80
|
+
import { resolveBlueprint, type ResolvedBlueprint } from "../shared/blueprint-resolver.js";
|
|
81
|
+
import { resolveExecutionEnv } from "../shared/env-resolver.js";
|
|
82
|
+
import { provisionSessionWorkspace } from "../shared/workspace/session-provision.js";
|
|
83
|
+
import { WriteBackCoordinator } from "../shared/workspace/writeback-coordinator.js";
|
|
84
|
+
import { statusProtoWriter } from "../shared/execution-status-writer.js";
|
|
85
|
+
import { isGitWorkTree } from "../shared/filereview/git-substrate.js";
|
|
86
|
+
import { applyCaptureDecisions, deriveCaptureMode } from "../shared/filereview/capture.js";
|
|
87
|
+
import { casBlobReader } from "../shared/filereview/cas-substrate.js";
|
|
88
|
+
import {
|
|
89
|
+
acquireWorkspaceLock,
|
|
90
|
+
WorkspaceLockCancelledError,
|
|
91
|
+
WorkspaceLockTimeoutError,
|
|
92
|
+
type ReleaseWorkspaceLock,
|
|
93
|
+
} from "../shared/workspace/workspace-lock.js";
|
|
94
|
+
import { LocalWorkspaceBackend } from "../shared/workspace/local-backend.js";
|
|
95
|
+
import { resolveMcpServers } from "../shared/mcp-resolver.js";
|
|
96
|
+
import { resolveMcpTransportPosture } from "../shared/mcp-transport-guard.js";
|
|
97
|
+
import { injectCallerIdentityEnv, resolveCallerIdentity } from "../shared/caller-identity.js";
|
|
98
|
+
import { backfillMcpServersIfNeeded } from "../shared/connect-backfill.js";
|
|
99
|
+
import { discoverChannelMessaging, synthesizeChannelAttachment } from "../shared/channel-attachment.js";
|
|
100
|
+
import { readChannelConversationId, synthesizeConversationAttachment } from "../shared/conversation-attachment.js";
|
|
101
|
+
import { synthesizeMemoryAttachment } from "../shared/memory-attachment.js";
|
|
102
|
+
import { injectSynthesizedAttachment } from "../shared/synthesized-attachment.js";
|
|
103
|
+
import { deriveActiveLeases, mergeApprovalPolicies } from "../shared/approval-policy.js";
|
|
104
|
+
import { resolveAttachments } from "../shared/attachment-resolver.js";
|
|
105
|
+
import { resolveSkills, type SkillMetadata } from "../shared/skill-resolver.js";
|
|
106
|
+
import { VisionBudget, type NotViewableEntry, type VisionProfile } from "../shared/attachment-vision.js";
|
|
107
|
+
import { getModelVisionCapability } from "../shared/model-registry.js";
|
|
108
|
+
import { applyApprovedWholeFileWrites } from "../shared/exact-apply.js";
|
|
109
|
+
import { resolveEffectiveServiceTier } from "../shared/service-tier.js";
|
|
110
|
+
import { resolveEffectiveThinkingMode } from "../shared/thinking-mode.js";
|
|
111
|
+
import { readContextBridge } from "../shared/context-bridge.js";
|
|
112
|
+
import { readSenderIdentity } from "../shared/sender-identity.js";
|
|
113
|
+
import { readSessionContext } from "../shared/session-context.js";
|
|
114
|
+
import { readDeclaredPreferences } from "../shared/declared-preferences.js";
|
|
115
|
+
import { readConversationCatchup } from "../shared/conversation-catchup.js";
|
|
116
|
+
import { selectRecalledFacts } from "../shared/memory-retrieval.js";
|
|
117
|
+
import type { RecalledMemoriesContent } from "../shared/recalled-memories.js";
|
|
118
|
+
import type { FileReviewIdentity, HarnessCapabilities, StateIdSource } from "./capabilities.js";
|
|
119
|
+
import type {
|
|
120
|
+
TurnAttachments,
|
|
121
|
+
TurnEnvironment,
|
|
122
|
+
TurnInput,
|
|
123
|
+
TurnMcp,
|
|
124
|
+
TurnModelPreferences,
|
|
125
|
+
TurnStandingContext,
|
|
126
|
+
TurnWorkspace,
|
|
127
|
+
} from "./types.js";
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// What the phases take
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What every phase reads from its caller. The activity's identity and the
|
|
135
|
+
* runtime's own seams; nothing harness-specific. The four function members
|
|
136
|
+
* are the caller's, injected so the phase bodies are the same code under
|
|
137
|
+
* the Cursor orchestrator today and under `run-turn.ts` in M3 (only the
|
|
138
|
+
* binding changes), and so a test can drive a phase without a Temporal
|
|
139
|
+
* activity context.
|
|
140
|
+
*/
|
|
141
|
+
export interface ResolutionDeps {
|
|
142
|
+
/** The activity's own identity: execution id, engine state id, HITL cycle index. */
|
|
143
|
+
readonly input: NormalizedActivityInput;
|
|
144
|
+
readonly client: StigmerClient;
|
|
145
|
+
/**
|
|
146
|
+
* The whole runner config. The runtime is its rightful reader; a shared
|
|
147
|
+
* helper that needs two fields is handed a narrowed view of it
|
|
148
|
+
* (`SessionProvisionConfig`), never the other way round.
|
|
149
|
+
*/
|
|
150
|
+
readonly config: Config;
|
|
151
|
+
/**
|
|
152
|
+
* The one execution status this turn folds into. Phases that carry state
|
|
153
|
+
* across invocations write here (the seeded transcript, the file-review
|
|
154
|
+
* reconcile's events, the memory-selection report); nothing else does.
|
|
155
|
+
*/
|
|
156
|
+
readonly status: AgentExecutionStatus;
|
|
157
|
+
/** Resolved once by the caller before any phase; absent when no substrate works. */
|
|
158
|
+
readonly artifactStorage: ArtifactStorage | undefined;
|
|
159
|
+
/** The cold-start timeline; each phase marks its own segment(s) exactly as before. */
|
|
160
|
+
readonly timing: TimingRecorder;
|
|
161
|
+
/** Cancellation for the one wait a phase can block on (the workspace lock). */
|
|
162
|
+
readonly signal: AbortSignal;
|
|
163
|
+
/** The Temporal heartbeat, handed to helpers that pulse mid-call (the lock wait, the MCP backfill). */
|
|
164
|
+
readonly heartbeat: () => void;
|
|
165
|
+
/**
|
|
166
|
+
* "Resolution is now at this step": labels the caller's periodic heartbeat
|
|
167
|
+
* and pulses once. Every phase calls it first with its own step name.
|
|
168
|
+
*/
|
|
169
|
+
readonly enterPhase: (step: string) => void;
|
|
170
|
+
/** The user-visible setup label (`setupProgress`); the six strings are unchanged from the orchestrator. */
|
|
171
|
+
readonly reportProgress: (label: string) => Promise<void>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// What the reinvocation phase answers
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* What the previous invocation left for this one (phase 3). Not part of
|
|
180
|
+
* `TurnInput`: `isReinvocation` is the adapter's own derivation from
|
|
181
|
+
* `threadId` (the contract carries no flag by design, `types.ts` header) and
|
|
182
|
+
* the runtime reads it only inside {@link resolveTurnContext}; the decisions
|
|
183
|
+
* ARE on the input. The seeded sub-agent rows are the adapter's to clone from
|
|
184
|
+
* `execution.status` (its accumulator owns `status.subAgentExecutions` and
|
|
185
|
+
* overwrites it on every flush), so they ride nothing here.
|
|
186
|
+
*/
|
|
187
|
+
export interface TurnReinvocation {
|
|
188
|
+
/** The engine already holds state for this execution (derived per {@link isReinvocation}). */
|
|
189
|
+
readonly isReinvocation: boolean;
|
|
190
|
+
/** The adjudicated WAITING rows, keyed by tool-call id; empty when nothing was decided. */
|
|
191
|
+
readonly approvalDecisions: ReadonlyMap<string, ApprovalAction>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Settlements
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* A turn that ended during resolution, before any harness ran. Each arm
|
|
200
|
+
* names what was decided and carries only the facts the caller's status
|
|
201
|
+
* writes need; the copy, the phase flip, the persist and the slim return are
|
|
202
|
+
* the caller's (the orchestrator today, the runtime's terminal table in M3).
|
|
203
|
+
*
|
|
204
|
+
* - `workspace-lock-timeout`: another turn held the primary tree past
|
|
205
|
+
* `Config.workspaceLockTimeoutMs`. FAILED, returned (a Temporal retry
|
|
206
|
+
* would queue behind the same holder).
|
|
207
|
+
* - `rejected-by-user`: an adjudicated irreversible action (shell / MCP)
|
|
208
|
+
* was REJECTed. FAILED, returned.
|
|
209
|
+
* - `file-review-resolved`: a pure file-review resume; the agent already
|
|
210
|
+
* finished its turn during capture and the reconcile is the whole act.
|
|
211
|
+
* COMPLETED, returned, with the write-back finalized first when the
|
|
212
|
+
* reconcile held.
|
|
213
|
+
*/
|
|
214
|
+
export type TurnSettlement =
|
|
215
|
+
| { readonly kind: "workspace-lock-timeout"; readonly error: WorkspaceLockTimeoutError }
|
|
216
|
+
| { readonly kind: "rejected-by-user" }
|
|
217
|
+
| {
|
|
218
|
+
readonly kind: "file-review-resolved";
|
|
219
|
+
readonly failed: boolean;
|
|
220
|
+
readonly failureDetail: string;
|
|
221
|
+
readonly discardedPaths: readonly string[];
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** The lock phase's answer: the release handle (absent when there is no primary tree), or the one settlement a lock can produce. */
|
|
225
|
+
export type WorkspaceLockOutcome =
|
|
226
|
+
| { readonly kind: "acquired"; readonly release: ReleaseWorkspaceLock | undefined }
|
|
227
|
+
| { readonly kind: "settled"; readonly settlement: Extract<TurnSettlement, { kind: "workspace-lock-timeout" }> };
|
|
228
|
+
|
|
229
|
+
/** The reinvocation phase's answer: what the previous invocation left, or one of the two settlements a reconcile can produce. */
|
|
230
|
+
export type ReinvocationOutcome =
|
|
231
|
+
| { readonly kind: "ready"; readonly reinvocation: TurnReinvocation }
|
|
232
|
+
| { readonly kind: "settled"; readonly settlement: Exclude<TurnSettlement, { kind: "workspace-lock-timeout" }> };
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Pure helpers (exported for their tests and for the adapter kit)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Whether the engine already holds state for this execution, so this
|
|
240
|
+
* invocation resumes rather than begins. One function keyed on how the
|
|
241
|
+
* harness mints its state id (Q-S2-8):
|
|
242
|
+
*
|
|
243
|
+
* - `engine-minted` (Cursor: the agent id): the runtime knows nothing until
|
|
244
|
+
* the adapter binds an id, so a non-empty `threadId` IS the evidence.
|
|
245
|
+
* Correct today because a pure-reconcile turn does not advance `turnSeq`
|
|
246
|
+
* and the legacy positional wire shape yields 0, so `turnSeq` cannot be
|
|
247
|
+
* the signal (`index.ts` L402-407).
|
|
248
|
+
* - `deterministic` (native: the runtime-minted thread id): the id exists
|
|
249
|
+
* on the FIRST turn too, so the evidence is the persisted transcript
|
|
250
|
+
* (pending approvals, DECIDED sets). That arm lands in S3 with the native
|
|
251
|
+
* adapter; until then this function refuses rather than guess.
|
|
252
|
+
*/
|
|
253
|
+
export function isReinvocation(stateIdSource: StateIdSource, input: NormalizedActivityInput): boolean {
|
|
254
|
+
switch (stateIdSource) {
|
|
255
|
+
case "engine-minted":
|
|
256
|
+
return input.threadId !== "";
|
|
257
|
+
case "deterministic":
|
|
258
|
+
throw new Error(
|
|
259
|
+
"isReinvocation: the deterministic arm lands with the native adapter (S3); no S2 harness mints its state id deterministically",
|
|
260
|
+
);
|
|
261
|
+
default: {
|
|
262
|
+
const exhaustive: never = stateIdSource;
|
|
263
|
+
throw new Error(`isReinvocation: unknown state id source ${String(exhaustive)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* The runtime's one reader of approval decisions: every top-level tool-call
|
|
270
|
+
* row still WAITING_APPROVAL whose `approvalAction` the server has set,
|
|
271
|
+
* keyed by tool-call id. Derived from the status on every invocation, never
|
|
272
|
+
* stored, so it cannot drift from the rows. The same rows, in the same
|
|
273
|
+
* order, that the Cursor adapter's `reconstructAdjudicatedApprovals` reads
|
|
274
|
+
* for its own facts (the pending-approval protos and content digests); a
|
|
275
|
+
* test pins the two readers' agreement.
|
|
276
|
+
*/
|
|
277
|
+
export function approvalDecisionsOf(status: AgentExecutionStatus): ReadonlyMap<string, ApprovalAction> {
|
|
278
|
+
const decisions = new Map<string, ApprovalAction>();
|
|
279
|
+
for (const message of status.messages) {
|
|
280
|
+
for (const row of message.toolCalls) {
|
|
281
|
+
if (row.status !== ToolCallStatus.TOOL_CALL_WAITING_APPROVAL) continue;
|
|
282
|
+
if (row.approvalAction === ApprovalAction.UNSPECIFIED) continue;
|
|
283
|
+
decisions.set(row.id, row.approvalAction);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return decisions;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The facts the reinvocation decision reads; produced by {@link reconcileReinvocation}, exposed for the decision's own tests. */
|
|
290
|
+
export interface ReinvocationFacts {
|
|
291
|
+
readonly approvalDecisions: ReadonlyMap<string, ApprovalAction>;
|
|
292
|
+
/** At least one DECIDED change set was reconciled this invocation. */
|
|
293
|
+
readonly reconciledFileReview: boolean;
|
|
294
|
+
/** A reconcile could not honor what-you-approve-is-what-applies (HASH_MISMATCH). */
|
|
295
|
+
readonly fileReviewFailed: boolean;
|
|
296
|
+
readonly fileReviewFailureDetail: string;
|
|
297
|
+
readonly discardedPaths: readonly string[];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* How a reinvocation proceeds, decided once from the facts (`index.ts`
|
|
302
|
+
* L547-618): a REJECT of an irreversible action fails the turn; any other
|
|
303
|
+
* adjudicated decision runs the agent (the approved shell/MCP proceeds and
|
|
304
|
+
* may produce further edits); a resume that reconciled file review and
|
|
305
|
+
* decided nothing else is complete, because keeping or discarding a file
|
|
306
|
+
* never re-prompts the agent (Cursor-like). `undefined` means "run the
|
|
307
|
+
* agent".
|
|
308
|
+
*/
|
|
309
|
+
export function decideReinvocation(
|
|
310
|
+
facts: ReinvocationFacts,
|
|
311
|
+
): Exclude<TurnSettlement, { kind: "workspace-lock-timeout" }> | undefined {
|
|
312
|
+
if (facts.approvalDecisions.size > 0) {
|
|
313
|
+
const hasReject = [...facts.approvalDecisions.values()].some((a) => a === ApprovalAction.REJECT);
|
|
314
|
+
return hasReject ? { kind: "rejected-by-user" } : undefined;
|
|
315
|
+
}
|
|
316
|
+
if (facts.reconciledFileReview) {
|
|
317
|
+
return {
|
|
318
|
+
kind: "file-review-resolved",
|
|
319
|
+
failed: facts.fileReviewFailed,
|
|
320
|
+
failureDetail: facts.fileReviewFailureDetail,
|
|
321
|
+
discardedPaths: facts.discardedPaths,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Seed an in-progress status from the persisted execution on a durable
|
|
329
|
+
* resume (HITL approval, pause/resume, transient recovery) so the upcoming
|
|
330
|
+
* turn APPENDS onto prior history instead of replacing it.
|
|
331
|
+
*
|
|
332
|
+
* Why it is required: a resumed engine re-issues the previously gated tool
|
|
333
|
+
* calls with brand-new call ids. Without seeding, the harness's accumulator
|
|
334
|
+
* would rebuild the transcript from empty and emit a status that drops the
|
|
335
|
+
* already-committed tool-call ids. The backend's append-only-at-identity
|
|
336
|
+
* guard rejects any non-terminal update that drops a committed tool-call id,
|
|
337
|
+
* so the resumed progress would never persist: the run stalls in
|
|
338
|
+
* WAITING_FOR_APPROVAL with no pending approvals and the workflow watchdog
|
|
339
|
+
* fails it. Seeding makes the resume status a strict superset; the re-runs
|
|
340
|
+
* are then reconciled in place onto these seeded calls by canonical identity
|
|
341
|
+
* inside the accumulator.
|
|
342
|
+
*
|
|
343
|
+
* The persisted protos are cloned so the input execution stays immutable.
|
|
344
|
+
* Messages are pushed into `status.messages` (which the accumulator wraps by
|
|
345
|
+
* reference) BEFORE the accumulator is constructed. Sub-agent executions are
|
|
346
|
+
* NOT seeded here: the Cursor accumulator owns `status.subAgentExecutions`
|
|
347
|
+
* and overwrites it on every flush, so the adapter clones them itself from
|
|
348
|
+
* `input.execution.status` under the same "left a transcript" test.
|
|
349
|
+
*
|
|
350
|
+
* Moved from `execute-cursor/index.ts` `seedCursorTranscriptFromExecution`
|
|
351
|
+
* (L2468-2478); the native twin is `execute-deep-agent/index.ts`
|
|
352
|
+
* `seedStatusFromExecution` (S3 retires it).
|
|
353
|
+
*/
|
|
354
|
+
export function seedTranscriptFromExecution(
|
|
355
|
+
status: AgentExecutionStatus,
|
|
356
|
+
execution: AgentExecution,
|
|
357
|
+
): void {
|
|
358
|
+
const persisted = execution.status;
|
|
359
|
+
if (!persisted || persisted.messages.length === 0) return;
|
|
360
|
+
for (const message of persisted.messages) {
|
|
361
|
+
status.messages.push(clone(AgentMessageSchema, message));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// The phases, in the order the turn runs them
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
/** Phase 1 (`index.ts` L325-330): hydrate the execution from the control plane. */
|
|
370
|
+
export async function fetchExecution(deps: ResolutionDeps): Promise<{
|
|
371
|
+
readonly execution: AgentExecution;
|
|
372
|
+
/** `execution.spec`, asserted once here: the control plane never dispatches an execution without one. */
|
|
373
|
+
readonly spec: AgentExecutionSpec;
|
|
374
|
+
readonly sessionId: string;
|
|
375
|
+
}> {
|
|
376
|
+
deps.enterPhase("fetch_execution");
|
|
377
|
+
await deps.reportProgress("Fetching execution");
|
|
378
|
+
const execution = await deps.client.getExecution(deps.input.executionId);
|
|
379
|
+
const spec = execution.spec!;
|
|
380
|
+
deps.timing.mark("fetch_execution");
|
|
381
|
+
return { execution, spec, sessionId: spec.sessionId };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** Phase 2 (`index.ts` L332-336): load the session and resolve the full agent blueprint through it. */
|
|
385
|
+
export async function resolveAgentBlueprint(
|
|
386
|
+
deps: ResolutionDeps,
|
|
387
|
+
sessionId: string,
|
|
388
|
+
): Promise<{ readonly session: Session; readonly blueprint: ResolvedBlueprint }> {
|
|
389
|
+
deps.enterPhase("resolve_blueprint");
|
|
390
|
+
await deps.reportProgress("Resolving agent blueprint");
|
|
391
|
+
const session = await deps.client.getSession(sessionId);
|
|
392
|
+
const blueprint = await resolveBlueprint(deps.client, session);
|
|
393
|
+
deps.timing.mark("resolve_blueprint");
|
|
394
|
+
return { session, blueprint };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** Phase 2b (`index.ts` L338-343): the execution environment (MCP server credentials). */
|
|
398
|
+
export async function resolveEnvironment(deps: ResolutionDeps): Promise<TurnEnvironment> {
|
|
399
|
+
deps.enterPhase("resolve_environment");
|
|
400
|
+
await deps.reportProgress("Resolving environment");
|
|
401
|
+
const { envVars, secretKeys } = await resolveExecutionEnv(deps.client, deps.input.executionId);
|
|
402
|
+
deps.timing.mark("resolve_environment");
|
|
403
|
+
return { envVars, secretKeys };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Phase 2c, first half (`index.ts` L351-408): provision the workspace (clone
|
|
408
|
+
* git repos / mount local paths) so the LOCAL agent operates on the actual
|
|
409
|
+
* repo, wire the git write-back coordinator, and decide the capture posture.
|
|
410
|
+
*
|
|
411
|
+
* Git provisioning is idempotent across multi-turn and HITL reinvocations.
|
|
412
|
+
* The write-back coordinator pushes the session's APPROVED tree to the
|
|
413
|
+
* session branch (stigmer/<session-id>) and keeps one PR open; finalize runs
|
|
414
|
+
* at exactly two seams (the pure file-review resume and terminal
|
|
415
|
+
* completion), never mid-turn, because the working tree is speculative until
|
|
416
|
+
* reviewed. Non-eligible workspaces (local paths, no credentials) make it a
|
|
417
|
+
* no-op coordinator, here `null`.
|
|
418
|
+
*
|
|
419
|
+
* Apply-then-review is the universal file-review model (Slice 2c): a git work
|
|
420
|
+
* tree captures per-file from the git diff at the turn boundary; a NON-git
|
|
421
|
+
* workspace captures every write via the path-scoped CAS substrate, which
|
|
422
|
+
* needs artifact storage to persist blobs, and falls back to the classic
|
|
423
|
+
* deny-gate without it. `gitWorkspace` selects the substrate; `captureMode`
|
|
424
|
+
* says whether file edits flow at all.
|
|
425
|
+
*
|
|
426
|
+
* The change-set id is minted from the workflow-threaded turn index so it is
|
|
427
|
+
* stable across a Temporal retry (idempotent ledger authoring) and unique per
|
|
428
|
+
* turn; a "wasted" id on a pure-reconcile resume (which never authors a
|
|
429
|
+
* baseline) is harmless.
|
|
430
|
+
*/
|
|
431
|
+
export async function provisionWorkspace(
|
|
432
|
+
deps: ResolutionDeps,
|
|
433
|
+
args: { readonly session: Session; readonly sessionId: string; readonly envVars: Record<string, string> },
|
|
434
|
+
): Promise<{ readonly workspace: TurnWorkspace; readonly writeback: WriteBackCoordinator | null }> {
|
|
435
|
+
deps.enterPhase("provision_workspace");
|
|
436
|
+
await deps.reportProgress("Provisioning workspace");
|
|
437
|
+
const provision = await provisionSessionWorkspace(deps.config, args.session, args.envVars, args.sessionId);
|
|
438
|
+
deps.timing.mark("provision_workspace");
|
|
439
|
+
|
|
440
|
+
const writeback = provision.provisionResults.length > 0
|
|
441
|
+
? new WriteBackCoordinator({
|
|
442
|
+
statusWriter: statusProtoWriter(deps.status),
|
|
443
|
+
executionId: deps.input.executionId,
|
|
444
|
+
sessionId: args.sessionId,
|
|
445
|
+
githubToken: args.envVars.GITHUB_TOKEN ?? "",
|
|
446
|
+
provisionResults: provision.provisionResults,
|
|
447
|
+
workspaceEntries: args.session.spec?.workspaceEntries ?? [],
|
|
448
|
+
workspaceBackend: provision.workspaceBackend,
|
|
449
|
+
})
|
|
450
|
+
: null;
|
|
451
|
+
|
|
452
|
+
const primaryDir = provision.workspaceDirs[0];
|
|
453
|
+
const gitWorkspace = primaryDir ? await isGitWorkTree(primaryDir) : false;
|
|
454
|
+
const captureMode = deriveCaptureMode(primaryDir, gitWorkspace, !!deps.artifactStorage);
|
|
455
|
+
const changeSetId = `${deps.input.executionId}:${deps.input.turnSeq}`;
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
workspace: { dirs: provision.workspaceDirs, primaryDir, gitWorkspace, captureMode, changeSetId, provision },
|
|
459
|
+
writeback,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Phase 2c, second half (`index.ts` L410-449): serialize this turn against
|
|
465
|
+
* every other execution sharing the primary working tree. Sessions declaring
|
|
466
|
+
* the same localPath (or the shared runner root) resolve to ONE directory,
|
|
467
|
+
* and an unserialized concurrent write lands inside this turn's
|
|
468
|
+
* baseline→candidate window, misattributing another session's file to this
|
|
469
|
+
* turn's review. Acquired before ANY tree mutation (decision reconcile, gate
|
|
470
|
+
* install, agent writes, capture); while another turn holds the lock this
|
|
471
|
+
* surfaces a visible waiting state and heartbeats; a cancel aborts the wait
|
|
472
|
+
* at once.
|
|
473
|
+
*
|
|
474
|
+
* Its own phase so the release handle reaches the caller the instant the
|
|
475
|
+
* lock exists: nothing runs between acquisition and hand-over, so nothing
|
|
476
|
+
* can throw and leave a held lock the caller's `finally` never sees.
|
|
477
|
+
*/
|
|
478
|
+
export async function acquireWorkspaceTurnLock(
|
|
479
|
+
deps: ResolutionDeps,
|
|
480
|
+
primaryDir: string,
|
|
481
|
+
): Promise<WorkspaceLockOutcome> {
|
|
482
|
+
deps.enterPhase("acquire_workspace_lock");
|
|
483
|
+
let release: ReleaseWorkspaceLock | undefined;
|
|
484
|
+
if (primaryDir) {
|
|
485
|
+
try {
|
|
486
|
+
release = await acquireWorkspaceLock(primaryDir, {
|
|
487
|
+
onWaiting: () => deps.reportProgress("Waiting for workspace — in use by another session"),
|
|
488
|
+
heartbeat: deps.heartbeat,
|
|
489
|
+
signal: deps.signal,
|
|
490
|
+
timeoutMs: deps.config.workspaceLockTimeoutMs,
|
|
491
|
+
});
|
|
492
|
+
} catch (lockErr) {
|
|
493
|
+
if (lockErr instanceof WorkspaceLockCancelledError) {
|
|
494
|
+
throw new CancelledFailure("Activity cancelled while waiting for the workspace lock");
|
|
495
|
+
}
|
|
496
|
+
if (lockErr instanceof WorkspaceLockTimeoutError) {
|
|
497
|
+
return { kind: "settled", settlement: { kind: "workspace-lock-timeout", error: lockErr } };
|
|
498
|
+
}
|
|
499
|
+
throw lockErr;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
deps.timing.mark("acquire_workspace_lock");
|
|
503
|
+
return { kind: "acquired", release };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* `index.ts` L451-461: set OTel baggage so downstream calls carry execution
|
|
508
|
+
* context. Best-effort; tracing not initialized is a silent skip. Runs after
|
|
509
|
+
* the lock, as it always has.
|
|
510
|
+
*/
|
|
511
|
+
export async function bindTelemetryBaggage(
|
|
512
|
+
deps: ResolutionDeps,
|
|
513
|
+
args: { readonly session: Session; readonly sessionId: string },
|
|
514
|
+
): Promise<void> {
|
|
515
|
+
try {
|
|
516
|
+
const { setBaggage, BAGGAGE_EXECUTION_ID, BAGGAGE_SESSION_ID, BAGGAGE_ORG_ID } = await import("../otel.js");
|
|
517
|
+
await setBaggage({
|
|
518
|
+
[BAGGAGE_EXECUTION_ID]: deps.input.executionId,
|
|
519
|
+
[BAGGAGE_SESSION_ID]: args.sessionId,
|
|
520
|
+
[BAGGAGE_ORG_ID]: args.session.metadata?.org ?? "",
|
|
521
|
+
});
|
|
522
|
+
} catch {
|
|
523
|
+
// Tracing not initialized — silently skip.
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Phase 3 (`index.ts` L475-619): what the previous invocation left, and
|
|
529
|
+
* whether this one runs the agent at all.
|
|
530
|
+
*
|
|
531
|
+
* On a reinvocation, first seed the in-progress status from the persisted
|
|
532
|
+
* execution ({@link seedTranscriptFromExecution}), BEFORE the harness's
|
|
533
|
+
* accumulator wraps `status.messages`. Then the file-review reconcile (the
|
|
534
|
+
* dual-source half): every change set the server projected as DECIDED is
|
|
535
|
+
* reconciled from the ledger decisions and the pinned git refs (approved
|
|
536
|
+
* kept at their "after" bytes, rejected snapped back to baseline, all
|
|
537
|
+
* uncommitted, hash-verified) through the shared reconcile under the
|
|
538
|
+
* harness's {@link FileReviewIdentity}. Independent of tool approvals: a
|
|
539
|
+
* single turn can carry BOTH a DECIDED file change set AND an approved
|
|
540
|
+
* shell/MCP action. Tool approvals (shell / MCP / gitignored writes) still
|
|
541
|
+
* resolve from the message transcript, the deny-gate path, read here as
|
|
542
|
+
* {@link approvalDecisionsOf} after the reconcile, at the same point the
|
|
543
|
+
* orchestrator always read them.
|
|
544
|
+
*
|
|
545
|
+
* The outcome is {@link decideReinvocation}'s: a settlement the caller
|
|
546
|
+
* writes and persists, or the facts the turn proceeds with.
|
|
547
|
+
*/
|
|
548
|
+
export async function reconcileReinvocation(
|
|
549
|
+
deps: ResolutionDeps,
|
|
550
|
+
args: {
|
|
551
|
+
readonly stateIdSource: StateIdSource;
|
|
552
|
+
readonly execution: AgentExecution;
|
|
553
|
+
readonly workspace: TurnWorkspace;
|
|
554
|
+
readonly fileReview: FileReviewIdentity;
|
|
555
|
+
},
|
|
556
|
+
): Promise<ReinvocationOutcome> {
|
|
557
|
+
deps.enterPhase("reconcile_reinvocation");
|
|
558
|
+
const reinvoked = isReinvocation(args.stateIdSource, deps.input);
|
|
559
|
+
let reconciledFileReview = false;
|
|
560
|
+
let fileReviewFailed = false;
|
|
561
|
+
let fileReviewFailureDetail = "";
|
|
562
|
+
const discardedPaths: string[] = [];
|
|
563
|
+
|
|
564
|
+
if (reinvoked) {
|
|
565
|
+
const existingStatus = args.execution.status;
|
|
566
|
+
seedTranscriptFromExecution(deps.status, args.execution);
|
|
567
|
+
|
|
568
|
+
if (args.workspace.captureMode && args.workspace.primaryDir) {
|
|
569
|
+
const decidedSets = (existingStatus?.fileChangeSets ?? []).filter(
|
|
570
|
+
(cs) => cs.status === FileChangeSetStatus.DECIDED,
|
|
571
|
+
);
|
|
572
|
+
for (const changeSet of decidedSets) {
|
|
573
|
+
const capResult = await applyCaptureDecisions({
|
|
574
|
+
status: deps.status,
|
|
575
|
+
gitRoot: args.workspace.primaryDir,
|
|
576
|
+
executionId: deps.input.executionId,
|
|
577
|
+
changeSet,
|
|
578
|
+
harnessId: args.fileReview.harnessId,
|
|
579
|
+
excludePaths: args.fileReview.excludePaths,
|
|
580
|
+
// Thread the CAS store so CAS-captured files in the change set
|
|
581
|
+
// reconcile from the durable manifest (approved after-blobs written,
|
|
582
|
+
// rejected snapped back). In a non-git workspace this is the ONLY
|
|
583
|
+
// reconcile; in a git tree it composes with the git-ref reconcile.
|
|
584
|
+
storage: deps.artifactStorage,
|
|
585
|
+
readBlob: deps.artifactStorage ? casBlobReader(deps.artifactStorage) : undefined,
|
|
586
|
+
gitWorkspace: args.workspace.gitWorkspace,
|
|
587
|
+
});
|
|
588
|
+
if (!capResult.isCaptureTurn) continue;
|
|
589
|
+
reconciledFileReview = true;
|
|
590
|
+
if (capResult.failed) {
|
|
591
|
+
fileReviewFailed = true;
|
|
592
|
+
fileReviewFailureDetail = capResult.failureDetail ?? "file review reconcile failed";
|
|
593
|
+
}
|
|
594
|
+
if (capResult.hadReject) discardedPaths.push(...capResult.rejectedPaths);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const approvalDecisions = approvalDecisionsOf(deps.status);
|
|
600
|
+
const settlement = decideReinvocation({
|
|
601
|
+
approvalDecisions,
|
|
602
|
+
reconciledFileReview,
|
|
603
|
+
fileReviewFailed,
|
|
604
|
+
fileReviewFailureDetail,
|
|
605
|
+
discardedPaths,
|
|
606
|
+
});
|
|
607
|
+
if (settlement) return { kind: "settled", settlement };
|
|
608
|
+
return { kind: "ready", reinvocation: { isReinvocation: reinvoked, approvalDecisions } };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Phases 4 to 4b (`index.ts` L620-770): the tool surface.
|
|
613
|
+
*
|
|
614
|
+
* The MCP-bound env map (and ONLY it, never the agent process env) carries
|
|
615
|
+
* the reserved caller-identity keys, so a server that declares them in
|
|
616
|
+
* spec.env can template the platform-verified caller into its headers;
|
|
617
|
+
* filterEnvToDeclaredKeys keeps every other server blind. The resolved list
|
|
618
|
+
* then mutates through the Connect backfill and three synthesized
|
|
619
|
+
* attachments, deliberately AFTER resolve + backfill so the backfill's
|
|
620
|
+
* destructiveHint tightener can never force-gate an attachment's tools:
|
|
621
|
+
*
|
|
622
|
+
* - channel messaging (DD-006 D7/D8): the discovery read IS the attachment
|
|
623
|
+
* decision, and every failure mode degrades to honest absence;
|
|
624
|
+
* - conversation participation (channel-conversations DD-008 D-c): the
|
|
625
|
+
* channel-id session label IS the decision, a free local read;
|
|
626
|
+
* - memory capture (DD-005 D1): the recall snapshot's enabled bit IS the
|
|
627
|
+
* decision, server-stamped at execution create.
|
|
628
|
+
*
|
|
629
|
+
* Their credential story (DD-006 D4): the exchanged scoped runner token
|
|
630
|
+
* authenticates the discovery reads per call; the exchange is opportunistic
|
|
631
|
+
* (every consumer degrades to an empty answer by contract, and the server
|
|
632
|
+
* refuses the ambient fallback safely), so a failed exchange must not kill
|
|
633
|
+
* the run, unlike the env read, where secrets are load-bearing.
|
|
634
|
+
*
|
|
635
|
+
* Approval policies merge last, from all layers, with two bypasses shared
|
|
636
|
+
* with the native harness (`shared/approval-policy.ts` `ActiveLeases`): the
|
|
637
|
+
* pre-armed spec.auto_approve_all is the one whole-run global bypass; an
|
|
638
|
+
* interactive APPROVE_ALL grants a run-lifetime lease scoped to that
|
|
639
|
+
* action's class. Layer-3 overrides ride each resolved server from its
|
|
640
|
+
* merged usage (issue #349). The harness projects its SDK config from
|
|
641
|
+
* `servers` after this returns, exactly once, so every mutation is visible
|
|
642
|
+
* by construction.
|
|
643
|
+
*/
|
|
644
|
+
export async function resolveMcpServersAndPolicies(
|
|
645
|
+
deps: ResolutionDeps,
|
|
646
|
+
args: {
|
|
647
|
+
readonly execution: AgentExecution;
|
|
648
|
+
readonly session: Session;
|
|
649
|
+
readonly sessionId: string;
|
|
650
|
+
readonly blueprint: ResolvedBlueprint;
|
|
651
|
+
readonly environment: TurnEnvironment;
|
|
652
|
+
},
|
|
653
|
+
): Promise<TurnMcp> {
|
|
654
|
+
const { client, config } = deps;
|
|
655
|
+
const { executionId } = deps.input;
|
|
656
|
+
const { execution, session, sessionId, blueprint, environment } = args;
|
|
657
|
+
|
|
658
|
+
deps.enterPhase("resolve_mcp_servers");
|
|
659
|
+
await deps.reportProgress("Resolving MCP servers");
|
|
660
|
+
const transportPosture = resolveMcpTransportPosture(config.mode);
|
|
661
|
+
const mcpEnvVars = injectCallerIdentityEnv(
|
|
662
|
+
environment.envVars,
|
|
663
|
+
resolveCallerIdentity(
|
|
664
|
+
blueprint.sessionSpec.metadata,
|
|
665
|
+
session.status?.audit?.specAudit?.createdBy,
|
|
666
|
+
),
|
|
667
|
+
sessionId,
|
|
668
|
+
);
|
|
669
|
+
let servers = (await resolveMcpServers(
|
|
670
|
+
client, blueprint.mergedMcpServerUsages, mcpEnvVars, transportPosture,
|
|
671
|
+
)).resolvedServers;
|
|
672
|
+
deps.timing.mark("resolve_mcp_servers");
|
|
673
|
+
|
|
674
|
+
// Phase 4a: Connect backfill for undiscovered MCP servers.
|
|
675
|
+
const sessionOrg = session.metadata?.org ?? "";
|
|
676
|
+
servers = await backfillMcpServersIfNeeded(
|
|
677
|
+
client, servers, blueprint.mergedMcpServerUsages, mcpEnvVars, sessionOrg,
|
|
678
|
+
transportPosture, deps.heartbeat, environment.secretKeys,
|
|
679
|
+
);
|
|
680
|
+
deps.timing.mark("backfill_mcp");
|
|
681
|
+
|
|
682
|
+
let exchangedRunnerToken: string | undefined;
|
|
683
|
+
try {
|
|
684
|
+
exchangedRunnerToken =
|
|
685
|
+
await client.acquireScopedRunnerToken({ agentExecutionId: executionId });
|
|
686
|
+
} catch (err) {
|
|
687
|
+
console.warn(
|
|
688
|
+
"[turn-context] Scoped-token exchange failed for attachment/discovery " +
|
|
689
|
+
`reads; degrading to the ambient credential: ${err instanceof Error ? err.message : err}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const attachmentCredential = exchangedRunnerToken
|
|
693
|
+
?? config.stigmerTokenRef?.current
|
|
694
|
+
?? config.stigmerToken;
|
|
695
|
+
const attachmentEndpoints = {
|
|
696
|
+
bridgeEndpoint: config.mcpBridgeEndpoint,
|
|
697
|
+
credential: attachmentCredential,
|
|
698
|
+
backendEndpoint: config.stigmerBackendEndpoint,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// Phase 4a2: the channel messaging attachment.
|
|
702
|
+
const channelMessaging = await discoverChannelMessaging(client, exchangedRunnerToken);
|
|
703
|
+
if (channelMessaging.length > 0) {
|
|
704
|
+
const attachment = synthesizeChannelAttachment(channelMessaging, attachmentEndpoints);
|
|
705
|
+
if (attachment) {
|
|
706
|
+
servers = injectSynthesizedAttachment(servers, attachment, "channel messaging");
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Phase 4a4: the conversation participation attachment. HTTP-only:
|
|
711
|
+
// synthesize answers undefined with no bridge endpoint by design.
|
|
712
|
+
const conversationAttachment = synthesizeConversationAttachment(
|
|
713
|
+
readChannelConversationId(session.metadata?.labels),
|
|
714
|
+
attachmentEndpoints,
|
|
715
|
+
);
|
|
716
|
+
if (conversationAttachment) {
|
|
717
|
+
servers = injectSynthesizedAttachment(servers, conversationAttachment, "conversation participation");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Phase 4a5: the memory capture attachment. The capture context is
|
|
721
|
+
// attribution the server verifies or trusts per edition; the subject is
|
|
722
|
+
// never threaded, it derives from the credential.
|
|
723
|
+
const memoryAttachment = synthesizeMemoryAttachment(
|
|
724
|
+
execution.spec?.recalledMemories,
|
|
725
|
+
{
|
|
726
|
+
org: session.metadata?.org ?? "",
|
|
727
|
+
agentId: blueprint.agent.metadata?.id ?? "",
|
|
728
|
+
sessionId,
|
|
729
|
+
agentExecutionId: executionId,
|
|
730
|
+
},
|
|
731
|
+
attachmentEndpoints,
|
|
732
|
+
);
|
|
733
|
+
if (memoryAttachment) {
|
|
734
|
+
servers = injectSynthesizedAttachment(servers, memoryAttachment, "memory capture");
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Phase 4b: merge approval policies from all layers.
|
|
738
|
+
const leases = deriveActiveLeases(execution);
|
|
739
|
+
const policies = mergeApprovalPolicies(servers, leases);
|
|
740
|
+
|
|
741
|
+
return { servers, channelMessaging, leases, policies };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Phase 5 (`index.ts` L493-501, the orchestrator's position between the
|
|
746
|
+
* tool surface and the attachments): mount the merged skills under the
|
|
747
|
+
* session's platform dir, reachable from the workspace through its
|
|
748
|
+
* `.stigmer` link, and return what a prompt renders (`shared/skill-
|
|
749
|
+
* resolver.ts`). Reads the control plane, which is why it is the runtime's
|
|
750
|
+
* and not a harness's; each harness places the returned metadata in its own
|
|
751
|
+
* prompt shape. The `.stigmer` link this and the attachment phase create is
|
|
752
|
+
* removed in the runtime's finally, after the harness's own teardown.
|
|
753
|
+
*/
|
|
754
|
+
export async function mountSkills(
|
|
755
|
+
deps: ResolutionDeps,
|
|
756
|
+
args: { readonly blueprint: ResolvedBlueprint; readonly sessionId: string; readonly primaryDir: string },
|
|
757
|
+
): Promise<readonly SkillMetadata[]> {
|
|
758
|
+
deps.enterPhase("resolve_skills");
|
|
759
|
+
await deps.reportProgress("Resolving skills");
|
|
760
|
+
const skills = await resolveSkills(deps.client, args.blueprint.mergedSkillRefs, {
|
|
761
|
+
sessionId: args.sessionId,
|
|
762
|
+
primaryWorkspaceDir: args.primaryDir,
|
|
763
|
+
});
|
|
764
|
+
deps.timing.mark("resolve_skills");
|
|
765
|
+
return skills;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Phase 5b (`index.ts` L803-838): resolve the turn's attachments, fail-hard
|
|
770
|
+
* (they are explicit user inputs; see `shared/attachment-resolver.ts`).
|
|
771
|
+
* Downloads by storage key through the same artifact storage resolved for
|
|
772
|
+
* status offload. The vision budget rides along so image attachments are
|
|
773
|
+
* selected for inline delivery while their bytes are already in hand
|
|
774
|
+
* (`shared/attachment-vision.ts` owns all policy); it carries the requested
|
|
775
|
+
* model's registry vision capability, looked up from the raw
|
|
776
|
+
* executionConfig name, because full model validation isn't needed for
|
|
777
|
+
* this and ""/"default" (the Auto pool) resolves to unknown, which the
|
|
778
|
+
* policy treats as sighted. The vision profile is the harness's (each
|
|
779
|
+
* engine accepts different image shapes), so it arrives as an argument.
|
|
780
|
+
*/
|
|
781
|
+
export async function resolveTurnAttachments(
|
|
782
|
+
deps: ResolutionDeps,
|
|
783
|
+
args: {
|
|
784
|
+
readonly spec: AgentExecutionSpec;
|
|
785
|
+
readonly sessionId: string;
|
|
786
|
+
readonly primaryDir: string;
|
|
787
|
+
readonly visionProfile: VisionProfile;
|
|
788
|
+
},
|
|
789
|
+
): Promise<TurnAttachments> {
|
|
790
|
+
deps.enterPhase("resolve_attachments");
|
|
791
|
+
const visionBudget = new VisionBudget(args.visionProfile, {
|
|
792
|
+
modelVision: await getModelVisionCapability(args.spec.executionConfig?.modelName ?? ""),
|
|
793
|
+
});
|
|
794
|
+
const results = await resolveAttachments(args.spec.attachments, {
|
|
795
|
+
sessionId: args.sessionId,
|
|
796
|
+
primaryWorkspaceDir: args.primaryDir,
|
|
797
|
+
mode: deps.config.mode,
|
|
798
|
+
storage: deps.artifactStorage,
|
|
799
|
+
visionBudget,
|
|
800
|
+
});
|
|
801
|
+
// Vision facts, derived once from the single resolution result: the
|
|
802
|
+
// images the model will see inline (in attachment order) and the ones
|
|
803
|
+
// that degraded to path-only, disclosed in the prompt.
|
|
804
|
+
const visionImages = results.flatMap((a) => (a.vision ? [a.vision] : []));
|
|
805
|
+
const visionNotViewable: NotViewableEntry[] = results.flatMap((a) =>
|
|
806
|
+
a.visionDegraded ? [{ path: a.relativePath, reason: a.visionDegraded }] : [],
|
|
807
|
+
);
|
|
808
|
+
if (visionImages.length > 0 || visionNotViewable.length > 0) {
|
|
809
|
+
console.log(
|
|
810
|
+
`[attachment-vision] execution=${deps.input.executionId} inline=${visionImages.length} ` +
|
|
811
|
+
`(${visionImages.reduce((n, v) => n + v.byteSize, 0)} bytes) ` +
|
|
812
|
+
`degraded=${JSON.stringify(visionNotViewable.map((d) => `${d.path}:${d.reason}`))}`,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
deps.timing.mark("resolve_attachments");
|
|
816
|
+
return { results, visionImages, visionNotViewable };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Phase 5b3 (`index.ts` L849-867): exact-apply approved whole-file writes
|
|
821
|
+
* (HITL "what you approve is what gets applied"). A deny-only harness
|
|
822
|
+
* reinvokes the model, which regenerates content, so a resource grant alone
|
|
823
|
+
* cannot guarantee the bytes that land match the bytes the user approved.
|
|
824
|
+
* The runner therefore writes the EXACT approved whole-file content itself
|
|
825
|
+
* and marks those tool calls COMPLETED; the harness then issues NO grant for
|
|
826
|
+
* them, so any FURTHER change the model makes to those files is re-gated.
|
|
827
|
+
* Hunk edits / shell / MCP stay on the grant + reinvocation path, and every
|
|
828
|
+
* uncertain case degrades to that path, so this can never corrupt a file
|
|
829
|
+
* (see `shared/exact-apply.ts`).
|
|
830
|
+
*
|
|
831
|
+
* Scoped OUT of capture mode, which does not reinvoke the model for file
|
|
832
|
+
* edits: it applies the exact captured bytes itself in the reconcile.
|
|
833
|
+
*
|
|
834
|
+
* The caller persists when the returned set is non-empty (the applied state
|
|
835
|
+
* must be durable before the continuation runs, and the UI reflects it at
|
|
836
|
+
* once); this phase, like every phase, never persists.
|
|
837
|
+
*/
|
|
838
|
+
export async function applyApprovedWrites(
|
|
839
|
+
deps: ResolutionDeps,
|
|
840
|
+
args: {
|
|
841
|
+
readonly workspace: TurnWorkspace;
|
|
842
|
+
readonly reinvocation: TurnReinvocation;
|
|
843
|
+
},
|
|
844
|
+
): Promise<ReadonlySet<string>> {
|
|
845
|
+
const { workspace, reinvocation } = args;
|
|
846
|
+
if (workspace.captureMode || !reinvocation.isReinvocation || reinvocation.approvalDecisions.size === 0) {
|
|
847
|
+
return new Set();
|
|
848
|
+
}
|
|
849
|
+
deps.enterPhase("apply_approved_writes");
|
|
850
|
+
return applyApprovedWholeFileWrites({
|
|
851
|
+
messages: deps.status.messages,
|
|
852
|
+
workspaceBackend: new LocalWorkspaceBackend(workspace.primaryDir),
|
|
853
|
+
workspaceDirs: workspace.dirs,
|
|
854
|
+
executionId: deps.input.executionId,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* Phase 6, the harness-agnostic half (`index.ts` L998, L1005-1006): the
|
|
860
|
+
* requested model name as the execution states it, and the effective service
|
|
861
|
+
* tier and thinking mode. UNSPECIFIED → STANDARD (#357) and UNSPECIFIED →
|
|
862
|
+
* DISABLED (#772) resolve here and nowhere else; every upstream layer
|
|
863
|
+
* preserves the caller's raw enum values. Validating the NAME against a
|
|
864
|
+
* catalog is the harness's (Cursor's `resolveModelId`), so it is not here.
|
|
865
|
+
*/
|
|
866
|
+
export function resolveModelPreferences(spec: AgentExecutionSpec): TurnModelPreferences {
|
|
867
|
+
return {
|
|
868
|
+
requested: spec.executionConfig?.modelName || "default",
|
|
869
|
+
serviceTier: resolveEffectiveServiceTier(spec.executionConfig?.serviceTier),
|
|
870
|
+
thinkingMode: resolveEffectiveThinkingMode(spec.executionConfig?.thinkingMode),
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/** Phase 9b (`index.ts` L1183-1184): the structured-output schema the execution asks for, if any. */
|
|
875
|
+
export function structuredOutputSchemaOf(spec: AgentExecutionSpec): Record<string, unknown> | undefined {
|
|
876
|
+
return spec.executionConfig?.structuredOutputSchema as Record<string, unknown> | undefined;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Phase 9c (`index.ts` L1186-1216, and the metadata reads at L1240-1245):
|
|
881
|
+
* the standing context, read once, and the memory selection prepared once.
|
|
882
|
+
*
|
|
883
|
+
* Selection runs against the frozen first message's semantics: above the
|
|
884
|
+
* activation threshold it picks top-k for spec.message, otherwise (and on
|
|
885
|
+
* any failure) it injects the full candidate set. The outcome report is
|
|
886
|
+
* stamped on the turn's status ONCE, picked up by the next persist; a
|
|
887
|
+
* re-invocation replays the report already persisted on the execution
|
|
888
|
+
* rather than re-selecting (the written-once rule). Memoized because a
|
|
889
|
+
* resumed-agent primary send carries no memories, but a mid-send
|
|
890
|
+
* poisoned-handle failure rebuilds on a FRESH agent whose recovery prompt
|
|
891
|
+
* does, so every memory-carrying build site awaits this lazily.
|
|
892
|
+
*/
|
|
893
|
+
export function resolveStandingContext(
|
|
894
|
+
deps: ResolutionDeps,
|
|
895
|
+
args: { readonly execution: AgentExecution; readonly spec: AgentExecutionSpec; readonly blueprint: ResolvedBlueprint },
|
|
896
|
+
): TurnStandingContext {
|
|
897
|
+
const { execution, spec, blueprint } = args;
|
|
898
|
+
let memorySelection: Promise<RecalledMemoriesContent | undefined> | undefined;
|
|
899
|
+
const selectRecalledMemories = (): Promise<RecalledMemoriesContent | undefined> => {
|
|
900
|
+
memorySelection ??= selectRecalledFacts(spec.recalledMemories, spec.message, {
|
|
901
|
+
proxyEndpoint: deps.config.proxyEndpoint,
|
|
902
|
+
stigmerToken: deps.config.stigmerToken,
|
|
903
|
+
executionId: deps.input.executionId,
|
|
904
|
+
priorReport: execution.status?.recalledMemoriesReport,
|
|
905
|
+
}).then((selection) => {
|
|
906
|
+
if (selection.report !== undefined) {
|
|
907
|
+
deps.status.recalledMemoriesReport = selection.report;
|
|
908
|
+
}
|
|
909
|
+
return selection.content;
|
|
910
|
+
});
|
|
911
|
+
return memorySelection;
|
|
912
|
+
};
|
|
913
|
+
return {
|
|
914
|
+
contextBridge: readContextBridge(blueprint.sessionSpec.metadata),
|
|
915
|
+
senderIdentity: readSenderIdentity(blueprint.sessionSpec.metadata),
|
|
916
|
+
sessionContext: readSessionContext(blueprint.sessionSpec.metadata),
|
|
917
|
+
declaredPreferences: readDeclaredPreferences(spec.declaredPreferences),
|
|
918
|
+
conversationCatchup: readConversationCatchup(spec.conversationCatchup),
|
|
919
|
+
selectRecalledMemories,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// ---------------------------------------------------------------------------
|
|
924
|
+
// The composer
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* The resources a turn holds that outlive the phase that produced them and
|
|
929
|
+
* must be released or finalized by the caller's `finally` and terminal
|
|
930
|
+
* writes — set on this frame the instant they exist, so no throw between
|
|
931
|
+
* acquisition and hand-over can leave a held lock the caller never sees.
|
|
932
|
+
* Not part of `TurnInput`: an adapter never releases the lock or finalizes
|
|
933
|
+
* the write-back.
|
|
934
|
+
*/
|
|
935
|
+
export interface TurnFrame {
|
|
936
|
+
/** The primary tree once provisioned: the `.stigmer` link the skill and attachment phases create under it is removed at turn end. */
|
|
937
|
+
primaryDir: string | undefined;
|
|
938
|
+
/** Exclusive turn lock on the primary tree; released LAST, after the adapter's own teardown. */
|
|
939
|
+
releaseWorkspaceLock: ReleaseWorkspaceLock | undefined;
|
|
940
|
+
/** Finalizes at exactly two seams: the pure file-review resume and terminal completion. `null` when nothing is eligible. */
|
|
941
|
+
writeback: WriteBackCoordinator | null;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/** What the composer answers: the whole record, or a settlement the caller writes and returns. */
|
|
945
|
+
export type TurnResolution =
|
|
946
|
+
| { readonly kind: "ready"; readonly input: TurnInput }
|
|
947
|
+
| { readonly kind: "settled"; readonly settlement: TurnSettlement };
|
|
948
|
+
|
|
949
|
+
/**
|
|
950
|
+
* Run the twelve phases in order and compose the turn's `TurnInput`, or
|
|
951
|
+
* stop at the first settlement.
|
|
952
|
+
*
|
|
953
|
+
* The order is the orchestrator's, with its harness-specific steps gone
|
|
954
|
+
* (they run inside `adapter.runTurn`, after this returns): the tool surface,
|
|
955
|
+
* the skills, the attachments, the exact-apply, then the reads that need no
|
|
956
|
+
* network.
|
|
957
|
+
*
|
|
958
|
+
* The one persist a phase requires — the applied writes must be durable
|
|
959
|
+
* before the continuation runs — goes through the caller's chokepoint
|
|
960
|
+
* (`persist`), so the rule that phases never persist holds here too.
|
|
961
|
+
*/
|
|
962
|
+
export async function resolveTurnContext(
|
|
963
|
+
deps: ResolutionDeps,
|
|
964
|
+
capabilities: HarnessCapabilities,
|
|
965
|
+
frame: TurnFrame,
|
|
966
|
+
persist: () => Promise<void>,
|
|
967
|
+
): Promise<TurnResolution> {
|
|
968
|
+
const { execution, spec, sessionId } = await fetchExecution(deps);
|
|
969
|
+
const { session, blueprint } = await resolveAgentBlueprint(deps, sessionId);
|
|
970
|
+
const environment = await resolveEnvironment(deps);
|
|
971
|
+
const { workspace, writeback } = await provisionWorkspace(deps, { session, sessionId, envVars: environment.envVars });
|
|
972
|
+
frame.primaryDir = workspace.primaryDir;
|
|
973
|
+
frame.writeback = writeback;
|
|
974
|
+
|
|
975
|
+
const lock = await acquireWorkspaceTurnLock(deps, workspace.primaryDir);
|
|
976
|
+
if (lock.kind === "settled") return { kind: "settled", settlement: lock.settlement };
|
|
977
|
+
frame.releaseWorkspaceLock = lock.release;
|
|
978
|
+
|
|
979
|
+
await bindTelemetryBaggage(deps, { session, sessionId });
|
|
980
|
+
|
|
981
|
+
const reinvoked = await reconcileReinvocation(deps, {
|
|
982
|
+
stateIdSource: capabilities.stateIdSource,
|
|
983
|
+
execution,
|
|
984
|
+
workspace,
|
|
985
|
+
fileReview: capabilities.fileReview,
|
|
986
|
+
});
|
|
987
|
+
if (reinvoked.kind === "settled") return { kind: "settled", settlement: reinvoked.settlement };
|
|
988
|
+
|
|
989
|
+
const mcp = await resolveMcpServersAndPolicies(deps, { execution, session, sessionId, blueprint, environment });
|
|
990
|
+
const skills = await mountSkills(deps, { blueprint, sessionId, primaryDir: workspace.primaryDir });
|
|
991
|
+
const attachments = await resolveTurnAttachments(deps, {
|
|
992
|
+
spec,
|
|
993
|
+
sessionId,
|
|
994
|
+
primaryDir: workspace.primaryDir,
|
|
995
|
+
visionProfile: capabilities.visionProfile,
|
|
996
|
+
});
|
|
997
|
+
const appliedToolCallIds = await applyApprovedWrites(deps, { workspace, reinvocation: reinvoked.reinvocation });
|
|
998
|
+
if (appliedToolCallIds.size > 0) {
|
|
999
|
+
// Persist the applied writes (tool calls now COMPLETED with the approved
|
|
1000
|
+
// diff) before reinvocation, so the applied state is durable even if the
|
|
1001
|
+
// continuation fails, and the UI reflects it immediately.
|
|
1002
|
+
await persist();
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return {
|
|
1006
|
+
kind: "ready",
|
|
1007
|
+
input: {
|
|
1008
|
+
executionId: deps.input.executionId,
|
|
1009
|
+
threadId: deps.input.threadId,
|
|
1010
|
+
turnSeq: deps.input.turnSeq,
|
|
1011
|
+
sessionId,
|
|
1012
|
+
approvalDecisions: reinvoked.reinvocation.approvalDecisions,
|
|
1013
|
+
execution,
|
|
1014
|
+
session,
|
|
1015
|
+
blueprint,
|
|
1016
|
+
environment,
|
|
1017
|
+
workspace,
|
|
1018
|
+
mcp,
|
|
1019
|
+
skills,
|
|
1020
|
+
attachments,
|
|
1021
|
+
appliedToolCallIds,
|
|
1022
|
+
model: resolveModelPreferences(spec),
|
|
1023
|
+
structuredOutputSchema: structuredOutputSchemaOf(spec),
|
|
1024
|
+
standing: resolveStandingContext(deps, { execution, spec, blueprint }),
|
|
1025
|
+
artifactStorage: deps.artifactStorage,
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
}
|