@deepstrike/wasm 0.2.36 → 0.2.37

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.
@@ -10,7 +10,7 @@ import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass,
10
10
  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
- import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
13
+ import { loopInstruction, classifyInstruction, judgeGoal, dependencyOutputsNote, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
14
14
  import { kernelObservationToSessionEvent } from "./kernel-event-log.js";
15
15
  import { assertNativeProfile } from "./os-profile.js";
16
16
  import { LargeResultSpool } from "./large-result-spool.js";
@@ -959,7 +959,14 @@ export class RuntimeRunner {
959
959
  catch { /* non-fatal */ }
960
960
  }
961
961
  }
962
- yield { type: "done", iterations: turnsUsed, totalTokens, status };
962
+ yield {
963
+ type: "done",
964
+ iterations: turnsUsed,
965
+ totalTokens,
966
+ status,
967
+ // ③ loop-agent: surface the kernel-adjudicated after-round decision to the driver.
968
+ ...(result?.paceDecision ? { paceDecision: result.paceDecision } : {}),
969
+ };
963
970
  this.activeKernel = null;
964
971
  this.currentSessionId = null;
965
972
  }
@@ -1009,7 +1016,10 @@ export class RuntimeRunner {
1009
1016
  const manifest = workflowNodeToManifest(node, parentSessionId);
1010
1017
  // G4: surface remaining workflow budget so a coordinator node can size its submission.
1011
1018
  const budgetNote = workflowBudgetNote(budget);
1012
- const withBudget = (goal) => (budgetNote ? `${goal}\n\n${budgetNote}` : goal);
1019
+ // W-N2: a DAG edge carries data every dependent node sees its dependencies' outputs (the
1020
+ // kernel sends `input_agent_ids` for all dependents; judges/reduce keep their special paths).
1021
+ const depsNote = dependencyOutputsNote(node.input_agent_ids, outputs);
1022
+ const withBudget = (goal) => [goal, depsNote, budgetNote].filter(Boolean).join("\n\n");
1013
1023
  const mkCtx = (goal) => ({
1014
1024
  parentOpts: this.opts,
1015
1025
  parentSessionId,
@@ -1018,6 +1028,10 @@ export class RuntimeRunner {
1018
1028
  sessionLog: this.opts.sessionLog,
1019
1029
  // M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel.
1020
1030
  isWorkflowNode: true,
1031
+ // W-N1: trusted workflow nodes run on the parent's execution plane (they carry no grant list
1032
+ // by design — filtering on the missing list ran every DAG node TOOL-LESS); quarantined nodes
1033
+ // stay deny-all filtered (they read untrusted content).
1034
+ toolAccess: (node.trust === "quarantined" ? "filtered" : "inherit"),
1021
1035
  // #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
1022
1036
  ...(abortSignal ? { abortSignal } : {}),
1023
1037
  });
@@ -1037,9 +1051,18 @@ export class RuntimeRunner {
1037
1051
  const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
1038
1052
  return withSignal(result, { tournamentWinner: winnerId });
1039
1053
  }
1040
- // A#2 v2 loop iteration: run the increment, then extract a stop signal. No signal ⇒ run to cap.
1054
+ // A#2 v2 loop iteration: run the increment under the armed pacing trap (workflowNodeToSpec set
1055
+ // `loopRound`, and the iteration resumes the loop's stable session — transcript-as-carry).
1056
+ // DW-3 one vocabulary: the kernel-adjudicated `pace` verb IS the continuation signal
1057
+ // (stop → loopContinue=false); the legacy text-sniffed JSON blob survives only as the fallback
1058
+ // when no pace decision arrives (stub orchestrators, harness children), where no signal still
1059
+ // means "run to max_iters" (v1).
1041
1060
  if (node.loop_max_iters != null) {
1042
- const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters)}`));
1061
+ const iteration = Number(/-i(\d+)$/.exec(node.agent_id)?.[1] ?? "0");
1062
+ const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters, iteration)}`));
1063
+ const pace = result.result.paceDecision;
1064
+ if (pace)
1065
+ return withSignal(result, { loopContinue: pace.action !== "stop" });
1043
1066
  const cont = extractLoopContinue(textOf(result));
