@deepstrike/sdk 0.2.60 → 0.2.62

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 (40) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.js +1 -0
  3. package/dist/kernel.d.ts +4 -0
  4. package/dist/os/public.d.ts +1 -1
  5. package/dist/os/public.js +1 -1
  6. package/dist/providers/anthropic-adapter.d.ts +13 -1
  7. package/dist/providers/anthropic-adapter.js +106 -20
  8. package/dist/providers/anthropic.d.ts +1 -0
  9. package/dist/providers/anthropic.js +10 -3
  10. package/dist/providers/base.d.ts +2 -2
  11. package/dist/providers/base.js +7 -5
  12. package/dist/providers/catalog.js +5 -20
  13. package/dist/providers/factories.js +17 -7
  14. package/dist/providers/gemini-adapter.js +6 -14
  15. package/dist/providers/model-registry.d.ts +22 -1
  16. package/dist/providers/model-registry.js +72 -8
  17. package/dist/providers/ollama-adapter.js +2 -2
  18. package/dist/providers/openai-chat.js +4 -6
  19. package/dist/providers/openai-responses-adapter.js +12 -10
  20. package/dist/providers/openai-responses.d.ts +6 -1
  21. package/dist/providers/openai-responses.js +33 -0
  22. package/dist/providers/protocol-adapter.d.ts +6 -1
  23. package/dist/providers/protocol-adapter.js +6 -2
  24. package/dist/providers/provider-error.js +5 -1
  25. package/dist/providers/request-plan.d.ts +4 -1
  26. package/dist/providers/request-plan.js +25 -1
  27. package/dist/providers/usage-normalizer.js +58 -15
  28. package/dist/runtime/canonical-kernel-step.d.ts +2 -2
  29. package/dist/runtime/canonical-kernel-step.js +180 -232
  30. package/dist/runtime/kernel-doctor.d.ts +36 -0
  31. package/dist/runtime/kernel-doctor.js +60 -0
  32. package/dist/runtime/kernel-step.d.ts +11 -4
  33. package/dist/runtime/kernel-step.js +20 -0
  34. package/dist/runtime/provider-replay.d.ts +4 -0
  35. package/dist/runtime/provider-replay.js +15 -1
  36. package/dist/runtime/runner.d.ts +6 -5
  37. package/dist/runtime/runner.js +103 -43
  38. package/dist/runtime/session-log.d.ts +12 -0
  39. package/dist/types.d.ts +8 -5
  40. package/package.json +3 -3
@@ -41,239 +41,186 @@ export function canonicalUnsupportedEffectResolution(effectId, effectKind) {
41
41
  };
42
42
  }
43
43
  /** The only ABI-v3 planned-step → Node host-action projection. */
