@esankhan3/anvil-agent-core 0.3.0 → 0.4.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.
Files changed (147) hide show
  1. package/README.md +1 -1
  2. package/dist/__tests__/gemini-adapter-recording.test.d.ts +11 -0
  3. package/dist/__tests__/gemini-adapter-recording.test.d.ts.map +1 -0
  4. package/dist/__tests__/gemini-adapter-recording.test.js +108 -0
  5. package/dist/__tests__/gemini-adapter-recording.test.js.map +1 -0
  6. package/dist/__tests__/legacy-adapter-language-model.test.d.ts +11 -0
  7. package/dist/__tests__/legacy-adapter-language-model.test.d.ts.map +1 -0
  8. package/dist/__tests__/legacy-adapter-language-model.test.js +102 -0
  9. package/dist/__tests__/legacy-adapter-language-model.test.js.map +1 -0
  10. package/dist/__tests__/ollama-adapter-recording.test.d.ts +15 -0
  11. package/dist/__tests__/ollama-adapter-recording.test.d.ts.map +1 -0
  12. package/dist/__tests__/ollama-adapter-recording.test.js +128 -0
  13. package/dist/__tests__/ollama-adapter-recording.test.js.map +1 -0
  14. package/dist/__tests__/openrouter-prefill.integration.test.d.ts +19 -0
  15. package/dist/__tests__/openrouter-prefill.integration.test.d.ts.map +1 -0
  16. package/dist/__tests__/openrouter-prefill.integration.test.js +410 -0
  17. package/dist/__tests__/openrouter-prefill.integration.test.js.map +1 -0
  18. package/dist/__tests__/prefill-truncate.test.d.ts +8 -0
  19. package/dist/__tests__/prefill-truncate.test.d.ts.map +1 -0
  20. package/dist/__tests__/prefill-truncate.test.js +75 -0
  21. package/dist/__tests__/prefill-truncate.test.js.map +1 -0
  22. package/dist/__tests__/router-circuit-breaker.test.js +1 -1
  23. package/dist/__tests__/router-circuit-breaker.test.js.map +1 -1
  24. package/dist/__tests__/router-classify-unified.test.d.ts +12 -0
  25. package/dist/__tests__/router-classify-unified.test.d.ts.map +1 -0
  26. package/dist/__tests__/router-classify-unified.test.js +108 -0
  27. package/dist/__tests__/router-classify-unified.test.js.map +1 -0
  28. package/dist/__tests__/router-fallback.test.js +1 -1
  29. package/dist/__tests__/router-fallback.test.js.map +1 -1
  30. package/dist/__tests__/router-model-registry.test.js +24 -0
  31. package/dist/__tests__/router-model-registry.test.js.map +1 -1
  32. package/dist/__tests__/router-retry.test.js +3 -5
  33. package/dist/__tests__/router-retry.test.js.map +1 -1
  34. package/dist/__tests__/router-runagent.test.d.ts +14 -0
  35. package/dist/__tests__/router-runagent.test.d.ts.map +1 -0
  36. package/dist/__tests__/router-runagent.test.js +95 -0
  37. package/dist/__tests__/router-runagent.test.js.map +1 -0
  38. package/dist/__tests__/router-scaffold.test.js +2 -1
  39. package/dist/__tests__/router-scaffold.test.js.map +1 -1
  40. package/dist/__tests__/router-spend-ledger.test.js +1 -1
  41. package/dist/__tests__/router-spend-ledger.test.js.map +1 -1
  42. package/dist/__tests__/router-stream-fallback.test.d.ts +13 -0
  43. package/dist/__tests__/router-stream-fallback.test.d.ts.map +1 -0
  44. package/dist/__tests__/router-stream-fallback.test.js +129 -0
  45. package/dist/__tests__/router-stream-fallback.test.js.map +1 -0
  46. package/dist/__tests__/router-telemetry.test.js +3 -1
  47. package/dist/__tests__/router-telemetry.test.js.map +1 -1
  48. package/dist/__tests__/util/mock-upstream.d.ts +77 -0
  49. package/dist/__tests__/util/mock-upstream.d.ts.map +1 -0
  50. package/dist/__tests__/util/mock-upstream.js +219 -0
  51. package/dist/__tests__/util/mock-upstream.js.map +1 -0
  52. package/dist/adk-adapter.d.ts.map +1 -1
  53. package/dist/adk-adapter.js +51 -0
  54. package/dist/adk-adapter.js.map +1 -1
  55. package/dist/agent/session/adapter.d.ts +15 -0
  56. package/dist/agent/session/adapter.d.ts.map +1 -1
  57. package/dist/agent/session/adapter.js +3 -0
  58. package/dist/agent/session/adapter.js.map +1 -1
  59. package/dist/agent/session/index.d.ts +1 -0
  60. package/dist/agent/session/index.d.ts.map +1 -1
  61. package/dist/agent/session/index.js +1 -0
  62. package/dist/agent/session/index.js.map +1 -1
  63. package/dist/agent/session/language-model-bridge.d.ts.map +1 -1
  64. package/dist/agent/session/language-model-bridge.js +22 -0
  65. package/dist/agent/session/language-model-bridge.js.map +1 -1
  66. package/dist/agent/session/legacy-adapter-language-model.d.ts +27 -0
  67. package/dist/agent/session/legacy-adapter-language-model.d.ts.map +1 -0
  68. package/dist/agent/session/legacy-adapter-language-model.js +221 -0
  69. package/dist/agent/session/legacy-adapter-language-model.js.map +1 -0
  70. package/dist/agent/session/types.d.ts +20 -0
  71. package/dist/agent/session/types.d.ts.map +1 -1
  72. package/dist/agent/session/types.js.map +1 -1
  73. package/dist/claude-adapter.d.ts.map +1 -1
  74. package/dist/claude-adapter.js +27 -0
  75. package/dist/claude-adapter.js.map +1 -1
  76. package/dist/gemini-adapter.d.ts.map +1 -1
  77. package/dist/gemini-adapter.js +46 -0
  78. package/dist/gemini-adapter.js.map +1 -1
  79. package/dist/index.d.ts +9 -0
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +5 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/ollama-adapter.d.ts.map +1 -1
  84. package/dist/ollama-adapter.js +118 -8
  85. package/dist/ollama-adapter.js.map +1 -1
  86. package/dist/openrouter-adapter.d.ts.map +1 -1
  87. package/dist/openrouter-adapter.js +289 -15
  88. package/dist/openrouter-adapter.js.map +1 -1
  89. package/dist/prefill/strip.d.ts +49 -0
  90. package/dist/prefill/strip.d.ts.map +1 -0
  91. package/dist/prefill/strip.js +93 -0
  92. package/dist/prefill/strip.js.map +1 -0
  93. package/dist/prefill/translate.d.ts +55 -0
  94. package/dist/prefill/translate.d.ts.map +1 -0
  95. package/dist/prefill/translate.js +162 -0
  96. package/dist/prefill/translate.js.map +1 -0
  97. package/dist/prefill/truncate.d.ts +62 -0
  98. package/dist/prefill/truncate.d.ts.map +1 -0
  99. package/dist/prefill/truncate.js +77 -0
  100. package/dist/prefill/truncate.js.map +1 -0
  101. package/dist/router/errors.d.ts +24 -7
  102. package/dist/router/errors.d.ts.map +1 -1
  103. package/dist/router/errors.js +71 -14
  104. package/dist/router/errors.js.map +1 -1
  105. package/dist/router/index.d.ts +3 -2
  106. package/dist/router/index.d.ts.map +1 -1
  107. package/dist/router/index.js +2 -1
  108. package/dist/router/index.js.map +1 -1
  109. package/dist/router/model-registry.d.ts +22 -0
  110. package/dist/router/model-registry.d.ts.map +1 -1
  111. package/dist/router/model-registry.js +64 -1
  112. package/dist/router/model-registry.js.map +1 -1
  113. package/dist/router/provider-registry-resolver.d.ts +25 -0
  114. package/dist/router/provider-registry-resolver.d.ts.map +1 -0
  115. package/dist/router/provider-registry-resolver.js +84 -0
  116. package/dist/router/provider-registry-resolver.js.map +1 -0
  117. package/dist/router/retry.d.ts +3 -0
  118. package/dist/router/retry.d.ts.map +1 -1
  119. package/dist/router/retry.js +4 -1
  120. package/dist/router/retry.js.map +1 -1
  121. package/dist/router/router.d.ts +79 -1
  122. package/dist/router/router.d.ts.map +1 -1
  123. package/dist/router/router.js +256 -2
  124. package/dist/router/router.js.map +1 -1
  125. package/dist/router/types.d.ts +1 -1
  126. package/dist/router/types.d.ts.map +1 -1
  127. package/dist/router/types.js +1 -0
  128. package/dist/router/types.js.map +1 -1
  129. package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts +20 -0
  130. package/dist/turn-recorder/__tests__/turn-recorder.test.d.ts.map +1 -0
  131. package/dist/turn-recorder/__tests__/turn-recorder.test.js +282 -0
  132. package/dist/turn-recorder/__tests__/turn-recorder.test.js.map +1 -0
  133. package/dist/turn-recorder/hash.d.ts +10 -0
  134. package/dist/turn-recorder/hash.d.ts.map +1 -0
  135. package/dist/turn-recorder/hash.js +35 -0
  136. package/dist/turn-recorder/hash.js.map +1 -0
  137. package/dist/turn-recorder/index.d.ts +119 -0
  138. package/dist/turn-recorder/index.d.ts.map +1 -0
  139. package/dist/turn-recorder/index.js +326 -0
  140. package/dist/turn-recorder/index.js.map +1 -0
  141. package/dist/turn-recorder/types.d.ts +227 -0
  142. package/dist/turn-recorder/types.d.ts.map +1 -0
  143. package/dist/turn-recorder/types.js +20 -0
  144. package/dist/turn-recorder/types.js.map +1 -0
  145. package/dist/types.d.ts +35 -2
  146. package/dist/types.d.ts.map +1 -1
  147. package/package.json +2 -2
