@stigmer/runner 3.14.0 → 3.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/hermetic-activity.d.ts +245 -0
- package/dist/__test-utils__/hermetic-activity.js +369 -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/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +172 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +331 -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 +167 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +239 -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 +97 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +132 -0
- package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
- package/dist/harness/capabilities.d.ts +71 -0
- package/dist/harness/capabilities.js +36 -0
- package/dist/harness/capabilities.js.map +1 -0
- package/dist/harness/registry.d.ts +67 -0
- package/dist/harness/registry.js +112 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/types.d.ts +268 -0
- package/dist/harness/types.js +55 -0
- package/dist/harness/types.js.map +1 -0
- package/package.json +4 -4
- package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +229 -0
- package/src/__test-utils__/config-fixture.ts +63 -0
- package/src/__test-utils__/harness-contract/contract.ts +536 -0
- package/src/__test-utils__/harness-contract/recording-sink.ts +96 -0
- package/src/__test-utils__/harness-contract/scripted-adapter.ts +289 -0
- package/src/__test-utils__/harness-contract/types.ts +100 -0
- package/src/__test-utils__/hermetic-activity.ts +477 -0
- package/src/__test-utils__/proto-helpers.ts +25 -0
- package/src/__tests__/harness-contract.test.ts +25 -0
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +422 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +342 -0
- package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +166 -0
- 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/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 +45 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +47 -0
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +201 -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/tool-call.test.ts +137 -0
- package/src/harness/__tests__/registry.test.ts +167 -0
- package/src/harness/capabilities.ts +75 -0
- package/src/harness/registry.ts +123 -0
- package/src/harness/types.ts +278 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness adapter contract — the line between what the turn runtime owns
|
|
3
|
+
* and what a harness owns.
|
|
4
|
+
*
|
|
5
|
+
* Stigmer runs an agent turn through one of several engines ("harnesses"):
|
|
6
|
+
* the native LangGraph deep-agent, the Cursor SDK, and in future the Claude
|
|
7
|
+
* Agent SDK and the Codex SDK. Everything about a turn that does NOT touch a
|
|
8
|
+
* vendor SDK — fetching the execution, resolving the blueprint and the
|
|
9
|
+
* environment, provisioning and locking the workspace, mounting skills,
|
|
10
|
+
* resolving MCP servers and approval policies, seeding the transcript, the
|
|
11
|
+
* persist cadence, the stall watchdog, the Temporal heartbeat, pause vs
|
|
12
|
+
* shutdown, the cost cap, the file-review boundary, the terminal mapping —
|
|
13
|
+
* is the RUNTIME's, written once. What a harness owns is its SDK slice: how
|
|
14
|
+
* the engine is created or resumed, how the prompt is placed, how MCP servers
|
|
15
|
+
* are bound, how the engine is made to stop before a gated side effect, and
|
|
16
|
+
* how its events become transcript rows. This file is the whole of what a
|
|
17
|
+
* harness author has to implement; `__test-utils__/harness-contract/` is the
|
|
18
|
+
* kit every implementation has to pass.
|
|
19
|
+
*
|
|
20
|
+
* Every member here is a rename of a function the Cursor loop already injects
|
|
21
|
+
* (`execute-cursor/turn-stream.ts` `CursorTurnStreamDeps`) or a fact the
|
|
22
|
+
* runtime cannot read anywhere else. Nothing here is speculative: where the
|
|
23
|
+
* program's original sketch and the code disagreed, the code won, and the
|
|
24
|
+
* disagreement was ruled at the entry's gate
|
|
25
|
+
* (stigmer-cloud `_projects/2026-09/20260911.02.sp.harness-contract-and-kit/`).
|
|
26
|
+
*
|
|
27
|
+
* What is deliberately NOT on this contract, and why:
|
|
28
|
+
*
|
|
29
|
+
* - No `dispose()`. One adapter object serves many concurrent turns
|
|
30
|
+
* (`maxConcurrentActivities`), so a per-turn teardown method on the
|
|
31
|
+
* adapter is a race. The adapter owns its per-turn teardown in its own
|
|
32
|
+
* `finally` inside `runTurn` (the Cursor harness already parks its agent
|
|
33
|
+
* there).
|
|
34
|
+
* - No `isCancelled()`, no `heartbeat(details)`, no `ExecutionStatusWriter`
|
|
35
|
+
* base. Each would be a second way of saying something `stopSignal`,
|
|
36
|
+
* `recordActivity()` or `requestPersist()` already says, and two writers of
|
|
37
|
+
* one fact drift (the native builders' `forceNextUpdate` flag is the same
|
|
38
|
+
* fact as a `requestPersist()` call).
|
|
39
|
+
* - No `reason` on `interrupted`, no payload on `completed`, no `retryable`
|
|
40
|
+
* on `failed`. Every cause of stopping is the runtime's own evidence; the
|
|
41
|
+
* final text and structured output are already folded onto the status;
|
|
42
|
+
* Temporal never retries a returned activity, so a retryable flag would
|
|
43
|
+
* have no reader.
|
|
44
|
+
* - No token-rotation hook. `Config.stigmerTokenRef` is the canonical
|
|
45
|
+
* mutable ref; an adapter's transport reads it per request.
|
|
46
|
+
* - No `TurnInput.status`. The runtime seeds `TurnSink.status` from the
|
|
47
|
+
* persisted transcript; a second copy on the input is the drift the
|
|
48
|
+
* single-source-of-truth mandate forbids.
|
|
49
|
+
*
|
|
50
|
+
* Module shape follows `shared/checkpointer/`: `types.ts`, `capabilities.ts`,
|
|
51
|
+
* `registry.ts`, no barrel. Nothing production-facing imports this module
|
|
52
|
+
* until the runtime that consumes it lands (S2 of the program).
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import type { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
56
|
+
import type { AgentExecutionStatus } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
57
|
+
|
|
58
|
+
import type { Config } from "../config.js";
|
|
59
|
+
import type { NormalizedActivityInput } from "../shared/activity-input.js";
|
|
60
|
+
import type { HarnessCapabilities } from "./capabilities.js";
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* One harness, as the runtime sees it. ONE adapter object exists per worker
|
|
64
|
+
* process; it serves every concurrent turn of its harness and holds no
|
|
65
|
+
* per-turn state (per-turn state lives in the `runTurn` frame). Three
|
|
66
|
+
* lifetimes meet here — worker (`boot`/`shutdown`), session
|
|
67
|
+
* (`releaseSession`) and turn (`runTurn`) — because the Cursor harness parks
|
|
68
|
+
* an engine per SESSION between turns, longer than a turn and shorter than
|
|
69
|
+
* the worker.
|
|
70
|
+
*
|
|
71
|
+
* `name` is a diagnostic identity (log lines, kit messages, the registry's
|
|
72
|
+
* duplicate check). It is NOT the activity the harness is bound to: the wire
|
|
73
|
+
* binding is the registry row's (`registry.ts` `HARNESS_ACTIVITY_NAMES`),
|
|
74
|
+
* so an adapter never declares a byte-pinned wire name and a test double can
|
|
75
|
+
* implement this interface under its own name.
|
|
76
|
+
*/
|
|
77
|
+
export interface HarnessAdapter {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly capabilities: HarnessCapabilities;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Worker lifetime, once per process, run by the registry in declaration
|
|
83
|
+
* order in BOTH composition roots (`runner.ts`, `runner-manager.ts`). Runs
|
|
84
|
+
* BEFORE bootstrap resolution — the Cursor interceptors must patch
|
|
85
|
+
* `node:http2` before the control plane is dialled — so `config` carries
|
|
86
|
+
* no Temporal coordinates yet. Vendor SDKs are imported lazily inside, so a
|
|
87
|
+
* harness that is not selected costs nothing at boot. A rejection here
|
|
88
|
+
* fails the worker's boot; a worker that cannot boot a harness must not
|
|
89
|
+
* start.
|
|
90
|
+
*/
|
|
91
|
+
boot(config: Config): Promise<void>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Worker lifetime, once per process, after the Temporal worker has drained.
|
|
95
|
+
* Releases everything the adapter still holds (the Cursor harness closes
|
|
96
|
+
* every parked agent). Must resolve even when nothing is held.
|
|
97
|
+
*/
|
|
98
|
+
shutdown(): Promise<void>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Session lifetime: the session is done on this host, release anything
|
|
102
|
+
* parked for it (the Cursor harness: the parked agent, its executor and the
|
|
103
|
+
* MCP subprocesses the lease pins, #215). Called by the registry from the
|
|
104
|
+
* manager's `removeSession`. A harness that parks nothing per session
|
|
105
|
+
* resolves as a no-op and says so in its header. Unknown session ids are a
|
|
106
|
+
* no-op, never an error: the runtime does not track which host parked what.
|
|
107
|
+
*/
|
|
108
|
+
releaseSession(sessionId: string): Promise<void>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Turn lifetime: run ONE engine turn against the resolved input, folding
|
|
112
|
+
* the engine's transcript rows into `sink.status` as they arrive, and
|
|
113
|
+
* settle with a {@link TurnOutcome}.
|
|
114
|
+
*
|
|
115
|
+
* The rules every implementation is held to (the kit's invariants):
|
|
116
|
+
*
|
|
117
|
+
* - Resolves, never rejects. A vendor failure becomes
|
|
118
|
+
* `{ kind: "failed", message }` with the user-facing sentence the
|
|
119
|
+
* adapter's classifier produced. A `CancelledFailure` never escapes: the
|
|
120
|
+
* runtime, not the adapter, decides what is a pause and what is a
|
|
121
|
+
* shutdown, and it throws exactly where Temporal semantics require.
|
|
122
|
+
* - Stops promptly when `sink.stopSignal` aborts, whatever the cause,
|
|
123
|
+
* settling `interrupted`. Every call the adapter makes is bounded: the
|
|
124
|
+
* runtime's heartbeat is live for the whole activity, and a live
|
|
125
|
+
* heartbeat over an unbounded call keeps a dead activity alive forever.
|
|
126
|
+
* - Proposes, never adjudicates. A gated side effect surfaces as a
|
|
127
|
+
* WAITING_APPROVAL row on `sink.status` and the turn ends
|
|
128
|
+
* `awaiting_approval`; the decision arrives on the next invocation in
|
|
129
|
+
* `input.approvalDecisions`. APPROVE executes exactly once; REJECT and
|
|
130
|
+
* SKIP never execute.
|
|
131
|
+
* - Owns its own per-turn teardown in a `finally` inside this method.
|
|
132
|
+
*/
|
|
133
|
+
runTurn(input: TurnInput, sink: TurnSink): Promise<TurnOutcome>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What the runtime resolved for this turn and the adapter cannot read
|
|
138
|
+
* anywhere else. This is the core; the runtime extraction (S2) grows it one
|
|
139
|
+
* typed field per phase it takes over from the orchestrators (environment,
|
|
140
|
+
* workspace, skills, MCP servers with merged policies, attachments, memory,
|
|
141
|
+
* prompt bundle, model), in the `setup.ts` `SetupResult` mold.
|
|
142
|
+
*
|
|
143
|
+
* `threadId` is the engine's state id as the runtime knows it: empty on an
|
|
144
|
+
* `engine-minted` harness's first turn (nothing minted yet) and the id the
|
|
145
|
+
* adapter bound through {@link TurnSink.bindHarnessState} on every later
|
|
146
|
+
* invocation; the runtime-minted id on every turn of a `deterministic`
|
|
147
|
+
* harness. An adapter derives create-vs-resume from it and its own state; the
|
|
148
|
+
* contract carries no `isReinvocation` flag because the two harnesses would
|
|
149
|
+
* derive it differently.
|
|
150
|
+
*/
|
|
151
|
+
export interface TurnInput extends NormalizedActivityInput {
|
|
152
|
+
/**
|
|
153
|
+
* The session this turn belongs to. Read by the runtime from the fetched
|
|
154
|
+
* execution; the adapter needs it to key anything it parks per session and
|
|
155
|
+
* to recognise a later {@link HarnessAdapter.releaseSession}.
|
|
156
|
+
*/
|
|
157
|
+
readonly sessionId: string;
|
|
158
|
+
/**
|
|
159
|
+
* The approval decisions the user has made on this execution's WAITING
|
|
160
|
+
* rows, keyed by tool-call id: the one projection both harness readers
|
|
161
|
+
* agree on (`status.messages[].toolCalls[]` where `approvalAction` is set
|
|
162
|
+
* and `status` is WAITING_APPROVAL). Derived by the runtime from
|
|
163
|
+
* `sink.status` on every invocation, never stored, so it cannot drift from
|
|
164
|
+
* the rows. An adapter reads the ROW for anything else it needs (args,
|
|
165
|
+
* content digest) and this map for the verdict; it never re-derives the
|
|
166
|
+
* verdict from the rows itself.
|
|
167
|
+
*/
|
|
168
|
+
readonly approvalDecisions: ReadonlyMap<string, ApprovalAction>;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The runtime's face during one turn: the five things an adapter may ask of
|
|
173
|
+
* it, and the one status it folds into. One sink per turn, owned by the
|
|
174
|
+
* runtime; the adapter never constructs one.
|
|
175
|
+
*
|
|
176
|
+
* Field ownership on `status` before the canonical transcript lands (S4):
|
|
177
|
+
* the adapter appends the engine's transcript rows (assistant messages,
|
|
178
|
+
* tool-call rows and their approval status, sub-agent rows, todos); the
|
|
179
|
+
* runtime writes the phase, the terminal system messages, `streamingUsage`,
|
|
180
|
+
* artifacts, write-backs and the file-review projection. An adapter never
|
|
181
|
+
* writes a phase or a terminal copy: those are Temporal semantics the
|
|
182
|
+
* runtime owns once.
|
|
183
|
+
*/
|
|
184
|
+
export interface TurnSink {
|
|
185
|
+
/**
|
|
186
|
+
* The one execution status this turn folds into. On a reinvocation it is
|
|
187
|
+
* seeded by the runtime from the persisted transcript, so the WAITING rows
|
|
188
|
+
* the adapter wrote last time, and their decisions, are already on it.
|
|
189
|
+
*/
|
|
190
|
+
readonly status: AgentExecutionStatus;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The ONE way a turn is told to stop, whatever the cause: user pause,
|
|
194
|
+
* worker shutdown, stall, cost cap, platform STOP. The runtime knows why
|
|
195
|
+
* and maps the outcome; the adapter's only job is to settle promptly as
|
|
196
|
+
* `interrupted`. `stopSignal.reason` is the runtime's own evidence — an
|
|
197
|
+
* adapter never branches on it. Check `aborted` at every step boundary and
|
|
198
|
+
* listen for `abort` inside anything long-running. May already be aborted
|
|
199
|
+
* when `runTurn` is entered; then return `interrupted` before doing any
|
|
200
|
+
* work.
|
|
201
|
+
*/
|
|
202
|
+
readonly stopSignal: AbortSignal;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* "Persist the status soon": schedules a write through the runtime's single
|
|
206
|
+
* persist chokepoint (tool-output offload, size cap, secret withholding,
|
|
207
|
+
* the streaming scheduler). Fire and forget — never awaited by the adapter,
|
|
208
|
+
* never a promise. The runtime persists unconditionally when the turn
|
|
209
|
+
* settles, so no adapter has to flush before returning.
|
|
210
|
+
*/
|
|
211
|
+
requestPersist(): void;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* "I made progress": resets the runtime's stall watchdog and is carried
|
|
215
|
+
* into the next Temporal heartbeat. Call it on every engine event and every
|
|
216
|
+
* token delta — a long generation emits deltas but few discrete events, and
|
|
217
|
+
* resetting only on events false-positives a stall. Never throws.
|
|
218
|
+
*/
|
|
219
|
+
recordActivity(): void;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Token counts for one engine turn. The runtime accumulates, prices and
|
|
223
|
+
* enforces `max_cost_usd`; an adapter reports and never accounts. Every
|
|
224
|
+
* count is a non-negative delta since the previous report.
|
|
225
|
+
*/
|
|
226
|
+
reportUsage(delta: UsageDelta): void;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The engine-minted state id, the moment it exists and BEFORE the turn
|
|
230
|
+
* proceeds, so a crash mid-turn still resumes on the next invocation. The
|
|
231
|
+
* runtime writes it to the session at once (the Cursor harness's
|
|
232
|
+
* `harness_state_id`). Called only by adapters whose
|
|
233
|
+
* `capabilities.stateIdSource` is `"engine-minted"`, and before their first
|
|
234
|
+
* `requestPersist`; a `deterministic` harness never calls it. Rejects when
|
|
235
|
+
* the session write fails; the adapter then ends the turn `failed` with
|
|
236
|
+
* that error and executes nothing further.
|
|
237
|
+
*/
|
|
238
|
+
bindHarnessState(harnessStateId: string): Promise<void>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Token counts for one engine turn, as the Cursor loop reads them from the
|
|
243
|
+
* SDK's `turn-ended` delta (`execute-cursor/usage-accumulator.ts`
|
|
244
|
+
* `TurnUsage` is this shape; the runtime extraction collapses the two). Every
|
|
245
|
+
* field is optional because engines report different subsets; a missing
|
|
246
|
+
* field means zero, never "unknown".
|
|
247
|
+
*/
|
|
248
|
+
export interface UsageDelta {
|
|
249
|
+
readonly inputTokens?: number;
|
|
250
|
+
readonly outputTokens?: number;
|
|
251
|
+
readonly cacheReadTokens?: number;
|
|
252
|
+
readonly cacheWriteTokens?: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* How one turn ended, carrying ONLY what the runtime cannot read from
|
|
257
|
+
* `sink.status` or its own evidence.
|
|
258
|
+
*
|
|
259
|
+
* - `completed`: the engine finished. The final text and any structured
|
|
260
|
+
* output are already folded onto the status; nothing rides here.
|
|
261
|
+
* - `awaiting_approval`: the engine proposed at least one gated side effect
|
|
262
|
+
* and stopped. The WAITING_APPROVAL rows are already on the status; the
|
|
263
|
+
* runtime persists them and returns to the workflow, which reinvokes with
|
|
264
|
+
* the decisions.
|
|
265
|
+
* - `failed`: the engine or its transport failed in a way the adapter can
|
|
266
|
+
* name. `message` is the user-facing sentence; `cause` is for the log.
|
|
267
|
+
* The runtime persists FAILED and RETURNS (Temporal does not retry a
|
|
268
|
+
* returned activity; re-running the same prompt would fail the same way).
|
|
269
|
+
* - `interrupted`: `sink.stopSignal` aborted and the adapter stopped. WHY it
|
|
270
|
+
* aborted is the runtime's evidence (its watchdog, its accounting, its
|
|
271
|
+
* heartbeat, its shutdown signal), so no reason rides here; the runtime
|
|
272
|
+
* classifies and applies the throw-vs-return table.
|
|
273
|
+
*/
|
|
274
|
+
export type TurnOutcome =
|
|
275
|
+
| { readonly kind: "completed" }
|
|
276
|
+
| { readonly kind: "awaiting_approval" }
|
|
277
|
+
| { readonly kind: "failed"; readonly message: string; readonly cause?: unknown }
|
|
278
|
+
| { readonly kind: "interrupted" };
|