1044
1067
  return cont === undefined ? result : withSignal(result, { loopContinue: cont });
1045
1068
  }
@@ -1215,10 +1238,22 @@ export class RuntimeRunner {
1215
1238
  parent_session_id: parentSessionId,
1216
1239
  // W0-ABI resume: skip nodes already completed before an interruption.
1217
1240
  ...(opts?.resumedCompleted && opts.resumedCompleted.length ? { resumed_completed: opts.resumedCompleted } : {}),
1241
+ // W-1: signal-carrying completion records (classify branch / loop stop replay).
1242
+ ...(opts?.resumedResults?.length
1243
+ ? {
1244
+ resumed_results: opts.resumedResults.map(r => ({
1245
+ agent_id: r.agentId,
1246
+ ...(r.classifyBranch !== undefined ? { classify_branch: r.classifyBranch } : {}),
1247
+ ...(r.tournamentWinner !== undefined ? { tournament_winner: r.tournamentWinner } : {}),
1248
+ ...(r.loopContinue !== undefined ? { loop_continue: r.loopContinue } : {}),
1249
+ })),
1250
+ }
1251
+ : {}),
1218
1252
  // R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
1219
1253
  ...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
1254
+ ...(opts?.resumedSubmissionBases && opts.resumedSubmissionBases.length ? { resumed_submission_bases: opts.resumedSubmissionBases } : {}),
1220
1255
  });
1221
- return await this.driveWorkflow(observations, parentSessionId, runtime);
1256
+ return await this.driveWorkflow(observations, parentSessionId, runtime, opts?.resumedOutputs);
1222
1257
  }
1223
1258
  finally {
1224
1259
  if (bootstrapped) {
@@ -1241,6 +1276,18 @@ export class RuntimeRunner {
1241
1276
  const parentSessionId = this.currentSessionId;
1242
1277
  const runtime = this.activeKernel;
1243
1278
  const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
1279
+ // W-3: persist the agent-authored batch (bootstrap base 0 / flatten base N — the kernel now
1280
+ // announces BOTH) so an interrupted authored workflow reconstructs on resume; the host never
1281
+ // had this spec, unlike the `runWorkflow` path.
1282
+ const submitted = observations.find(o => o.kind === "workflow_nodes_submitted");
1283
+ if (submitted) {
1284
+ await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1285
+ turn: runtime.turn(),
1286
+ nodes: workflowSpecToKernel(spec).nodes ?? [],
1287
+ baseIndex: submitted.base,
1288
+ submitterAgentId: opts?.submitterAgentId,
1289
+ }));
1290
+ }
1244
1291
  return this.driveWorkflow(observations, parentSessionId, runtime);
1245
1292
  }
1246
1293
  /**
@@ -1297,7 +1344,7 @@ export class RuntimeRunner {
1297
1344
  * `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
1298
1345
  * agent-submitted nodes first), and loop until the kernel reports the workflow complete.
1299
1346
  */