@@ -0,0 +1,326 @@
1
+ /**
2
+ * TurnRecorder — splits one LLM invocation into per-turn sub-effects so
3
+ * the durable engine can replay tool results and resume mid-stream after
4
+ * a chain-fallback model swap.
5
+ *
6
+ * Effect-name vocabulary (per the v2 ADR §2.1):
7
+ * - `turn:<N>:assistant-start` — request payload (recorded, NOT input-hash-guarded: see startTurn)
8
+ * - `turn:<N>:tool_use:<M>` — intent (name + parsed args)
9
+ * - `turn:<N>:tool_result:<M>` — neutral result; replay short-circuits the executor
10
+ * - `turn:<N>:assistant-end` — completed assistant text + provenance
11
+ *
12
+ * Surface note vs ADR §2.5: the ADR sketches recordToolUse / recordToolResult
13
+ * as two separate calls. That split has no clean replay-vs-execute boundary
14
+ * through the `effect(name, fn)` interface — once the runtime accepts a fn
15
+ * for `tool_result:N`, fn already either ran (live) or didn't (replay), and
16
+ * the caller can't tell from outside. So the implementation collapses to a
17
+ * single `runTool(turn, name, args, key, exec)` that owns the
18
+ * intent-record → execute-or-replay → result-record sequence atomically.
19
+ * Same on-disk shape; cleaner adapter code; ADR §2.5 to be amended in
20
+ * follow-up.
21
+ *
22
+ * NullTurnRecorder factory: pass a `createNullRuntime()`-built runtime to
23
+ * make every call a no-op pass-through with live execution. Adapters that
24
+ * haven't been ported through their own H-phase use this default, so the
25
+ * recorder calls compile and run today without behavior change.
26
+ */
27
+ const DEFAULT_TOOL_RESULT_CAP_BYTES = 65_536;
28
+ /**
29
+ * Resolve the persisted-tool-result cap. `ANVIL_DURABLE_MAX_TOOL_RESULT_BYTES`
30
+ * overrides the 64 KiB default; `0` disables the cap (persist the full result).
31
+ */
32
+ function toolResultPersistCapBytes() {
33
+ const raw = process.env.ANVIL_DURABLE_MAX_TOOL_RESULT_BYTES;
34
+ if (raw === undefined)
35
+ return DEFAULT_TOOL_RESULT_CAP_BYTES;
36
+ const n = Number(raw);
37
+ if (n === 0)
38
+ return Infinity; // explicit opt-out — persist full payloads
39
+ return Number.isFinite(n) && n > 0 ? n : DEFAULT_TOOL_RESULT_CAP_BYTES;
40
+ }
41
+ /**
42
+ * Cap the `content` of a NeutralToolResult for durable storage. Only string
43
+ * content past the cap is truncated (with a marker); the result keeps a valid
44
+ * NeutralToolResult shape so resume's message reconstruction still works. The
45
+ * live caller is unaffected — this runs only on the persisted copy via
46
+ * `EffectOptions.persistTransform`.
47
+ */
48
+ function capToolResultForPersist(result) {
49
+ const cap = toolResultPersistCapBytes();
50
+ if (cap === Infinity || !result || typeof result !== 'object')
51
+ return result;
52
+ const r = result;
53
+ if (typeof r.content === 'string' && r.content.length > cap) {
54
+ const omitted = r.content.length - cap;
55
+ return {
56
+ ...r,
57
+ content: r.content.slice(0, cap) +
58
+ `\n…[${omitted} bytes truncated for durable storage; full output was sent to the model live]`,
59
+ };
60
+ }
61
+ return result;
62
+ }
63
+ const DEFAULT_UUID = () => (globalThis.crypto?.randomUUID?.() ?? fallbackUuid());
64
+ function fallbackUuid() {
65
+ // Minimal v4 fallback for runtimes without globalThis.crypto.
66
+ const hex = (n) => n.toString(16).padStart(2, '0');
67
+ const bytes = new Uint8Array(16);
68
+ for (let i = 0; i < 16; i++)
69
+ bytes[i] = Math.floor(Math.random() * 256);
70
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
71
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
72
+ const h = Array.from(bytes, hex);
73
+ return `${h.slice(0, 4).join('')}-${h.slice(4, 6).join('')}-${h.slice(6, 8).join('')}-${h.slice(8, 10).join('')}-${h.slice(10, 16).join('')}`;
74
+ }
75
+ export class TurnRecorder {
76
+ deps;
77
+ /** Monotonic turn counter, scoped to this recorder (one per adapter.run).
78
+ * Seeded from deps.initialTurn so a sendInput-resume recorder continues
79
+ * past the prior adapter's turns (§2.5.1). */
80
+ turnCounter;
81
+ /** Per-turn tool_use index counter — bumped serially per call. */
82
+ toolIdxByTurn = new Map();
83
+ /** Per-turn uuid stash so flushPartial can echo the right one. */
84
+ turnUuids = new Map();
85
+ /** Per-turn (model, provider) stash from startTurn so endTurn can
86
+ * stamp the assistant-end payload — the H3 cost rollup prices each
87
+ * turn by its authoring model. */
88
+ turnMeta = new Map();
89
+ /** Per-repo effect-name scope (e.g. `service-a:`); '' for single-stage. */
90
+ prefix;
91
+ constructor(deps) {
92
+ this.deps = deps;
93
+ this.turnCounter = deps.initialTurn ?? 0;
94
+ this.prefix = deps.effectPrefix ?? '';
95
+ }
96
+ /** Scoped effect name for a turn sub-effect. */
97
+ key(name) {
98
+ return `${this.prefix}${name}`;
99
+ }
100
+ /**
101
+ * Open a new assistant turn. Records the request payload as an
102
+ * `assistant-start` effect; on replay, returns the recorded turn's
103
+ * full transcript so the adapter can skip the upstream call entirely.
104
+ */
105
+ async startTurn(req) {
106
+ const turn = this.turnCounter++;
107
+ const turnUuid = this.uuid();
108
+ this.turnUuids.set(turn, turnUuid);
109
+ this.toolIdxByTurn.set(turn, 0);
110
+ this.turnMeta.set(turn, { model: req.model, provider: req.provider });
111
+ // Record the start. On replay this is a no-op that returns the
112
+ // recorded payload — we don't use the return value here because
113
+ // the meaningful replay signal is whether `assistant-end` exists.
114
+ //
115
+ // Deliberately NO `idempotencyKey` (input-hash) guard on this effect.
116
+ // The turn's input (system prompt + messages) embeds live-queried,
117
+ // process-local context: the Knowledge Base graph and memory block are
118
+ // re-queried in a fresh process on resume (the "stable" prompt getters
119
+ // only memoise WITHIN one run, not across a restart), and a cross-vendor
120
+ // continuation legitimately swaps the model/prefill. So the input is NOT
121
+ // byte-stable across resume — and it doesn't need to be: when
122
+ // `assistant-end` exists the recorded transcript is replayed verbatim
123
+ // (the upstream call is skipped, so the rebuilt prompt is never sent);
124
+ // when it doesn't, the turn re-runs live (the whole point of resume).
125
+ // Hashing the input into a FATAL determinism guard turned that benign
126
+ // drift into a `DeterminismViolationError` and broke resume entirely.
127
+ // The effect-SEQUENCE guard (name + idx) still catches genuine step-logic
128
+ // divergence; `runTool` still pins tool args via its own idempotencyKey.
129
+ await this.deps.runtime.effect(this.key(`turn:${turn}:assistant-start`), async () => ({
130
+ turnUuid,
131
+ model: req.model,
132
+ provider: req.provider,
133
+ prefillTurnUuid: req.prefill?.turnUuid ?? null,
134
+ // §Tier 2: persist the phase's user prompt so a stateful resume can
135
+ // reconstruct prior turns WITH their prompts. Additive only — never
136
+ // re-read by replay-skip (which reads assistant-end).
137
+ userPrompt: req.userPrompt ?? null,
138
+ }), { smallResult: true });
139
+ // H3 replay-skip: if this turn's `assistant-end` is already in the
140
+ // durable log, the whole turn ran on a prior process. Reconstruct
141
+ // it so the adapter skips the upstream call and re-appends the
142
+ // recorded native history verbatim. `peekRecorded` is a pure read
143
+ // (no cursor/idx mutation) — the adapter still calls runTool/endTurn
144
+ // in order to advance the replay cursor over the recorded sub-effects.
145
+ const replayed = this.tryReconstructTurn(turn, turnUuid);
146
+ if (replayed)
147
+ return { turn, turnUuid, replayed };
148
+ return { turn, turnUuid };
149
+ }
150
+ /**
151
+ * Peek the durable log for a fully-recorded turn N. Returns the
152
+ * reconstructed `AssistantTurn` iff `turn:N:assistant-end` exists;
153
+ * undefined on the live path (peek unsupported, or end not yet
154
+ * recorded). Pure reads only — no effect cursor is advanced here.
155
+ */
156
+ tryReconstructTurn(turn, turnUuid) {
157
+ const peek = this.deps.runtime.peekRecorded?.bind(this.deps.runtime);
158
+ if (!peek)
159
+ return undefined;
160
+ const end = peek(this.key(`turn:${turn}:assistant-end`));
161
+ if (!end)
162
+ return undefined;
163
+ const toolUses = [];
164
+ const toolResults = [];
165
+ for (let m = 0;; m += 1) {
166
+ const tu = peek(this.key(`turn:${turn}:tool_use:${m}`));
167
+ if (!tu)
168
+ break;
169
+ const tr = peek(this.key(`turn:${turn}:tool_result:${m}`));
170
+ toolUses.push({
171
+ // `id` isn't in the tool_use payload; the matching tool_result
172
+ // carries `toolUseId` (the provider id). Used only for cosmetic
173
+ // re-emit on replay — runTool keys off (name, idx, idempotencyKey).
174
+ id: tr?.toolUseId ?? '',
175
+ name: tu.name ?? '',
176
+ arguments: tu.arguments ?? {},
177
+ idempotencyKey: tu.idempotencyKey ?? '',
178
+ });
179
+ if (tr)
180
+ toolResults.push(tr);
181
+ }
182
+ return {
183
+ turnUuid,
184
+ turn,
185
+ text: end.text ?? '',
186
+ stopReason: end.stopReason ?? 'end_turn',
187
+ usage: end.usage ?? { inputTokens: 0, outputTokens: 0 },
188
+ provenance: end.provenance ?? { segments: [] },
189
+ toolUses,
190
+ toolResults,
191
+ historyDelta: end.historyDelta ?? [],
192
+ };
193
+ }
194
+ /**
195
+ * Record a tool intent + execute (or replay) the tool. Combines what
196
+ * the ADR drafted as two calls into one atomic sequence. The
197
+ * `idempotencyKey` is hashed by the caller (or fed in pre-computed)
198
+ * and guards against args drift on replay.
199
+ *
200
+ * Replay behavior: the inner `tool_result:M` effect is replayed
201
+ * verbatim — `exec()` is NOT invoked. Side effects (file writes, bash
202
+ * runs) stay as the prior model left them.
203
+ */
204
+ async runTool(turn, name, args, idempotencyKey, exec) {
205
+ const idx = this.bumpToolIdx(turn);
206
+ await this.deps.runtime.effect(this.key(`turn:${turn}:tool_use:${idx}`), async () => ({ name, arguments: args, idempotencyKey }), { idempotencyKey, smallResult: true });
207
+ const result = await this.deps.runtime.effect(this.key(`turn:${turn}:tool_result:${idx}`), exec, {
208
+ // Cap the PERSISTED copy of large tool results (file dumps, bash
209
+ // stdout). A single agentic turn can fire many tool calls; persisting
210
+ // multi-MB payloads synchronously into SQLite blocks the event loop
211
+ // and is the dominant "LLM feels slow" cost. The live model still
212
+ // receives the full result — only the durable copy (replayed on
213
+ // resume) is truncated. Default cap 64 KiB; set
214
+ // ANVIL_DURABLE_MAX_TOOL_RESULT_BYTES=0 to disable.
215
+ persistTransform: (r) => capToolResultForPersist(r),
216
+ });
217
+ return result;
218
+ }
219
+ /**
220
+ * Close an assistant turn with the completed text, stop reason, usage,
221
+ * and provenance. Records `assistant-end`; on replay this is a no-op.
222
+ */
223
+ async endTurn(turn, text, stopReason, usage, provenance,
224
+ /**
225
+ * Provider-native messages appended to history this turn (assistant
226
+ * + tool-result messages). Recorded so crash-resume re-appends them
227
+ * byte-for-byte (§ AssistantTurn.historyDelta). Default [] for the
228
+ * terminal no-tool turn (nothing appended). Opaque to the recorder.
229
+ */
230
+ historyDelta = []) {
231
+ const meta = this.turnMeta.get(turn);
232
+ await this.deps.runtime.effect(this.key(`turn:${turn}:assistant-end`), async () => ({
233
+ text,
234
+ stopReason,
235
+ usage,
236
+ provenance,
237
+ historyDelta,
238
+ // model + provider stamp lets the H3 per-model cost rollup price
239
+ // this turn's tokens by its authoring model.
240
+ model: meta?.model,
241
+ provider: meta?.provider,
242
+ }), { smallResult: text.length < 4_000 });
243
+ }
244
+ /**
245
+ * Flush an in-progress assistant text to the partial sink. Called from
246
+ * an adapter's catch block BEFORE re-throwing UpstreamError. Does NOT
247
+ * go through `effect()` — partial events are not subject to the
248
+ * (name, idx) counter; they're durable signals scoped by turnUuid.
249
+ *
250
+ * Safe to call when text is empty (sink decides whether to bother).
251
+ * Safe to call multiple times for the same turn — the sink handles
252
+ * dedup via turnUuid + recordedAt.
253
+ */
254
+ flushPartial(turn, text, toolUsesEmitted, reason) {
255
+ const turnUuid = this.turnUuids.get(turn);
256
+ if (!turnUuid)
257
+ return; // startTurn was never called — nothing to flush.
258
+ const partial = {
259
+ runId: this.deps.runId,
260
+ stepId: this.deps.stepId,
261
+ turnUuid,
262
+ turn,
263
+ text,
264
+ toolUsesEmitted,
265
+ reason,
266
+ recordedAt: this.nowIso(),
267
+ };
268
+ try {
269
+ this.deps.partialSink(partial);
270
+ }
271
+ catch (err) {
272
+ // Sink should be a fire-and-forget; if it throws we still re-throw
273
+ // the upstream error — log to stderr so the failure is visible.
274
+ process.stderr.write(`[turn-recorder] partialSink threw: ${err instanceof Error ? err.message : String(err)}\n`);
275
+ }
276
+ }
277
+ // ─────────────────────────────────────────────────────────────────
278
+ // Internals
279
+ // ─────────────────────────────────────────────────────────────────
280
+ bumpToolIdx(turn) {
281
+ const next = this.toolIdxByTurn.get(turn) ?? 0;
282
+ this.toolIdxByTurn.set(turn, next + 1);
283
+ return next;
284
+ }
285
+ uuid() {
286
+ return this.deps.uuid?.() ?? DEFAULT_UUID();
287
+ }
288
+ nowIso() {
289
+ return this.deps.nowIso?.() ?? new Date().toISOString();
290
+ }
291
+ }
292
+ // ─────────────────────────────────────────────────────────────────────
293
+ // Null runtime — for adapters that haven't yet been wired to a real
294
+ // EffectRuntime. Calls execute live; nothing is recorded; replay is a
295
+ // no-op (since nothing is on disk). Lets us land the H1 recorder calls
296
+ // without forcing every adapter or every call-site to thread a real
297
+ // EffectRuntime through agent-core's bridge today.
298
+ // ─────────────────────────────────────────────────────────────────────
299
+ export function createNullEffectRuntime() {
300
+ return {
301
+ async effect(_name, fn) {
302
+ return await fn();
303
+ },
304
+ };
305
+ }
306
+ export function createNullPartialSink() {
307
+ return () => { };
308
+ }
309
+ /**
310
+ * Convenience factory: a TurnRecorder that does the right structural
311
+ * thing (records → executes → emits) but persists nothing. Adapters use
312
+ * this when their caller hasn't supplied a real runtime + sink. Identical
313
+ * observable behavior to today's adapters; opens the door for the H2+
314
+ * wiring to inject the real instances without touching adapter code
315
+ * again.
316
+ */
317
+ export function createNullTurnRecorder(opts) {
318
+ return new TurnRecorder({
319
+ runtime: createNullEffectRuntime(),
320
+ partialSink: createNullPartialSink(),
321
+ runId: opts?.runId ?? 'null-run',
322
+ stepId: opts?.stepId ?? 'null-step',
323
+ });
324
+ }
325
+ export * from './types.js';
326
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/turn-recorder/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,227 @@
1
+ /**
2
+ * TurnRecorder types — public shapes consumed by adapters that drive a
3
+ * multi-turn LLM tool loop and want their turns split into sub-effects
4
+ * so chain-fallback can resume mid-stream.
5
+ *
6
+ * Design notes:
7
+ * - agent-core does NOT depend on `@anvil/core-pipeline`. The
8
+ * `EffectRuntimeLike` interface here is the structural shape an
9
+ * adapter needs — core-pipeline's concrete `EffectRuntime` satisfies
10
+ * it, but agent-core never imports the class. This avoids a
11
+ * circular dep direction.
12
+ * - The recorder is constructed per `adapter.run()` call. It holds a
13
+ * monotonic `turn` counter scoped to the (stepId, sessionId) pair.
14
+ * - `flushPartial` is intentionally non-async and not routed through
15
+ * `effect()` — it MUST be safe to fire from a `catch` block that is
16
+ * about to re-throw an `UpstreamError`. The sink may queue the
17
+ * write durably under the hood; the recorder doesn't wait.
18
+ */
19
+ import type { ProviderName } from '../types.js';
20
+ /**
21
+ * Minimal effect-runtime shape the recorder relies on. Mirrors the
22
+ * `effect(name, fn, opts?)` method on core-pipeline's `EffectRuntime`.
23
+ * Anything else on the real class (now/uuid/random/sleep/waitForSignal)
24
+ * is irrelevant to the recorder.
25
+ *
26
+ * Replay semantics: when an effect with `(name, idx)` is already in the
27
+ * durable log, `fn` is NOT invoked — the recorded payload is returned.
28
+ * Recording side effects (`record*` methods) thus become no-ops on
29
+ * replay, which is exactly what we want.
30
+ */
31
+ export interface EffectRuntimeLike {
32
+ effect<T>(name: string, fn: () => Promise<T>, opts?: EffectInvokeOptions): Promise<T>;
33
+ /**
34
+ * H3 replay-skip seam: peek the recorded payload of a COMPLETED effect
35
+ * by name WITHOUT advancing the replay cursor or the per-step idx
36
+ * counter. Returns the recorded `effect:completed` payload if one
37
+ * exists in this step's log, else undefined. Lets `startTurn` detect
38
+ * that a turn was already fully recorded (its `assistant-end` exists)
39
+ * so the adapter can skip the upstream call entirely on crash-resume.
40
+ *
41
+ * Optional: the NullEffectRuntime omits it, so non-durable callers
42
+ * (single-shot / no DurableStore) always run live (peek → undefined →
43
+ * no replay). The concrete core-pipeline `EffectRuntime` implements it
44
+ * as a pure read over the pre-loaded recorded-effect set.
45
+ */
46
+ peekRecorded?<T = unknown>(name: string): T | undefined;
47
+ }
48
+ export interface EffectInvokeOptions {
49
+ /** Stable input hash; mismatches on replay throw a determinism error. */
50
+ idempotencyKey?: string;
51
+ /** Hint that the payload is small enough to inline; runtime may skip blobs. */
52
+ smallResult?: boolean;
53
+ /**
54
+ * Transform applied to the result before it is persisted to the durable
55
+ * log; the live caller still gets the full result. Used to cap large
56
+ * tool-result payloads so a turn doesn't block the loop serialising them.
57
+ * Mirrors `EffectOptions.persistTransform` in core-pipeline.
58
+ */
59
+ persistTransform?: (result: unknown) => unknown;
60
+ }
61
+ export interface RecordedToolUse {
62
+ /** Provider-supplied tool_use id (e.g. OpenAI's `tool_call_id`). */
63
+ id: string;
64
+ name: string;
65
+ /** Parsed JSON arguments. UNPARSED args MUST NOT be recorded (§2.1.1). */
66
+ arguments: Record<string, unknown>;
67
+ /** sha256(name || JSON.stringify(arguments)). */
68
+ idempotencyKey: string;
69
+ }
70
+ export interface NeutralToolResult {
71
+ toolUseId: string;
72
+ toolName: string;
73
+ ok: boolean;
74
+ content: string | unknown;
75
+ durationMs?: number;
76
+ }
77
+ export interface ProvenanceSegment {
78
+ model: string;
79
+ provider: ProviderName;
80
+ range: [number, number];
81
+ source: 'live' | 'prefill';
82
+ }
83
+ export interface Provenance {
84
+ segments: ProvenanceSegment[];
85
+ }
86
+ export interface TurnTokenUsage {
87
+ inputTokens: number;
88
+ outputTokens: number;
89
+ cacheReadTokens?: number;
90
+ cacheWriteTokens?: number;
91
+ /** Subset of inputTokens carved out for §2.6 prefill-reinjection. */
92
+ prefilledInputTokens?: number;
93
+ }
94
+ export interface PrefillToolUse {
95
+ id: string;
96
+ name: string;
97
+ input: unknown;
98
+ result: NeutralToolResult;
99
+ producedBy: ProviderName;
100
+ }
101
+ export interface Prefill {
102
+ turnUuid: string;
103
+ text: string;
104
+ toolUses: PrefillToolUse[];
105
+ sourceProvider: ProviderName;
106
+ /** Model id that authored the partial — drives §2.7 provenance
107
+ * segment[0].model on the resuming turn. Optional: omitted by
108
+ * pre-H3 callers / when the source model is unknown. */
109
+ sourceModel?: string;
110
+ /** Tokens (as billed by sourceProvider) for `text`; H3 cost accounting. */
111
+ sourceTokens: number;
112
+ }
113
+ /**
114
+ * §Tier 2 — one COMPLETED prior turn of a stateful session, reconstructed
115
+ * from the durable log so a cross-model (or merely non-claude) resume can
116
+ * re-present the full conversation in the target provider's wire format.
117
+ * Unlike `Prefill` (the in-progress burned turn), these are finished turns.
118
+ *
119
+ * `userPrompt` is present on the FIRST turn of each phase (the new user
120
+ * message that opened that phase); it's undefined on tool-loop continuation
121
+ * turns within a phase, and on pre-Tier-2 logs that never recorded it.
122
+ */
123
+ export interface PrefillTurn {
124
+ userPrompt?: string;
125
+ text: string;
126
+ toolUses: PrefillToolUse[];
127
+ /**
128
+ * Authoring provider of this completed turn — informational / provenance.
129
+ * `materializePriorTurns` flattens all turns to the resuming target's wire
130
+ * format and does NOT run per-turn `stripForTarget`: completed turns are
131
+ * rebuilt from neutral fields (text + `NeutralToolResult`) that carry no
132
+ * vendor keys (no `reasoning_details` / `cache_control` / `prompt_cache_key`),
133
+ * so there is nothing to strip. (Contrast the `prefill` path, which does
134
+ * `stripForTarget` because it can re-present source-shaped messages.)
135
+ */
136
+ producedBy: ProviderName;
137
+ }
138
+ export type PartialReason = 'upstream' | 'abort' | 'timeout';
139
+ export interface AssistantPartial {
140
+ runId: string;
141
+ stepId: string;
142
+ turnUuid: string;
143
+ turn: number;
144
+ text: string;
145
+ /** Count of FULLY-PARSED tool_use blocks emitted before the throw. */
146
+ toolUsesEmitted: number;
147
+ reason: PartialReason;
148
+ recordedAt: string;
149
+ }
150
+ export type PartialSink = (partial: AssistantPartial) => void;
151
+ export interface AssistantStartRequest {
152
+ model: string;
153
+ provider: ProviderName;
154
+ system?: string;
155
+ /** Raw conversation messages sent to the upstream. Recorded for provenance. */
156
+ messages: unknown;
157
+ prefill?: Prefill;
158
+ /**
159
+ * §Tier 2 — the new user message that opened this phase. Recorded in the
160
+ * assistant-start payload so `reconstructSessionHistory` can re-present
161
+ * prior turns with their prompts on a stateful (non-claude) session resume.
162
+ */
163
+ userPrompt?: string;
164
+ }
165
+ export interface AssistantTurn {
166
+ turnUuid: string;
167
+ turn: number;
168
+ text: string;
169
+ stopReason: string;
170
+ usage: TurnTokenUsage;
171
+ provenance: Provenance;
172
+ toolUses: RecordedToolUse[];
173
+ toolResults: NeutralToolResult[];
174
+ /**
175
+ * H3 replay determinism: the EXACT provider-native messages the
176
+ * adapter appended to its conversation history during this turn
177
+ * (assistant message + tool-result messages), as opaque objects. On
178
+ * crash-resume the adapter re-appends these verbatim instead of
179
+ * reconstructing them from the neutral `toolUses`/`text` — raw vs
180
+ * canonical `tool_calls` arguments and vendor `reasoning_details` echoes
181
+ * would otherwise drift, leaving the FIRST live turn past the replay
182
+ * frontier with a malformed upstream request (e.g. unpaired tool_calls,
183
+ * or "reasoning_content is missing"). Empty for a terminal (no-tool)
184
+ * turn that appended nothing before breaking.
185
+ */
186
+ historyDelta: unknown[];
187
+ }
188
+ export interface TurnRecorderDeps {
189
+ /** EffectRuntime to record sub-effects against. */
190
+ runtime: EffectRuntimeLike;
191
+ /** Fire-and-forget partial sink; called from adapter catch blocks. */
192
+ partialSink: PartialSink;
193
+ /** Stable runId — propagated into AssistantPartial rows. */
194
+ runId: string;
195
+ /** Stable stepId — scopes the recorder to one pipeline Step. */
196
+ stepId: string;
197
+ /** v4 uuid factory — overridable for tests. Defaults to crypto.randomUUID. */
198
+ uuid?: () => string;
199
+ /** Wall-clock helper — overridable for tests. Defaults to () => new Date().toISOString(). */
200
+ nowIso?: () => string;
201
+ /**
202
+ * Effect-name prefix (v2 ADR §2.4 per-repo scoping). When a step fans
203
+ * out across repos in parallel, every repo's recorder writes effects
204
+ * under the SAME durable step id, so a bare `turn:0:assistant-start`
205
+ * from two repos would collide. The caller passes a per-repo prefix
206
+ * (e.g. `service-a:`) so the recorded keys become
207
+ * `service-a:turn:0:assistant-start` — unique per repo AND matched by
208
+ * the durable engine's per-repo effect filter (which keys on delimited
209
+ * scope tokens). Empty ('') for single-stage paths. Applied uniformly
210
+ * to every effect name AND to the `peekRecorded` replay lookup.
211
+ */
212
+ effectPrefix?: string;
213
+ /**
214
+ * Seed for the per-recorder turn counter (v2 ADR §2.5.1). When an
215
+ * `AgentProcess.sendInput()` resume spawns a NEW adapter (hence a new
216
+ * recorder) for a session whose prior adapter already recorded turns
217
+ * 0..N, the new recorder MUST continue at N+1 so `(stepId, turnUuid)`
218
+ * stays globally unique and replay's effect-idx matching doesn't
219
+ * collide. The recorder itself cannot read the durable log (it only
220
+ * holds a structural `EffectRuntimeLike`), so the CALLER computes the
221
+ * highest completed turn for (runId, stepId) and passes it here; the
222
+ * recorder seeds `turnCounter = initialTurn`. Defaults to 0 (fresh
223
+ * session / no prior turns).
224
+ */
225
+ initialTurn?: number;
226
+ }
227
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/turn-recorder/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * TurnRecorder types — public shapes consumed by adapters that drive a
3
+ * multi-turn LLM tool loop and want their turns split into sub-effects
4
+ * so chain-fallback can resume mid-stream.
5
+ *
6
+ * Design notes:
7
+ * - agent-core does NOT depend on `@anvil/core-pipeline`. The
8
+ * `EffectRuntimeLike` interface here is the structural shape an
9
+ * adapter needs — core-pipeline's concrete `EffectRuntime` satisfies
10
+ * it, but agent-core never imports the class. This avoids a
11
+ * circular dep direction.
12
+ * - The recorder is constructed per `adapter.run()` call. It holds a
13
+ * monotonic `turn` counter scoped to the (stepId, sessionId) pair.
14
+ * - `flushPartial` is intentionally non-async and not routed through
15
+ * `effect()` — it MUST be safe to fire from a `catch` block that is
16
+ * about to re-throw an `UpstreamError`. The sink may queue the
17
+ * write durably under the hood; the recorder doesn't wait.
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/turn-recorder/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
package/dist/types.d.ts CHANGED
@@ -129,8 +129,13 @@ export interface LanguageModel {
129
129
  version?: string;
130
130
  error?: string;
131
131
  }>;