44
- export function canonicalActionFromPlannedStep(plannedStep) {
45
- if (plannedStep.disposition.kind === "terminal") {
46
- const terminal = plannedStep.disposition.terminal;
47
- const usage = asObject(terminal.usage);
48
- let termination = String(terminal.kind ?? "failed");
49
- let turnsUsed = Number(usage.turns ?? 0);
50
- if (terminal.kind === "agent") {
51
- const result = asObject(terminal.result);
52
- termination = String(result.termination ?? "completed");
53
- turnsUsed = Number(result.turns_used ?? turnsUsed);
54
- const finalMessage = asObject(result.final_message);
55
- const pace = asObject(result.pace_decision);
56
- if (Object.keys(finalMessage).length > 0) {
57
- return {
58
- kind: "done",
59
- effectId: "",
60
- result: {
61
- termination,
62
- turnsUsed,
63
- totalTokensUsed: totalUsageTokens(terminal),
64
- finalMessage: {
65
- role: String(finalMessage.role ?? "assistant"),
66
- content: String(finalMessage.content ?? ""),
67
- toolCalls: (Array.isArray(finalMessage.tool_calls) ? finalMessage.tool_calls : [])
68
- .map(value => {
69
- const call = asObject(value);
70
- return {
71
- id: String(call.call_id ?? ""),
72
- name: String(call.name ?? ""),
73
- arguments: JSON.stringify(call.arguments ?? {}),
74
- };
75
- }),
76
- },
77
- ...(Object.keys(pace).length > 0
78
- ? {
79
- paceDecision: {
80
- action: String(pace.action ?? "stop"),
81
- ...(pace.delay_ms !== undefined ? { delayMs: Number(pace.delay_ms) } : {}),
82
- reason: String(pace.reason ?? ""),
83
- ...(pace.coerced_from ? { coercedFrom: String(pace.coerced_from) } : {}),
84
- },
85
- }
86
- : {}),
87
- },
88
- };
89
- }
90
- }
91
- else if (terminal.kind === "workflow") {
92
- const outcome = asObject(terminal.outcome);
93
- termination = String(outcome.status ?? "completed");
94
- }
95
- else if (terminal.kind === "cancelled") {
96
- termination = String(terminal.reason ?? "cancelled");
97
- }
98
- else if (terminal.kind === "failed") {
99
- const failure = asObject(terminal.failure);
100
- termination = failure.code === "provider_recovery_exhausted"
101
- ? "context_overflow"
102
- : "error";
103
- }
44
+ function canonicalDoneFromTerminal(terminal) {
45
+ const usage = asObject(terminal.usage);
46
+ let termination = String(terminal.kind ?? "failed");
47
+ let turnsUsed = Number(usage.turns ?? 0);
48
+ if (terminal.kind === "agent") {
49
+ const result = asObject(terminal.result);
50
+ termination = String(result.termination ?? "completed");
51
+ turnsUsed = Number(result.turns_used ?? turnsUsed);
52
+ const finalMessage = asObject(result.final_message);
53
+ const pace = asObject(result.pace_decision);
104
54
  return {
105
- kind: "done",
106
- effectId: "",
55
+ kind: "done", effectId: "",
107
56
  result: {
108
- termination,
109
- turnsUsed,
110
- totalTokensUsed: totalUsageTokens(terminal),
57
+ termination, turnsUsed, totalTokensUsed: totalUsageTokens(terminal),
58
+ ...(Object.keys(finalMessage).length > 0 ? { finalMessage: {
59
+ role: String(finalMessage.role ?? "assistant"),
60
+ content: String(finalMessage.content ?? ""),
61
+ toolCalls: (Array.isArray(finalMessage.tool_calls) ? finalMessage.tool_calls : []).map(value => {
62
+ const call = asObject(value);
63
+ return { id: String(call.call_id ?? ""), name: String(call.name ?? ""), arguments: JSON.stringify(call.arguments ?? {}) };
64
+ }),
65
+ } } : {}),
66
+ ...(Object.keys(pace).length > 0 ? { paceDecision: {
67
+ action: String(pace.action ?? "stop"),
68
+ ...(pace.delay_ms !== undefined ? { delayMs: Number(pace.delay_ms) } : {}),
69
+ reason: String(pace.reason ?? ""),
70
+ ...(pace.coerced_from ? { coercedFrom: String(pace.coerced_from) } : {}),
71
+ } } : {}),
111
72
  },
112
73
  };
113
74
  }
114
- const published = plannedStep.disposition.effects ?? [];
115
- if (published.length === 0)
75
+ if (terminal.kind === "workflow")
76
+ termination = String(asObject(terminal.outcome).status ?? "completed");
77
+ if (terminal.kind === "cancelled")
78
+ termination = String(terminal.reason ?? "cancelled");
79
+ if (terminal.kind === "failed")
80
+ termination = asObject(terminal.failure).code === "provider_recovery_exhausted" ? "context_overflow" : "error";
81
+ return { kind: "done", effectId: "", result: { termination, turnsUsed, totalTokensUsed: totalUsageTokens(terminal) } };
82
+ }
83
+ /** Adapt the additive core CurrentProjection JSON into the existing Node action surface. */
84
+ export function canonicalActionFromProjectionJson(raw) {
85
+ const projection = asObject(JSON.parse(raw));
86
+ const state = String(projection.state ?? "idle");
87
+ if (state === "idle")
116
88
  return null;
117
- if (published.length !== 1) {
118
- throw new Error(`Node runner expects one canonical effect at a time, received ${published.length}`);
89
+ if (state === "terminal") {
90
+ return canonicalDoneFromTerminal(asObject(projection.action));
119
91
  }
120
- const envelope = asObject(published[0]);
121
- const effectId = String(envelope.effect_id ?? "");
122
- const effect = asObject(envelope.effect);
123
- if (!effectId)
124
- throw new Error("canonical effect is missing effect_id");
125
- switch (effect.kind) {
126
- case "call_provider":
127
- return {
128
- kind: "call_provider",
129
- effectId,
130
- context: renderedContextToSdk(asObject(effect.context)),
131
- tools: (Array.isArray(effect.tools) ? effect.tools : []).map(raw => {
132
- const tool = asObject(raw);
133
- return {
134
- name: String(tool.name ?? ""),
135
- description: String(tool.description ?? ""),
136
- parameters: JSON.stringify(tool.parameters ?? {}),
137
- };
138
- }),
139
- };
140
- case "execute_tools":
141
- return {
142
- kind: "execute_tool",
143
- effectId,
144
- calls: (Array.isArray(effect.calls) ? effect.calls : []).map(raw => {
145
- const call = asObject(raw);
146
- return {
147
- id: String(call.call_id ?? ""),
148
- name: String(call.name ?? ""),
149
- arguments: JSON.stringify(call.arguments ?? {}),
150
- };
151
- }),
152
- };
153
- case "request_approval":
154
- return {
155
- kind: "request_approval",
156
- effectId,
157
- requests: (Array.isArray(effect.requests) ? effect.requests : []).map(raw => {
158
- const request = asObject(raw);
159
- return {
160
- callId: String(request.call_id ?? ""),
161
- tool: String(request.tool_name ?? ""),
162
- arguments: JSON.stringify(request.arguments ?? {}),
163
- reason: String(request.reason ?? ""),
164
- };
165
- }),
166
- };
167
- case "spawn_tasks":
168
- return {
169
- kind: "spawn_workflow",
170
- effectId,
171
- nodes: (Array.isArray(effect.tasks) ? effect.tasks : []).map(raw => {
172
- const task = asObject(raw);
173
- const spec = asObject(task.spec);
174
- return {
175
- agent_id: String(task.task_id ?? ""),
176
- task_id: String(task.task_id ?? ""),
177
- attempt_id: String(task.attempt_id ?? ""),
178
- launch_token: String(task.launch_token ?? ""),
179
- node_id: String(task.node_id ?? ""),
180
- goal: String(spec.goal ?? ""),
181
- role: String(spec.role ?? "custom"),
182
- isolation: String(spec.isolation ?? "shared"),
183
- context_inheritance: String(spec.context_inheritance ?? "none"),
184
- ...(spec.metadata && typeof spec.metadata === "object"
185
- ? asObject(spec.metadata)
186
- : {}),
187
- };
188
- }),
189
- ...(effect.budget ? { budget: asObject(effect.budget) } : {}),
190
- };
191
- case "preempt_tasks": {
192
- const attempts = (Array.isArray(effect.attempts) ? effect.attempts : []).map(raw => {
193
- const attempt = asObject(raw);
92
+ const action = asObject(projection.action);
93
+ const payload = asObject(action.payload);
94
+ const effectId = String(action.effect_id ?? "");
95
+ if (action.kind === "query_memory") {
96
+ return {
97
+ kind: "query_memory",
98
+ effectId,
99
+ query: asObject(payload.query),
100
+ requestedK: Number(payload.requested_k ?? 0),
101
+ };
102
+ }
103
+ if (action.kind === "execute_tools") {
104
+ return {
105
+ kind: "execute_tool",
106
+ effectId,
107
+ calls: (Array.isArray(payload.calls) ? payload.calls : []).map(raw => {
108
+ const call = asObject(raw);
194
109
  return {
195
- task_id: String(attempt.task_id ?? ""),
196
- attempt_id: String(attempt.attempt_id ?? ""),
110
+ id: String(call.call_id ?? ""),
111
+ name: String(call.name ?? ""),
112
+ arguments: JSON.stringify(call.arguments ?? {}),
197
113
  };
198
- });
199
- return {
200
- kind: "preempt_sub_agents",
201
- effectId,
202
- attempts,
203
- agentIds: attempts.map(attempt => attempt.task_id),
204
- reason: String(effect.reason ?? ""),
205
- };
206
- }
207
- case "persist_memory":
208
- return {
209
- kind: "persist_memory",
210
- effectId,
211
- memory: asObject(effect.memory),
212
- };
213
- case "query_memory":
214
- return {
215
- kind: "query_memory",
216
- effectId,
217
- query: asObject(effect.query),
218
- requestedK: Number(effect.requested_k ?? 0),
219
- };
220
- case "archive_page_out": {
221
- const payload = asObject(effect.payload);
222
- let archived = [];
223
- try {
224
- const decoded = JSON.parse(String(payload.content ?? ""));
225
- if (Array.isArray(decoded)) {
226
- archived = decoded.map(value => kernelMessageToSdk(asObject(value)));
227
- }
228
- }
229
- catch {
230
- // Persistence still uses the opaque body and digest. Only optional presentation-side
231
- // summarization is skipped if the archived message batch cannot be decoded.
232
- }
233
- const compressed = (plannedStep.observations ?? [])
234
- .find(observation => observation.kind === "compressed");
235
- const pressureAction = compressed ? String(compressed.action ?? "") : "";
236
- return {
237
- kind: "archive_page_out",
238
- effectId,
239
- handleId: String(effect.handle_id ?? ""),
240
- payload,
241
- archived,
242
- ...(pressureAction ? { action: pressureAction } : {}),
243
- ...(compressed?.summary ? { summary: String(compressed.summary) } : {}),
244
- ...(pressureAction
245
- ? {
246
- tier: ["context_collapse", "auto_compact"].includes(pressureAction)
247
- ? "semantic"
248
- : "durable",
249
- }
250
- : {}),
251
- };
114
+ }),
115
+ };
116
+ }
117
+ if (action.kind === "call_provider") {
118
+ const context = asObject(payload.context);
119
+ return {
120
+ kind: "call_provider",
121
+ effectId,
122
+ context: renderedContextToSdk(context),
123
+ tools: (Array.isArray(payload.tools) ? payload.tools : []).map(raw => {
124
+ const tool = asObject(raw);
125
+ return {
126
+ name: String(tool.name ?? ""),
127
+ description: String(tool.description ?? ""),
128
+ parameters: JSON.stringify(tool.parameters ?? {}),
129
+ };
130
+ }),
131
+ };
132
+ }
133
+ if (action.kind === "request_approval") {
134
+ return {
135
+ kind: "request_approval",
136
+ effectId,
137
+ requests: (Array.isArray(payload.requests) ? payload.requests : []).map(raw => {
138
+ const request = asObject(raw);
139
+ return {
140
+ callId: String(request.call_id ?? ""),
141
+ tool: String(request.tool_name ?? ""),
142
+ arguments: JSON.stringify(request.arguments ?? {}),
143
+ reason: String(request.reason ?? ""),
144
+ };
145
+ }),
146
+ };
147
+ }
148
+ if (action.kind === "load_payload") {
149
+ return {
150
+ kind: "load_payload",
151
+ effectId,
152
+ handleId: String(payload.handle_id ?? ""),
153
+ payloadRef: String(payload.payload_ref ?? ""),
154
+ };
155
+ }
156
+ if (action.kind === "evaluate_milestone") {
157
+ const request = asObject(payload.request);
158
+ return {
159
+ kind: "evaluate_milestone",
160
+ effectId,
161
+ phaseId: String(request.phase_id ?? ""),
162
+ criteria: [],
163
+ requiredEvidence: [],
164
+ };
165
+ }
166
+ if (action.kind === "spawn_tasks") {
167
+ return {
168
+ kind: "spawn_workflow",
169
+ effectId,
170
+ nodes: (Array.isArray(payload.tasks) ? payload.tasks : []).map(raw => {
171
+ const task = asObject(raw);
172
+ const spec = asObject(task.spec);
173
+ return {
174
+ agent_id: String(task.task_id ?? ""), task_id: String(task.task_id ?? ""),
175
+ attempt_id: String(task.attempt_id ?? ""), launch_token: String(task.launch_token ?? ""),
176
+ node_id: String(task.node_id ?? ""), goal: String(spec.goal ?? ""),
177
+ role: String(spec.role ?? "custom"), isolation: String(spec.isolation ?? "shared"),
178
+ context_inheritance: String(spec.context_inheritance ?? "none"),
179
+ ...(spec.metadata && typeof spec.metadata === "object" ? asObject(spec.metadata) : {}),
180
+ };
181
+ }),
182
+ ...(payload.budget ? { budget: asObject(payload.budget) } : {}),
183
+ };
184
+ }
185
+ if (action.kind === "preempt_tasks") {
186
+ const attempts = (Array.isArray(payload.attempts) ? payload.attempts : []).map(raw => {
187
+ const attempt = asObject(raw);
188
+ return { task_id: String(attempt.task_id ?? ""), attempt_id: String(attempt.attempt_id ?? "") };
189
+ });
190
+ return { kind: "preempt_sub_agents", effectId, attempts, agentIds: attempts.map(a => a.task_id), reason: String(payload.reason ?? "") };
191
+ }
192
+ if (action.kind === "persist_memory") {
193
+ return { kind: "persist_memory", effectId, memory: asObject(payload.memory) };
194
+ }
195
+ if (action.kind === "archive_page_out") {
196
+ const archivePayload = asObject(payload.payload);
197
+ let archived = [];
198
+ try {
199
+ const decoded = JSON.parse(String(archivePayload.content ?? ""));
200
+ if (Array.isArray(decoded))
201
+ archived = decoded.map(value => kernelMessageToSdk(asObject(value)));
252
202
  }
253
- case "load_payload":
254
- return {
255
- kind: "load_payload",
256
- effectId,
257
- handleId: String(effect.handle_id ?? ""),
258
- payloadRef: String(effect.payload_ref ?? ""),
259
- };
260
- case "evaluate_milestone": {
261
- const request = asObject(effect.request);
262
- return {
263
- kind: "evaluate_milestone",
264
- effectId,
265
- phaseId: String(request.phase_id ?? ""),
266
- criteria: [],
267
- requiredEvidence: [],
268
- };
203
+ catch {
204
+ // The opaque archive payload remains authoritative even when presentation decoding fails.
269
205
  }
270
- default:
271
- return {
272
- kind: "unsupported_effect",
273
- effectId,
274
- effectKind: String(effect.kind),
275
- };
206
+ return {
207
+ kind: "archive_page_out",
208
+ effectId,
209
+ handleId: String(payload.handle_id ?? ""),
210
+ payload: {
211
+ content: String(archivePayload.content ?? ""),
212
+ digest: String(archivePayload.digest ?? ""),
213
+ original_size: String(archivePayload.original_size ?? "0"),
214
+ ...(archivePayload.preview ? { preview: String(archivePayload.preview) } : {}),
215
+ },
216
+ archived,
217
+ };
276
218
  }
219
+ return {
220
+ kind: "unsupported_effect",
221
+ effectId,
222
+ effectKind: String(action.kind ?? ""),
223
+ };
277
224
  }
278
225
  export class CanonicalKernelRejectedError extends Error {
279
226
  fault;
@@ -1178,6 +1125,16 @@ export class CanonicalRunnerRuntime {
1178
1125
  throw new Error("canonical observation is missing kind");
1179
1126
  this.hostObservations.push({ ...raw, kind });
1180
1127
  }
1128
+ // §7.11 · a committed step that publishes effects is a fact worth recording in the
1129
+ // host event log: the journal stores only a digest of the step, so this manifest is
1130
+ // what makes the published effect ids + kinds recoverable post-hoc without replaying.
1131
+ const published = JSON.parse(this.host.kernel.publishedEffectsManifestJson(JSON.stringify(transition.plannedStep)));
1132
+ if (published.length > 0) {
1133
+ this.hostObservations.push({
1134
+ kind: "step_published_effects",
1135
+ effects: published,
1136
+ });
1137
+ }
1181
1138
  if (transition.checkpointAdvice) {
1182
1139
  this.hostObservations.push({
1183
1140
  kind: "checkpoint_advised",
@@ -1191,7 +1148,7 @@ export class CanonicalRunnerRuntime {
1191
1148
  });
1192
1149
  }
1193
1150
  }
1194
- this.lastAction = canonicalActionFromPlannedStep(transition.plannedStep);
1151
+ this.lastAction = canonicalActionFromProjectionJson(this.host.kernel.projectPlannedStepJson(JSON.stringify(transition.plannedStep)));
1195
1152
  }
1196
1153
  if (this.lastAction?.kind !== "unsupported_effect")
1197
1154
  return this.lastAction;
@@ -1199,16 +1156,7 @@ export class CanonicalRunnerRuntime {
1199
1156
  }
1200
1157
  }
1201
1158
  currentAction() {
1202
- const terminal = this.host.kernel.terminalJson();
1203
- if (terminal) {
1204
- return canonicalActionFromPlannedStep({
1205
- disposition: { kind: "terminal", terminal: JSON.parse(terminal) },
1206
- });
1207
- }
1208
- const effects = this.pendingEffects();
1209
- return canonicalActionFromPlannedStep({
1210
- disposition: { kind: "effects", effects },
1211
- });
1159
+ return canonicalActionFromProjectionJson(this.host.kernel.currentProjectionJson());
1212
1160
  }
1213
1161
  pendingEffects() {
1214
1162
  return JSON.parse(this.host.kernel.pendingEffectsJson());
@@ -0,0 +1,36 @@
1
+ import type { CanonicalKernelInstance } from "../kernel.js";
2
+ import type { KernelJournal } from "./kernel-journal.js";
3
+ /**
4
+ * What a journal inspection knows: whether the operation restores, and — when it does not — which
5
+ * record is the first this binary cannot replay. The effect manifest (which effects each step
6
+ * published) lives in the host event log via the `step_published_effects` observation, not here:
7
+ * a journal stores only each record's input and digests, never the step it produced (§8.1).
8
+ */
9
+ export interface KernelJournalDiagnosis {
10
+ operationId: string;
11
+ recordCount: number;
12
+ /** True when `restore()` replayed the whole journal and verified every step digest. */
13
+ restorable: boolean;
14
+ /**
15
+ * The `step_seq` of the first record whose replay diverged from the durable digest, when known.
16
+ * The restore failure message names this step; a fault before any record replays leaves it null.
17
+ */
18
+ divergenceStep: number | null;
19
+ divergenceReason: string | null;
20
+ /** The operation's terminal, only when the journal restored cleanly. */
21
+ terminal: Record<string, unknown> | undefined;
22
+ /** Pending effects at the head, only when the journal restored cleanly. */
23
+ pendingEffects: Array<{
24
+ effect_id: string;
25
+ kind: string;
26
+ }>;
27
+ }
28
+ /**
29
+ * Inspect a durable journal without committing anything: replay it through the kernel and report
30
+ * whether it still restores. This turns a "silent brick" — an operation whose journal was appended
31
+ * but whose step can no longer be re-derived by this binary — into an actionable diagnosis: which
32
+ * record diverges, and what the head still holds if it does not.
33
+ *
34
+ * Nothing is mutated; the same journal can be diagnosed repeatedly and then restored normally.
35
+ */
36
+ export declare function diagnoseKernelJournal(kernel: CanonicalKernelInstance, journal: KernelJournal, operationId: string): Promise<KernelJournalDiagnosis>;
@@ -0,0 +1,60 @@
1
+ const DIVERGENCE_STEP = /at step (\d+)/;
2
+ /**
3
+ * Inspect a durable journal without committing anything: replay it through the kernel and report
4
+ * whether it still restores. This turns a "silent brick" — an operation whose journal was appended
5
+ * but whose step can no longer be re-derived by this binary — into an actionable diagnosis: which
6
+ * record diverges, and what the head still holds if it does not.
7
+ *
8
+ * Nothing is mutated; the same journal can be diagnosed repeatedly and then restored normally.
9
+ */
10
+ export async function diagnoseKernelJournal(kernel, journal, operationId) {
11
+ const checkpoint = await journal.latestCheckpoint(operationId);
12
+ const records = await journal.recordsAfter(operationId, checkpoint?.covered_head);
13
+ try {
14
+ const cost = kernel.restore(checkpoint ? Buffer.from(checkpoint.checkpoint_bytes) : undefined, records.map(record => Buffer.from(record.record_bytes)));
15
+ return {
16
+ operationId,
17
+ recordCount: records.length,
18
+ restorable: true,
19
+ divergenceStep: null,
20
+ divergenceReason: null,
21
+ terminal: parseJson(kernel.terminalJson()),
22
+ pendingEffects: parsePendingEffects(kernel.pendingEffectsJson()),
23
+ };
24
+ }
25
+ catch (error) {
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ const match = DIVERGENCE_STEP.exec(message);
28
+ return {
29
+ operationId,
30
+ recordCount: records.length,
31
+ restorable: false,
32
+ divergenceStep: match ? Number(match[1]) : null,
33
+ divergenceReason: message,
34
+ terminal: undefined,
35
+ pendingEffects: [],
36
+ };
37
+ }
38
+ }
39
+ function parseJson(value) {
40
+ if (!value)
41
+ return undefined;
42
+ try {
43
+ return JSON.parse(value);
44
+ }
45
+ catch {
46
+ return undefined;
47
+ }
48
+ }
49
+ function parsePendingEffects(raw) {
50
+ const parsed = parseJson(raw);
51
+ if (!Array.isArray(parsed))
52
+ return [];
53
+ return parsed.map(envelope => {
54
+ const effect = envelope?.effect;
55
+ return {
56
+ effect_id: String(envelope?.effect_id ?? ""),
57
+ kind: String(effect?.kind ?? ""),
58
+ };
59
+ });
60
+ }
@@ -73,11 +73,7 @@ export type KernelRunnerAction = {
73
73
  } | {
74
74
  kind: "archive_page_out";
75
75
  effectId: string;
76
- turn?: number;
77
- action?: string;
78
- summary?: string;
79
76
  archived?: Message[];
80
- tier?: string;
81
77
  handleId?: string;
82
78
  payload?: {
83
79
  content: string;
@@ -200,7 +196,18 @@ export interface KernelObservation {
200
196
  rollbacks_in_window?: number;
201
197
  window_turns?: number;
202
198
  threshold?: number;
199
+ effects?: Array<{
200
+ effect_id: string;
201
+ kind: string;
202
+ }>;
203
203
  }
204
+ /** Presentation/policy facts for an archive action come from committed observations, never from
205
+ * the wire effect projection. */
206
+ export declare function archivePresentationFromObservations(observations: readonly KernelObservation[]): {
207
+ action?: string;
208
+ summary?: string;
209
+ tier?: "semantic" | "durable";
210
+ };
204
211
  export declare function toolSchemaToKernel(schema: ToolSchema): Record<string, unknown>;
205
212
  export declare function skillMetadataToKernel(skill: SkillMetadata): Record<string, unknown>;
206
213
  export declare function messageToKernelMessage(message: Message): Record<string, unknown>;
@@ -15,6 +15,26 @@ function decodeCanonicalContentParts(content) {
15
15
  return undefined;
16
16
  }
17
17
  }
18
+ /** Presentation/policy facts for an archive action come from committed observations, never from
19
+ * the wire effect projection. */
20
+ export function archivePresentationFromObservations(observations) {
21
+ const compressed = [...observations].reverse().find(observation => observation.kind === "compressed");
22
+ if (!compressed)
23
+ return {};
24
+ const action = compressionActionFromObservation(compressed.action);
25
+ if (!action)
26
+ return {};
27
+ return {
28
+ action,
29
+ ...(compressed.summary ? { summary: compressed.summary } : {}),
30
+ tier: action === "context_collapse" || action === "auto_compact" ? "semantic" : "durable",
31
+ };
32
+ }
33
+ function compressionActionFromObservation(action) {
34
+ return action === "snip_compact" || action === "micro_compact" || action === "context_collapse" || action === "auto_compact"
35
+ ? action
36
+ : undefined;
37
+ }
18
38
  function tryParseJson(s) {
19
39
  try {
20
40
  return JSON.parse(s);
@@ -1,5 +1,9 @@
1
1
  import type { LLMProvider, Message, ProviderDescriptor, ProviderReplay, RenderedContext, ReplayabilityAssessment, ToolCall } from "../types.js";
2
2
  import type { SessionEvent } from "./session-log.js";
3
+ export declare class ProviderReplayProtocolMismatchError extends Error {
4
+ readonly code: "provider_replay_protocol_mismatch";
5
+ constructor(provider: string, storedProtocol: string, resolvedProtocol: string);
6
+ }
3
7
  export declare function assistantReplayKey(message: Pick<Message, "content" | "toolCalls">): string;
4
8
  /**
5
9
  * A stored replay may only be seeded into a provider speaking the same wire
@@ -1,3 +1,11 @@
1
+ export class ProviderReplayProtocolMismatchError extends Error {
2
+ code = "provider_replay_protocol_mismatch";
3
+ constructor(provider, storedProtocol, resolvedProtocol) {
4
+ super(`Stored ${storedProtocol} tool replay is incompatible with resolved ${provider}/${resolvedProtocol}; `
5
+ + `pin the previous ${storedProtocol} endpoint explicitly to resume this session`);
6
+ this.name = "ProviderReplayProtocolMismatchError";
7
+ }
8
+ }
1
9
  function sortObjectKeys(val) {
2
10
  if (val === null || typeof val !== "object") {
3
11
  return val;
@@ -60,8 +68,14 @@ export function seedProviderReplayFromEvents(provider, events) {
60
68
  continue;
61
69
  const toolCalls = event.tool_calls ?? [];
62
70
  const stored = event.provider_replay;
63
- if (!stored || !isReplayCompatibleWithProvider(stored, descriptor))
71
+ if (!stored)
64
72
  continue;
73
+ if (!isReplayCompatibleWithProvider(stored, descriptor)) {
74
+ if (toolCalls.length > 0 && descriptor) {
75
+ throw new ProviderReplayProtocolMismatchError(descriptor.provider, stored.protocol, descriptor.protocol);
76
+ }
77
+ continue;
78
+ }
65
79
  provider.seedProviderReplay({ content: event.content, toolCalls }, stored);
66
80
  }
67
81
  }