@deepstrike/wasm 0.2.22 → 0.2.26

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.
@@ -1,8 +1,9 @@
1
1
  import { resolvePermissionRequest } from "./execution-plane.js";
2
- import { governancePolicyToKernelEvent } from "../governance.js";
2
+ import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
3
3
  import { getKernel } from "./kernel.js";
4
4
  import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
5
5
  import { sanitizeReplayText } from "./replay-sanitize.js";
6
+ import { formatToolError } from "../tools/errors.js";
6
7
  import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
7
8
  import { forceCompact, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
8
9
  import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
@@ -434,335 +435,402 @@ export class RuntimeRunner {
434
435
  ...(m.maxNameLength !== undefined ? { max_name_length: m.maxNameLength } : {}),
435
436
  });
436
437
  }
438
+ // I4: pre-fetch memory into the knowledge partition before the first LLM turn (mirrors Node).
439
+ if (!resumeMidRun && this.opts.preQueryMemory && this.opts.dreamStore && this.opts.agentId) {
440
+ try {
441
+ const queries = await this.opts.preQueryMemory({ goal });
442
+ const entries = [];
443
+ for (const q of queries ?? []) {
444
+ if (typeof q !== "string" || !q.trim())
445
+ continue;
446
+ const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
447
+ for (const hit of hits) {
448
+ entries.push({ content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`, source: "memory" });
449
+ }
450
+ }
451
+ if (entries.length > 0) {
452
+ kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
453
+ }
454
+ }
455
+ catch { /* errs-open */ }
456
+ }
437
457
  let action = resumeMidRun
438
458
  ? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
439
459
  : kernelAction(runtime, this.pendingObservations, startPayload);
440
460
  let hasAttemptedReactiveCompact = false;
441
461
  // P0-C: the skill loaded and in effect going into the current turn → per-turn `activeSkill` metric.
442
462
  let activeSkill;
443
- while (!runtime.isTerminal()) {
444
- if (action.kind === "execute_tool") {
445
- await this.applyKernelPageIn(runtime, sessionId);
446
- }
447
- nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
448
- if (this.interrupted) {
449
- action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
450
- break;
451
- }
452
- if (this.opts.signalSource) {
453
- const sig = await this.opts.signalSource.nextSignal();
454
- if (sig) {
455
- const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
456
- if (sigAction)
457
- action = sigAction;
463
+ // I0b: kernel-throw safety net — see Node runner for full rationale.
464
+ try {
465
+ while (!runtime.isTerminal()) {
466
+ if (action.kind === "execute_tool") {
467
+ await this.applyKernelPageIn(runtime, sessionId);
458
468
  }
459
- }
460
- if (runtime.isTerminal())
461
- break;
462
- if (action.kind === "call_provider") {
463
- // M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
464
- // placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
465
- // authored spec is never stranded. Drains the queue; fires once per authored batch.
466
- if (this.pendingAuthoredWorkflows.length > 0) {
467
- action = await this.driveAuthoredWorkflows(runtime, action);
469
+ nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
470
+ if (this.interrupted) {
471
+ action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
472
+ break;
468
473
  }
469
- const finalToolCalls = [];
470
- let finalText = "";
471
- const context = action.context;
472
- const tools = action.tools;
473
- let turnTokens = 0;
474
- let turnInputTokens = 0;
475
- let turnOutputTokens = 0;
476
- let turnCacheReadTokens = 0;
477
- let turnCacheCreationTokens = 0;
478
- let shouldRetry = false;
479
- const abortSignal = this.abortController?.signal;
480
- try {
481
- for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
482
- // #2-B-ii: a preempting interrupt fires abortController — stop consuming the live stream.
483
- if (abortSignal?.aborted)
484
- break;
485
- if (evt.type === "usage") {
486
- const usageEvt = evt;
487
- turnTokens = usageEvt.totalTokens;
488
- turnInputTokens = usageEvt.inputTokens ?? 0;
489
- turnOutputTokens = usageEvt.outputTokens ?? 0;
490
- // P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
491
- turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
492
- turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
493
- continue;
494
- }
495
- yield evt;
496
- if (evt.type === "text_delta")
497
- finalText += evt.delta;
498
- else if (evt.type === "tool_call") {
499
- const tc = evt;
500
- finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
501
- }
474
+ if (this.opts.signalSource) {
475
+ const sig = await this.opts.signalSource.nextSignal();
476
+ if (sig) {
477
+ const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
478
+ if (sigAction)
479
+ action = sigAction;
480
+ // I0a: Critical signal carries user_abort intent; see Node runner for full rationale.
481
+ if (sig.urgency === "critical")
482
+ this.interrupted = true;
502
483
  }
503
484
  }
504
- catch (err) {
505
- // #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat as an interrupt.
506
- if (abortSignal?.aborted) {
507
- this.interrupted = true;
485
+ if (runtime.isTerminal())
486
+ break;
487
+ if (action.kind === "call_provider") {
488
+ // M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
489
+ // placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
490
+ // authored spec is never stranded. Drains the queue; fires once per authored batch.
491
+ if (this.pendingAuthoredWorkflows.length > 0) {
492
+ action = await this.driveAuthoredWorkflows(runtime, action);
493
+ }
494
+ const finalToolCalls = [];
495
+ let finalText = "";
496
+ // I5: governance schema-level pre-filter — see Node runner for full rationale.
497
+ let context = action.context;
498
+ let tools = action.tools;
499
+ if (this.opts.governancePolicy && this.opts.governancePolicy.surfaceDeniedInSystem !== false) {
500
+ const { allowed, denied } = governanceFilterSchema(tools, this.opts.governancePolicy);
501
+ if (denied.length > 0) {
502
+ tools = allowed;
503
+ const note = `[governance] the following tools are denied for this run and will fail if called: ${denied.join(", ")}.`;
504
+ context = {
505
+ ...context,
506
+ systemKnowledge: context.systemKnowledge
507
+ ? `${context.systemKnowledge}\n\n${note}`
508
+ : note,
509
+ };
510
+ }
511
+ }
512
+ let turnTokens = 0;
513
+ let turnInputTokens = 0;
514
+ let turnOutputTokens = 0;
515
+ let turnCacheReadTokens = 0;
516
+ let turnCacheCreationTokens = 0;
517
+ let turnCacheReadBySlot;
518
+ let shouldRetry = false;
519
+ const abortSignal = this.abortController?.signal;
520
+ try {
521
+ for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
522
+ // #2-B-ii: a preempting interrupt fires abortController — stop consuming the live stream.
523
+ if (abortSignal?.aborted)
524
+ break;
525
+ if (evt.type === "usage") {
526
+ const usageEvt = evt;
527
+ turnTokens = usageEvt.totalTokens;
528
+ turnInputTokens = usageEvt.inputTokens ?? 0;
529
+ turnOutputTokens = usageEvt.outputTokens ?? 0;
530
+ // P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
531
+ turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
532
+ turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
533
+ // I1: per-slot attribution forwarded to TurnMetrics; undefined on non-Anthropic providers.
534
+ turnCacheReadBySlot = usageEvt.cacheReadInputTokensBySlot;
535
+ continue;
536
+ }
537
+ yield evt;
538
+ if (evt.type === "text_delta")
539
+ finalText += evt.delta;
540
+ else if (evt.type === "tool_call") {
541
+ const tc = evt;
542
+ finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
543
+ }
544
+ }
508
545
  }
509
- const errMsg = String(err).toLowerCase();
510
- if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
511
- !hasAttemptedReactiveCompact) {
512
- hasAttemptedReactiveCompact = true;
513
- if (forceCompact(runtime, this.pendingObservations)) {
514
- nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
515
- shouldRetry = true;
546
+ catch (err) {
547
+ // #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat as an interrupt.
548
+ if (abortSignal?.aborted) {
549
+ this.interrupted = true;
550
+ }
551
+ const errMsg = formatToolError(err).toLowerCase();
552
+ if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
553
+ !hasAttemptedReactiveCompact) {
554
+ hasAttemptedReactiveCompact = true;
555
+ if (forceCompact(runtime, this.pendingObservations)) {
556
+ nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
557
+ shouldRetry = true;
558
+ }
559
+ }
560
+ if (!shouldRetry) {
561
+ yield { type: "error", message: formatToolError(err) };
562
+ action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
563
+ break;
516
564
  }
517
565
  }
518
- if (!shouldRetry) {
519
- yield { type: "error", message: String(err) };
566
+ // #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
567
+ if (abortSignal?.aborted) {
520
568
  action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
521
569
  break;
522
570
  }
523
- }
524
- // #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
525
- if (abortSignal?.aborted) {
526
- action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
527
- break;
528
- }
529
- if (shouldRetry) {
530
- action = {
531
- kind: "call_provider",
532
- context: runtime.render(),
533
- tools,
571
+ if (shouldRetry) {
572
+ action = {
573
+ kind: "call_provider",
574
+ context: runtime.render(),
575
+ tools,
576
+ };
577
+ continue;
578
+ }
579
+ const assistantMessage = {
580
+ role: "assistant",
581
+ content: finalText,
582
+ toolCalls: finalToolCalls,
583
+ tokenCount: turnOutputTokens || turnTokens || undefined,
534
584
  };
535
- continue;
536
- }
537
- const assistantMessage = {
538
- role: "assistant",
539
- content: finalText,
540
- toolCalls: finalToolCalls,
541
- tokenCount: turnOutputTokens || turnTokens || undefined,
542
- };
543
- const providerEvent = {
544
- kind: "provider_result",
545
- message: messageToKernelMessage(assistantMessage),
546
- ...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
547
- ...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
548
- now_ms: Date.now(),
549
- };
550
- let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
551
- const hasSuspended = this.pendingObservations.some(o => o.kind === "suspended");
552
- if (!nextAction && hasSuspended) {
553
- const resolved = await this.resolveKernelSuspend(runtime, sessionId);
554
- for (const evt of resolved.events)
555
- yield evt;
556
- nextAction = kernelAction(runtime, this.pendingObservations, {
557
- kind: "resume",
558
- approved_calls: resolved.approved,
559
- denied_calls: resolved.denied,
560
- });
561
- }
562
- action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
563
- const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
564
- await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
565
- turn: runtime.turn(),
566
- content: finalText,
567
- tokenCount: turnOutputTokens || turnTokens || undefined,
568
- toolCalls: finalToolCalls,
569
- providerReplay,
570
- }));
571
- // P0-C: per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect GOING INTO
572
- // this turn; a `skill` call here only takes effect next turn — emit first, then advance.
573
- if (this.opts.onTurnMetrics) {
574
- try {
575
- this.opts.onTurnMetrics({
576
- turn: runtime.turn(),
577
- toolsExposed: tools.length,
578
- toolsCalled: finalToolCalls.length,
579
- activeSkill,
580
- inputTokens: turnInputTokens,
581
- cacheReadTokens: turnCacheReadTokens,
582
- cacheCreationTokens: turnCacheCreationTokens,
585
+ const providerEvent = {
586
+ kind: "provider_result",
587
+ message: messageToKernelMessage(assistantMessage),
588
+ ...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
589
+ ...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
590
+ now_ms: Date.now(),
591
+ };
592
+ let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
593
+ const hasSuspended = this.pendingObservations.some(o => o.kind === "suspended");
594
+ if (!nextAction && hasSuspended) {
595
+ const resolved = await this.resolveKernelSuspend(runtime, sessionId);
596
+ for (const evt of resolved.events)
597
+ yield evt;
598
+ nextAction = kernelAction(runtime, this.pendingObservations, {
599
+ kind: "resume",
600
+ approved_calls: resolved.approved,
601
+ denied_calls: resolved.denied,
583
602
  });
584
603
  }
585
- catch { /* metrics must never break the run */ }
586
- }
587
- const skillCall = finalToolCalls.find(c => c.name === "skill");
588
- if (skillCall) {
589
- try {
590
- const name = JSON.parse(skillCall.arguments || "{}").name;
591
- if (name)
592
- activeSkill = name;
604
+ action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
605
+ const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
606
+ await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
607
+ turn: runtime.turn(),
608
+ content: finalText,
609
+ tokenCount: turnOutputTokens || turnTokens || undefined,
610
+ toolCalls: finalToolCalls,
611
+ providerReplay,
612
+ }));
613
+ // P0-C: per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect GOING INTO
614
+ // this turn; a `skill` call here only takes effect next turn — emit first, then advance.
615
+ if (this.opts.onTurnMetrics) {
616
+ try {
617
+ this.opts.onTurnMetrics({
618
+ turn: runtime.turn(),
619
+ toolsExposed: tools.length,
620
+ toolsCalled: finalToolCalls.length,
621
+ activeSkill,
622
+ inputTokens: turnInputTokens,
623
+ cacheReadTokens: turnCacheReadTokens,
624
+ cacheCreationTokens: turnCacheCreationTokens,
625
+ ...(turnCacheReadBySlot ? { cacheReadTokensBySlot: turnCacheReadBySlot } : {}),
626
+ });
627
+ }
628
+ catch { /* metrics must never break the run */ }
593
629
  }
594
- catch { /* malformed skill args leave activeSkill unchanged */ }
595
- }
596
- }
597
- else if (action.kind === "execute_tool") {
598
- const allCalls = action.calls;
599
- await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
600
- const runCtx = {
601
- agentId: this.opts.agentId,
602
- skillContentMap: this.opts.skillContentMap,
603
- dreamStore: this.opts.dreamStore,
604
- knowledgeSource: this.opts.knowledgeSource,
605
- onToolSuspend: this.opts.onToolSuspend,
606
- onPermissionRequest: this.opts.onPermissionRequest,
607
- };
608
- const toolResults = [];
609
- // R3-1: intercept `submit_workflow_nodes` — it can't apply to this runner's kernel (when this
610
- // runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
611
- // the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
612
- // M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
613
- const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
614
- const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
615
- for (const call of submitCalls) {
616
- // M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
617
- // spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
618
- // every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
619
- if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
620
- const spec = parseStartWorkflowSpec(call.arguments);
621
- if (spec) {
622
- this.pendingAuthoredWorkflows.push(spec);
623
- const out = "workflow authored; executing now";
624
- toolResults.push({ callId: call.id, output: out, isError: false });
625
- yield { type: "tool_result", callId: call.id, content: out, isError: false };
626
- continue;
630
+ const skillCall = finalToolCalls.find(c => c.name === "skill");
631
+ if (skillCall) {
632
+ try {
633
+ const name = JSON.parse(skillCall.arguments || "{}").name;
634
+ if (name)
635
+ activeSkill = name;
627
636
  }
637
+ catch { /* malformed skill args — leave activeSkill unchanged */ }
628
638
  }
629
- const nodes = call.name === "start_workflow"
630
- ? parseStartWorkflowArgs(call.arguments)
631
- : parseSubmitWorkflowNodesArgs(call.arguments);
632
- yield { type: "workflow_nodes_submitted", nodes };
633
- toolResults.push({ callId: call.id, output: "submitted", isError: false });
634
- yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
635
639
  }
636
- for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
637
- yield evt;
638
- if (evt.type === "tool_result") {
639
- const tre = evt;
640
- toolResults.push({
641
- callId: tre.callId,
642
- output: tre.content,
643
- isError: tre.isError,
644
- isFatal: tre.isFatal,
645
- errorKind: tre.errorKind,
646
- });
640
+ else if (action.kind === "execute_tool") {
641
+ const allCalls = action.calls;
642
+ await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
643
+ const runCtx = {
644
+ agentId: this.opts.agentId,
645
+ skillContentMap: this.opts.skillContentMap,
646
+ dreamStore: this.opts.dreamStore,
647
+ knowledgeSource: this.opts.knowledgeSource,
648
+ onToolSuspend: this.opts.onToolSuspend,
649
+ onPermissionRequest: this.opts.onPermissionRequest,
650
+ };
651
+ const toolResults = [];
652
+ // R3-1: intercept `submit_workflow_nodes` — it can't apply to this runner's kernel (when this
653
+ // runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
654
+ // the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
655
+ // M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
656
+ const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
657
+ const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
658
+ for (const call of submitCalls) {
659
+ // M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
660
+ // spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
661
+ // every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
662
+ if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
663
+ const spec = parseStartWorkflowSpec(call.arguments);
664
+ if (spec) {
665
+ this.pendingAuthoredWorkflows.push(spec);
666
+ const out = "workflow authored; executing now";
667
+ toolResults.push({ callId: call.id, output: out, isError: false });
668
+ yield { type: "tool_result", callId: call.id, content: out, isError: false };
669
+ continue;
670
+ }
671
+ }
672
+ const nodes = call.name === "start_workflow"
673
+ ? parseStartWorkflowArgs(call.arguments)
674
+ : parseSubmitWorkflowNodesArgs(call.arguments);
675
+ yield { type: "workflow_nodes_submitted", nodes };
676
+ toolResults.push({ callId: call.id, output: "submitted", isError: false });
677
+ yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
647
678
  }
648
- else if (evt.type === "tool_argument_repaired") {
649
- const tare = evt;
650
- await this.opts.sessionLog.append(sessionId, {
651
- kind: "tool_argument_repaired",
652
- turn: runtime.turn(),
653
- tool: tare.name,
654
- original_arguments: tare.originalArguments,
655
- repaired_arguments: tare.repairedArguments,
656
- });
679
+ for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
680
+ yield evt;
681
+ if (evt.type === "tool_result") {
682
+ const tre = evt;
683
+ toolResults.push({
684
+ callId: tre.callId,
685
+ output: tre.content,
686
+ isError: tre.isError,
687
+ isFatal: tre.isFatal,
688
+ errorKind: tre.errorKind,
689
+ });
690
+ }
691
+ else if (evt.type === "tool_argument_repaired") {
692
+ const tare = evt;
693
+ await this.opts.sessionLog.append(sessionId, {
694
+ kind: "tool_argument_repaired",
695
+ turn: runtime.turn(),
696
+ tool: tare.name,
697
+ original_arguments: tare.originalArguments,
698
+ repaired_arguments: tare.repairedArguments,
699
+ });
700
+ }
701
+ else if (evt.type === "tool_denied") {
702
+ const tde = evt;
703
+ await this.opts.sessionLog.append(sessionId, {
704
+ kind: "tool_denied",
705
+ turn: runtime.turn(),
706
+ call_id: tde.callId,
707
+ tool_name: tde.toolName,
708
+ reason: tde.reason,
709
+ });
710
+ }
711
+ else if (evt.type === "permission_request") {
712
+ const pre = evt;
713
+ const turn = runtime.turn();
714
+ await this.opts.sessionLog.append(sessionId, {
715
+ kind: "permission_requested",
716
+ turn,
717
+ tool: pre.toolName,
718
+ arguments: typeof pre.arguments === "string" ? pre.arguments : JSON.stringify(pre.arguments),
719
+ reason: pre.reason,
720
+ });
721
+ }
722
+ else if (evt.type === "permission_resolved") {
723
+ const resolved = evt;
724
+ const turn = runtime.turn();
725
+ await this.opts.sessionLog.append(sessionId, {
726
+ kind: "permission_resolved",
727
+ turn,
728
+ approved: resolved.approved,
729
+ responder: resolved.responder,
730
+ });
731
+ }
657
732
  }
658
- else if (evt.type === "tool_denied") {
659
- const tde = evt;
660
- await this.opts.sessionLog.append(sessionId, {
661
- kind: "tool_denied",
662
- turn: runtime.turn(),
663
- call_id: tde.callId,
664
- tool_name: tde.toolName,
665
- reason: tde.reason,
666
- });
733
+ await this.opts.sessionLog.append(sessionId, {
734
+ kind: "tool_completed",
735
+ turn: runtime.turn(),
736
+ results: toolResults.map(r => ({
737
+ call_id: r.callId,
738
+ output: r.output,
739
+ is_error: r.isError,
740
+ token_count: r.tokenCount,
741
+ })),
742
+ });
743
+ for (const call of allCalls) {
744
+ const result = toolResults.find(r => r.callId === call.id);
745
+ if (result) {
746
+ this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
747
+ }
667
748
  }
668
- else if (evt.type === "permission_request") {
669
- const pre = evt;
670
- const turn = runtime.turn();
671
- await this.opts.sessionLog.append(sessionId, {
672
- kind: "permission_requested",
673
- turn,
674
- tool: pre.toolName,
675
- arguments: typeof pre.arguments === "string" ? pre.arguments : JSON.stringify(pre.arguments),
676
- reason: pre.reason,
677
- });
749
+ // P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
750
+ for (const call of allCalls) {
751
+ if (call.name !== "skill")
752
+ continue;
753
+ const res = toolResults.find(r => r.callId === call.id);
754
+ if (!res || res.isError)
755
+ continue;
756
+ try {
757
+ const name = JSON.parse(call.arguments || "{}").name;
758
+ if (name)
759
+ kernelApply(runtime, this.pendingObservations, { kind: "skill_activated", name });
760
+ }
761
+ catch { /* skip */ }
678
762
  }
679
- else if (evt.type === "permission_resolved") {
680
- const resolved = evt;
681
- const turn = runtime.turn();
682
- await this.opts.sessionLog.append(sessionId, {
683
- kind: "permission_resolved",
684
- turn,
685
- approved: resolved.approved,
686
- responder: resolved.responder,
763
+ action = kernelAction(runtime, this.pendingObservations, {
764
+ kind: "tool_results",
765
+ results: toolResults.map(toolResultToKernel),
766
+ });
767
+ }
768
+ else if (action.kind === "evaluate_milestone") {
769
+ const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
770
+ if (milestonePolicy === "auto_pass") {
771
+ action = kernelAction(runtime, this.pendingObservations, {
772
+ kind: "milestone_result",
773
+ result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
687
774
  });
775
+ nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
688
776
  }
689
- }
690
- await this.opts.sessionLog.append(sessionId, {
691
- kind: "tool_completed",
692
- turn: runtime.turn(),
693
- results: toolResults.map(r => ({
694
- call_id: r.callId,
695
- output: r.output,
696
- is_error: r.isError,
697
- token_count: r.tokenCount,
698
- })),
699
- });
700
- for (const call of allCalls) {
701
- const result = toolResults.find(r => r.callId === call.id);
702
- if (result) {
703
- this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
777
+ else if (this.opts.onMilestoneEvaluate) {
778
+ const check = await this.opts.onMilestoneEvaluate({
779
+ phaseId: action.phaseId,
780
+ criteria: action.criteria ?? [],
781
+ requiredEvidence: action.requiredEvidence ?? [],
782
+ });
783
+ action = kernelAction(runtime, this.pendingObservations, {
784
+ kind: "milestone_result",
785
+ result: milestoneCheckResultToKernel(check),
786
+ });
787
+ nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
704
788
  }
705
- }
706
- // P1-B B3: a successfully-resolved `skill` call activates that skill for the next turn.
707
- for (const call of allCalls) {
708
- if (call.name !== "skill")
709
- continue;
710
- const res = toolResults.find(r => r.callId === call.id);
711
- if (!res || res.isError)
712
- continue;
713
- try {
714
- const name = JSON.parse(call.arguments || "{}").name;
715
- if (name)
716
- kernelApply(runtime, this.pendingObservations, { kind: "skill_activated", name });
789
+ else {
790
+ nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
791
+ const turnsUsed = Math.max(1, runtime.turn());
792
+ await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
793
+ reason: "milestone_pending",
794
+ turnsUsed,
795
+ totalTokens: 0,
796
+ }));
797
+ yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
798
+ this.activeKernel = null;
799
+ this.currentSessionId = null;
800
+ return;
717
801
  }
718
- catch { /* skip */ }
719
802
  }
720
- action = kernelAction(runtime, this.pendingObservations, {
721
- kind: "tool_results",
722
- results: toolResults.map(toolResultToKernel),
723
- });
724
- }
725
- else if (action.kind === "evaluate_milestone") {
726
- const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
727
- if (milestonePolicy === "auto_pass") {
728
- action = kernelAction(runtime, this.pendingObservations, {
729
- kind: "milestone_result",
730
- result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
731
- });
732
- nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
733
- }
734
- else if (this.opts.onMilestoneEvaluate) {
735
- const check = await this.opts.onMilestoneEvaluate({
736
- phaseId: action.phaseId,
737
- criteria: action.criteria ?? [],
738
- requiredEvidence: action.requiredEvidence ?? [],
739
- });
740
- action = kernelAction(runtime, this.pendingObservations, {
741
- kind: "milestone_result",
742
- result: milestoneCheckResultToKernel(check),
743
- });
744
- nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
745
- }
746
- else {
747
- nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
748
- const turnsUsed = Math.max(1, runtime.turn());
749
- await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
750
- reason: "milestone_pending",
751
- turnsUsed,
752
- totalTokens: 0,
753
- }));
754
- yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
755
- this.activeKernel = null;
756
- this.currentSessionId = null;
757
- return;
803
+ else if (action.kind === "done") {
804
+ break;
758
805
  }
759
806
  }
760
- else if (action.kind === "done") {
761
- break;
807
+ }
808
+ catch (err) {
809
+ // I0b: kernel rejection (or any other thrown error inside the loop) is observable here —
810
+ // emit run_terminal so downstream code sees a clean end rather than mid-loop EOF.
811
+ const errMsg = formatToolError(err);
812
+ const code = err.code;
813
+ const isInvalidArg = code === "InvalidArg" ||
814
+ errMsg.toLowerCase().includes("invalidarg") ||
815
+ errMsg.toLowerCase().includes("invalid argument");
816
+ const reason = isInvalidArg ? "invalid_arg" : "error";
817
+ yield { type: "error", message: errMsg };
818
+ try {
819
+ await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
820
+ reason,
821
+ turnsUsed: runtime.turn() || 0,
822
+ totalTokens: 0,
823
+ }));
762
824
  }
825
+ catch { /* session log failure must not mask the original error */ }
826
+ yield { type: "done", iterations: runtime.turn() || 0, totalTokens: 0, status: reason };
827
+ this.activeKernel = null;
828
+ this.currentSessionId = null;
829
+ return;
763
830
  }
764
831
  const result = action.kind === "done" ? action.result : undefined;
765
- const status = result?.termination ?? "error";
832
+ // I0a: preserve preempt intent when loop exits without clean kernel-done (see Node runner for full rationale).
833
+ const status = result?.termination ?? (this.interrupted ? "user_abort" : "error");
766
834
  const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
767
835
  const totalTokens = result?.totalTokensUsed ?? 0;
768
836
  nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
@@ -931,7 +999,7 @@ export class RuntimeRunner {
931
999
  return ok(reducer(inputs), "completed");
932
1000
  }
933
1001
  catch (err) {
934
- return ok(`reducer "${node.reducer}" threw: ${err instanceof Error ? err.message : String(err)}`, "error");
1002
+ return ok(`reducer "${node.reducer}" threw: ${formatToolError(err)}`, "error");
935
1003
  }
936
1004
  }
937
1005
  /**