132
- /** Streaming surface — yields events as the provider produces them. */
133
- invokeStream(opts: LanguageModelInvokeOptions): AsyncIterable<StreamEvent>;
132
+ /**
133
+ * Streaming surface — yields events as the provider produces them and
134
+ * RETURNS the assembled `InvokeResult` (cost/usage/finish) on completion,
135
+ * so a streaming consumer (the router's chain walk) gets the same outcome
136
+ * data a single-shot `invoke()` would, without re-running the call.
137
+ */
138
+ invokeStream(opts: LanguageModelInvokeOptions): AsyncGenerator<StreamEvent, InvokeResult>;
134
139
  /** Single-shot surface — drains the stream and returns the final block. */
135
140
  invoke(opts: LanguageModelInvokeOptions): Promise<InvokeResult>;
136
141
  }
@@ -189,6 +194,34 @@ export interface ModelAdapterConfig {
189
194
  * ADR §C5.
190
195
  */
191
196
  mcpConfigPath?: string;
197
+ /**
198
+ * Turn-level durable recorder (v2 ADR §2.5). When supplied by the
199
+ * caller, the adapter splits its LLM invocation into per-turn
200
+ * sub-effects (`assistant-start` / `tool_use:N` / `tool_result:N` /
201
+ * `assistant-end`) recorded against the caller's `EffectRuntime`.
202
+ * When absent, adapters construct a `NullTurnRecorder` internally —
203
+ * structural calls still happen so the code path is the same, but
204
+ * nothing is persisted. Recording is implemented for every adapter
205
+ * except `gemini-cli` (a CLI subprocess with no token-level stream),
206
+ * which always no-ops the recorder.
207
+ */
208
+ turnRecorder?: import('./turn-recorder/index.js').TurnRecorder;
209
+ /**
210
+ * Prefill from a prior model that burned mid-stream (v2 ADR §2.3).
211
+ * When present, the adapter materializes the trailing-assistant +
212
+ * recorded tool-result history into its provider's wire format and
213
+ * continues from the exact character where the prior model stopped.
214
+ */
215
+ prefill?: import('./turn-recorder/index.js').Prefill;
216
+ /**
217
+ * §Tier 2 — COMPLETED prior turns of a stateful session, reconstructed
218
+ * from the durable log. Spliced (materialized into this provider's wire
219
+ * format) BEFORE the new user message so a non-claude session resume
220
+ * re-presents the full conversation instead of starting fresh. Claude
221
+ * uses native `--resume` and is gated out at the bridge. Empty/undefined
222
+ * → unchanged single-turn behavior.
223
+ */
224
+ priorMessages?: import('./turn-recorder/index.js').PrefillTurn[];
192
225
  }
193
226
  export interface ToolExecutorLike {
194
227
  listSchemas(): ToolSchema[];