@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.
Files changed (56) hide show
  1. package/dist/.build-fingerprint +1 -1
  2. package/dist/__test-utils__/hermetic-activity.d.ts +245 -0
  3. package/dist/__test-utils__/hermetic-activity.js +369 -0
  4. package/dist/__test-utils__/hermetic-activity.js.map +1 -0
  5. package/dist/__test-utils__/mock-client.d.ts +13 -0
  6. package/dist/__test-utils__/mock-client.js +45 -0
  7. package/dist/__test-utils__/mock-client.js.map +1 -0
  8. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.d.ts +172 -0
  9. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +331 -0
  10. package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -0
  11. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.d.ts +167 -0
  12. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js +239 -0
  13. package/dist/activities/execute-cursor/__test-utils__/scripted-agent.js.map +1 -0
  14. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.d.ts +97 -0
  15. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js +132 -0
  16. package/dist/activities/execute-cursor/__test-utils__/scripted-sdk.js.map +1 -0
  17. package/dist/harness/capabilities.d.ts +71 -0
  18. package/dist/harness/capabilities.js +36 -0
  19. package/dist/harness/capabilities.js.map +1 -0
  20. package/dist/harness/registry.d.ts +67 -0
  21. package/dist/harness/registry.js +112 -0
  22. package/dist/harness/registry.js.map +1 -0
  23. package/dist/harness/types.d.ts +268 -0
  24. package/dist/harness/types.js +55 -0
  25. package/dist/harness/types.js.map +1 -0
  26. package/package.json +4 -4
  27. package/src/__test-utils__/__tests__/harness-contract-self-check.test.ts +229 -0
  28. package/src/__test-utils__/config-fixture.ts +63 -0
  29. package/src/__test-utils__/harness-contract/contract.ts +536 -0
  30. package/src/__test-utils__/harness-contract/recording-sink.ts +96 -0
  31. package/src/__test-utils__/harness-contract/scripted-adapter.ts +289 -0
  32. package/src/__test-utils__/harness-contract/types.ts +100 -0
  33. package/src/__test-utils__/hermetic-activity.ts +477 -0
  34. package/src/__test-utils__/proto-helpers.ts +25 -0
  35. package/src/__tests__/harness-contract.test.ts +25 -0
  36. package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +422 -0
  37. package/src/activities/execute-cursor/__test-utils__/scripted-agent.ts +342 -0
  38. package/src/activities/execute-cursor/__test-utils__/scripted-sdk.ts +166 -0
  39. package/src/activities/execute-cursor/__tests__/hermetic/deny-and-retry.test.ts +228 -0
  40. package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +180 -0
  41. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn1.status.json +55 -0
  42. package/src/activities/execute-cursor/__tests__/hermetic/goldens/deny-and-retry.turn2.status.json +77 -0
  43. package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +126 -0
  44. package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +45 -0
  45. package/src/activities/execute-cursor/__tests__/hermetic/goldens/plain-turn.status.json +48 -0
  46. package/src/activities/execute-cursor/__tests__/hermetic/goldens/recovery-fresh-agent.status.json +53 -0
  47. package/src/activities/execute-cursor/__tests__/hermetic/goldens/tool-call.status.json +68 -0
  48. package/src/activities/execute-cursor/__tests__/hermetic/goldens/worker-shutdown.status.json +47 -0
  49. package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +201 -0
  50. package/src/activities/execute-cursor/__tests__/hermetic/plain-turn.test.ts +171 -0
  51. package/src/activities/execute-cursor/__tests__/hermetic/recovery-fresh-agent.test.ts +156 -0
  52. package/src/activities/execute-cursor/__tests__/hermetic/tool-call.test.ts +137 -0
  53. package/src/harness/__tests__/registry.test.ts +167 -0
  54. package/src/harness/capabilities.ts +75 -0
  55. package/src/harness/registry.ts +123 -0
  56. 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" };