1300
- async driveWorkflow(initial, parentSessionId, runtime) {
1347
+ async driveWorkflow(initial, parentSessionId, runtime, seedOutputs) {
1301
1348
  const observations = initial;
1302
1349
  const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
1303
1350
  const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
@@ -1310,7 +1357,9 @@ export class RuntimeRunner {
1310
1357
  let nodes = collectNodes(observations);
1311
1358
  let budget = collectBudget(observations);
1312
1359
  // G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
1313
- const outputs = new Map();
1360
+ // W-1: on resume it is pre-seeded from the persisted node outputs, so post-resume dependents
1361
+ // still see their (pre-crash) dependencies' outputs.
1362
+ const outputs = new Map(seedOutputs ?? []);
1314
1363
  for (;;) {
1315
1364
  if (nodes.length === 0)
1316
1365
  return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
@@ -1330,7 +1379,13 @@ export class RuntimeRunner {
1330
1379
  for (const result of results) {
1331
1380
  // G2: record this node's output so a downstream reduce node can consume it.
1332
1381
  const outContent = result.result.finalMessage?.content;
1333
- outputs.set(result.agentId, typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "");
1382
+ const outText = typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "";
1383
+ outputs.set(result.agentId, outText);
1384
+ // A loop iteration completes under `wf-node{N}-i{k}` but its dependents consume the STABLE
1385
+ // node id `wf-node{N}` — alias it so the LAST iteration's output is what dependents see.
1386
+ const stableId = result.agentId.replace(/-i\d+$/, "");
1387
+ if (stableId !== result.agentId)
1388
+ outputs.set(stableId, outText);
1334
1389
  // R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
1335
1390
  // reporting the node's completion — the workflow is still active, so even a last-node
1336
1391
  // submission keeps the DAG alive.
@@ -1341,10 +1396,15 @@ export class RuntimeRunner {
1341
1396
  const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
1342
1397
  nextNodes.push(...collectNodes(subObs));
1343
1398
  budget = collectBudget(subObs) ?? budget;
1344
- // R3-1: persist the submission (kernel-shape nodes) so resume can re-apply it.
1399
+ // R3-1: persist the submission (kernel-shape nodes) + its kernel-reported base index
1400
+ // so resume can re-apply the batch at the exact original graph position. W-N3: also the
1401
+ // submitter, so resume drops batches whose submitter re-runs (it will re-submit).
1402
+ const submitted = subObs.find(o => o.kind === "workflow_nodes_submitted");
1345
1403
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
1346
1404
  turn: runtime.turn(),
1347
1405
  nodes: submitEvent.nodes ?? [],
1406
+ baseIndex: submitted?.base,
1407
+ submitterAgentId: result.agentId,
1348
1408
  }));
1349
1409
  }
1350
1410
  const obs = kernelApply(runtime, this.pendingObservations, {
@@ -1356,11 +1416,17 @@ export class RuntimeRunner {
1356
1416
  const d = findDone(obs);
1357
1417
  if (d)
1358
1418
  done = d;
1359
- // Persist node completion for resume recovery.
1419
+ // Persist node completion for resume recovery. W-1: the result-borne control signals ride
1420
+ // along (a resumed classifier re-prunes; a recorded loop stop is honored) plus the output
1421
+ // text (post-resume dependents/reduce still see this node's output).
1360
1422
  await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
1361
1423
  turn: runtime.turn(),
1362
1424
  agentId: result.agentId,
1363
1425
  termination: result.result.termination,
1426
+ classifyBranch: result.result.classifyBranch,
1427
+ tournamentWinner: result.result.tournamentWinner,
1428
+ loopContinue: result.result.loopContinue,
1429
+ ...(result.result.termination === "completed" && outText ? { output: outText } : {}),
1364
1430
  }));
1365
1431
  }
1366
1432
  if (done && nextNodes.length === 0) {
@@ -1371,8 +1437,9 @@ export class RuntimeRunner {
1371
1437
  }
1372
1438
  /**
1373
1439
  * Resume a workflow from the parent session's completed nodes.
1374
- * Reads the session log, extracts completed workflow node agent_ids, and
1375
- * calls runWorkflow with resumedCompleted so the kernel skips those nodes.
1440
+ * Reads the session log, extracts completed workflow node records (with their W-1 control
1441
+ * signals + outputs), and calls runWorkflow so the kernel skips those nodes, replays control
1442
+ * flow (classify prune / loop stop), and the driver re-seeds its outputs map.
1376
1443
  */
1377
1444
  async resumeWorkflow(spec, opts) {
1378
1445
  // Standalone resume: a stateless handler passes the prior `sessionId`; mid-run callers omit it.
@@ -1381,9 +1448,34 @@ export class RuntimeRunner {
1381
1448
  throw new Error("resumeWorkflow requires an active parent run or an explicit sessionId");
1382
1449
  }
1383
1450
  const events = await this.opts.sessionLog.read(sessionId);
1384
- const resumedCompleted = recoverCompletedWorkflowNodes(events);
1385
- const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
1386
- return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions, sessionId });
1451
+ const resumedResults = recoverCompletedWorkflowNodes(events);
1452
+ const completedIds = new Set(resumedResults.map(r => r.agentId));
1453
+ const recovered = recoverSubmittedWorkflowNodes(events);
1454
+ // W-N3: DROP batches whose submitter did NOT complete — that node re-runs on resume and will
1455
+ // re-submit its batch; replaying the logged copy too would duplicate its nodes in the DAG.
1456
+ // Only safe with exact bases (the dropped batch's slots become inert placeholders); a legacy
1457
+ // order-only log keeps every batch, since dropping would shift all later indices.
1458
+ let { submissions, bases } = recovered;
1459
+ if (bases.length === submissions.length && submissions.length > 0) {
1460
+ const keep = recovered.submitters.map(s => s === undefined || completedIds.has(s));
1461
+ submissions = submissions.filter((_, i) => keep[i]);
1462
+ bases = bases.filter((_, i) => keep[i]);
1463
+ }
1464
+ const resumedOutputs = new Map(resumedResults.filter(r => r.output).map(r => [r.agentId, r.output]));
1465
+ // Alias loop iterations onto their stable node id (last iteration wins) — dependents consume
1466
+ // `wf-node{N}`, not `wf-node{N}-i{k}`.
1467
+ for (const r of resumedResults) {
1468
+ const stableId = r.agentId.replace(/-i\d+$/, "");
1469
+ if (stableId !== r.agentId && r.output)
1470
+ resumedOutputs.set(stableId, r.output);
1471
+ }
1472
+ return this.runWorkflow(spec, {
1473
+ resumedResults,
1474
+ resumedSubmissions: submissions,
1475
+ resumedSubmissionBases: bases,
1476
+ resumedOutputs,
1477
+ sessionId,
1478
+ });
1387
1479
  }
1388
1480
  async appendObservations(sessionId, runtime, nextArchiveStart) {
1389
1481
  const turn = runtime.turn();
@@ -1422,12 +1514,23 @@ export class RuntimeRunner {
1422
1514
  const compressedSeq = await this.opts.sessionLog.append(sessionId, event);
1423
1515
  if (event.kind === "compressed") {
1424
1516
  nextArchiveStart = compressedSeq + 1;
1425
- }
1426
- if (obs.kind === "page_out"
1427
- && obs.tier_hint === "semantic"
1428
- && Array.isArray(obs.archived)
1429
- && obs.archived.length > 0) {
1430
- void this.archiveSemanticPageOut(obs.archived, compressionAction(obs.action));
1517
+ // One compaction = one kernel observation: the page_out session record (and the
1518
+ // semantic-archive branch) is DERIVED from Compressed.tier_hint, preserving the
1519
+ // session-log format and OsSnapshot page_out_count.
1520
+ const archived = obs.archived;
1521
+ if (obs.tier_hint && Array.isArray(archived) && archived.length > 0) {
1522
+ await this.opts.sessionLog.append(sessionId, {
1523
+ kind: "page_out",
1524
+ turn: obs.turn ?? turn,
1525
+ action: compressionAction(obs.action),
1526
+ summary: obs.summary,
1527
+ tier_hint: obs.tier_hint ?? "durable",
1528
+ message_count: archived.length,
1529
+ });
1530
+ if (obs.tier_hint === "semantic") {
1531
+ void this.archiveSemanticPageOut(archived, compressionAction(obs.action));
1532
+ }
1533
+ }
1431
1534
  }
1432
1535
  // K4: a sprint renewal dropped the old history — including any earlier memory hits — so
1433
1536
  // re-run the preQueryMemory prefetch for the new sprint (live observations only).
@@ -1443,10 +1546,14 @@ export class RuntimeRunner {
1443
1546
  * re-fired after each sprint renewal (renewal drops the old history INCLUDING earlier memory
1444
1547
  * hits). Errs-open throughout. */
1445
1548
  async prefetchMemoryIntoHistory(runtime, phase) {
1446
- if (!this.opts.preQueryMemory || !this.opts.dreamStore || !this.opts.agentId)
1549
+ if (!this.opts.dreamStore || !this.opts.agentId)
1447
1550
  return;
1551
+ // P10: recall is default-on (CC session-start recall) — with no hook configured,
1552
+ // the goal itself is the query. preQueryMemory stays as the targeting override.
1553
+ const preQuery = this.opts.preQueryMemory
1554
+ ?? ((ctx) => [ctx.goal]);
1448
1555
  try {
1449
- const queries = await this.opts.preQueryMemory({ goal: this.currentGoal, phase });
1556
+ const queries = await preQuery({ goal: this.currentGoal, phase });
1450
1557
  const lines = [];
1451
1558
  for (const q of queries ?? []) {
1452
1559
  if (typeof q !== "string" || !q.trim())
@@ -1473,8 +1580,12 @@ export class RuntimeRunner {
1473
1580
  ? await this.opts.dreamSummarizer.summarize(archived, { action })
1474
1581
  : await summarizeForLongTermMemory(this.opts.dreamProvider ?? this.opts.provider, archived, this.opts.dreamSystemPrompt);
1475
1582
  const existing = await this.opts.dreamStore.loadMemories(this.opts.agentId);
1583
+ // P2 write-funnel (wasm surface has no kernel write gate yet): jaccard dedup +
1584
+ // advisory 0.6 score — an automatic summary must never outrank curated content.
1585
+ if (existing.some(e => jaccardSimilarity(e.text, summary) >= 0.9))
1586
+ return;
1476
1587
  await this.opts.dreamStore.commit(this.opts.agentId, {
1477
- toAdd: [{ text: summary, score: 1.0, metadata: { source: "semantic_page_out", action } }],
1588
+ toAdd: [{ text: summary, score: 0.6, metadata: { source: "semantic_page_out", action } }],
1478
1589
  toRemoveIndices: [],
1479
1590
  stats: {
1480
1591
  insightsProcessed: 1,
@@ -1489,6 +1600,19 @@ export class RuntimeRunner {
1489
1600
  }
1490
1601
  }
1491
1602
  }
1603
+ /** Word-set jaccard similarity — the curator's dedup rule at the write funnel. */
1604
+ function jaccardSimilarity(a, b) {
1605
+ const sa = new Set(a.split(/\s+/).filter(Boolean));
1606
+ const sb = new Set(b.split(/\s+/).filter(Boolean));
1607
+ if (sa.size === 0 && sb.size === 0)
1608
+ return 1;
1609
+ let inter = 0;
1610
+ for (const w of sa)
1611
+ if (sb.has(w))
1612
+ inter++;
1613
+ const union = sa.size + sb.size - inter;
1614
+ return union === 0 ? 0 : inter / union;
1615
+ }
1492
1616
  async function summarizeForLongTermMemory(provider, archived, systemPrompt) {
1493
1617
  const transcript = archived
1494
1618
  .map(m => `${m.role}: ${m.content}`)
@@ -1,5 +1,5 @@
1
1
  import type { ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
2
- import type { KernelEventCategory, KernelPrimitive } from "./kernel-event-log.js";
2
+ import type { KernelPrimitive } from "./kernel-event-log.js";
3
3
  export type RollbackReason = {
4
4
  kind: "fatal_tool_error";
5
5
  tool_name: string;
@@ -74,8 +74,6 @@ export type SessionEvent = {
74
74
  } | {
75
75
  kind: "compressed";
76
76
  turn: number;
77
- category?: KernelEventCategory;
78
- primitive?: KernelPrimitive;
79
77
  archived_seq_range: [number, number];
80
78
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
81
79
  summary?: string;
@@ -85,8 +83,6 @@ export type SessionEvent = {
85
83
  } | {
86
84
  kind: "page_out";
87
85
  turn: number;
88
- category?: KernelEventCategory;
89
- primitive?: KernelPrimitive;
90
86
  action?: "snip_compact" | "micro_compact" | "context_collapse" | "auto_compact";
91
87
  summary?: string;
92
88
  tier_hint?: string;
@@ -94,14 +90,10 @@ export type SessionEvent = {
94
90
  } | {
95
91
  kind: "page_in";
96
92
  turn: number;
97
- category?: KernelEventCategory;
98
- primitive?: KernelPrimitive;
99
93
  entry_count: number;
100
94
  } | {
101
95
  kind: "large_result_spooled";
102
96
  turn: number;
103
- category?: KernelEventCategory;
104
- primitive?: KernelPrimitive;
105
97
  call_id: string;
106
98
  tool: string;
107
99
  original_size: number;
@@ -110,15 +102,11 @@ export type SessionEvent = {
110
102
  } | {
111
103
  kind: "rollbacked";
112
104
  turn: number;
113
- category?: KernelEventCategory;
114
- primitive?: KernelPrimitive;
115
105
  checkpoint_history_len: number;
116
106
  reason?: RollbackReason;
117
107
  } | {
118
108
  kind: "capability_changed";
119
109
  turn: number;
120
- category?: KernelEventCategory;
121
- primitive?: KernelPrimitive;
122
110
  added: string[];
123
111
  removed: string[];
124
112
  change_kind?: string;
@@ -129,78 +117,51 @@ export type SessionEvent = {
129
117
  } | {
130
118
  kind: "context_renewed";
131
119
  turn: number;
132
- category?: KernelEventCategory;
133
- primitive?: KernelPrimitive;
134
120
  sprint: number;
135
121
  handoff_ref: string;
136
122
  } | {
137
123
  kind: "suspended";
138
124
  turn: number;
139
- category?: KernelEventCategory;
140
- primitive?: KernelPrimitive;
141
125
  reason: string;
142
126
  pending_calls?: string[];
143
127
  } | {
144
128
  kind: "resumed";
145
129
  turn: number;
146
- category?: KernelEventCategory;
147
- primitive?: KernelPrimitive;
148
130
  approved?: string[];
149
131
  denied?: string[];
150
132
  } | {
151
133
  kind: "tool_gated";
152
134
  turn: number;
153
- category?: KernelEventCategory;
154
- primitive?: KernelPrimitive;
155
135
  call_id: string;
156
136
  tool: string;
157
137
  reason: string;
158
138
  } | {
159
139
  kind: "signal_disposed";
160
140
  turn: number;
161
- category?: KernelEventCategory;
162
- primitive?: KernelPrimitive;
163
141
  signal_id: string;
164
142
  disposition: string;
165
143
  queue_depth: number;
166
144
  } | {
167
145
  kind: "budget_exceeded";
168
146
  turn: number;
169
- category?: KernelEventCategory;
170
- primitive?: KernelPrimitive;
171
147
  budget: string;
172
148
  } | {
173
149
  kind: "milestone_advanced";
174
150
  turn: number;
175
- category?: KernelEventCategory;
176
- primitive?: KernelPrimitive;
177
151
  phase_id: string;
178
152
  capabilities_unlocked: string[];
179
153
  } | {
180
154
  kind: "milestone_blocked";
181
155
  turn: number;
182
- category?: KernelEventCategory;
183
- primitive?: KernelPrimitive;
184
156
  phase_id: string;
185
157
  reason: string;
186
- } | {
187
- kind: "milestone_evidence";
188
- turn: number;
189
- category?: KernelEventCategory;
190
- primitive?: KernelPrimitive;
191
- phase_id: string;
192
- evidence: string[];
193
158
  } | {
194
159
  kind: "checkpoint_taken";
195
160
  turn: number;
196
- category?: KernelEventCategory;
197
- primitive?: KernelPrimitive;
198
161
  history_len: number;
199
162
  } | {
200
163
  kind: "agent_process_changed";
201
164
  turn: number;
202
- category?: KernelEventCategory;
203
- primitive?: KernelPrimitive;
204
165
  agent_id: string;
205
166
  parent_session_id: string;
206
167
  role: string;
@@ -212,52 +173,52 @@ export type SessionEvent = {
212
173
  } | {
213
174
  kind: "memory_written";
214
175
  turn: number;
215
- category?: KernelEventCategory;
216
- primitive?: KernelPrimitive;
217
176
  memory_id: string;
218
177
  memory_kind: string;
219
178
  size_bytes: number;
220
179
  } | {
221
180
  kind: "memory_queried";
222
181
  turn: number;
223
- category?: KernelEventCategory;
224
- primitive?: KernelPrimitive;
225
182
  query_context: string;
226
183
  requested_k: number;
227
184
  requires_async_response: boolean;
228
185
  } | {
229
186
  kind: "memory_validation_failed";
230
187
  turn: number;
231
- category?: KernelEventCategory;
232
- primitive?: KernelPrimitive;
233
188
  memory_id: string;
234
189
  error: string;
235
190
  } | {
236
191
  kind: "workflow_node_completed";
237
192
  turn: number;
238
- category?: KernelEventCategory;
239
- primitive?: KernelPrimitive;
240
193
  agent_id: string;
241
194
  termination: string;
195
+ /** W-1: result-borne control signals, persisted so resume replays control flow faithfully —
196
+ * a classifier re-prunes its rejected branches, a recorded loop stop is honored. */
197
+ classify_branch?: string;
198
+ tournament_winner?: string;
199
+ loop_continue?: boolean;
200
+ /** W-1: the node's final output text — resume re-seeds the driver's outputs map from it so
201
+ * post-resume reduce/judge/dependent nodes still see their dependencies' outputs. */
202
+ output?: string;
242
203
  } | {
243
204
  kind: "workflow_nodes_submitted";
244
205
  turn: number;
245
- category?: KernelEventCategory;
246
- primitive?: KernelPrimitive;
247
206
  /** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
248
207
  nodes: Record<string, unknown>[];
208
+ /** R3-1: graph base index the batch was appended at (from the kernel's
209
+ * WorkflowNodesSubmitted observation) — lets resume rebuild exact indices. */
210
+ base_index?: number;
211
+ /** W-N3: the submitting node's agent id (absent = host/bootstrap). Resume DROPS batches whose
212
+ * submitter re-runs — it will re-submit — instead of duplicating their nodes. */
213
+ submitter_agent_id?: string;
249
214
  } | {
250
215
  kind: "workflow_batch_spawned";
251
216
  turn: number;
252
- category?: KernelEventCategory;
253
- primitive?: KernelPrimitive;
254
217
  node_count: number;
255
218
  node_ids: string[];
256
219
  } | {
257
220
  kind: "workflow_completed";
258
221
  turn: number;
259
- category?: KernelEventCategory;
260
- primitive?: KernelPrimitive;
261
222
  completed: string[];
262
223
  failed: string[];
263
224
  total_nodes: number;
@@ -36,33 +36,56 @@ export declare function buildRunTerminalEvent(input: {
36
36
  }): Extract<SessionEvent, {
37
37
  kind: "run_terminal";
38
38
  }>;
39
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
40
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
39
41
  export declare function buildWorkflowNodeCompletedEvent(input: {
40
42
  turn: number;
41
43
  agentId: string;
42
44
  termination: string;
45
+ classifyBranch?: string;
46
+ tournamentWinner?: string;
47
+ loopContinue?: boolean;
48
+ output?: string;
43
49
  }): Extract<SessionEvent, {
44
50
  kind: "workflow_node_completed";
45
51
  }>;
52
+ /** One recovered node completion: the agent id plus its persisted control signals and output. */
53
+ export interface RecoveredNodeCompletion {
54
+ agentId: string;
55
+ classifyBranch?: string;
56
+ tournamentWinner?: string;
57
+ loopContinue?: boolean;
58
+ output?: string;
59
+ }
46
60
  /**
47
- * Recover completed workflow node agent_ids from a session event stream.
48
- * Scans for workflow_node_completed events and returns the agent_ids whose
49
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
61
+ * Recover completed workflow node records from a session event stream. Scans for
62
+ * workflow_node_completed events with termination "completed" and returns them WITH their
63
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
64
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
65
+ * driver's outputs map from the persisted output text.
50
66
  */
51
67
  export declare function recoverCompletedWorkflowNodes(events: Array<{
52
68
  seq: number;
53
69
  event: SessionEvent;
54
- }>): string[];
70
+ }>): RecoveredNodeCompletion[];
55
71
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
56
72
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
57
73
  export declare function buildWorkflowNodesSubmittedEvent(input: {
58
74
  turn: number;
59
75
  nodes: Record<string, unknown>[];
76
+ baseIndex?: number;
77
+ submitterAgentId?: string;
60
78
  }): Extract<SessionEvent, {
61
79
  kind: "workflow_nodes_submitted";
62
80
  }>;
63
81
  /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
64
- * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
82
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed.
83
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
65
84
  export declare function recoverSubmittedWorkflowNodes(events: Array<{
66
85
  seq: number;
67
86
  event: SessionEvent;
68
- }>): Record<string, unknown>[][];
87
+ }>): {
88
+ submissions: Record<string, unknown>[][];
89
+ bases: number[];
90
+ submitters: Array<string | undefined>;
91
+ };
@@ -48,24 +48,38 @@ export function buildRunTerminalEvent(input) {
48
48
  total_tokens: Math.max(0, input.totalTokens),
49
49
  };
50
50
  }
51
+ /** Build workflow_node_completed for persistence after a node finishes. W-1: carries the
52
+ * result-borne control signals + output so resume replays control flow and re-seeds outputs. */
51
53
  export function buildWorkflowNodeCompletedEvent(input) {
52
54
  return {
53
55
  kind: "workflow_node_completed",
54
56
  turn: input.turn,
55
57
  agent_id: input.agentId,
56
58
  termination: input.termination,
59
+ ...(input.classifyBranch !== undefined ? { classify_branch: input.classifyBranch } : {}),
60
+ ...(input.tournamentWinner !== undefined ? { tournament_winner: input.tournamentWinner } : {}),
61
+ ...(input.loopContinue !== undefined ? { loop_continue: input.loopContinue } : {}),
62
+ ...(input.output ? { output: input.output } : {}),
57
63
  };
58
64
  }
59
65
  /**
60
- * Recover completed workflow node agent_ids from a session event stream.
61
- * Scans for workflow_node_completed events and returns the agent_ids whose
62
- * termination was "completed". Used to rebuild resumedCompleted for resumeWorkflow.
66
+ * Recover completed workflow node records from a session event stream. Scans for
67
+ * workflow_node_completed events with termination "completed" and returns them WITH their
68
+ * result-borne control signals (W-1) resumeWorkflow lowers these to the kernel's
69
+ * `resumed_results` so a classifier re-prunes and a loop stop is honored, and re-seeds the
70
+ * driver's outputs map from the persisted output text.
63
71
  */
64
72
  export function recoverCompletedWorkflowNodes(events) {
65
73
  const completed = [];
66
74
  for (const { event } of events) {
67
75
  if (event.kind === "workflow_node_completed" && event.termination === "completed") {
68
- completed.push(event.agent_id);
76
+ completed.push({
77
+ agentId: event.agent_id,
78
+ ...(event.classify_branch !== undefined ? { classifyBranch: event.classify_branch } : {}),
79
+ ...(event.tournament_winner !== undefined ? { tournamentWinner: event.tournament_winner } : {}),
80
+ ...(event.loop_continue !== undefined ? { loopContinue: event.loop_continue } : {}),
81
+ ...(event.output !== undefined ? { output: event.output } : {}),
82
+ });
69
83
  }
70
84
  }
71
85
  return completed;
@@ -73,15 +87,30 @@ export function recoverCompletedWorkflowNodes(events) {
73
87
  /** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
74
88
  * re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
75
89
  export function buildWorkflowNodesSubmittedEvent(input) {
76
- return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
90
+ return {
91
+ kind: "workflow_nodes_submitted",
92
+ turn: input.turn,
93
+ nodes: input.nodes,
94
+ ...(input.baseIndex !== undefined ? { base_index: input.baseIndex } : {}),
95
+ ...(input.submitterAgentId !== undefined ? { submitter_agent_id: input.submitterAgentId } : {}),
96
+ };
77
97
  }
78
98
  /** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
79
- * resumeWorkflow, so dynamically-appended nodes are reconstructed. */
99
+ * resumeWorkflow, so dynamically-appended nodes are reconstructed.
100
+ * `submitters` is parallel to `submissions` (undefined = host/bootstrap submission). */
80
101
  export function recoverSubmittedWorkflowNodes(events) {
81
102
  const submissions = [];
103
+ const bases = [];
104
+ const submitters = [];
82
105
  for (const { event } of events) {
83
- if (event.kind === "workflow_nodes_submitted")
106
+ if (event.kind === "workflow_nodes_submitted") {
84
107
  submissions.push(event.nodes);
108
+ submitters.push(event.submitter_agent_id);
109
+ // Absent on legacy logs → order-only replay (bases array stays parallel-short only
110
+ // if ALL records carry it; a mixed log degrades to order-only for safety).
111
+ if (event.base_index !== undefined)
112
+ bases.push(event.base_index);
113
+ }
85
114
  }
86
- return submissions;
115
+ return { submissions, bases: bases.length === submissions.length ? bases : [], submitters };
87
116
  }