@deepstrike/sdk 0.2.22 → 0.2.24
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.
- package/dist/governance.d.ts +21 -0
- package/dist/governance.js +32 -0
- package/dist/harness/harness.d.ts +30 -0
- package/dist/harness/harness.js +40 -20
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -0
- package/dist/providers/anthropic.js +60 -1
- package/dist/runtime/execution-plane.js +32 -4
- package/dist/runtime/mcp-proxy-plane.js +2 -1
- package/dist/runtime/process-sandbox-plane.js +2 -1
- package/dist/runtime/remote-vpc-plane.js +2 -1
- package/dist/runtime/runner.d.ts +21 -0
- package/dist/runtime/runner.js +425 -333
- package/dist/tools/errors.d.ts +59 -0
- package/dist/tools/errors.js +113 -0
- package/dist/tools/index.d.ts +9 -1
- package/dist/tools/index.js +36 -1
- package/dist/types.d.ts +19 -0
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -11,10 +11,11 @@ import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
|
11
11
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
12
12
|
import { resolveReducer } from "./reducers.js";
|
|
13
13
|
import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
14
|
-
import { governancePolicyToKernelEvent } from "../governance.js";
|
|
14
|
+
import { governancePolicyToKernelEvent, governanceFilterSchema } from "../governance.js";
|
|
15
15
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
16
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
17
17
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
18
|
+
import { formatToolError } from "../tools/errors.js";
|
|
18
19
|
export class RuntimeRunner {
|
|
19
20
|
opts;
|
|
20
21
|
interrupted = false;
|
|
@@ -381,7 +382,7 @@ export class RuntimeRunner {
|
|
|
381
382
|
return ok(reducer(inputs), "completed");
|
|
382
383
|
}
|
|
383
384
|
catch (err) {
|
|
384
|
-
return ok(`reducer "${node.reducer}" threw: ${
|
|
385
|
+
return ok(`reducer "${node.reducer}" threw: ${formatToolError(err)}`, "error");
|
|
385
386
|
}
|
|
386
387
|
}
|
|
387
388
|
/**
|
|
@@ -973,6 +974,26 @@ export class RuntimeRunner {
|
|
|
973
974
|
message: attachmentsToKernelMessage(attachments),
|
|
974
975
|
});
|
|
975
976
|
}
|
|
977
|
+
// I4: pre-fetch memory into the knowledge partition before the first LLM turn. Skipped on
|
|
978
|
+
// resumes (memory was already on the prior context) and when dreamStore/agentId is absent.
|
|
979
|
+
if (!resumeMidRun && this.opts.preQueryMemory && this.opts.dreamStore && this.opts.agentId) {
|
|
980
|
+
try {
|
|
981
|
+
const queries = await this.opts.preQueryMemory({ goal, runSpec: this.opts.runSpec });
|
|
982
|
+
const entries = [];
|
|
983
|
+
for (const q of queries ?? []) {
|
|
984
|
+
if (typeof q !== "string" || !q.trim())
|
|
985
|
+
continue;
|
|
986
|
+
const hits = await this.opts.dreamStore.search(this.opts.agentId, q, 5);
|
|
987
|
+
for (const hit of hits) {
|
|
988
|
+
entries.push({ content: `[memory score=${hit.score.toFixed(3)}] ${hit.text}`, source: "memory" });
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (entries.length > 0) {
|
|
992
|
+
kernelApply(runtime, this.pendingObservations, { kind: "page_in", entries });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
catch { /* errs-open — a faulty pre-fetch never breaks the run */ }
|
|
996
|
+
}
|
|
976
997
|
let action = resumeMidRun
|
|
977
998
|
? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
978
999
|
: kernelAction(runtime, this.pendingObservations, startPayload);
|
|
@@ -980,369 +1001,440 @@ export class RuntimeRunner {
|
|
|
980
1001
|
// P0-C: the skill loaded and in effect going into the current turn (updated when the model's
|
|
981
1002
|
// `skill` tool call resolves). Drives the per-turn `activeSkill` metric → dwell measurement.
|
|
982
1003
|
let activeSkill;
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
if (this.opts.signalSource) {
|
|
995
|
-
const sig = await this.opts.signalSource.nextSignal();
|
|
996
|
-
if (sig) {
|
|
997
|
-
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
998
|
-
// `signal_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
999
|
-
// ignored yields none (kernel buffers).
|
|
1000
|
-
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
1001
|
-
if (sigAction)
|
|
1002
|
-
action = sigAction;
|
|
1004
|
+
// I0b: wrap the main loop so any uncaught kernel exception (typically a NAPI
|
|
1005
|
+
// Status::InvalidArg from a malformed input — e.g. RuntimeSignal.source with a wrong shape,
|
|
1006
|
+
// or an unrecognized event kind) is observable rather than silently propagating out of the
|
|
1007
|
+
// async generator. Without this wrap the runner emits no `run_terminal` event, so downstream
|
|
1008
|
+
// observability (session log, bench mechanism hooks) can't distinguish "the kernel rejected
|
|
1009
|
+
// an input" from "the run is still in progress."
|
|
1010
|
+
try {
|
|
1011
|
+
while (!runtime.isTerminal()) {
|
|
1012
|
+
// Page-in must run before appendObservations drains pending kernel observations.
|
|
1013
|
+
if (action.kind === "execute_tool") {
|
|
1014
|
+
await this.applyKernelPageIn(runtime, sessionId);
|
|
1003
1015
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1010
|
-
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1011
|
-
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1012
|
-
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1013
|
-
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1014
|
-
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1015
|
-
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1016
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1017
|
+
this.nextArchiveStart = nextCompressedArchiveStart;
|
|
1018
|
+
if (this.interrupted) {
|
|
1019
|
+
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1020
|
+
break;
|
|
1016
1021
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
//
|
|
1031
|
-
//
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
break;
|
|
1035
|
-
if (evt.type === "usage") {
|
|
1036
|
-
const usageEvt = evt;
|
|
1037
|
-
turnTokens = usageEvt.totalTokens;
|
|
1038
|
-
turnInputTokens = usageEvt.inputTokens ?? 0;
|
|
1039
|
-
turnOutputTokens = usageEvt.outputTokens ?? 0;
|
|
1040
|
-
// P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
|
|
1041
|
-
turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
|
|
1042
|
-
turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
|
|
1043
|
-
continue;
|
|
1044
|
-
}
|
|
1045
|
-
yield evt;
|
|
1046
|
-
if (evt.type === "text_delta")
|
|
1047
|
-
finalText += evt.delta;
|
|
1048
|
-
else if (evt.type === "tool_call") {
|
|
1049
|
-
const tc = evt;
|
|
1050
|
-
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
1051
|
-
}
|
|
1022
|
+
if (this.opts.signalSource) {
|
|
1023
|
+
const sig = await this.opts.signalSource.nextSignal();
|
|
1024
|
+
if (sig) {
|
|
1025
|
+
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
1026
|
+
// `signal_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
1027
|
+
// ignored yields none (kernel buffers).
|
|
1028
|
+
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
1029
|
+
if (sigAction)
|
|
1030
|
+
action = sigAction;
|
|
1031
|
+
// I0a: a Critical-urgency signal carries user_abort intent. The kernel disposes it as
|
|
1032
|
+
// InterruptNow (forces a Reason turn) but does NOT call abortController.abort() unless
|
|
1033
|
+
// sub-agents are suspended — so the no-sub-agent path (e.g. the signal-injection bench
|
|
1034
|
+
// scenario) wouldn't otherwise set `this.interrupted`, and the eventual run_terminal would
|
|
1035
|
+
// report `reason: "error"` indistinguishable from a crash. Mark it here so the final
|
|
1036
|
+
// classification in the run_terminal emit picks `user_abort`.
|
|
1037
|
+
if (sig.urgency === "critical")
|
|
1038
|
+
this.interrupted = true;
|
|
1052
1039
|
}
|
|
1053
1040
|
}
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1041
|
+
if (runtime.isTerminal())
|
|
1042
|
+
break;
|
|
1043
|
+
if (action.kind === "call_provider") {
|
|
1044
|
+
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
1045
|
+
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
1046
|
+
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
1047
|
+
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
1048
|
+
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
1049
|
+
// is never stranded. Drains the queue; fires once per authored batch.
|
|
1050
|
+
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
1051
|
+
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
1059
1052
|
}
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1053
|
+
const finalToolCalls = [];
|
|
1054
|
+
let finalText = "";
|
|
1055
|
+
// I5: governance schema-level pre-filter. When a declarative GovernancePolicy is loaded
|
|
1056
|
+
// and `surfaceDeniedInSystem !== false`, drop denied tools from the schema BEFORE the
|
|
1057
|
+
// model sees them — the model can't plan a call it doesn't know about, so the rollback
|
|
1058
|
+
// overhead disappears. The list of denied names is appended to systemKnowledge so the
|
|
1059
|
+
// model knows not to plan around them.
|
|
1060
|
+
let context = action.context;
|
|
1061
|
+
let tools = action.tools;
|
|
1062
|
+
if (this.opts.governancePolicy && this.opts.governancePolicy.surfaceDeniedInSystem !== false) {
|
|
1063
|
+
const { allowed, denied } = governanceFilterSchema(tools, this.opts.governancePolicy);
|
|
1064
|
+
if (denied.length > 0) {
|
|
1065
|
+
tools = allowed;
|
|
1066
|
+
const note = `[governance] the following tools are denied for this run and will fail if called: ${denied.join(", ")}.`;
|
|
1067
|
+
context = {
|
|
1068
|
+
...context,
|
|
1069
|
+
systemKnowledge: context.systemKnowledge
|
|
1070
|
+
? `${context.systemKnowledge}\n\n${note}`
|
|
1071
|
+
: note,
|
|
1072
|
+
};
|
|
1067
1073
|
}
|
|
1068
1074
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1075
|
+
let turnTokens = 0;
|
|
1076
|
+
let turnInputTokens = 0;
|
|
1077
|
+
let turnOutputTokens = 0;
|
|
1078
|
+
let turnCacheReadTokens = 0;
|
|
1079
|
+
let turnCacheCreationTokens = 0;
|
|
1080
|
+
let turnCacheReadBySlot;
|
|
1081
|
+
let shouldRetry = false;
|
|
1082
|
+
const abortSignal = this.abortController?.signal;
|
|
1083
|
+
try {
|
|
1084
|
+
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
1085
|
+
// #2-B-ii: a preempting `interrupt()` fires `abortController` — stop consuming the live
|
|
1086
|
+
// stream immediately (providers that forward `signal` also abort the socket; the rest at
|
|
1087
|
+
// least stop here at the next event). The loop-top `interrupted` check then ends the run.
|
|
1088
|
+
if (abortSignal?.aborted)
|
|
1089
|
+
break;
|
|
1090
|
+
if (evt.type === "usage") {
|
|
1091
|
+
const usageEvt = evt;
|
|
1092
|
+
turnTokens = usageEvt.totalTokens;
|
|
1093
|
+
turnInputTokens = usageEvt.inputTokens ?? 0;
|
|
1094
|
+
turnOutputTokens = usageEvt.outputTokens ?? 0;
|
|
1095
|
+
// P0-C: capture the prompt-cache split for the tool-gating hit-rate baseline.
|
|
1096
|
+
turnCacheReadTokens = usageEvt.cacheReadInputTokens ?? 0;
|
|
1097
|
+
turnCacheCreationTokens = usageEvt.cacheCreationInputTokens ?? 0;
|
|
1098
|
+
// I1: per-slot attribution forwarded into TurnMetrics. Undefined when the provider
|
|
1099
|
+
// doesn't honor cache_control (OpenAI-family auto-cache).
|
|
1100
|
+
turnCacheReadBySlot = usageEvt.cacheReadInputTokensBySlot;
|
|
1101
|
+
continue;
|
|
1102
|
+
}
|
|
1103
|
+
yield evt;
|
|
1104
|
+
if (evt.type === "text_delta")
|
|
1105
|
+
finalText += evt.delta;
|
|
1106
|
+
else if (evt.type === "tool_call") {
|
|
1107
|
+
const tc = evt;
|
|
1108
|
+
finalToolCalls.push({ id: tc.id, name: tc.name, arguments: JSON.stringify(tc.arguments) });
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch (err) {
|
|
1113
|
+
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat it as an interrupt
|
|
1114
|
+
// (the loop-top `interrupted` check converts it to a clean `timeout`/UserAbort), not a crash.
|
|
1115
|
+
if (abortSignal?.aborted) {
|
|
1116
|
+
this.interrupted = true;
|
|
1117
|
+
}
|
|
1118
|
+
const errMsg = formatToolError(err).toLowerCase();
|
|
1119
|
+
if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
|
|
1120
|
+
!hasAttemptedReactiveCompact) {
|
|
1121
|
+
hasAttemptedReactiveCompact = true;
|
|
1122
|
+
if (forceCompact(runtime, this.pendingObservations)) {
|
|
1123
|
+
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|
|
1124
|
+
shouldRetry = true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (!shouldRetry) {
|
|
1128
|
+
yield { type: "error", message: formatToolError(err) };
|
|
1129
|
+
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
// #2-B-ii: stream aborted (preempt/interrupt) via the break path (provider yielded no error) —
|
|
1134
|
+
// end the turn now with a timeout so the kernel terminates the run, rather than feeding the
|
|
1135
|
+
// partial assistant output as a normal turn.
|
|
1136
|
+
if (abortSignal?.aborted) {
|
|
1071
1137
|
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1072
1138
|
break;
|
|
1073
1139
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1140
|
+
if (shouldRetry) {
|
|
1141
|
+
action = {
|
|
1142
|
+
kind: "call_provider",
|
|
1143
|
+
context: runtime.render(),
|
|
1144
|
+
tools,
|
|
1145
|
+
};
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const assistantMessage = {
|
|
1149
|
+
role: "assistant",
|
|
1150
|
+
content: finalText,
|
|
1151
|
+
toolCalls: finalToolCalls,
|
|
1152
|
+
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1087
1153
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (!nextAction && this.pendingObservations.some(o => o.kind === "suspended")) {
|
|
1105
|
-
const resolved = await this.resolveKernelSuspend(runtime, sessionId);
|
|
1106
|
-
for (const evt of resolved.events)
|
|
1107
|
-
yield evt;
|
|
1108
|
-
nextAction = kernelAction(runtime, this.pendingObservations, {
|
|
1109
|
-
kind: "resume",
|
|
1110
|
-
approved_calls: resolved.approved,
|
|
1111
|
-
denied_calls: resolved.denied,
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1115
|
-
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
1116
|
-
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
1117
|
-
turn: runtime.turn(),
|
|
1118
|
-
content: finalText,
|
|
1119
|
-
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1120
|
-
toolCalls: finalToolCalls,
|
|
1121
|
-
providerReplay,
|
|
1122
|
-
}));
|
|
1123
|
-
// P0-C: emit per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect
|
|
1124
|
-
// GOING INTO this turn; a `skill` call here only takes effect next turn, so emit first, then
|
|
1125
|
-
// advance. Wrapped so a faulty sink can never break the run (pure observation).
|
|
1126
|
-
if (this.opts.onTurnMetrics) {
|
|
1127
|
-
try {
|
|
1128
|
-
this.opts.onTurnMetrics({
|
|
1129
|
-
turn: runtime.turn(),
|
|
1130
|
-
toolsExposed: tools.length,
|
|
1131
|
-
toolsCalled: finalToolCalls.length,
|
|
1132
|
-
activeSkill,
|
|
1133
|
-
inputTokens: turnInputTokens,
|
|
1134
|
-
cacheReadTokens: turnCacheReadTokens,
|
|
1135
|
-
cacheCreationTokens: turnCacheCreationTokens,
|
|
1154
|
+
const providerEvent = {
|
|
1155
|
+
kind: "provider_result",
|
|
1156
|
+
message: messageToKernelMessage(assistantMessage),
|
|
1157
|
+
...(turnInputTokens > 0 ? { observed_input_tokens: turnInputTokens } : {}),
|
|
1158
|
+
...(turnOutputTokens > 0 ? { observed_output_tokens: turnOutputTokens } : {}),
|
|
1159
|
+
now_ms: Date.now(),
|
|
1160
|
+
};
|
|
1161
|
+
let nextAction = kernelMaybeAction(runtime, this.pendingObservations, providerEvent);
|
|
1162
|
+
if (!nextAction && this.pendingObservations.some(o => o.kind === "suspended")) {
|
|
1163
|
+
const resolved = await this.resolveKernelSuspend(runtime, sessionId);
|
|
1164
|
+
for (const evt of resolved.events)
|
|
1165
|
+
yield evt;
|
|
1166
|
+
nextAction = kernelAction(runtime, this.pendingObservations, {
|
|
1167
|
+
kind: "resume",
|
|
1168
|
+
approved_calls: resolved.approved,
|
|
1169
|
+
denied_calls: resolved.denied,
|
|
1136
1170
|
});
|
|
1137
1171
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
const runCtx = {
|
|
1154
|
-
agentId: this.opts.agentId,
|
|
1155
|
-
skillDir: this.opts.skillDir,
|
|
1156
|
-
dreamStore: this.opts.dreamStore,
|
|
1157
|
-
knowledgeSource: this.opts.knowledgeSource,
|
|
1158
|
-
onToolSuspend: this.opts.onToolSuspend,
|
|
1159
|
-
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1160
|
-
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1161
|
-
};
|
|
1162
|
-
const toolResults = [];
|
|
1163
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
1164
|
-
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1165
|
-
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1166
|
-
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1167
|
-
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1168
|
-
for (const call of planCalls) {
|
|
1169
|
-
const update = parseUpdatePlanArgs(call.arguments);
|
|
1170
|
-
kernelApply(runtime, this.pendingObservations, {
|
|
1171
|
-
kind: "update_task",
|
|
1172
|
-
update: taskUpdateToKernel(update),
|
|
1173
|
-
});
|
|
1174
|
-
const result = { callId: call.id, output: "success", isError: false };
|
|
1175
|
-
toolResults.push(result);
|
|
1176
|
-
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1177
|
-
}
|
|
1178
|
-
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
1179
|
-
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
1180
|
-
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
1181
|
-
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
1182
|
-
// simply unconsumed — a no-op.)
|
|
1183
|
-
for (const call of submitCalls) {
|
|
1184
|
-
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
1185
|
-
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
1186
|
-
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
1187
|
-
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
1188
|
-
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
1189
|
-
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1190
|
-
if (spec) {
|
|
1191
|
-
this.pendingAuthoredWorkflows.push(spec);
|
|
1192
|
-
const out = "workflow authored; executing now";
|
|
1193
|
-
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1194
|
-
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1195
|
-
continue;
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
1199
|
-
const nodes = call.name === "start_workflow"
|
|
1200
|
-
? parseStartWorkflowArgs(call.arguments)
|
|
1201
|
-
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
1202
|
-
yield { type: "workflow_nodes_submitted", nodes };
|
|
1203
|
-
const result = { callId: call.id, output: "submitted", isError: false };
|
|
1204
|
-
toolResults.push(result);
|
|
1205
|
-
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
1206
|
-
}
|
|
1207
|
-
if (normalCalls.length > 0) {
|
|
1208
|
-
for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
|
|
1209
|
-
yield evt;
|
|
1210
|
-
if (evt.type === "tool_result") {
|
|
1211
|
-
const tre = evt;
|
|
1212
|
-
toolResults.push({
|
|
1213
|
-
callId: tre.callId,
|
|
1214
|
-
output: tre.content,
|
|
1215
|
-
isError: tre.isError,
|
|
1216
|
-
isFatal: tre.isFatal,
|
|
1217
|
-
errorKind: tre.errorKind,
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
else if (evt.type === "tool_argument_repaired") {
|
|
1221
|
-
const tare = evt;
|
|
1222
|
-
await this.opts.sessionLog.append(sessionId, {
|
|
1223
|
-
kind: "tool_argument_repaired",
|
|
1172
|
+
action = nextAction ?? kernelAction(runtime, this.pendingObservations, providerEvent);
|
|
1173
|
+
const providerReplay = peekProviderReplay(this.opts.provider, finalText, finalToolCalls);
|
|
1174
|
+
await this.opts.sessionLog.append(sessionId, buildLlmCompletedEvent({
|
|
1175
|
+
turn: runtime.turn(),
|
|
1176
|
+
content: finalText,
|
|
1177
|
+
tokenCount: turnOutputTokens || turnTokens || undefined,
|
|
1178
|
+
toolCalls: finalToolCalls,
|
|
1179
|
+
providerReplay,
|
|
1180
|
+
}));
|
|
1181
|
+
// P0-C: emit per-turn tool-gating telemetry. `activeSkill` reflects the skill in effect
|
|
1182
|
+
// GOING INTO this turn; a `skill` call here only takes effect next turn, so emit first, then
|
|
1183
|
+
// advance. Wrapped so a faulty sink can never break the run (pure observation).
|
|
1184
|
+
if (this.opts.onTurnMetrics) {
|
|
1185
|
+
try {
|
|
1186
|
+
this.opts.onTurnMetrics({
|
|
1224
1187
|
turn: runtime.turn(),
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1188
|
+
toolsExposed: tools.length,
|
|
1189
|
+
toolsCalled: finalToolCalls.length,
|
|
1190
|
+
activeSkill,
|
|
1191
|
+
inputTokens: turnInputTokens,
|
|
1192
|
+
cacheReadTokens: turnCacheReadTokens,
|
|
1193
|
+
cacheCreationTokens: turnCacheCreationTokens,
|
|
1194
|
+
...(turnCacheReadBySlot ? { cacheReadTokensBySlot: turnCacheReadBySlot } : {}),
|
|
1228
1195
|
});
|
|
1229
1196
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
});
|
|
1197
|
+
catch { /* metrics must never break the run */ }
|
|
1198
|
+
}
|
|
1199
|
+
const skillCall = finalToolCalls.find(c => c.name === "skill");
|
|
1200
|
+
if (skillCall) {
|
|
1201
|
+
try {
|
|
1202
|
+
const name = JSON.parse(skillCall.arguments || "{}").name;
|
|
1203
|
+
if (name)
|
|
1204
|
+
activeSkill = name;
|
|
1239
1205
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1206
|
+
catch { /* malformed skill args — leave activeSkill unchanged */ }
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
else if (action.kind === "execute_tool") {
|
|
1210
|
+
const allCalls = action.calls;
|
|
1211
|
+
await this.opts.sessionLog.append(sessionId, { kind: "tool_requested", turn: runtime.turn(), calls: allCalls });
|
|
1212
|
+
const runCtx = {
|
|
1213
|
+
agentId: this.opts.agentId,
|
|
1214
|
+
skillDir: this.opts.skillDir,
|
|
1215
|
+
dreamStore: this.opts.dreamStore,
|
|
1216
|
+
knowledgeSource: this.opts.knowledgeSource,
|
|
1217
|
+
onToolSuspend: this.opts.onToolSuspend,
|
|
1218
|
+
onPermissionRequest: this.opts.onPermissionRequest,
|
|
1219
|
+
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
1220
|
+
};
|
|
1221
|
+
const toolResults = [];
|
|
1222
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
1223
|
+
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
1224
|
+
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1225
|
+
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1226
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
1227
|
+
for (const call of planCalls) {
|
|
1228
|
+
const update = parseUpdatePlanArgs(call.arguments);
|
|
1229
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1230
|
+
kind: "update_task",
|
|
1231
|
+
update: taskUpdateToKernel(update),
|
|
1232
|
+
});
|
|
1233
|
+
const result = { callId: call.id, output: "success", isError: false };
|
|
1234
|
+
toolResults.push(result);
|
|
1235
|
+
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
1236
|
+
}
|
|
1237
|
+
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
1238
|
+
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
1239
|
+
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
1240
|
+
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
1241
|
+
// simply unconsumed — a no-op.)
|
|
1242
|
+
for (const call of submitCalls) {
|
|
1243
|
+
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
1244
|
+
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
1245
|
+
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
1246
|
+
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
1247
|
+
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
1248
|
+
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1249
|
+
if (spec) {
|
|
1250
|
+
this.pendingAuthoredWorkflows.push(spec);
|
|
1251
|
+
const out = "workflow authored; executing now";
|
|
1252
|
+
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1253
|
+
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1250
1256
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1257
|
+
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
1258
|
+
const nodes = call.name === "start_workflow"
|
|
1259
|
+
? parseStartWorkflowArgs(call.arguments)
|
|
1260
|
+
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
1261
|
+
yield { type: "workflow_nodes_submitted", nodes };
|
|
1262
|
+
const result = { callId: call.id, output: "submitted", isError: false };
|
|
1263
|
+
toolResults.push(result);
|
|
1264
|
+
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
1265
|
+
}
|
|
1266
|
+
if (normalCalls.length > 0) {
|
|
1267
|
+
for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
|
|
1268
|
+
yield evt;
|
|
1269
|
+
if (evt.type === "tool_result") {
|
|
1270
|
+
const tre = evt;
|
|
1271
|
+
toolResults.push({
|
|
1272
|
+
callId: tre.callId,
|
|
1273
|
+
output: tre.content,
|
|
1274
|
+
isError: tre.isError,
|
|
1275
|
+
isFatal: tre.isFatal,
|
|
1276
|
+
errorKind: tre.errorKind,
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
else if (evt.type === "tool_argument_repaired") {
|
|
1280
|
+
const tare = evt;
|
|
1281
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1282
|
+
kind: "tool_argument_repaired",
|
|
1283
|
+
turn: runtime.turn(),
|
|
1284
|
+
tool: tare.name,
|
|
1285
|
+
original_arguments: tare.originalArguments,
|
|
1286
|
+
repaired_arguments: tare.repairedArguments,
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
else if (evt.type === "tool_denied") {
|
|
1290
|
+
const tde = evt;
|
|
1291
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1292
|
+
kind: "tool_denied",
|
|
1293
|
+
turn: runtime.turn(),
|
|
1294
|
+
call_id: tde.callId,
|
|
1295
|
+
tool_name: tde.toolName,
|
|
1296
|
+
reason: tde.reason,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
else if (evt.type === "permission_request") {
|
|
1300
|
+
const pre = evt;
|
|
1301
|
+
const turn = runtime.turn();
|
|
1302
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1303
|
+
kind: "permission_requested",
|
|
1304
|
+
turn,
|
|
1305
|
+
tool: pre.toolName,
|
|
1306
|
+
arguments: pre.arguments,
|
|
1307
|
+
reason: pre.reason,
|
|
1308
|
+
});
|
|
1309
|
+
}
|
|
1310
|
+
else if (evt.type === "permission_resolved") {
|
|
1311
|
+
const resolved = evt;
|
|
1312
|
+
const turn = runtime.turn();
|
|
1313
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1314
|
+
kind: "permission_resolved",
|
|
1315
|
+
turn,
|
|
1316
|
+
approved: resolved.approved,
|
|
1317
|
+
responder: resolved.responder,
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1260
1320
|
}
|
|
1321
|
+
const names = normalCalls.map(c => c.name).join(", ");
|
|
1322
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
1323
|
+
kind: "update_task",
|
|
1324
|
+
update: taskUpdateToKernel({ progress: `Executed tools: ${names}` }),
|
|
1325
|
+
});
|
|
1261
1326
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1327
|
+
await this.opts.sessionLog.append(sessionId, {
|
|
1328
|
+
kind: "tool_completed",
|
|
1329
|
+
turn: runtime.turn(),
|
|
1330
|
+
results: toolResults.map(r => ({
|
|
1331
|
+
call_id: r.callId,
|
|
1332
|
+
output: r.output,
|
|
1333
|
+
is_error: r.isError,
|
|
1334
|
+
token_count: r.tokenCount,
|
|
1335
|
+
})),
|
|
1266
1336
|
});
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
call_id: r.callId,
|
|
1273
|
-
output: r.output,
|
|
1274
|
-
is_error: r.isError,
|
|
1275
|
-
token_count: r.tokenCount,
|
|
1276
|
-
})),
|
|
1277
|
-
});
|
|
1278
|
-
for (const call of normalCalls) {
|
|
1279
|
-
const result = toolResults.find(r => r.callId === call.id);
|
|
1280
|
-
if (result) {
|
|
1281
|
-
this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
|
|
1337
|
+
for (const call of normalCalls) {
|
|
1338
|
+
const result = toolResults.find(r => r.callId === call.id);
|
|
1339
|
+
if (result) {
|
|
1340
|
+
this.pendingSpoolOutputs.set(call.id, { tool: call.name, output: result.output });
|
|
1341
|
+
}
|
|
1282
1342
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1343
|
+
// P1-B B3: a `skill` call that resolved successfully activates that skill in the kernel, so
|
|
1344
|
+
// the next `call_provider` narrows the toolset to its declared tools. Fed before `tool_results`
|
|
1345
|
+
// (which computes the next action). Errs-open: a failed/missing skill load doesn't activate.
|
|
1346
|
+
for (const call of allCalls) {
|
|
1347
|
+
if (call.name !== "skill")
|
|
1348
|
+
continue;
|
|
1349
|
+
const res = toolResults.find(r => r.callId === call.id);
|
|
1350
|
+
if (!res || res.isError)
|
|
1351
|
+
continue;
|
|
1352
|
+
try {
|
|
1353
|
+
const name = JSON.parse(call.arguments || "{}").name;
|
|
1354
|
+
if (name)
|
|
1355
|
+
kernelApply(runtime, this.pendingObservations, { kind: "skill_activated", name });
|
|
1356
|
+
}
|
|
1357
|
+
catch { /* malformed skill args — skip activation */ }
|
|
1297
1358
|
}
|
|
1298
|
-
catch { /* malformed skill args — skip activation */ }
|
|
1299
|
-
}
|
|
1300
|
-
action = kernelAction(runtime, this.pendingObservations, {
|
|
1301
|
-
kind: "tool_results",
|
|
1302
|
-
results: toolResults.map(toolResultToKernel),
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
else if (action.kind === "evaluate_milestone") {
|
|
1306
|
-
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1307
|
-
if (milestonePolicy === "auto_pass") {
|
|
1308
1359
|
action = kernelAction(runtime, this.pendingObservations, {
|
|
1309
|
-
kind: "
|
|
1310
|
-
|
|
1360
|
+
kind: "tool_results",
|
|
1361
|
+
results: toolResults.map(toolResultToKernel),
|
|
1311
1362
|
});
|
|
1312
|
-
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1313
1363
|
}
|
|
1314
|
-
else if (
|
|
1315
|
-
const
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1364
|
+
else if (action.kind === "evaluate_milestone") {
|
|
1365
|
+
const milestonePolicy = this.opts.milestonePolicy ?? "require_verifier";
|
|
1366
|
+
if (milestonePolicy === "auto_pass") {
|
|
1367
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
1368
|
+
kind: "milestone_result",
|
|
1369
|
+
result: milestoneCheckResultToKernel(milestoneCheckPass(action.phaseId)),
|
|
1370
|
+
});
|
|
1371
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1372
|
+
}
|
|
1373
|
+
else if (this.opts.onMilestoneEvaluate) {
|
|
1374
|
+
const check = await this.opts.onMilestoneEvaluate({
|
|
1375
|
+
phaseId: action.phaseId,
|
|
1376
|
+
criteria: action.criteria,
|
|
1377
|
+
requiredEvidence: action.requiredEvidence,
|
|
1378
|
+
});
|
|
1379
|
+
action = kernelAction(runtime, this.pendingObservations, {
|
|
1380
|
+
kind: "milestone_result",
|
|
1381
|
+
result: milestoneCheckResultToKernel(check),
|
|
1382
|
+
});
|
|
1383
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
this.nextArchiveStart = await this.appendObservations(sessionId, runtime, this.nextArchiveStart);
|
|
1387
|
+
const turnsUsed = Math.max(1, runtime.turn());
|
|
1388
|
+
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
1389
|
+
reason: "milestone_pending",
|
|
1390
|
+
turnsUsed,
|
|
1391
|
+
totalTokens: 0,
|
|
1392
|
+
}));
|
|
1393
|
+
yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
|
|
1394
|
+
this.activeKernel = null;
|
|
1395
|
+
this.currentSessionId = null;
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1325
1398
|
}
|
|
1326
|
-
else {
|
|
1327
|
-
|
|
1328
|
-
const turnsUsed = Math.max(1, runtime.turn());
|
|
1329
|
-
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
1330
|
-
reason: "milestone_pending",
|
|
1331
|
-
turnsUsed,
|
|
1332
|
-
totalTokens: 0,
|
|
1333
|
-
}));
|
|
1334
|
-
yield { type: "done", iterations: turnsUsed, totalTokens: 0, status: "milestone_pending" };
|
|
1335
|
-
this.activeKernel = null;
|
|
1336
|
-
this.currentSessionId = null;
|
|
1337
|
-
return;
|
|
1399
|
+
else if (action.kind === "done") {
|
|
1400
|
+
break;
|
|
1338
1401
|
}
|
|
1339
1402
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1403
|
+
}
|
|
1404
|
+
catch (err) {
|
|
1405
|
+
// I0b: kernel rejection (or any other thrown error inside the loop) reaches us here.
|
|
1406
|
+
// Classify by NAPI status code or message pattern — `invalid_arg` for surface-shape rejects,
|
|
1407
|
+
// `error` for everything else — then emit run_terminal so observability sees a clean end.
|
|
1408
|
+
// The yield-error path mirrors what the in-flight provider-stream catch does.
|
|
1409
|
+
const errMsg = formatToolError(err);
|
|
1410
|
+
const code = err.code;
|
|
1411
|
+
const isInvalidArg = code === "InvalidArg" ||
|
|
1412
|
+
errMsg.toLowerCase().includes("invalidarg") ||
|
|
1413
|
+
errMsg.toLowerCase().includes("invalid argument");
|
|
1414
|
+
const reason = isInvalidArg ? "invalid_arg" : "error";
|
|
1415
|
+
yield { type: "error", message: errMsg };
|
|
1416
|
+
try {
|
|
1417
|
+
await this.opts.sessionLog.append(sessionId, buildRunTerminalEvent({
|
|
1418
|
+
reason,
|
|
1419
|
+
turnsUsed: runtime.turn() || 0,
|
|
1420
|
+
totalTokens: 0,
|
|
1421
|
+
}));
|
|
1342
1422
|
}
|
|
1423
|
+
catch { /* session log failure must not mask the original error */ }
|
|
1424
|
+
yield { type: "done", iterations: runtime.turn() || 0, totalTokens: 0, status: reason };
|
|
1425
|
+
this.activeKernel = null;
|
|
1426
|
+
this.currentSessionId = null;
|
|
1427
|
+
this.dashboard = null;
|
|
1428
|
+
return;
|
|
1343
1429
|
}
|
|
1344
1430
|
const result = action.kind === "done" ? action.result : undefined;
|
|
1345
|
-
|
|
1431
|
+
// I0a: when the loop exits without a clean kernel-done — typically because a hard interrupt
|
|
1432
|
+
// aborted the in-flight LLM stream and the catch path sent `timeout` (which the kernel handles
|
|
1433
|
+
// by injecting a rollback note and continuing, not by terminating) — preserve the preempt
|
|
1434
|
+
// intent in the run_terminal reason. Without this, every interrupt-curtailed run reports
|
|
1435
|
+
// `reason: "error"` and the bench / observability layer can't distinguish preemption from a
|
|
1436
|
+
// genuine crash. Mirrors WASM/Python/Rust.
|
|
1437
|
+
const status = result?.termination ?? (this.interrupted ? "user_abort" : "error");
|
|
1346
1438
|
const turnsUsed = result ? Math.max(1, result.turnsUsed) : runtime.turn() || 0;
|
|
1347
1439
|
const totalTokens = result?.totalTokensUsed ?? 0;
|
|
1348
1440
|
nextCompressedArchiveStart = await this.appendObservations(sessionId, runtime, nextCompressedArchiveStart);
|