@deepstrike/sdk 0.2.15 → 0.2.17
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/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +7 -5
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -2
- package/dist/runtime/execution-plane.d.ts +5 -0
- package/dist/runtime/execution-plane.js +3 -1
- package/dist/runtime/kernel-step.js +8 -1
- package/dist/runtime/process-sandbox-plane.js +14 -8
- package/dist/runtime/runner.d.ts +69 -0
- package/dist/runtime/runner.js +261 -35
- package/dist/runtime/sub-agent-orchestrator.d.ts +11 -0
- package/dist/runtime/sub-agent-orchestrator.js +78 -13
- package/dist/runtime/workflow-control-flow.d.ts +17 -0
- package/dist/runtime/workflow-control-flow.js +78 -0
- package/dist/runtime/workflow-store.d.ts +15 -0
- package/dist/runtime/workflow-store.js +47 -0
- package/dist/runtime/worktree-plane.d.ts +43 -0
- package/dist/runtime/worktree-plane.js +81 -0
- package/dist/tools/index.d.ts +9 -3
- package/dist/tools/index.js +2 -2
- package/dist/types/agent.d.ts +63 -0
- package/dist/types/agent.js +184 -44
- package/dist/types.d.ts +6 -1
- package/package.json +2 -2
package/dist/runtime/runner.js
CHANGED
|
@@ -6,10 +6,11 @@ import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
|
6
6
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
7
7
|
import { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
8
8
|
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, kernelAction, kernelApply, kernelMaybeAction, forceCompact, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
9
|
-
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
9
|
+
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
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
14
|
import { governancePolicyToKernelEvent } from "../governance.js";
|
|
14
15
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
15
16
|
import { assertNativeProfile } from "./os-profile.js";
|
|
@@ -17,6 +18,9 @@ import { LargeResultSpool } from "./large-result-spool.js";
|
|
|
17
18
|
export class RuntimeRunner {
|
|
18
19
|
opts;
|
|
19
20
|
interrupted = false;
|
|
21
|
+
/** #2-B-ii: aborts the in-flight provider stream when the run is interrupted/preempted. Recreated
|
|
22
|
+
* per `execute`; `interrupt()` fires it so a Critical `InterruptNow` cancels the live LLM call. */
|
|
23
|
+
abortController = null;
|
|
20
24
|
activeKernel = null;
|
|
21
25
|
pendingObservations = [];
|
|
22
26
|
currentSessionId = null;
|
|
@@ -25,6 +29,9 @@ export class RuntimeRunner {
|
|
|
25
29
|
pendingSpoolOutputs = new Map();
|
|
26
30
|
/** Local cache of paged-out/archived messages for priority memory retrieval. */
|
|
27
31
|
localPageOutCache = [];
|
|
32
|
+
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
33
|
+
* at the next safe point (after the tool turn resolves, kernel back in Reason — not suspended). */
|
|
34
|
+
pendingAuthoredWorkflows = [];
|
|
28
35
|
dashboard = null;
|
|
29
36
|
constructor(opts) {
|
|
30
37
|
this.opts = opts;
|
|
@@ -126,6 +133,8 @@ export class RuntimeRunner {
|
|
|
126
133
|
return new KernelRuntime({
|
|
127
134
|
maxTokens: this.opts.maxTokens,
|
|
128
135
|
maxTurns: this.opts.maxTurns,
|
|
136
|
+
// M4/G5: per-node token cap → child run's cumulative token budget.
|
|
137
|
+
maxTotalTokens: this.opts.maxTotalTokens !== undefined ? BigInt(this.opts.maxTotalTokens) : undefined,
|
|
129
138
|
timeoutMs: this.opts.timeoutMs !== undefined ? BigInt(this.opts.timeoutMs) : undefined,
|
|
130
139
|
});
|
|
131
140
|
}
|
|
@@ -266,7 +275,7 @@ export class RuntimeRunner {
|
|
|
266
275
|
* validation reason — a node that cannot meet its declared output contract starves its dependents,
|
|
267
276
|
* exactly as a denied spawn does.
|
|
268
277
|
*/
|
|
269
|
-
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
|
|
278
|
+
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs, abortSignal) {
|
|
270
279
|
// G2: a reduce node runs no LLM — execute the registered pure function over its dependency
|
|
271
280
|
// outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
|
|
272
281
|
if (node.reducer) {
|
|
@@ -284,8 +293,45 @@ export class RuntimeRunner {
|
|
|
284
293
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
285
294
|
manifest,
|
|
286
295
|
sessionLog: this.opts.sessionLog,
|
|
296
|
+
// M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel (the
|
|
297
|
+
// workflow it would author joins the running DAG) rather than bootstrapping a nested pivot.
|
|
298
|
+
isWorkflowNode: true,
|
|
299
|
+
// #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
|
|
300
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
287
301
|
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
288
302
|
});
|
|
303
|
+
const textOf = (r) => {
|
|
304
|
+
const c = r.result.finalMessage?.content;
|
|
305
|
+
return typeof c === "string" ? c : c != null ? JSON.stringify(c) : "";
|
|
306
|
+
};
|
|
307
|
+
const withSignal = (r, patch) => ({ ...r, result: { ...r.result, ...patch } });
|
|
308
|
+
// A#2 tournament judge: this node compares two entrants' produced outputs rather than running its
|
|
309
|
+
// own goal. Look up both candidates, run a judge over the controller's criterion, and report the
|
|
310
|
+
// winning entrant's agent id as `tournamentWinner` (the kernel advances the bracket with it).
|
|
311
|
+
if (node.judge_match) {
|
|
312
|
+
const out = outputs ?? new Map();
|
|
313
|
+
const left = out.get(node.judge_match.left) ?? "";
|
|
314
|
+
const right = out.get(node.judge_match.right) ?? "";
|
|
315
|
+
const result = await orchestrator.run(mkCtx(judgeGoal(baseSpec.goal, left, right)));
|
|
316
|
+
const winner = extractJudgeWinner(textOf(result));
|
|
317
|
+
const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
|
|
318
|
+
return withSignal(result, { tournamentWinner: winnerId });
|
|
319
|
+
}
|
|
320
|
+
// A#2 v2 loop iteration: run the increment, then extract a stop signal so the kernel can end the
|
|
321
|
+
// loop early (`loopContinue: false`). No signal ⇒ run to `max_iters`.
|
|
322
|
+
if (node.loop_max_iters != null) {
|
|
323
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters)}`));
|
|
324
|
+
const cont = extractLoopContinue(textOf(result));
|
|
325
|
+
return cont === undefined ? result : withSignal(result, { loopContinue: cont });
|
|
326
|
+
}
|
|
327
|
+
// A#2 classify: run the classifier, then extract the chosen branch label; the kernel runs that
|
|
328
|
+
// branch and prunes the rest. No recognizable choice ⇒ leave unset (kernel prunes all branches).
|
|
329
|
+
if (node.classify_labels && node.classify_labels.length) {
|
|
330
|
+
const labels = node.classify_labels;
|
|
331
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${classifyInstruction(labels)}`));
|
|
332
|
+
const branch = extractClassifyBranch(textOf(result), labels);
|
|
333
|
+
return branch === undefined ? result : withSignal(result, { classifyBranch: branch });
|
|
334
|
+
}
|
|
289
335
|
const schema = node.output_schema;
|
|
290
336
|
if (!schema)
|
|
291
337
|
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
@@ -350,8 +396,7 @@ export class RuntimeRunner {
|
|
|
350
396
|
}
|
|
351
397
|
const parentSessionId = this.currentSessionId;
|
|
352
398
|
const runtime = this.activeKernel;
|
|
353
|
-
const
|
|
354
|
-
let observations = kernelApply(runtime, this.pendingObservations, {
|
|
399
|
+
const observations = kernelApply(runtime, this.pendingObservations, {
|
|
355
400
|
kind: "load_workflow",
|
|
356
401
|
spec: workflowSpecToKernel(spec),
|
|
357
402
|
parent_session_id: parentSessionId,
|
|
@@ -360,6 +405,87 @@ export class RuntimeRunner {
|
|
|
360
405
|
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
361
406
|
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
362
407
|
});
|
|
408
|
+
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Unlike
|
|
412
|
+
* `runWorkflow` (the host fires the privileged `load_workflow`), this routes the spec through the
|
|
413
|
+
* agent-reachable `Syscall::LoadWorkflow` (the `submit_workflow` event): with no workflow active the
|
|
414
|
+
* kernel **bootstraps** the DAG; if one is already active it **flattens** the spec's nodes onto it
|
|
415
|
+
* (bootstrap-or-flatten — one kernel, one quota, never a workflow stack). Gated by the same
|
|
416
|
+
* `max_workflow_nodes` backstop as runtime submission, so an authored harness can't overgrow the run.
|
|
417
|
+
* The resulting batches are driven by the same shared driver as `runWorkflow`.
|
|
418
|
+
*/
|
|
419
|
+
async bootstrapWorkflow(spec, opts) {
|
|
420
|
+
if (!this.activeKernel || !this.currentSessionId) {
|
|
421
|
+
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
422
|
+
}
|
|
423
|
+
const parentSessionId = this.currentSessionId;
|
|
424
|
+
const runtime = this.activeKernel;
|
|
425
|
+
const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
426
|
+
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`. Called at the
|
|
430
|
+
* verified-safe point (right after the tool turn resolved to `call_provider` — kernel in Reason, not
|
|
431
|
+
* suspended). For each authored spec: `bootstrapWorkflow` runs it in THIS kernel (the kernel resumes
|
|
432
|
+
* the agent reason loop on `workflow_completed` — `finish_workflow` sets phase=Reason), then the
|
|
433
|
+
* outcome is injected as a user message so the agent's next turn sees the result. Returns a fresh
|
|
434
|
+
* `call_provider` synthesized from the updated context (the workflow drive consumed its own kernel
|
|
435
|
+
* actions, so we re-render — the same pattern as the reactive-compact retry path).
|
|
436
|
+
*/
|
|
437
|
+
async driveAuthoredWorkflows(runtime, action) {
|
|
438
|
+
const specs = this.pendingAuthoredWorkflows;
|
|
439
|
+
this.pendingAuthoredWorkflows = [];
|
|
440
|
+
for (const spec of specs) {
|
|
441
|
+
const outcome = await this.bootstrapWorkflow(spec);
|
|
442
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
443
|
+
kind: "add_history_message",
|
|
444
|
+
message: messageToKernelMessage({ role: "user", content: authoredWorkflowOutcomeNote(outcome) }),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return { kind: "call_provider", context: runtime.render(), tools: action.tools };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* #2-B-ii: while a workflow batch is in flight, poll the signal source. A Critical `InterruptNow`
|
|
451
|
+
* routes through the kernel (which, with the root suspended in `SubAgentAwait`, preempts — marks the
|
|
452
|
+
* running nodes `UserAbort`, tears the `WorkflowRun` down, emits `AgentPreempted`); we then abort the
|
|
453
|
+
* matching children's in-flight LLM calls (via their per-node `AbortController` → `interrupt()`).
|
|
454
|
+
* Returns the torn-down workflow's outcome on preemption, else `null`. No-op (null) without a signal
|
|
455
|
+
* source. Non-preempting signals (queue/observe/soft-interrupt) are still applied as they arrive.
|
|
456
|
+
*/
|
|
457
|
+
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
458
|
+
const source = this.opts.signalSource;
|
|
459
|
+
if (!source)
|
|
460
|
+
return null;
|
|
461
|
+
while (!batchState.settled) {
|
|
462
|
+
const sig = await source.nextSignal();
|
|
463
|
+
if (batchState.settled)
|
|
464
|
+
break;
|
|
465
|
+
if (!sig) {
|
|
466
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
const obs = kernelApply(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
470
|
+
const preempted = obs.find(o => o.kind === "agent_preempted");
|
|
471
|
+
if (preempted) {
|
|
472
|
+
for (const id of preempted.agent_ids ?? [])
|
|
473
|
+
controllers.get(id)?.abort();
|
|
474
|
+
const wc = obs.find(o => o.kind === "workflow_completed");
|
|
475
|
+
return { completed: wc?.completed ?? [], failed: wc?.failed ?? [] };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Shared workflow driver for `runWorkflow` (host `load_workflow`) and `bootstrapWorkflow` (agent
|
|
482
|
+
* `submit_workflow`): given the observations from the initial load/bootstrap, run each kernel-emitted
|
|
483
|
+
* batch in parallel, feed completions back (appending any agent-submitted nodes first), and loop
|
|
484
|
+
* until the kernel reports the workflow complete. Returns the completed / failed node agent-ids.
|
|
485
|
+
*/
|
|
486
|
+
async driveWorkflow(initial, parentSessionId, runtime) {
|
|
487
|
+
let observations = initial;
|
|
488
|
+
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
363
489
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
|
|
364
490
|
?.nodes ?? [];
|
|
365
491
|
// G4: the batch observation also carries the workflow's remaining budget; track the latest so a
|
|
@@ -368,7 +494,7 @@ export class RuntimeRunner {
|
|
|
368
494
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
369
495
|
let done = findDone(observations);
|
|
370
496
|
if (done)
|
|
371
|
-
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
497
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: {} };
|
|
372
498
|
let nodes = collectNodes(observations);
|
|
373
499
|
let budget = collectBudget(observations);
|
|
374
500
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its dependencies'
|
|
@@ -377,10 +503,21 @@ export class RuntimeRunner {
|
|
|
377
503
|
const outputs = new Map();
|
|
378
504
|
for (;;) {
|
|
379
505
|
if (nodes.length === 0)
|
|
380
|
-
return { completed: [], failed: [] }; // nothing to run (e.g. all gated)
|
|
506
|
+
return { completed: [], failed: [], outputs: Object.fromEntries(outputs) }; // nothing to run (e.g. all gated)
|
|
381
507
|
// Run the currently-runnable nodes in parallel — each is independent within a round.
|
|
382
508
|
const roundBudget = budget;
|
|
383
|
-
|
|
509
|
+
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor. While the batch is in
|
|
510
|
+
// flight the monitor polls the signal source; a Critical `InterruptNow` routes through the kernel
|
|
511
|
+
// (which preempts → `AgentPreempted` + tears the workflow down) and we abort the matching child's
|
|
512
|
+
// in-flight LLM call. If the kernel preempted, stop driving and return the torn-down outcome.
|
|
513
|
+
const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
|
|
514
|
+
const batchState = { settled: false };
|
|
515
|
+
const monitor = this.monitorWorkflowPreemption(runtime, controllers, batchState);
|
|
516
|
+
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs, controllers.get(node.agent_id)?.signal)));
|
|
517
|
+
batchState.settled = true;
|
|
518
|
+
const preempted = await monitor;
|
|
519
|
+
if (preempted)
|
|
520
|
+
return { ...preempted, outputs: Object.fromEntries(outputs) };
|
|
384
521
|
// Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
|
|
385
522
|
// dependents the moment *that* node completes (per-node unblock), so each feed can emit its
|
|
386
523
|
// own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
|
|
@@ -427,7 +564,7 @@ export class RuntimeRunner {
|
|
|
427
564
|
}));
|
|
428
565
|
}
|
|
429
566
|
if (done && nextNodes.length === 0) {
|
|
430
|
-
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
567
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: Object.fromEntries(outputs) };
|
|
431
568
|
}
|
|
432
569
|
nodes = nextNodes;
|
|
433
570
|
}
|
|
@@ -446,7 +583,7 @@ export class RuntimeRunner {
|
|
|
446
583
|
const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
|
|
447
584
|
return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions });
|
|
448
585
|
}
|
|
449
|
-
interrupt() { this.interrupted = true; }
|
|
586
|
+
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
450
587
|
async *run(req) {
|
|
451
588
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
452
589
|
const midRun = isMidRun(prior);
|
|
@@ -626,6 +763,7 @@ export class RuntimeRunner {
|
|
|
626
763
|
}
|
|
627
764
|
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
628
765
|
this.interrupted = false;
|
|
766
|
+
this.abortController = new AbortController();
|
|
629
767
|
this.pendingObservations = [];
|
|
630
768
|
this.pendingSpoolOutputs.clear();
|
|
631
769
|
this.currentSessionId = sessionId;
|
|
@@ -820,28 +958,10 @@ export class RuntimeRunner {
|
|
|
820
958
|
if (this.opts.signalSource) {
|
|
821
959
|
const sig = await this.opts.signalSource.nextSignal();
|
|
822
960
|
if (sig) {
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const
|
|
827
|
-
const summary = String(sig.payload?.goal ?? sig.kind ?? "signal");
|
|
828
|
-
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt)
|
|
829
|
-
// and emits `signal_disposed`. An actionable disposition yields a new
|
|
830
|
-
// action to adopt; queued/observed/ignored yields none (kernel buffers).
|
|
831
|
-
// Wire shape is snake_case RuntimeSignal with an object payload.
|
|
832
|
-
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
833
|
-
kind: "signal",
|
|
834
|
-
signal: {
|
|
835
|
-
id,
|
|
836
|
-
source,
|
|
837
|
-
signal_type: signalType,
|
|
838
|
-
urgency,
|
|
839
|
-
summary,
|
|
840
|
-
payload: sig.payload ?? {},
|
|
841
|
-
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
842
|
-
timestamp_ms: Date.now(),
|
|
843
|
-
},
|
|
844
|
-
});
|
|
961
|
+
// Kernel-routed: the kernel decides disposition (dedup/queue/interrupt) and emits
|
|
962
|
+
// `signal_disposed`. An actionable disposition yields a new action to adopt; queued/observed/
|
|
963
|
+
// ignored yields none (kernel buffers).
|
|
964
|
+
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
845
965
|
if (sigAction)
|
|
846
966
|
action = sigAction;
|
|
847
967
|
}
|
|
@@ -849,6 +969,15 @@ export class RuntimeRunner {
|
|
|
849
969
|
if (runtime.isTerminal())
|
|
850
970
|
break;
|
|
851
971
|
if (action.kind === "call_provider") {
|
|
972
|
+
// M5 v2.1: top-level auto-pivot at the safe point. If the agent authored sub-workflow(s) via
|
|
973
|
+
// `start_workflow`, drive each in THIS kernel now (the kernel is in Reason / `call_provider`,
|
|
974
|
+
// NOT suspended — driving mid-suspend would clobber the single-slot suspend state), inject the
|
|
975
|
+
// outcome into context, and re-render. Loop-top placement (vs only after `tool_results`) catches
|
|
976
|
+
// EVERY path to `call_provider` — including resuming after an approval gate — so a queued spec
|
|
977
|
+
// is never stranded. Drains the queue; fires once per authored batch.
|
|
978
|
+
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
979
|
+
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
980
|
+
}
|
|
852
981
|
const finalToolCalls = [];
|
|
853
982
|
let finalText = "";
|
|
854
983
|
const context = action.context;
|
|
@@ -857,8 +986,14 @@ export class RuntimeRunner {
|
|
|
857
986
|
let turnInputTokens = 0;
|
|
858
987
|
let turnOutputTokens = 0;
|
|
859
988
|
let shouldRetry = false;
|
|
989
|
+
const abortSignal = this.abortController?.signal;
|
|
860
990
|
try {
|
|
861
|
-
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState)) {
|
|
991
|
+
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
992
|
+
// #2-B-ii: a preempting `interrupt()` fires `abortController` — stop consuming the live
|
|
993
|
+
// stream immediately (providers that forward `signal` also abort the socket; the rest at
|
|
994
|
+
// least stop here at the next event). The loop-top `interrupted` check then ends the run.
|
|
995
|
+
if (abortSignal?.aborted)
|
|
996
|
+
break;
|
|
862
997
|
if (evt.type === "usage") {
|
|
863
998
|
const usageEvt = evt;
|
|
864
999
|
turnTokens = usageEvt.totalTokens;
|
|
@@ -876,6 +1011,11 @@ export class RuntimeRunner {
|
|
|
876
1011
|
}
|
|
877
1012
|
}
|
|
878
1013
|
catch (err) {
|
|
1014
|
+
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat it as an interrupt
|
|
1015
|
+
// (the loop-top `interrupted` check converts it to a clean `timeout`/UserAbort), not a crash.
|
|
1016
|
+
if (abortSignal?.aborted) {
|
|
1017
|
+
this.interrupted = true;
|
|
1018
|
+
}
|
|
879
1019
|
const errMsg = String(err).toLowerCase();
|
|
880
1020
|
if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
|
|
881
1021
|
!hasAttemptedReactiveCompact) {
|
|
@@ -891,6 +1031,13 @@ export class RuntimeRunner {
|
|
|
891
1031
|
break;
|
|
892
1032
|
}
|
|
893
1033
|
}
|
|
1034
|
+
// #2-B-ii: stream aborted (preempt/interrupt) via the break path (provider yielded no error) —
|
|
1035
|
+
// end the turn now with a timeout so the kernel terminates the run, rather than feeding the
|
|
1036
|
+
// partial assistant output as a normal turn.
|
|
1037
|
+
if (abortSignal?.aborted) {
|
|
1038
|
+
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
894
1041
|
if (shouldRetry) {
|
|
895
1042
|
action = {
|
|
896
1043
|
kind: "call_provider",
|
|
@@ -946,9 +1093,11 @@ export class RuntimeRunner {
|
|
|
946
1093
|
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
947
1094
|
};
|
|
948
1095
|
const toolResults = [];
|
|
949
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes");
|
|
1096
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
950
1097
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
951
|
-
|
|
1098
|
+
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path as
|
|
1099
|
+
// `submit_workflow_nodes` — a `WorkflowSpec` is a node batch. (v2 adds top-level bootstrap.)
|
|
1100
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
952
1101
|
for (const call of planCalls) {
|
|
953
1102
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
954
1103
|
kernelApply(runtime, this.pendingObservations, {
|
|
@@ -965,7 +1114,24 @@ export class RuntimeRunner {
|
|
|
965
1114
|
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
966
1115
|
// simply unconsumed — a no-op.)
|
|
967
1116
|
for (const call of submitCalls) {
|
|
968
|
-
|
|
1117
|
+
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
1118
|
+
// full spec and AUTO-PIVOT once this tool turn resolves (the loop drives it in this kernel and
|
|
1119
|
+
// injects the outcome). A workflow-NODE's `start_workflow` (and every `submit_workflow_nodes`)
|
|
1120
|
+
// instead FLATTENS: the batch is surfaced for the parent `runWorkflow` to append.
|
|
1121
|
+
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
1122
|
+
const spec = parseStartWorkflowSpec(call.arguments);
|
|
1123
|
+
if (spec) {
|
|
1124
|
+
this.pendingAuthoredWorkflows.push(spec);
|
|
1125
|
+
const out = "workflow authored; executing now";
|
|
1126
|
+
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
1127
|
+
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
1128
|
+
continue;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// `start_workflow` wraps the batch as `{ spec: { nodes } }`; `submit_workflow_nodes` is `{ nodes }`.
|
|
1132
|
+
const nodes = call.name === "start_workflow"
|
|
1133
|
+
? parseStartWorkflowArgs(call.arguments)
|
|
1134
|
+
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
969
1135
|
yield { type: "workflow_nodes_submitted", nodes };
|
|
970
1136
|
const result = { callId: call.id, output: "submitted", isError: false };
|
|
971
1137
|
toolResults.push(result);
|
|
@@ -1487,3 +1653,63 @@ function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
|
1487
1653
|
}
|
|
1488
1654
|
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
1489
1655
|
}
|
|
1656
|
+
/** M5 v1: parse the `start_workflow` tool arguments (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into
|
|
1657
|
+
* the spec's node batch — flattened onto the running workflow via the same append path. A malformed
|
|
1658
|
+
* payload yields no nodes rather than throwing. */
|
|
1659
|
+
function parseStartWorkflowArgs(argsStr) {
|
|
1660
|
+
let parsed = {};
|
|
1661
|
+
try {
|
|
1662
|
+
parsed = JSON.parse(argsStr);
|
|
1663
|
+
}
|
|
1664
|
+
catch {
|
|
1665
|
+
// Ignore parse error → no nodes.
|
|
1666
|
+
}
|
|
1667
|
+
const spec = parsed.spec;
|
|
1668
|
+
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
1669
|
+
}
|
|
1670
|
+
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call, for auto-pivot drive
|
|
1671
|
+
* (vs `parseStartWorkflowArgs`, which returns only the node batch for the flatten path). Returns
|
|
1672
|
+
* `undefined` on a malformed / empty payload so the caller falls back to the flatten path. */
|
|
1673
|
+
function parseStartWorkflowSpec(argsStr) {
|
|
1674
|
+
try {
|
|
1675
|
+
const parsed = JSON.parse(argsStr);
|
|
1676
|
+
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
1677
|
+
return { nodes: parsed.spec.nodes };
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
catch {
|
|
1681
|
+
// Ignore parse error → undefined (fall back to flatten).
|
|
1682
|
+
}
|
|
1683
|
+
return undefined;
|
|
1684
|
+
}
|
|
1685
|
+
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
1686
|
+
* agent's context, so the agent's next turn continues with the sub-workflow's results in view. */
|
|
1687
|
+
function authoredWorkflowOutcomeNote(outcome) {
|
|
1688
|
+
const lines = [
|
|
1689
|
+
`[authored workflow result] ${outcome.completed.length} node(s) completed` +
|
|
1690
|
+
(outcome.failed.length ? `, ${outcome.failed.length} failed` : "") + ".",
|
|
1691
|
+
];
|
|
1692
|
+
for (const id of outcome.completed) {
|
|
1693
|
+
const out = outcome.outputs[id];
|
|
1694
|
+
if (out)
|
|
1695
|
+
lines.push(`- ${id}: ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
1696
|
+
}
|
|
1697
|
+
return lines.join("\n");
|
|
1698
|
+
}
|
|
1699
|
+
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
1700
|
+
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
1701
|
+
function signalToKernelEvent(sig) {
|
|
1702
|
+
return {
|
|
1703
|
+
kind: "signal",
|
|
1704
|
+
signal: {
|
|
1705
|
+
id: crypto.randomUUID(),
|
|
1706
|
+
source: sig.source ?? "custom",
|
|
1707
|
+
signal_type: sig.signalType ?? "event",
|
|
1708
|
+
urgency: sig.urgency ?? "normal",
|
|
1709
|
+
summary: String(sig.payload?.goal ?? sig.kind ?? "signal"),
|
|
1710
|
+
payload: sig.payload ?? {},
|
|
1711
|
+
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
1712
|
+
timestamp_ms: Date.now(),
|
|
1713
|
+
},
|
|
1714
|
+
};
|
|
1715
|
+
}
|
|
@@ -12,7 +12,18 @@ export interface SubAgentRunContext {
|
|
|
12
12
|
evalProvider: import("../types.js").LLMProvider;
|
|
13
13
|
maxAttempts?: number;
|
|
14
14
|
};
|
|
15
|
+
/** M5 v2.1: set when this child is a workflow node (spawned by the workflow driver). Propagated to
|
|
16
|
+
* the child runner so a nested `start_workflow` FLATTENS to the parent kernel rather than
|
|
17
|
+
* auto-pivoting into its own bootstrap (which would fragment the one-kernel/one-quota governance). */
|
|
18
|
+
isWorkflowNode?: boolean;
|
|
19
|
+
/** #2-B-ii: parent-controlled abort. When this fires (the kernel preempted this node via
|
|
20
|
+
* `InterruptNow` → `AgentPreempted`), the orchestrator interrupts the child runner, cancelling its
|
|
21
|
+
* in-flight LLM call. */
|
|
22
|
+
abortSignal?: AbortSignal;
|
|
15
23
|
}
|
|
24
|
+
/** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
|
|
25
|
+
* Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
|
|
26
|
+
export declare function resolveProvider(opts: RuntimeOptions, modelHint?: string): RuntimeOptions["provider"];
|
|
16
27
|
/** Host-side driver for kernel-isolated sub-agent runs. */
|
|
17
28
|
export declare class SubAgentOrchestrator {
|
|
18
29
|
stream(ctx: SubAgentRunContext): AsyncIterable<StreamEvent>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { agentRunSpecToKernel, findSpawnProcessObservation, spawnObservationToManifest } from "../types/agent.js";
|
|
2
2
|
import { FilteredExecutionPlane } from "./filtered-plane.js";
|
|
3
|
+
import { WorktreeExecutionPlane } from "./worktree-plane.js";
|
|
3
4
|
import { kernelApply } from "./kernel-step.js";
|
|
4
5
|
function terminationFromStatus(status) {
|
|
5
6
|
const normalized = status.toLowerCase();
|
|
@@ -14,6 +15,38 @@ function terminationFromStatus(status) {
|
|
|
14
15
|
}
|
|
15
16
|
return status;
|
|
16
17
|
}
|
|
18
|
+
/** M3/G4: if this sub-agent is an `isolation: "worktree"` node and a worktree manager is configured,
|
|
19
|
+
* wrap its plane in a `WorktreeExecutionPlane` (creates a git worktree, injects it as `cwd`, removes
|
|
20
|
+
* it on cleanup). Returns the plane to use plus a cleanup hook the caller must run when the sub-agent
|
|
21
|
+
* finishes. Without a manager (or for non-worktree nodes) this is a pass-through with a no-op cleanup. */
|
|
22
|
+
function withWorktree(ctx, plane) {
|
|
23
|
+
if (ctx.manifest.isolation === "worktree" && ctx.parentOpts.worktreeManager) {
|
|
24
|
+
const wt = new WorktreeExecutionPlane(plane, ctx.parentOpts.worktreeManager, ctx.spec.identity.agentId);
|
|
25
|
+
return { plane: wt, cleanup: () => wt.cleanup() };
|
|
26
|
+
}
|
|
27
|
+
return { plane, cleanup: async () => { } };
|
|
28
|
+
}
|
|
29
|
+
/** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
|
|
30
|
+
* Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
|
|
31
|
+
export function resolveProvider(opts, modelHint) {
|
|
32
|
+
if (modelHint && opts.providerFor) {
|
|
33
|
+
const routed = opts.providerFor(modelHint);
|
|
34
|
+
if (routed)
|
|
35
|
+
return routed;
|
|
36
|
+
}
|
|
37
|
+
return opts.provider;
|
|
38
|
+
}
|
|
39
|
+
/** #2-B-ii: bridge a parent-controlled AbortSignal to a child runner's `interrupt()` — fires now if
|
|
40
|
+
* the signal is already aborted (creation race), else once when it aborts. */
|
|
41
|
+
function linkAbort(signal, runner) {
|
|
42
|
+
if (!signal)
|
|
43
|
+
return;
|
|
44
|
+
if (signal.aborted) {
|
|
45
|
+
runner.interrupt();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
signal.addEventListener("abort", () => runner.interrupt(), { once: true });
|
|
49
|
+
}
|
|
17
50
|
/** Derive which meta-tools a child runner should expose based on permitted IDs and available sources. */
|
|
18
51
|
function deriveMetaTools(permitted, opts) {
|
|
19
52
|
const metaTools = new Set();
|
|
@@ -33,6 +66,8 @@ export class SubAgentOrchestrator {
|
|
|
33
66
|
const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
|
|
34
67
|
const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
|
|
35
68
|
const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
|
|
69
|
+
// M3/G4: a worktree node runs inside its own git worktree (created here, removed in `finally`).
|
|
70
|
+
const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, filteredPlane);
|
|
36
71
|
let systemPrompt = ctx.parentOpts.systemPrompt;
|
|
37
72
|
let inheritEvents;
|
|
38
73
|
if (ctx.manifest.context_inheritance === "full") {
|
|
@@ -48,7 +83,11 @@ export class SubAgentOrchestrator {
|
|
|
48
83
|
const { RuntimeRunner } = await import("./runner.js");
|
|
49
84
|
const childRunner = new RuntimeRunner({
|
|
50
85
|
...ctx.parentOpts,
|
|
51
|
-
|
|
86
|
+
// M1/G3: route to the node's hinted model (falls back to the parent provider).
|
|
87
|
+
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
88
|
+
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
89
|
+
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
90
|
+
executionPlane: execPlane,
|
|
52
91
|
agentId: ctx.spec.identity.agentId,
|
|
53
92
|
systemPrompt,
|
|
54
93
|
sessionLog: ctx.sessionLog,
|
|
@@ -56,12 +95,22 @@ export class SubAgentOrchestrator {
|
|
|
56
95
|
dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
|
|
57
96
|
knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
|
|
58
97
|
enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
|
|
98
|
+
// M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
|
|
99
|
+
isWorkflowNode: ctx.isWorkflowNode,
|
|
59
100
|
});
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
101
|
+
// #2-B-ii: when the parent preempts this node (kernel `AgentPreempted`), interrupt the child —
|
|
102
|
+
// cancelling its in-flight LLM call. Handle an already-aborted signal too (creation race).
|
|
103
|
+
linkAbort(ctx.abortSignal, childRunner);
|
|
104
|
+
try {
|
|
105
|
+
yield* childRunner.run({
|
|
106
|
+
sessionId: ctx.spec.identity.sessionId,
|
|
107
|
+
goal: ctx.spec.goal,
|
|
108
|
+
inheritEvents,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await cleanupWorktree();
|
|
113
|
+
}
|
|
65
114
|
}
|
|
66
115
|
async run(ctx) {
|
|
67
116
|
if (ctx.harness) {
|
|
@@ -70,25 +119,41 @@ export class SubAgentOrchestrator {
|
|
|
70
119
|
const permitted = new Set(ctx.manifest.permitted_capability_ids ?? []);
|
|
71
120
|
const metaTools = deriveMetaTools(permitted, ctx.parentOpts);
|
|
72
121
|
const filteredPlane = new FilteredExecutionPlane(ctx.parentOpts.executionPlane, permitted, metaTools);
|
|
122
|
+
// M3/G4: worktree isolation for a worktree node (cleaned up in `finally` below).
|
|
123
|
+
const { plane: execPlane, cleanup: cleanupWorktree } = withWorktree(ctx, filteredPlane);
|
|
73
124
|
const childRunner = new RuntimeRunner({
|
|
74
125
|
...ctx.parentOpts,
|
|
75
|
-
|
|
126
|
+
// M1/G3: route to the node's hinted model (falls back to the parent provider).
|
|
127
|
+
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
128
|
+
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
129
|
+
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
130
|
+
executionPlane: execPlane,
|
|
76
131
|
agentId: ctx.spec.identity.agentId,
|
|
77
132
|
sessionLog: ctx.sessionLog,
|
|
78
133
|
skillDir: metaTools.has("skill") ? ctx.parentOpts.skillDir : undefined,
|
|
79
134
|
dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
|
|
80
135
|
knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
|
|
81
136
|
enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
|
|
137
|
+
// M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
|
|
138
|
+
isWorkflowNode: ctx.isWorkflowNode,
|
|
82
139
|
});
|
|
140
|
+
// #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
|
|
141
|
+
linkAbort(ctx.abortSignal, childRunner);
|
|
83
142
|
const loop = new HarnessLoop(childRunner, ctx.harness.evalProvider, {
|
|
84
143
|
maxAttempts: ctx.harness.maxAttempts ?? 3,
|
|
85
144
|
});
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
|
|
145
|
+
let outcome;
|
|
146
|
+
try {
|
|
147
|
+
outcome = await loop.run({
|
|
148
|
+
goal: ctx.spec.goal,
|
|
149
|
+
criteria: (ctx.spec.milestones?.phases.flatMap(p => p.criteria) ?? [])
|
|
150
|
+
.filter((t) => typeof t === "string")
|
|
151
|
+
.map(text => ({ text, required: true })),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await cleanupWorktree();
|
|
156
|
+
}
|
|
92
157
|
return {
|
|
93
158
|
agentId: ctx.spec.identity.agentId,
|
|
94
159
|
result: {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Instruction appended to a loop node's goal: do the next increment, and signal when done. */
|
|
2
|
+
export declare function loopInstruction(maxIters: number): string;
|
|
3
|
+
/** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
|
|
4
|
+
export declare function classifyInstruction(labels: string[]): string;
|
|
5
|
+
/** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */
|
|
6
|
+
export declare function judgeGoal(criterion: string, leftOutput: string, rightOutput: string): string;
|
|
7
|
+
/** Extract a loop stop signal from a loop iteration's output. Returns the `loopContinue` value, or
|
|
8
|
+
* `undefined` when the agent gave no clear signal (⇒ the kernel runs the loop to `max_iters`).
|
|
9
|
+
* Accepts `{loop_continue: bool}` or, leniently, `{done: bool}` (continue = !done). */
|
|
10
|
+
export declare function extractLoopContinue(text: string): boolean | undefined;
|
|
11
|
+
/** Extract the chosen branch label from a classifier's output. Prefers `{branch: "..."}`; falls back
|
|
12
|
+
* to a bare label string that exactly matches one of the valid labels. Returns `undefined` when no
|
|
13
|
+
* recognizable choice was made (the kernel then prunes every branch — a safe "none matched"). */
|
|
14
|
+
export declare function extractClassifyBranch(text: string, labels: string[]): string | undefined;
|
|
15
|
+
/** Extract a tournament judge's verdict ("left" or "right"). Defaults to "left" when the verdict is
|
|
16
|
+
* unparseable, so the bracket always advances to a champion rather than stalling with no winner. */
|
|
17
|
+
export declare function extractJudgeWinner(text: string): "left" | "right";
|