@deepstrike/sdk 0.2.50 → 0.2.52

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 (39) hide show
  1. package/README.md +83 -60
  2. package/dist/index.d.ts +5 -7
  3. package/dist/index.js +3 -3
  4. package/dist/kernel.d.ts +61 -31
  5. package/dist/runtime/canonical-kernel-step.d.ts +152 -0
  6. package/dist/runtime/canonical-kernel-step.js +1483 -0
  7. package/dist/runtime/execution-plane.d.ts +0 -3
  8. package/dist/runtime/execution-plane.js +0 -24
  9. package/dist/runtime/facade.js +3 -0
  10. package/dist/runtime/kernel-event-log.js +7 -13
  11. package/dist/runtime/kernel-journal.d.ts +264 -0
  12. package/dist/runtime/kernel-journal.js +741 -0
  13. package/dist/runtime/kernel-primitives-dashboard.d.ts +0 -2
  14. package/dist/runtime/kernel-primitives-dashboard.js +1 -8
  15. package/dist/runtime/kernel-step.d.ts +29 -109
  16. package/dist/runtime/kernel-step.js +47 -317
  17. package/dist/runtime/os-snapshot.d.ts +2 -2
  18. package/dist/runtime/os-snapshot.js +2 -6
  19. package/dist/runtime/payload-store.d.ts +16 -0
  20. package/dist/runtime/payload-store.js +80 -0
  21. package/dist/runtime/runner.d.ts +31 -114
  22. package/dist/runtime/runner.js +689 -774
  23. package/dist/runtime/session-log.d.ts +34 -32
  24. package/dist/runtime/session-log.js +21 -131
  25. package/dist/runtime/session-repair.d.ts +2 -36
  26. package/dist/runtime/session-repair.js +2 -47
  27. package/dist/runtime/sub-agent-orchestrator.d.ts +1 -1
  28. package/dist/runtime/sub-agent-orchestrator.js +42 -40
  29. package/dist/types/agent.d.ts +22 -19
  30. package/dist/types/agent.js +26 -42
  31. package/dist/workflow/public.d.ts +1 -1
  32. package/dist/workflow/public.js +1 -1
  33. package/package.json +2 -2
  34. package/dist/runtime/kernel-rebuild.d.ts +0 -13
  35. package/dist/runtime/kernel-rebuild.js +0 -75
  36. package/dist/runtime/kernel-transaction-log.d.ts +0 -61
  37. package/dist/runtime/kernel-transaction-log.js +0 -149
  38. package/dist/runtime/large-result-spool.d.ts +0 -93
  39. package/dist/runtime/large-result-spool.js +0 -214
@@ -0,0 +1,1483 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { getKernel } from "../kernel.js";
3
+ import { JournalCasConflictError, MAX_CHAIN_POSITION as JOURNAL_MAX_CHAIN_POSITION, } from "./kernel-journal.js";
4
+ import { encodeCanonicalContentParts, kernelMessageToSdk, renderedContextToSdk, } from "./kernel-step.js";
5
+ export const MAX_CHAIN_POSITION = JOURNAL_MAX_CHAIN_POSITION;
6
+ function asObject(value) {
7
+ return value && typeof value === "object" ? value : {};
8
+ }
9
+ function totalUsageTokens(terminal) {
10
+ const usage = asObject(terminal.usage);
11
+ const input = Number(usage.input_tokens ?? 0);
12
+ const output = Number(usage.output_tokens ?? 0);
13
+ return Number.isSafeInteger(input + output) ? input + output : 0;
14
+ }
15
+ export function canonicalUnsupportedEffectResolution(effectId, effectKind) {
16
+ return {
17
+ kind: "resolve_effect",
18
+ effect_id: effectId,
19
+ outcome: {
20
+ status: "failed",
21
+ failure: {
22
+ kind: "protocol_error",
23
+ message: `unknown canonical effect kind: ${effectKind}`,
24
+ retryable: false,
25
+ },
26
+ },
27
+ };
28
+ }
29
+ /** The only ABI-v3 planned-step → Node host-action projection. */
30
+ export function canonicalActionFromPlannedStep(plannedStep) {
31
+ if (plannedStep.disposition.kind === "terminal") {
32
+ const terminal = plannedStep.disposition.terminal;
33
+ const usage = asObject(terminal.usage);
34
+ let termination = String(terminal.kind ?? "failed");
35
+ let turnsUsed = Number(usage.turns ?? 0);
36
+ if (terminal.kind === "agent") {
37
+ const result = asObject(terminal.result);
38
+ termination = String(result.termination ?? "completed");
39
+ turnsUsed = Number(result.turns_used ?? turnsUsed);
40
+ const finalMessage = asObject(result.final_message);
41
+ const pace = asObject(result.pace_decision);
42
+ if (Object.keys(finalMessage).length > 0) {
43
+ return {
44
+ kind: "done",
45
+ effectId: "",
46
+ result: {
47
+ termination,
48
+ turnsUsed,
49
+ totalTokensUsed: totalUsageTokens(terminal),
50
+ finalMessage: {
51
+ role: String(finalMessage.role ?? "assistant"),
52
+ content: String(finalMessage.content ?? ""),
53
+ toolCalls: (Array.isArray(finalMessage.tool_calls) ? finalMessage.tool_calls : [])
54
+ .map(value => {
55
+ const call = asObject(value);
56
+ return {
57
+ id: String(call.call_id ?? ""),
58
+ name: String(call.name ?? ""),
59
+ arguments: JSON.stringify(call.arguments ?? {}),
60
+ };
61
+ }),
62
+ },
63
+ ...(Object.keys(pace).length > 0
64
+ ? {
65
+ paceDecision: {
66
+ action: String(pace.action ?? "stop"),
67
+ ...(pace.delay_ms !== undefined ? { delayMs: Number(pace.delay_ms) } : {}),
68
+ reason: String(pace.reason ?? ""),
69
+ ...(pace.coerced_from ? { coercedFrom: String(pace.coerced_from) } : {}),
70
+ },
71
+ }
72
+ : {}),
73
+ },
74
+ };
75
+ }
76
+ }
77
+ else if (terminal.kind === "workflow") {
78
+ const outcome = asObject(terminal.outcome);
79
+ termination = String(outcome.status ?? "completed");
80
+ }
81
+ else if (terminal.kind === "cancelled") {
82
+ termination = String(terminal.reason ?? "cancelled");
83
+ }
84
+ else if (terminal.kind === "failed") {
85
+ const failure = asObject(terminal.failure);
86
+ termination = failure.code === "provider_recovery_exhausted"
87
+ ? "context_overflow"
88
+ : "error";
89
+ }
90
+ return {
91
+ kind: "done",
92
+ effectId: "",
93
+ result: {
94
+ termination,
95
+ turnsUsed,
96
+ totalTokensUsed: totalUsageTokens(terminal),
97
+ },
98
+ };
99
+ }
100
+ const published = plannedStep.disposition.effects ?? [];
101
+ if (published.length === 0)
102
+ return null;
103
+ if (published.length !== 1) {
104
+ throw new Error(`Node runner expects one canonical effect at a time, received ${published.length}`);
105
+ }
106
+ const envelope = asObject(published[0]);
107
+ const effectId = String(envelope.effect_id ?? "");
108
+ const effect = asObject(envelope.effect);
109
+ if (!effectId)
110
+ throw new Error("canonical effect is missing effect_id");
111
+ switch (effect.kind) {
112
+ case "call_provider":
113
+ return {
114
+ kind: "call_provider",
115
+ effectId,
116
+ context: renderedContextToSdk(asObject(effect.context)),
117
+ tools: (Array.isArray(effect.tools) ? effect.tools : []).map(raw => {
118
+ const tool = asObject(raw);
119
+ return {
120
+ name: String(tool.name ?? ""),
121
+ description: String(tool.description ?? ""),
122
+ parameters: JSON.stringify(tool.parameters ?? {}),
123
+ };
124
+ }),
125
+ };
126
+ case "execute_tools":
127
+ return {
128
+ kind: "execute_tool",
129
+ effectId,
130
+ calls: (Array.isArray(effect.calls) ? effect.calls : []).map(raw => {
131
+ const call = asObject(raw);
132
+ return {
133
+ id: String(call.call_id ?? ""),
134
+ name: String(call.name ?? ""),
135
+ arguments: JSON.stringify(call.arguments ?? {}),
136
+ };
137
+ }),
138
+ };
139
+ case "request_approval":
140
+ return {
141
+ kind: "request_approval",
142
+ effectId,
143
+ requests: (Array.isArray(effect.requests) ? effect.requests : []).map(raw => {
144
+ const request = asObject(raw);
145
+ return {
146
+ callId: String(request.call_id ?? ""),
147
+ tool: String(request.tool_name ?? ""),
148
+ arguments: JSON.stringify(request.arguments ?? {}),
149
+ reason: String(request.reason ?? ""),
150
+ };
151
+ }),
152
+ };
153
+ case "spawn_tasks":
154
+ return {
155
+ kind: "spawn_workflow",
156
+ effectId,
157
+ nodes: (Array.isArray(effect.tasks) ? effect.tasks : []).map(raw => {
158
+ const task = asObject(raw);
159
+ const spec = asObject(task.spec);
160
+ return {
161
+ agent_id: String(task.task_id ?? ""),
162
+ task_id: String(task.task_id ?? ""),
163
+ attempt_id: String(task.attempt_id ?? ""),
164
+ launch_token: String(task.launch_token ?? ""),
165
+ node_id: String(task.node_id ?? ""),
166
+ goal: String(spec.goal ?? ""),
167
+ role: String(spec.role ?? "custom"),
168
+ isolation: String(spec.isolation ?? "shared"),
169
+ context_inheritance: String(spec.context_inheritance ?? "none"),
170
+ ...(spec.metadata && typeof spec.metadata === "object"
171
+ ? asObject(spec.metadata)
172
+ : {}),
173
+ };
174
+ }),
175
+ ...(effect.budget ? { budget: asObject(effect.budget) } : {}),
176
+ };
177
+ case "preempt_tasks": {
178
+ const attempts = (Array.isArray(effect.attempts) ? effect.attempts : []).map(raw => {
179
+ const attempt = asObject(raw);
180
+ return {
181
+ task_id: String(attempt.task_id ?? ""),
182
+ attempt_id: String(attempt.attempt_id ?? ""),
183
+ };
184
+ });
185
+ return {
186
+ kind: "preempt_sub_agents",
187
+ effectId,
188
+ attempts,
189
+ agentIds: attempts.map(attempt => attempt.task_id),
190
+ reason: String(effect.reason ?? ""),
191
+ };
192
+ }
193
+ case "persist_memory":
194
+ return {
195
+ kind: "persist_memory",
196
+ effectId,
197
+ memory: asObject(effect.memory),
198
+ };
199
+ case "query_memory":
200
+ return {
201
+ kind: "query_memory",
202
+ effectId,
203
+ query: asObject(effect.query),
204
+ requestedK: Number(effect.requested_k ?? 0),
205
+ };
206
+ case "archive_page_out": {
207
+ const payload = asObject(effect.payload);
208
+ let archived = [];
209
+ try {
210
+ const decoded = JSON.parse(String(payload.content ?? ""));
211
+ if (Array.isArray(decoded)) {
212
+ archived = decoded.map(value => kernelMessageToSdk(asObject(value)));
213
+ }
214
+ }
215
+ catch {
216
+ // Persistence still uses the opaque body and digest. Only optional presentation-side
217
+ // summarization is skipped if the archived message batch cannot be decoded.
218
+ }
219
+ const compressed = (plannedStep.observations ?? [])
220
+ .find(observation => observation.kind === "compressed");
221
+ const pressureAction = compressed ? String(compressed.action ?? "") : "";
222
+ return {
223
+ kind: "archive_page_out",
224
+ effectId,
225
+ handleId: String(effect.handle_id ?? ""),
226
+ payload,
227
+ archived,
228
+ ...(pressureAction ? { action: pressureAction } : {}),
229
+ ...(compressed?.summary ? { summary: String(compressed.summary) } : {}),
230
+ ...(pressureAction
231
+ ? {
232
+ tier: ["context_collapse", "auto_compact"].includes(pressureAction)
233
+ ? "semantic"
234
+ : "durable",
235
+ }
236
+ : {}),
237
+ };
238
+ }
239
+ case "load_payload":
240
+ return {
241
+ kind: "load_payload",
242
+ effectId,
243
+ handleId: String(effect.handle_id ?? ""),
244
+ payloadRef: String(effect.payload_ref ?? ""),
245
+ };
246
+ case "evaluate_milestone": {
247
+ const request = asObject(effect.request);
248
+ return {
249
+ kind: "evaluate_milestone",
250
+ effectId,
251
+ phaseId: String(request.phase_id ?? ""),
252
+ criteria: [],
253
+ requiredEvidence: [],
254
+ };
255
+ }
256
+ default:
257
+ return {
258
+ kind: "unsupported_effect",
259
+ effectId,
260
+ effectKind: String(effect.kind),
261
+ };
262
+ }
263
+ }
264
+ export class CanonicalKernelRejectedError extends Error {
265
+ fault;
266
+ constructor(faultJson) {
267
+ let fault;
268
+ try {
269
+ fault = JSON.parse(faultJson);
270
+ }
271
+ catch {
272
+ fault = { code: "invalid_fault", message: faultJson };
273
+ }
274
+ super(`${String(fault.code ?? "kernel_rejected")}: ${String(fault.message ?? "canonical input rejected")}`);
275
+ this.name = "CanonicalKernelRejectedError";
276
+ this.fault = fault;
277
+ }
278
+ }
279
+ /**
280
+ * A record is already authoritative once the journal append returns. A later native commit failure
281
+ * is therefore a rebuild boundary, never an abort boundary.
282
+ */
283
+ export class CanonicalKernelRebuildRequiredError extends Error {
284
+ /** True when the runtime was already rebuilt from the journal with the durable record
285
+ * applied — the caller can resync and continue. False when the rebuild itself failed
286
+ * and the operation cannot proceed without journal repair. */
287
+ rebuilt;
288
+ constructor(message, options) {
289
+ super(message, options?.cause !== undefined ? { cause: options.cause } : undefined);
290
+ this.name = "CanonicalKernelRebuildRequiredError";
291
+ this.rebuilt = options?.rebuilt ?? false;
292
+ }
293
+ }
294
+ function chainPosition(value, label) {
295
+ if (!/^(0|[1-9]\d*)$/.test(value)) {
296
+ throw new RangeError(`${label} must be a canonical decimal integer`);
297
+ }
298
+ const parsed = Number(value);
299
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed >= MAX_CHAIN_POSITION) {
300
+ throw new RangeError(`${label} must be below ${MAX_CHAIN_POSITION}`);
301
+ }
302
+ return parsed;
303
+ }
304
+ function parsePlannedStep(json) {
305
+ if (!json)
306
+ throw new Error("canonical replay is missing plannedStepJson");
307
+ return JSON.parse(json);
308
+ }
309
+ function parseAdvice(json) {
310
+ return json ? JSON.parse(json) : undefined;
311
+ }
312
+ function isCasConflict(error) {
313
+ return error instanceof JournalCasConflictError ||
314
+ error?.name === "JournalCasConflictError";
315
+ }
316
+ function isCheckpointRequired(preparation) {
317
+ if (preparation.status !== "rejected")
318
+ return false;
319
+ try {
320
+ const fault = JSON.parse(preparation.faultJson);
321
+ return fault.code === "checkpoint_required";
322
+ }
323
+ catch {
324
+ return false;
325
+ }
326
+ }
327
+ /**
328
+ * Node's canonical durable staged-transition host.
329
+ *
330
+ * It owns no scheduler truth. Core prepares opaque record bytes; `KernelJournal` makes those bytes
331
+ * authoritative; only then may core commit and expose the planned effects/terminal to the runner.
332
+ */
333
+ export class CanonicalKernelHost {
334
+ kernel;
335
+ journal;
336
+ operationId;
337
+ constructor(kernel, journal, operationId) {
338
+ this.kernel = kernel;
339
+ this.journal = journal;
340
+ this.operationId = operationId;
341
+ if (!operationId)
342
+ throw new TypeError("canonical kernel operationId must not be empty");
343
+ }
344
+ async transition(input, options = {}) {
345
+ const inputJson = JSON.stringify({
346
+ abi_version: getKernel().kernelAbiVersion(),
347
+ operation_id: this.operationId,
348
+ input_id: options.inputId ?? `node-input-${randomUUID()}`,
349
+ observed_at_ms: options.observedAtMs ?? String(Date.now()),
350
+ input,
351
+ });
352
+ await this.journal.stageOutboundEnvelope(this.operationId, inputJson);
353
+ try {
354
+ const transition = await this.transitionEnvelope(inputJson, 1, 1);
355
+ await this.journal.clearOutboundEnvelope(this.operationId);
356
+ return transition;
357
+ }
358
+ catch (error) {
359
+ // Append-acked records own the input; rejected inputs will not retry this envelope.
360
+ // Append-before failures leave the staged bytes so wake can drain byte-identical retries.
361
+ if (error instanceof CanonicalKernelRebuildRequiredError
362
+ || error instanceof CanonicalKernelRejectedError) {
363
+ await this.journal.clearOutboundEnvelope(this.operationId);
364
+ }
365
+ throw error;
366
+ }
367
+ }
368
+ /** Restore the latest installed checkpoint and its authoritative record tail in place. */
369
+ async restore() {
370
+ const checkpoint = await this.journal.latestCheckpoint(this.operationId);
371
+ const records = await this.journal.recordsAfter(this.operationId, checkpoint?.covered_head);
372
+ return this.kernel.restore(checkpoint ? Buffer.from(checkpoint.checkpoint_bytes) : undefined, records.map(record => Buffer.from(record.record_bytes)));
373
+ }
374
+ /**
375
+ * Replay a crash-window outbound envelope with identical bytes (adjudication 5e.3).
376
+ * No-op when nothing is staged. Clears the stage after commit/replay/reject.
377
+ */
378
+ async drainOutboundEnvelope() {
379
+ const pending = await this.journal.readOutboundEnvelope(this.operationId);
380
+ if (!pending)
381
+ return undefined;
382
+ try {
383
+ const transition = await this.transitionEnvelope(pending, 1, 1);
384
+ await this.journal.clearOutboundEnvelope(this.operationId);
385
+ return transition;
386
+ }
387
+ catch (error) {
388
+ if (error instanceof CanonicalKernelRebuildRequiredError
389
+ || error instanceof CanonicalKernelRejectedError) {
390
+ await this.journal.clearOutboundEnvelope(this.operationId);
391
+ }
392
+ throw error;
393
+ }
394
+ }
395
+ /** Execute the full §12.3 install/ack/reclaim boundary. */
396
+ async checkpoint() {
397
+ const candidate = this.kernel.checkpointCandidate();
398
+ const throughStepSeq = chainPosition(candidate.throughStepSeq, "checkpoint throughStepSeq");
399
+ const previous = await this.journal.latestCheckpoint(this.operationId);
400
+ let installed;
401
+ try {
402
+ installed = await this.journal.compareAndInstallCheckpoint(this.operationId, previous?.checkpoint_id, candidate.coveredHead, {
403
+ checkpoint_id: candidate.ackToken,
404
+ through_step_seq: throughStepSeq,
405
+ state_digest: candidate.stateDigest,
406
+ checkpoint_bytes: candidate.checkpointBytes,
407
+ });
408
+ }
409
+ catch (error) {
410
+ if (!isCasConflict(error))
411
+ throw error;
412
+ const winner = await this.journal.latestCheckpoint(this.operationId);
413
+ if (!winner || winner.checkpoint_id !== candidate.ackToken)
414
+ throw error;
415
+ installed = winner;
416
+ }
417
+ await this.journal.ackCheckpoint(this.operationId, installed.checkpoint_id);
418
+ this.kernel.ackCheckpoint(candidate.throughStepSeq, candidate.coveredHead);
419
+ await this.journal.pruneAckedPrefix(this.operationId);
420
+ return { ...installed, acknowledged: true };
421
+ }
422
+ async transitionEnvelope(inputJson, casRetriesLeft, checkpointRetriesLeft) {
423
+ const preparation = this.kernel.prepare(inputJson);
424
+ if (preparation.status === "rejected") {
425
+ if (checkpointRetriesLeft > 0 && isCheckpointRequired(preparation)) {
426
+ await this.checkpoint();
427
+ return this.transitionEnvelope(inputJson, casRetriesLeft, checkpointRetriesLeft - 1);
428
+ }
429
+ throw new CanonicalKernelRejectedError(preparation.faultJson);
430
+ }
431
+ if (preparation.status === "replayed") {
432
+ return {
433
+ inputJson,
434
+ stepSeq: chainPosition(preparation.stepSeq, "replayed stepSeq"),
435
+ recordDigest: preparation.recordDigest,
436
+ plannedStep: parsePlannedStep(preparation.plannedStepJson),
437
+ replayed: true,
438
+ };
439
+ }
440
+ const stepSeq = chainPosition(preparation.stepSeq, "prepared stepSeq");
441
+ let appended = false;
442
+ let transition;
443
+ try {
444
+ const receipt = await this.journal.compareAndAppend(this.operationId, preparation.expectedHead, {
445
+ step_seq: stepSeq,
446
+ record_digest: preparation.recordDigest,
447
+ record_bytes: preparation.recordBytes,
448
+ });
449
+ appended = true;
450
+ const committed = this.kernel.commit(preparation.prepareToken, receipt.record_digest);
451
+ if (committed.recordDigest !== preparation.recordDigest ||
452
+ chainPosition(committed.stepSeq, "committed stepSeq") !== stepSeq) {
453
+ throw new Error("canonical commit receipt disagrees with the durably appended record");
454
+ }
455
+ const checkpointAdvice = parseAdvice(committed.checkpointAdviceJson);
456
+ transition = {
457
+ inputJson,
458
+ stepSeq,
459
+ recordDigest: committed.recordDigest,
460
+ plannedStep: parsePlannedStep(committed.plannedStepJson),
461
+ ...(checkpointAdvice ? { checkpointAdvice } : {}),
462
+ replayed: false,
463
+ };
464
+ }
465
+ catch (error) {
466
+ if (appended) {
467
+ try {
468
+ await this.restore();
469
+ }
470
+ catch (restoreError) {
471
+ throw new CanonicalKernelRebuildRequiredError("canonical record is durable, commit failed, and journal rebuild also failed", { cause: new AggregateError([error, restoreError]) });
472
+ }
473
+ throw new CanonicalKernelRebuildRequiredError("canonical record is durable but commit could not be published; runtime rebuilt from journal", { cause: error, rebuilt: true });
474
+ }
475
+ try {
476
+ this.kernel.abort(preparation.prepareToken);
477
+ }
478
+ catch (abortError) {
479
+ throw new AggregateError([error, abortError], "canonical append failed and prepare could not be aborted");
480
+ }
481
+ if (casRetriesLeft > 0 && isCasConflict(error)) {
482
+ await this.restore();
483
+ return this.transitionEnvelope(inputJson, casRetriesLeft - 1, checkpointRetriesLeft);
484
+ }
485
+ throw error;
486
+ }
487
+ // Past this point the step is durable AND committed. A failing advised checkpoint
488
+ // must not be misdiagnosed as a lost commit: it is deferred housekeeping — the next
489
+ // advice retries it, and the checkpoint_required prepare gate is the hard backstop.
490
+ if (transition.checkpointAdvice) {
491
+ try {
492
+ await this.checkpoint();
493
+ }
494
+ catch (checkpointError) {
495
+ transition.checkpointFailure =
496
+ checkpointError instanceof Error ? checkpointError.message : String(checkpointError);
497
+ }
498
+ }
499
+ return transition;
500
+ }
501
+ }
502
+ function canonicalProviderMessage(raw) {
503
+ const content = raw.content;
504
+ return {
505
+ role: String(raw.role ?? "assistant"),
506
+ content: typeof content === "string" ? content : JSON.stringify(content ?? ""),
507
+ ...((Array.isArray(raw.tool_calls) && raw.tool_calls.length > 0)
508
+ ? {
509
+ tool_calls: raw.tool_calls.map(value => {
510
+ const call = asObject(value);
511
+ return {
512
+ call_id: String(call.call_id ?? call.id ?? ""),
513
+ name: String(call.name ?? ""),
514
+ arguments: canonicalProviderToolArguments(String(call.name ?? ""), asObject(call.arguments)),
515
+ };
516
+ }),
517
+ }
518
+ : {}),
519
+ ...(raw.token_count !== undefined ? { tokens: Number(raw.token_count) } : {}),
520
+ };
521
+ }
522
+ function canonicalProviderToolArguments(name, argumentsValue) {
523
+ if (name === "start_workflow") {
524
+ const wrapped = asObject(argumentsValue.spec);
525
+ return canonicalWorkflowSpec(Object.keys(wrapped).length > 0 ? wrapped : argumentsValue);
526
+ }
527
+ if (name === "submit_workflow_nodes") {
528
+ const spec = canonicalWorkflowSpec({
529
+ nodes: Array.isArray(argumentsValue.nodes) ? argumentsValue.nodes : [],
530
+ });
531
+ return { nodes: spec.nodes };
532
+ }
533
+ return argumentsValue;
534
+ }
535
+ function canonicalInitialMessage(raw) {
536
+ if (Array.isArray(raw.content)) {
537
+ return {
538
+ role: String(raw.role ?? "user"),
539
+ content: encodeCanonicalContentParts(raw.content),
540
+ ...(raw.token_count !== undefined ? { tokens: Number(raw.token_count) } : {}),
541
+ };
542
+ }
543
+ const message = canonicalProviderMessage(raw);
544
+ delete message.tool_calls;
545
+ return message;
546
+ }
547
+ function logicalRunSpec(raw, goal) {
548
+ if (!raw)
549
+ return undefined;
550
+ const filter = asObject(raw.capability_filter);
551
+ return {
552
+ goal: String(raw.goal ?? goal),
553
+ ...(raw.role ? { role: raw.role } : {}),
554
+ ...(raw.isolation ? { isolation: raw.isolation } : {}),
555
+ ...(raw.context_inheritance ? { context_inheritance: raw.context_inheritance } : {}),
556
+ ...(raw.verification_contract_id ? { verification_contract_id: raw.verification_contract_id } : {}),
557
+ ...((Array.isArray(filter.allowed_kinds) && filter.allowed_kinds.length > 0) ||
558
+ (Array.isArray(filter.allowed_ids) && filter.allowed_ids.length > 0)
559
+ ? {
560
+ capability_filter: {
561
+ ...(Array.isArray(filter.allowed_kinds) && filter.allowed_kinds.length > 0
562
+ ? { allowed_kinds: filter.allowed_kinds }
563
+ : {}),
564
+ ...(Array.isArray(filter.allowed_ids) && filter.allowed_ids.length > 0
565
+ ? { allowed_ids: filter.allowed_ids }
566
+ : {}),
567
+ },
568
+ }
569
+ : {}),
570
+ ...(Object.prototype.hasOwnProperty.call(raw, "exposure_baseline")
571
+ ? { exposure_baseline: raw.exposure_baseline }
572
+ : {}),
573
+ ...(raw.loop_round && typeof raw.loop_round === "object"
574
+ ? {
575
+ loop_round: {
576
+ ...(asObject(raw.loop_round).max_rounds !== undefined
577
+ ? { max_rounds: Number(asObject(raw.loop_round).max_rounds) }
578
+ : {}),
579
+ ...(asObject(raw.loop_round).min_sleep_ms !== undefined
580
+ ? { min_sleep_ms: String(asObject(raw.loop_round).min_sleep_ms) }
581
+ : {}),
582
+ ...(asObject(raw.loop_round).max_sleep_ms !== undefined
583
+ ? { max_sleep_ms: String(asObject(raw.loop_round).max_sleep_ms) }
584
+ : {}),
585
+ ...(asObject(raw.loop_round).default_action !== undefined
586
+ ? { default_action: asObject(raw.loop_round).default_action }
587
+ : {}),
588
+ },
589
+ }
590
+ : {}),
591
+ ...(raw.metadata && typeof raw.metadata === "object" ? { metadata: raw.metadata } : {}),
592
+ };
593
+ }
594
+ function canonicalWorkflowSpec(raw) {
595
+ const nodes = Array.isArray(raw.nodes) ? raw.nodes.map(asObject) : [];
596
+ const nodeIds = nodes.map((_node, index) => `wf-node${index}`);
597
+ return {
598
+ nodes: nodes.map((node, index) => {
599
+ const unsupported = [];
600
+ if (node.kind !== undefined
601
+ || node.reducer !== undefined
602
+ || node.loop !== undefined
603
+ || node.classify !== undefined
604
+ || node.tournament !== undefined)
605
+ unsupported.push("kind");
606
+ if (node.trust !== undefined && node.trust !== "trusted")
607
+ unsupported.push("trust");
608
+ const depPolicy = node.dep_policy ?? node.depPolicy;
609
+ if (depPolicy !== undefined && depPolicy !== "all_success")
610
+ unsupported.push("dep_policy");
611
+ if (node.token_budget !== undefined || node.tokenBudget !== undefined)
612
+ unsupported.push("token_budget");
613
+ if (node.max_turns !== undefined || node.maxTurns !== undefined)
614
+ unsupported.push("max_turns");
615
+ if (node.max_wall_ms !== undefined || node.maxWallMs !== undefined)
616
+ unsupported.push("max_wall_ms");
617
+ const inheritance = node.context_inheritance ?? node.contextInheritance;
618
+ if (unsupported.length > 0) {
619
+ throw new CanonicalKernelRejectedError(JSON.stringify({
620
+ code: "unsupported_effect",
621
+ message: `workflow node ${index} uses fields absent from canonical WorkflowNode: ${unsupported.join(", ")}`,
622
+ }));
623
+ }
624
+ const taskValue = node.task;
625
+ const task = asObject(taskValue);
626
+ const goal = typeof taskValue === "string"
627
+ ? taskValue
628
+ : String(task.goal ?? node.goal ?? "");
629
+ const rawDependsOn = Array.isArray(node.depends_on)
630
+ ? node.depends_on
631
+ : Array.isArray(node.dependsOn) ? node.dependsOn : [];
632
+ const dependsOn = rawDependsOn.length > 0
633
+ ? rawDependsOn.map(value => nodeIds[Number(value)] ?? String(value))
634
+ : [];
635
+ const modelHint = node.model_hint ?? node.modelHint;
636
+ const outputSchema = node.output_schema ?? node.outputSchema;
637
+ const runSpec = logicalRunSpec({
638
+ goal,
639
+ ...(node.role ? { role: node.role } : {}),
640
+ ...(node.isolation ? { isolation: node.isolation } : {}),
641
+ ...(inheritance ? { context_inheritance: inheritance } : {}),
642
+ ...((modelHint !== undefined || outputSchema !== undefined)
643
+ ? {
644
+ metadata: {
645
+ ...(modelHint !== undefined ? { model_hint: modelHint } : {}),
646
+ ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}),
647
+ },
648
+ }
649
+ : {}),
650
+ }, goal);
651
+ return {
652
+ node_id: nodeIds[index],
653
+ task: {
654
+ goal,
655
+ ...(Array.isArray(task.criteria) && task.criteria.length > 0 ? { criteria: task.criteria } : {}),
656
+ ...(task.lane ? { lane: task.lane } : {}),
657
+ },
658
+ ...(dependsOn.length > 0 ? { depends_on: dependsOn } : {}),
659
+ ...(runSpec ? { run_spec: runSpec } : {}),
660
+ };
661
+ }),
662
+ };
663
+ }
664
+ function sha256(value) {
665
+ return `sha256:${createHash("sha256").update(value).digest("hex")}`;
666
+ }
667
+ function providerStopReason(value) {
668
+ if (typeof value !== "string" || value.length === 0)
669
+ return undefined;
670
+ const normalized = value.toLowerCase();
671
+ if (["end_turn", "tool_use", "max_tokens", "stop_sequence", "content_filter"].includes(normalized)) {
672
+ return normalized;
673
+ }
674
+ return "other";
675
+ }
676
+ /**
677
+ * Canonical operation runtime used by the Node host.
678
+ * Every durable transition below is one of the canonical ABI's five input classes; no legacy
679
+ * envelope or synthesized host transaction reaches core or storage.
680
+ */
681
+ export class CanonicalRunnerRuntime {
682
+ options;
683
+ host;
684
+ config;
685
+ initialContext = { messages: [], knowledge: [], capabilities: [] };
686
+ configured = false;
687
+ started = false;
688
+ turns = 0;
689
+ lastAction = null;
690
+ newMessages = [];
691
+ hostObservations = [];
692
+ spawnedTasks = 0;
693
+ memoryBindingId;
694
+ payloadInlineThreshold = 50 * 1024;
695
+ payloadPreviewBytes = 2 * 1024;
696
+ constructor(kernel, journal, operationId, options) {
697
+ this.options = options;
698
+ this.host = new CanonicalKernelHost(kernel, journal, operationId);
699
+ this.memoryBindingId = options.memoryBindingId ?? "node-memory";
700
+ this.config = {
701
+ execution_policy: {
702
+ max_context_tokens: options.maxContextTokens,
703
+ ...(options.maxTurns !== undefined ? { max_turns: options.maxTurns } : {}),
704
+ ...(options.maxTotalTokens !== undefined ? { max_total_tokens: String(options.maxTotalTokens) } : {}),
705
+ ...(options.maxWallMs !== undefined ? { max_wall_ms: String(options.maxWallMs) } : {}),
706
+ },
707
+ host_effect_support: {
708
+ supported: [
709
+ "call_provider",
710
+ "execute_tools",
711
+ "request_approval",
712
+ "spawn_tasks",
713
+ "preempt_tasks",
714
+ "persist_memory",
715
+ "query_memory",
716
+ "archive_page_out",
717
+ "load_payload",
718
+ "evaluate_milestone",
719
+ ],
720
+ },
721
+ kernel_limits: {
722
+ max_json_depth: 64,
723
+ max_collection_entries: 65_536,
724
+ collection_limits: {
725
+ tool_catalog: 4_096,
726
+ skill_catalog: 4_096,
727
+ knowledge_entries: 65_536,
728
+ initial_messages: 65_536,
729
+ capability_grants: 65_536,
730
+ governance_rules: 65_536,
731
+ },
732
+ },
733
+ };
734
+ }
735
+ get operationId() {
736
+ return this.host.operationId;
737
+ }
738
+ get journal() {
739
+ return this.host.journal;
740
+ }
741
+ turn() {
742
+ return this.turns;
743
+ }
744
+ isTerminal() {
745
+ return ["completed", "cancelled", "failed"].includes(this.host.kernel.lifecycle());
746
+ }
747
+ recoveryContentBytes() {
748
+ return Math.max(1_024, this.options.maxContextTokens * 4);
749
+ }
750
+ preservedRefs() {
751
+ return [];
752
+ }
753
+ drainNewMessages() {
754
+ return this.newMessages.splice(0);
755
+ }
756
+ drainHostObservations() {
757
+ return this.hostObservations.splice(0);
758
+ }
759
+ terminal() {
760
+ const terminal = this.host.kernel.terminalJson();
761
+ return terminal ? JSON.parse(terminal) : undefined;
762
+ }
763
+ localSubagentsSpawned() {
764
+ return this.spawnedTasks;
765
+ }
766
+ async restore() {
767
+ await this.host.restore();
768
+ this.configured = this.host.kernel.lifecycle() !== "created";
769
+ this.started = !["created", "configured"].includes(this.host.kernel.lifecycle());
770
+ // A crash between stage and append-ack leaves a byte-identical envelope; drain it before
771
+ // the host effect loop so retries never remint observed_at_ms.
772
+ await this.host.drainOutboundEnvelope();
773
+ this.lastAction = this.currentAction();
774
+ }
775
+ resumeAction() {
776
+ this.lastAction = this.currentAction();
777
+ return this.lastAction;
778
+ }
779
+ async startAgent(taskValue, runSpecValue) {
780
+ await this.ensureConfigured();
781
+ const goal = String(taskValue.goal ?? "");
782
+ const action = await this.commit({
783
+ kind: "start_operation",
784
+ entry: {
785
+ kind: "agent",
786
+ task: {
787
+ goal,
788
+ ...(Array.isArray(taskValue.criteria) && taskValue.criteria.length > 0
789
+ ? { criteria: taskValue.criteria }
790
+ : {}),
791
+ },
792
+ ...(runSpecValue ? { run_spec: logicalRunSpec(runSpecValue, goal) } : {}),
793
+ },
794
+ initial_context: this.initialContext,
795
+ });
796
+ this.started = true;
797
+ return action;
798
+ }
799
+ async startWorkflow(specValue) {
800
+ await this.ensureConfigured();
801
+ const action = await this.commit({
802
+ kind: "start_operation",
803
+ entry: {
804
+ kind: "workflow",
805
+ spec: canonicalWorkflowSpec(specValue),
806
+ },
807
+ initial_context: this.initialContext,
808
+ });
809
+ this.started = true;
810
+ return action;
811
+ }
812
+ async applyHostEvent(event) {
813
+ if (!this.started && this.applyBootstrapEvent(event))
814
+ return null;
815
+ let input;
816
+ switch (event.kind) {
817
+ case "provider_result": {
818
+ const message = canonicalProviderMessage(asObject(event.message));
819
+ this.newMessages.push({
820
+ role: message.role,
821
+ content: String(message.content ?? ""),
822
+ toolCalls: (Array.isArray(message.tool_calls) ? message.tool_calls : []).map(raw => {
823
+ const call = asObject(raw);
824
+ return {
825
+ id: String(call.call_id ?? ""),
826
+ name: String(call.name ?? ""),
827
+ arguments: JSON.stringify(call.arguments ?? {}),
828
+ };
829
+ }),
830
+ });
831
+ this.turns += 1;
832
+ input = {
833
+ kind: "resolve_effect",
834
+ effect_id: String(event.effect_id ?? ""),
835
+ outcome: {
836
+ status: "succeeded",
837
+ result: {
838
+ kind: "provider",
839
+ outcome: {
840
+ kind: "completed",
841
+ message,
842
+ ...(event.observed_input_tokens !== undefined
843
+ ? { observed_input_tokens: Number(event.observed_input_tokens) }
844
+ : {}),
845
+ ...(event.observed_output_tokens !== undefined
846
+ ? { observed_output_tokens: Number(event.observed_output_tokens) }
847
+ : {}),
848
+ ...(providerStopReason(event.stop_reason)
849
+ ? { stop_reason: providerStopReason(event.stop_reason) }
850
+ : {}),
851
+ },
852
+ },
853
+ },
854
+ };
855
+ break;
856
+ }
857
+ case "provider_error": {
858
+ const message = String(event.message ?? "");
859
+ const contextOverflow = /context|token.*limit|too long/i.test(message);
860
+ input = contextOverflow
861
+ ? {
862
+ kind: "resolve_effect",
863
+ effect_id: String(event.effect_id ?? ""),
864
+ outcome: {
865
+ status: "succeeded",
866
+ result: { kind: "provider", outcome: { kind: "context_overflow" } },
867
+ },
868
+ }
869
+ : this.failedEffect(event, "transport_exhausted", message, true);
870
+ break;
871
+ }
872
+ case "tool_results": {
873
+ const results = [];
874
+ for (const value of Array.isArray(event.results) ? event.results : []) {
875
+ const result = asObject(value);
876
+ const callId = String(result.call_id ?? "");
877
+ const output = String(result.output ?? "");
878
+ const isError = Boolean(result.is_error);
879
+ const disposition = result.is_fatal ? "fatal" : "recoverable";
880
+ const bytes = Buffer.byteLength(output, "utf8");
881
+ if (bytes > this.payloadInlineThreshold && this.options.persistPayload) {
882
+ const persisted = await this.options.persistPayload(callId, output, this.payloadPreviewBytes);
883
+ results.push({
884
+ kind: "external",
885
+ call_id: callId,
886
+ payload_ref: persisted.payloadRef,
887
+ digest: persisted.digest,
888
+ original_size: persisted.originalSize,
889
+ preview: persisted.preview,
890
+ ...(isError ? { is_error: true } : {}),
891
+ disposition,
892
+ });
893
+ }
894
+ else {
895
+ results.push({
896
+ kind: "inline",
897
+ call_id: callId,
898
+ result: {
899
+ output,
900
+ ...(isError ? { is_error: true } : {}),
901
+ disposition,
902
+ ...(result.token_count !== null && result.token_count !== undefined
903
+ ? { tokens: Number(result.token_count) }
904
+ : {}),
905
+ },
906
+ });
907
+ }
908
+ this.newMessages.push({ role: "tool", content: output, toolCalls: [] });
909
+ }
910
+ input = this.succeededEffect(event, { kind: "tools", results });
911
+ break;
912
+ }
913
+ case "approval_result":
914
+ input = this.succeededEffect(event, {
915
+ kind: "approval",
916
+ approved_call_ids: Array.isArray(event.approved_calls) ? event.approved_calls : [],
917
+ denied_call_ids: Array.isArray(event.denied_calls) ? event.denied_calls : [],
918
+ });
919
+ break;
920
+ case "workflow_spawn_result": {
921
+ const spawn = this.lastAction?.kind === "spawn_workflow" ? this.lastAction : undefined;
922
+ this.spawnedTasks += spawn?.nodes.length ?? 0;
923
+ input = this.succeededEffect(event, {
924
+ kind: "tasks_spawned",
925
+ attempts: (spawn?.nodes ?? []).map(node => ({
926
+ task_id: String(node.task_id ?? node.agent_id ?? ""),
927
+ attempt_id: String(node.attempt_id ?? ""),
928
+ outcome: { status: "started" },
929
+ })),
930
+ });
931
+ break;
932
+ }
933
+ case "preempt_result": {
934
+ const preempt = this.lastAction?.kind === "preempt_sub_agents" ? this.lastAction : undefined;
935
+ input = this.succeededEffect(event, {
936
+ kind: "tasks_preempted",
937
+ attempts: (preempt?.attempts ?? []).map(attempt => ({
938
+ ...attempt,
939
+ outcome: { status: "preempted" },
940
+ })),
941
+ });
942
+ break;
943
+ }
944
+ case "sub_agent_completed": {
945
+ const raw = asObject(event.result);
946
+ const result = asObject(raw.result);
947
+ const submittedNodes = Array.isArray(raw.submitted_nodes)
948
+ ? raw.submitted_nodes.map(asObject)
949
+ : [];
950
+ const taskId = String(raw.agent_id ?? "");
951
+ const pending = this.pendingEffects().find(effect => {
952
+ const kind = asObject(effect.effect);
953
+ return kind.kind === "spawn_tasks" &&
954
+ (Array.isArray(kind.tasks) ? kind.tasks : []).some(task => asObject(task).task_id === taskId);
955
+ });
956
+ const launch = pending
957
+ ? (Array.isArray(asObject(pending.effect).tasks) ? asObject(pending.effect).tasks : [])
958
+ .map(asObject).find(task => task.task_id === taskId)
959
+ : undefined;
960
+ input = {
961
+ kind: "deliver_external_event",
962
+ event: {
963
+ kind: "child_completed",
964
+ task_id: taskId,
965
+ attempt_id: String(launch?.attempt_id ?? `${taskId}:attempt:1`),
966
+ result: {
967
+ status: result.termination === "completed" ? "completed" : "failed",
968
+ ...(asObject(result.final_message).content
969
+ ? { output: String(asObject(result.final_message).content) }
970
+ : {}),
971
+ ...(!["completed", "max_turns", "token_budget"].includes(String(result.termination))
972
+ ? { error: String(result.termination ?? "failed") }
973
+ : {}),
974
+ usage: {
975
+ input_tokens: "0",
976
+ output_tokens: String(result.total_tokens_used ?? 0),
977
+ turns: Number(result.turns_used ?? 0),
978
+ },
979
+ },
980
+ ...(submittedNodes.length > 0
981
+ ? {
982
+ parent_requests: [{
983
+ kind: "append_workflow_nodes",
984
+ nodes: asObject(canonicalWorkflowSpec({ nodes: submittedNodes })).nodes,
985
+ }],
986
+ }
987
+ : {}),
988
+ },
989
+ };
990
+ break;
991
+ }
992
+ case "memory_persist_result":
993
+ input = event.error
994
+ ? this.failedEffect(event, "storage_unavailable", String(event.error), true)
995
+ : this.succeededEffect(event, {
996
+ kind: "memory_persisted",
997
+ receipt: {
998
+ binding_id: this.memoryBindingId,
999
+ record_ref: String(event.record_ref ?? `memory:${randomUUID()}`),
1000
+ digest: String(event.digest ?? sha256(String(event.record_ref ?? event.effect_id ?? ""))),
1001
+ },
1002
+ });
1003
+ break;
1004
+ case "memory_query_result":
1005
+ input = event.error
1006
+ ? this.failedEffect(event, "storage_unavailable", String(event.error), true)
1007
+ : this.succeededEffect(event, {
1008
+ kind: "memory_queried",
1009
+ recalls: (Array.isArray(event.hits) ? event.hits : []).map(value => {
1010
+ const hit = asObject(value);
1011
+ const record = asObject(hit.record);
1012
+ return {
1013
+ record_ref: String(record.record_id ?? `memory:${randomUUID()}`),
1014
+ name: String(record.name ?? ""),
1015
+ kind: String(record.kind ?? "reference"),
1016
+ content: String(record.content ?? ""),
1017
+ ...(typeof hit.score === "number" ? { score: hit.score } : {}),
1018
+ };
1019
+ }),
1020
+ });
1021
+ break;
1022
+ case "page_out_archive_result":
1023
+ input = event.error
1024
+ ? this.failedEffect(event, "storage_unavailable", String(event.error), true)
1025
+ : this.pageOutResolution(event);
1026
+ break;
1027
+ case "milestone_result": {
1028
+ const result = asObject(event.result);
1029
+ input = this.succeededEffect(event, {
1030
+ kind: "milestone_evaluated",
1031
+ result: {
1032
+ phase_id: String(result.phase_id ?? ""),
1033
+ passed: Boolean(result.passed),
1034
+ ...(!result.passed && result.reason ? { notes: String(result.reason) } : {}),
1035
+ },
1036
+ });
1037
+ break;
1038
+ }
1039
+ case "payload_loaded":
1040
+ input = this.succeededEffect(event, {
1041
+ kind: "payload_loaded",
1042
+ handle_id: String(event.handle_id ?? ""),
1043
+ payload: event.payload,
1044
+ });
1045
+ break;
1046
+ case "payload_load_failed":
1047
+ input = this.failedEffect(event, "storage_unavailable", String(event.error ?? "payload load failed"), true);
1048
+ break;
1049
+ case "cancel_operation":
1050
+ input = {
1051
+ kind: "host_control",
1052
+ command: {
1053
+ kind: "cancel",
1054
+ reason: event.reason ?? "user",
1055
+ pending_call_ids: Array.isArray(event.pending_call_ids) ? event.pending_call_ids : [],
1056
+ },
1057
+ };
1058
+ break;
1059
+ case "deliver_signal":
1060
+ input = { kind: "deliver_external_event", event: this.canonicalSignal(event) };
1061
+ break;
1062
+ case "update_task":
1063
+ input = { kind: "host_control", command: { kind: "update_task", update: event.update } };
1064
+ break;
1065
+ case "add_knowledge_message":
1066
+ input = {
1067
+ kind: "host_control",
1068
+ command: {
1069
+ kind: "seed_knowledge",
1070
+ entries: [{
1071
+ content: String(event.content ?? ""),
1072
+ ...(event.key ? { key: event.key } : {}),
1073
+ ...(event.tokens !== undefined ? { tokens: Number(event.tokens) } : {}),
1074
+ ...(event.pinned ? { pinned: true } : {}),
1075
+ }],
1076
+ },
1077
+ };
1078
+ break;
1079
+ case "remove_knowledge":
1080
+ input = {
1081
+ kind: "host_control",
1082
+ command: {
1083
+ kind: "apply_knowledge_mutation",
1084
+ mutation: { remove: [String(event.key ?? "")] },
1085
+ },
1086
+ };
1087
+ break;
1088
+ case "skill_deactivated":
1089
+ input = {
1090
+ kind: "host_control",
1091
+ command: {
1092
+ kind: "apply_skill_activation",
1093
+ deactivate: [String(event.name ?? "")],
1094
+ },
1095
+ };
1096
+ break;
1097
+ case "unsupported_effect":
1098
+ input = canonicalUnsupportedEffectResolution(String(event.effect_id ?? ""), String(event.effect_kind ?? ""));
1099
+ break;
1100
+ case "capability_command":
1101
+ input = { kind: "host_control", command: this.canonicalCapabilityCommand(asObject(event.command)) };
1102
+ break;
1103
+ case "add_history_message":
1104
+ throw new Error("running ABI v3 operations accept history only through effects or external events");
1105
+ default:
1106
+ throw new Error(`Node host fact has no canonical ABI input: ${String(event.kind)}`);
1107
+ }
1108
+ return this.commit(input);
1109
+ }
1110
+ async ensureConfigured() {
1111
+ if (this.configured)
1112
+ return;
1113
+ await this.commit({ kind: "configure_operation", config: this.config });
1114
+ this.configured = true;
1115
+ }
1116
+ async commit(input) {
1117
+ let nextInput = input;
1118
+ for (;;) {
1119
+ let transition;
1120
+ try {
1121
+ transition = await this.host.transition(nextInput);
1122
+ }
1123
+ catch (error) {
1124
+ if (!(error instanceof CanonicalKernelRebuildRequiredError) || !error.rebuilt)
1125
+ throw error;
1126
+ // The input's record is durable and the kernel was rebuilt from the journal with it
1127
+ // applied; only that step's observations are lost (the crash-window cost). Resync and
1128
+ // publish the rebuilt kernel's pending work instead of failing a healthy run.
1129
+ const lifecycle = this.host.kernel.lifecycle();
1130
+ this.configured = lifecycle !== "created";
1131
+ this.started = !["created", "configured"].includes(lifecycle);
1132
+ this.hostObservations.push({ kind: "kernel_rebuilt", reason: error.message });
1133
+ this.lastAction = this.currentAction();
1134
+ }
1135
+ if (transition) {
1136
+ if (!transition.replayed) {
1137
+ for (const raw of transition.plannedStep.observations ?? []) {
1138
+ const kind = String(raw.kind ?? "");
1139
+ if (!kind)
1140
+ throw new Error("canonical observation is missing kind");
1141
+ this.hostObservations.push({ ...raw, kind });
1142
+ }
1143
+ if (transition.checkpointAdvice) {
1144
+ this.hostObservations.push({
1145
+ kind: "checkpoint_advised",
1146
+ ...transition.checkpointAdvice,
1147
+ });
1148
+ }
1149
+ if (transition.checkpointFailure) {
1150
+ this.hostObservations.push({
1151
+ kind: "checkpoint_deferred",
1152
+ reason: transition.checkpointFailure,
1153
+ });
1154
+ }
1155
+ }
1156
+ this.lastAction = canonicalActionFromPlannedStep(transition.plannedStep);
1157
+ }
1158
+ if (this.lastAction?.kind !== "unsupported_effect")
1159
+ return this.lastAction;
1160
+ nextInput = canonicalUnsupportedEffectResolution(this.lastAction.effectId, this.lastAction.effectKind);
1161
+ }
1162
+ }
1163
+ currentAction() {
1164
+ const terminal = this.host.kernel.terminalJson();
1165
+ if (terminal) {
1166
+ return canonicalActionFromPlannedStep({
1167
+ disposition: { kind: "terminal", terminal: JSON.parse(terminal) },
1168
+ });
1169
+ }
1170
+ const effects = this.pendingEffects();
1171
+ return canonicalActionFromPlannedStep({
1172
+ disposition: { kind: "effects", effects },
1173
+ });
1174
+ }
1175
+ pendingEffects() {
1176
+ return JSON.parse(this.host.kernel.pendingEffectsJson());
1177
+ }
1178
+ succeededEffect(event, result) {
1179
+ return {
1180
+ kind: "resolve_effect",
1181
+ effect_id: String(event.effect_id ?? ""),
1182
+ outcome: { status: "succeeded", result },
1183
+ };
1184
+ }
1185
+ failedEffect(event, kind, message, retryable) {
1186
+ return {
1187
+ kind: "resolve_effect",
1188
+ effect_id: String(event.effect_id ?? ""),
1189
+ outcome: {
1190
+ status: "failed",
1191
+ failure: {
1192
+ kind,
1193
+ message,
1194
+ ...(retryable !== undefined ? { retryable } : {}),
1195
+ },
1196
+ },
1197
+ };
1198
+ }
1199
+ pageOutResolution(event) {
1200
+ const pending = this.pendingEffects().find(effect => effect.effect_id === event.effect_id);
1201
+ const effect = asObject(pending?.effect);
1202
+ const payload = asObject(effect.payload);
1203
+ const content = String(payload.content ?? "");
1204
+ return this.succeededEffect(event, {
1205
+ kind: "page_out_archived",
1206
+ receipt: {
1207
+ handle_id: String(effect.handle_id ?? ""),
1208
+ payload_ref: String(event.payload_ref ?? `payload:${randomUUID()}`),
1209
+ digest: String(payload.digest ?? sha256(content)),
1210
+ original_size: String(payload.original_size ?? Buffer.byteLength(content, "utf8")),
1211
+ },
1212
+ });
1213
+ }
1214
+ canonicalSignal(event) {
1215
+ const signal = asObject(event.signal);
1216
+ const deliveryId = String(event.delivery_id ?? randomUUID());
1217
+ const payload = deliveryId.startsWith("injected-") && typeof signal.summary === "string"
1218
+ ? signal.summary
1219
+ : signal.payload ?? {};
1220
+ return {
1221
+ kind: "deliver_signal",
1222
+ delivery_id: deliveryId,
1223
+ attempt: Number(event.attempt ?? 1),
1224
+ signal: {
1225
+ signal_id: String(signal.signal_id ?? signal.id ?? randomUUID()),
1226
+ ...(signal.source ? { source: signal.source } : {}),
1227
+ target: signal.recipient
1228
+ ? { kind: "task", task_id: String(signal.recipient) }
1229
+ : { kind: "operation" },
1230
+ ...(signal.urgency ? { urgency: signal.urgency } : {}),
1231
+ payload,
1232
+ ...(signal.timestamp_ms !== undefined ? { source_timestamp_ms: String(signal.timestamp_ms) } : {}),
1233
+ ...(signal.dedupe_key ? { dedupe_key: signal.dedupe_key } : {}),
1234
+ },
1235
+ };
1236
+ }
1237
+ canonicalCapabilityCommand(command) {
1238
+ const capability = asObject(command.capability);
1239
+ if (command.action === "mount") {
1240
+ return {
1241
+ kind: "apply_capability_patch",
1242
+ patch: {
1243
+ mount: [{
1244
+ kind: String(capability.kind ?? "tool"),
1245
+ id: String(capability.id ?? ""),
1246
+ ...(capability.description ? { description: capability.description } : {}),
1247
+ }],
1248
+ },
1249
+ };
1250
+ }
1251
+ return {
1252
+ kind: "apply_capability_patch",
1253
+ patch: {
1254
+ unmount: [{
1255
+ kind: String(command.kind ?? "tool"),
1256
+ id: String(command.id ?? ""),
1257
+ }],
1258
+ },
1259
+ };
1260
+ }
1261
+ applyBootstrapEvent(event) {
1262
+ switch (event.kind) {
1263
+ case "set_tokenizer":
1264
+ return true;
1265
+ case "set_plan_tool_enabled":
1266
+ this.featurePolicy().plan_tool_enabled = Boolean(event.enabled);
1267
+ return true;
1268
+ case "set_tools":
1269
+ this.config.tool_catalog = Array.isArray(event.tools) ? event.tools.map(asObject) : [];
1270
+ return true;
1271
+ case "add_system_message":
1272
+ this.initialContext.messages.push({
1273
+ role: "system",
1274
+ content: String(event.content ?? ""),
1275
+ ...(event.tokens !== undefined ? { tokens: Number(event.tokens) } : {}),
1276
+ });
1277
+ return true;
1278
+ case "add_knowledge_message":
1279
+ this.initialContext.knowledge.push({
1280
+ content: String(event.content ?? ""),
1281
+ ...(event.key ? { key: event.key } : {}),
1282
+ ...(event.tokens !== undefined ? { tokens: Number(event.tokens) } : {}),
1283
+ ...(event.pinned ? { pinned: true } : {}),
1284
+ });
1285
+ return true;
1286
+ case "set_available_skills":
1287
+ this.config.skill_catalog = event.skills ?? [];
1288
+ return true;
1289
+ case "set_stable_core_tools":
1290
+ this.featurePolicy().stable_core_tool_ids = event.tool_ids ?? [];
1291
+ return true;
1292
+ case "set_memory_enabled":
1293
+ this.featurePolicy().memory_enabled = Boolean(event.enabled);
1294
+ if (event.enabled) {
1295
+ this.config.memory_access = {
1296
+ binding_id: this.memoryBindingId,
1297
+ capabilities: { read: true, write: true },
1298
+ };
1299
+ }
1300
+ return true;
1301
+ case "set_knowledge_enabled":
1302
+ this.featurePolicy().knowledge_enabled = Boolean(event.enabled);
1303
+ return true;
1304
+ case "set_memory_policy":
1305
+ this.config.memory_policy = {
1306
+ ...(event.stale_warning_days !== undefined ? { stale_warning_days: event.stale_warning_days } : {}),
1307
+ ...(event.retrieval_top_k !== undefined ? { retrieval_top_k: event.retrieval_top_k } : {}),
1308
+ ...(event.validation_enabled !== undefined ? { validation_enabled: event.validation_enabled } : {}),
1309
+ ...(event.max_content_bytes !== undefined ? { max_content_bytes: event.max_content_bytes } : {}),
1310
+ ...(event.max_name_length !== undefined ? { max_name_length: event.max_name_length } : {}),
1311
+ ...(event.promotion_recall_threshold !== undefined
1312
+ ? { promotion_recall_threshold: String(event.promotion_recall_threshold) }
1313
+ : {}),
1314
+ };
1315
+ return true;
1316
+ case "load_milestone_contract": {
1317
+ const contract = asObject(event.contract);
1318
+ this.config.verification_contracts = [{
1319
+ contract_id: "node-default",
1320
+ phases: (Array.isArray(contract.phases) ? contract.phases : []).map(value => {
1321
+ const phase = asObject(value);
1322
+ return {
1323
+ phase_id: String(phase.id ?? ""),
1324
+ unlocks: (Array.isArray(phase.unlocks) ? phase.unlocks : []).map(unlock => {
1325
+ const item = asObject(unlock);
1326
+ return typeof unlock === "string" ? unlock : String(item.id ?? "");
1327
+ }).filter(Boolean),
1328
+ };
1329
+ }),
1330
+ }];
1331
+ return true;
1332
+ }
1333
+ case "preload_history":
1334
+ this.initialContext.messages.push(...(Array.isArray(event.messages) ? event.messages : []).map(value => canonicalInitialMessage(asObject(value))));
1335
+ return true;
1336
+ case "add_history_message":
1337
+ this.initialContext.messages.push(canonicalInitialMessage(asObject(event.message)));
1338
+ return true;
1339
+ case "configure_run":
1340
+ this.mergeHostConfig(asObject(event.config));
1341
+ return true;
1342
+ default:
1343
+ return false;
1344
+ }
1345
+ }
1346
+ featurePolicy() {
1347
+ const current = asObject(this.config.feature_policy);
1348
+ this.config.feature_policy = current;
1349
+ return current;
1350
+ }
1351
+ executionPolicy() {
1352
+ return asObject(this.config.execution_policy);
1353
+ }
1354
+ mergeHostConfig(config) {
1355
+ if (config.governance) {
1356
+ const governance = asObject(config.governance);
1357
+ this.config.governance_policy = {
1358
+ ...governance,
1359
+ rate_limits: (Array.isArray(governance.rate_limits) ? governance.rate_limits : []).map(value => {
1360
+ const rule = asObject(value);
1361
+ return { ...rule, window_ms: String(rule.window_ms ?? 0) };
1362
+ }),
1363
+ };
1364
+ }
1365
+ if (config.context_policy)
1366
+ this.config.context_policy = config.context_policy;
1367
+ if (config.signal_policy) {
1368
+ const signal = asObject(config.signal_policy);
1369
+ const { version: _version, ...rest } = signal;
1370
+ this.config.signal_policy = {
1371
+ ...rest,
1372
+ ...(rest.ttl_ms !== undefined ? { ttl_ms: String(rest.ttl_ms) } : {}),
1373
+ };
1374
+ }
1375
+ if (config.scheduler_policy) {
1376
+ const scheduler = asObject(config.scheduler_policy);
1377
+ const { version: _version, ...rest } = scheduler;
1378
+ this.config.scheduler_policy = rest;
1379
+ }
1380
+ if (config.resource_quota) {
1381
+ const quota = asObject(config.resource_quota);
1382
+ const window = Array.isArray(quota.memory_writes_per_window)
1383
+ ? quota.memory_writes_per_window
1384
+ : undefined;
1385
+ this.config.resource_quota = {
1386
+ ...quota,
1387
+ ...(window
1388
+ ? {
1389
+ memory_writes_per_window: {
1390
+ max_events: Number(window[0] ?? 0),
1391
+ window_ms: String(window[1] ?? 0),
1392
+ },
1393
+ }
1394
+ : {}),
1395
+ };
1396
+ }
1397
+ if (config.budget_grant) {
1398
+ const grant = asObject(config.budget_grant);
1399
+ this.config.budget_grant = {
1400
+ ...grant,
1401
+ ...(grant.tokens !== undefined ? { tokens: String(grant.tokens) } : {}),
1402
+ };
1403
+ }
1404
+ if (config.prompt_budget) {
1405
+ const context = asObject(this.config.context_policy);
1406
+ context.prompt_budget = config.prompt_budget;
1407
+ this.config.context_policy = context;
1408
+ }
1409
+ const execution = this.executionPolicy();
1410
+ if (config.repeat_fuse)
1411
+ execution.repeat_fuse = config.repeat_fuse;
1412
+ if (config.criteria_gate !== undefined)
1413
+ execution.criteria_gate_enabled = config.criteria_gate;
1414
+ if (config.entropy_watch) {
1415
+ const entropy = asObject(config.entropy_watch);
1416
+ execution.entropy_watch = {
1417
+ ...entropy,
1418
+ ...(typeof entropy.threshold === "number"
1419
+ ? { threshold_ppm: Math.round(entropy.threshold * 1_000_000) }
1420
+ : {}),
1421
+ ...(typeof entropy.hysteresis === "number"
1422
+ ? { hysteresis_ppm: Math.round(entropy.hysteresis * 1_000_000) }
1423
+ : {}),
1424
+ };
1425
+ delete asObject(execution.entropy_watch).threshold;
1426
+ delete asObject(execution.entropy_watch).hysteresis;
1427
+ }
1428
+ if (config.tool_dispatch_gate !== undefined) {
1429
+ this.featurePolicy().tool_dispatch_gate = config.tool_dispatch_gate;
1430
+ }
1431
+ if (config.knowledge_budget_ratio !== undefined) {
1432
+ const context = asObject(this.config.context_policy);
1433
+ context.knowledge_budget_ppm = Math.round(Number(config.knowledge_budget_ratio) * 1_000_000);
1434
+ this.config.context_policy = context;
1435
+ }
1436
+ if (config.reliability) {
1437
+ const reliability = asObject(config.reliability);
1438
+ this.config.recovery_policy = {
1439
+ ...(reliability.provider_recovery_attempts !== undefined
1440
+ ? { provider_recovery_attempts: reliability.provider_recovery_attempts }
1441
+ : {}),
1442
+ ...(reliability.output_recovery_attempts !== undefined
1443
+ ? { output_recovery_attempts: reliability.output_recovery_attempts }
1444
+ : {}),
1445
+ };
1446
+ if (reliability.max_input_bytes !== undefined) {
1447
+ this.config.kernel_limits = {
1448
+ ...asObject(this.config.kernel_limits),
1449
+ max_input_bytes: reliability.max_input_bytes,
1450
+ };
1451
+ }
1452
+ }
1453
+ }
1454
+ }
1455
+ export async function canonicalKernelApply(runtime, pending, event) {
1456
+ await runtime.applyHostEvent(event);
1457
+ const observations = runtime.drainHostObservations();
1458
+ pending.push(...observations);
1459
+ return observations;
1460
+ }
1461
+ export async function canonicalKernelMaybeAction(runtime, pending, event) {
1462
+ const action = await runtime.applyHostEvent(event);
1463
+ pending.push(...runtime.drainHostObservations());
1464
+ return action;
1465
+ }
1466
+ export async function canonicalKernelAction(runtime, pending, event) {
1467
+ const action = await canonicalKernelMaybeAction(runtime, pending, event);
1468
+ if (!action)
1469
+ throw new Error("canonical kernel transition must return one host action");
1470
+ return action;
1471
+ }
1472
+ export async function canonicalStartAgent(runtime, pending, task, runSpec) {
1473
+ const action = await runtime.startAgent(task, runSpec);
1474
+ pending.push(...runtime.drainHostObservations());
1475
+ if (!action)
1476
+ throw new Error("canonical agent root must return one host action");
1477
+ return action;
1478
+ }
1479
+ export async function canonicalStartWorkflow(runtime, pending, spec) {
1480
+ const action = await runtime.startWorkflow(spec);
1481
+ pending.push(...runtime.drainHostObservations());
1482
+ return action;
1483
+ }