@deepstrike/wasm 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 +2 -1
- package/dist/index.js +2 -1
- package/dist/providers/anthropic.d.ts +1 -1
- package/dist/providers/anthropic.js +2 -1
- package/dist/providers/openai.d.ts +5 -5
- package/dist/providers/openai.js +10 -9
- package/dist/runtime/execution-plane.d.ts +3 -0
- package/dist/runtime/execution-plane.js +3 -1
- package/dist/runtime/kernel-step.js +8 -1
- package/dist/runtime/runner.d.ts +50 -0
- package/dist/runtime/runner.js +228 -30
- package/dist/runtime/sub-agent-orchestrator.d.ts +9 -0
- package/dist/runtime/sub-agent-orchestrator.js +28 -0
- package/dist/runtime/types/agent.d.ts +61 -2
- package/dist/runtime/types/agent.js +145 -29
- package/dist/runtime/workflow-control-flow.d.ts +17 -0
- package/dist/runtime/workflow-control-flow.js +78 -0
- package/dist/tools/index.d.ts +9 -2
- package/dist/tools/index.js +1 -1
- package/dist/types.d.ts +5 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
|
|
|
4
4
|
export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
|
|
5
5
|
export type { SubAgentRunContext } from "./runtime/sub-agent-orchestrator.js";
|
|
6
6
|
export type { AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRunSpec, AgentProcessChangedObservation, ContextInheritance, KernelAgentRole, LoopResult, MilestoneCheckResult, MilestoneContract, MilestonePhase, MilestonePolicy, SubAgentResult, TerminationReason, WorkflowSpec, WorkflowNodeSpec, WorkflowTaskSpec, WorkflowSpawnInfo, } from "./runtime/types/agent.js";
|
|
7
|
-
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
7
|
+
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, submitWorkflowNodesTool, startWorkflowTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
8
|
+
export { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./runtime/workflow-control-flow.js";
|
|
8
9
|
export { Governance } from "./governance.js";
|
|
9
10
|
export type { GovernanceVerdict } from "./governance.js";
|
|
10
11
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { RuntimeRunner, collectText, InMemorySessionLog, LocalExecutionPlane, DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./runtime/index.js";
|
|
2
2
|
export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
|
|
3
3
|
export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
|
|
4
|
-
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
4
|
+
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, submitWorkflowNodesTool, startWorkflowTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
5
|
+
export { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./runtime/workflow-control-flow.js";
|
|
5
6
|
export { Governance } from "./governance.js";
|
|
6
7
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
7
8
|
export { OpenAIProvider, QwenProvider, DeepSeekProvider, MiniMaxProvider, KimiProvider } from "./providers/openai.js";
|
|
@@ -9,6 +9,6 @@ export declare class AnthropicProvider implements LLMProvider {
|
|
|
9
9
|
peekProviderReplay(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
|
|
10
10
|
seedProviderReplay(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
|
|
11
11
|
complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
|
|
12
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown
|
|
12
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, _state?: unknown, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
13
13
|
private rememberNativeBlocks;
|
|
14
14
|
}
|
|
@@ -109,7 +109,7 @@ export class AnthropicProvider {
|
|
|
109
109
|
async complete(context, tools, extensions) {
|
|
110
110
|
return collectStreamMessage(this.stream(context, tools, extensions));
|
|
111
111
|
}
|
|
112
|
-
async *stream(context, tools, extensions) {
|
|
112
|
+
async *stream(context, tools, extensions, _state, signal) {
|
|
113
113
|
const systemBlocks = [];
|
|
114
114
|
if (context.systemStable) {
|
|
115
115
|
systemBlocks.push({ type: "text", text: context.systemStable, cache_control: { type: "ephemeral" } });
|
|
@@ -150,6 +150,7 @@ export class AnthropicProvider {
|
|
|
150
150
|
"anthropic-beta": "prompt-caching-2024-07-31",
|
|
151
151
|
},
|
|
152
152
|
body: JSON.stringify(body),
|
|
153
|
+
...(signal ? { signal } : {}), // #2-B-ii: a preempt aborts the in-flight request at the socket.
|
|
153
154
|
});
|
|
154
155
|
if (!resp.ok)
|
|
155
156
|
throw new Error(`Anthropic ${resp.status}: ${await resp.text()}`);
|
|
@@ -13,20 +13,20 @@ export declare class OpenAIProvider implements LLMProvider {
|
|
|
13
13
|
};
|
|
14
14
|
}[];
|
|
15
15
|
complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
|
|
16
|
-
protected streamInner(context: RenderedContext, tools: ToolSchema[], extraBody: Record<string, unknown>, exposeReasoning?: boolean): AsyncIterable<StreamEvent>;
|
|
17
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown
|
|
16
|
+
protected streamInner(context: RenderedContext, tools: ToolSchema[], extraBody: Record<string, unknown>, exposeReasoning?: boolean, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
17
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, _state?: unknown, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
18
18
|
}
|
|
19
19
|
export declare class QwenProvider extends OpenAIProvider {
|
|
20
20
|
constructor(apiKey: string, model?: string);
|
|
21
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown
|
|
21
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, _state?: unknown, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
22
22
|
}
|
|
23
23
|
export declare class DeepSeekProvider extends OpenAIProvider {
|
|
24
24
|
constructor(apiKey: string, model?: string);
|
|
25
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown
|
|
25
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, _state?: unknown, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
26
26
|
}
|
|
27
27
|
export declare class MiniMaxProvider extends OpenAIProvider {
|
|
28
28
|
constructor(apiKey: string, model?: string);
|
|
29
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown
|
|
29
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, _state?: unknown, signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
30
30
|
}
|
|
31
31
|
export declare class KimiProvider extends OpenAIProvider {
|
|
32
32
|
constructor(apiKey: string, model?: string);
|
package/dist/providers/openai.js
CHANGED
|
@@ -17,7 +17,7 @@ export class OpenAIProvider {
|
|
|
17
17
|
async complete(context, tools, extensions) {
|
|
18
18
|
return collectStreamMessage(this.stream(context, tools, extensions));
|
|
19
19
|
}
|
|
20
|
-
async *streamInner(context, tools, extraBody, exposeReasoning = false) {
|
|
20
|
+
async *streamInner(context, tools, extraBody, exposeReasoning = false, signal) {
|
|
21
21
|
const body = {
|
|
22
22
|
model: this.model,
|
|
23
23
|
messages: toOpenAIMessages(context),
|
|
@@ -29,6 +29,7 @@ export class OpenAIProvider {
|
|
|
29
29
|
method: "POST",
|
|
30
30
|
headers: { "Authorization": `Bearer ${this.apiKey}`, "Content-Type": "application/json" },
|
|
31
31
|
body: JSON.stringify(body),
|
|
32
|
+
...(signal ? { signal } : {}), // #2-B-ii: a preempt aborts the in-flight request at the socket.
|
|
32
33
|
});
|
|
33
34
|
if (!resp.ok)
|
|
34
35
|
throw new Error(`OpenAI ${resp.status}: ${await resp.text()}`);
|
|
@@ -79,16 +80,16 @@ export class OpenAIProvider {
|
|
|
79
80
|
yield { type: "tool_call", id: tb.id, name: tb.name, arguments: args };
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
|
-
async *stream(context, tools, extensions) {
|
|
83
|
+
async *stream(context, tools, extensions, _state, signal) {
|
|
83
84
|
const { expose_reasoning: _, exposeReasoning: __, ...passthrough } = extensions ?? {};
|
|
84
|
-
yield* this.streamInner(context, tools, passthrough);
|
|
85
|
+
yield* this.streamInner(context, tools, passthrough, false, signal);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
export class QwenProvider extends OpenAIProvider {
|
|
88
89
|
constructor(apiKey, model = "qwen-max") {
|
|
89
90
|
super(apiKey, model, "https://dashscope.aliyuncs.com/compatible-mode/v1");
|
|
90
91
|
}
|
|
91
|
-
async *stream(context, tools, extensions) {
|
|
92
|
+
async *stream(context, tools, extensions, _state, signal) {
|
|
92
93
|
const enableThinking = Boolean(extensions?.enableThinking);
|
|
93
94
|
const thinkingBudget = extensions?.thinkingBudget;
|
|
94
95
|
const { enableThinking: _, thinkingBudget: __, expose_reasoning: ___, exposeReasoning: ____, ...passthrough } = extensions ?? {};
|
|
@@ -96,31 +97,31 @@ export class QwenProvider extends OpenAIProvider {
|
|
|
96
97
|
...passthrough,
|
|
97
98
|
...(enableThinking ? { enable_thinking: true, ...(thinkingBudget ? { thinking_budget: thinkingBudget } : {}) } : {}),
|
|
98
99
|
};
|
|
99
|
-
yield* this.streamInner(context, tools, extra, enableThinking);
|
|
100
|
+
yield* this.streamInner(context, tools, extra, enableThinking, signal);
|
|
100
101
|
}
|
|
101
102
|
}
|
|
102
103
|
export class DeepSeekProvider extends OpenAIProvider {
|
|
103
104
|
constructor(apiKey, model = "deepseek-chat") {
|
|
104
105
|
super(apiKey, model, "https://api.deepseek.com/v1");
|
|
105
106
|
}
|
|
106
|
-
async *stream(context, tools, extensions) {
|
|
107
|
+
async *stream(context, tools, extensions, _state, signal) {
|
|
107
108
|
const exposeReasoning = Boolean(extensions?.exposeReasoning);
|
|
108
109
|
const isReasoner = DEEPSEEK_REASONERS.has(this.model);
|
|
109
110
|
const filteredTools = isReasoner ? [] : tools;
|
|
110
111
|
const { exposeReasoning: _, expose_reasoning: __, ...passthrough } = extensions ?? {};
|
|
111
|
-
yield* this.streamInner(context, filteredTools, passthrough, exposeReasoning);
|
|
112
|
+
yield* this.streamInner(context, filteredTools, passthrough, exposeReasoning, signal);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
export class MiniMaxProvider extends OpenAIProvider {
|
|
115
116
|
constructor(apiKey, model = "MiniMax-Text-01") {
|
|
116
117
|
super(apiKey, model, "https://api.minimax.chat/v1");
|
|
117
118
|
}
|
|
118
|
-
async *stream(context, tools, extensions) {
|
|
119
|
+
async *stream(context, tools, extensions, _state, signal) {
|
|
119
120
|
const exposeReasoning = Boolean(extensions?.exposeReasoning);
|
|
120
121
|
const isReasoner = MINIMAX_REASONERS.has(this.model);
|
|
121
122
|
const filteredTools = isReasoner ? [] : tools;
|
|
122
123
|
const { exposeReasoning: _, expose_reasoning: __, ...passthrough } = extensions ?? {};
|
|
123
|
-
yield* this.streamInner(context, filteredTools, passthrough, exposeReasoning);
|
|
124
|
+
yield* this.streamInner(context, filteredTools, passthrough, exposeReasoning, signal);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
export class KimiProvider extends OpenAIProvider {
|
|
@@ -18,6 +18,9 @@ export interface RunContext {
|
|
|
18
18
|
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
19
19
|
onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
|
|
20
20
|
resultSpool?: LargeResultSpool;
|
|
21
|
+
/** M3/G4: working directory a tool should run in. WASM has no filesystem, so this is carried for
|
|
22
|
+
* tool-ABI parity with Node/Python rather than consumed by a worktree plane. */
|
|
23
|
+
cwd?: string;
|
|
21
24
|
}
|
|
22
25
|
export interface ExecutionPlane {
|
|
23
26
|
register(...tools: RegisteredTool[]): this;
|
|
@@ -73,7 +73,9 @@ export class LocalExecutionPlane {
|
|
|
73
73
|
}
|
|
74
74
|
try {
|
|
75
75
|
const args = JSON.parse(call.arguments || "{}");
|
|
76
|
-
|
|
76
|
+
// M3/G4: pass the run context for tool-ABI parity with Node/Python (`RunContext` is
|
|
77
|
+
// structurally assignable to the tool's `ToolExecContext`).
|
|
78
|
+
const output = await registered.execute(args, ctx);
|
|
77
79
|
yield { type: "tool_result", callId: call.id, name: call.name, content: String(output), isError: false };
|
|
78
80
|
}
|
|
79
81
|
catch (err) {
|
|
@@ -112,12 +112,19 @@ function kernelMessageToSdk(raw) {
|
|
|
112
112
|
return message;
|
|
113
113
|
}
|
|
114
114
|
function renderedContextToSdk(raw) {
|
|
115
|
-
|
|
115
|
+
const rawStateTurn = (raw.state_turn ?? raw.stateTurn);
|
|
116
|
+
const frozenLen = (raw.frozen_prefix_len ?? raw.frozenPrefixLen);
|
|
117
|
+
const ctx = {
|
|
116
118
|
systemText: String(raw.system_text ?? raw.systemText ?? ""),
|
|
117
119
|
systemStable: String(raw.system_stable ?? raw.systemStable ?? ""),
|
|
118
120
|
systemKnowledge: String(raw.system_knowledge ?? raw.systemKnowledge ?? ""),
|
|
119
121
|
turns: (raw.turns ?? []).map(kernelMessageToSdk),
|
|
120
122
|
};
|
|
123
|
+
if (rawStateTurn)
|
|
124
|
+
ctx.stateTurn = kernelMessageToSdk(rawStateTurn);
|
|
125
|
+
if (typeof frozenLen === "number")
|
|
126
|
+
ctx.frozenPrefixLen = frozenLen;
|
|
127
|
+
return ctx;
|
|
121
128
|
}
|
|
122
129
|
function mapKernelAction(raw) {
|
|
123
130
|
switch (raw.kind) {
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export interface MemoryPolicy {
|
|
|
42
42
|
}
|
|
43
43
|
export interface RuntimeOptions {
|
|
44
44
|
provider: LLMProvider;
|
|
45
|
+
/** M1/G3 intelligence routing: resolve a per-node provider from a workflow node's `modelHint`.
|
|
46
|
+
* Returns undefined ⇒ fall back to `provider`. Without this hook the hint is a no-op. */
|
|
47
|
+
providerFor?: (modelHint: string) => LLMProvider | undefined;
|
|
48
|
+
/** M4/G5: cumulative token cap for this run (the kernel's `max_total_tokens`); a node's `tokenBudget`
|
|
49
|
+
* flows here for its child run. Undefined ⇒ the kernel default. */
|
|
50
|
+
maxTotalTokens?: number;
|
|
45
51
|
sessionLog: SessionLog;
|
|
46
52
|
executionPlane: ExecutionPlane;
|
|
47
53
|
maxTokens: number;
|
|
@@ -70,6 +76,10 @@ export interface RuntimeOptions {
|
|
|
70
76
|
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
71
77
|
onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
|
|
72
78
|
subAgentOrchestrator?: SubAgentOrchestrator;
|
|
79
|
+
/** M5 v2.1: marks this runner as a workflow node (child of the workflow driver). A workflow node's
|
|
80
|
+
* `start_workflow` FLATTENS to the parent kernel; a top-level run (unset) AUTO-PIVOTS — bootstraps +
|
|
81
|
+
* drives the authored workflow in its own kernel, then resumes the reason loop with the outcome. */
|
|
82
|
+
isWorkflowNode?: boolean;
|
|
73
83
|
/** G2: custom reducers for `NodeKind::Reduce` workflow nodes, merged over the built-ins. */
|
|
74
84
|
reducers?: ReducerRegistry;
|
|
75
85
|
milestonePolicy?: MilestonePolicy;
|
|
@@ -88,11 +98,16 @@ export interface RuntimeOptions {
|
|
|
88
98
|
export declare class RuntimeRunner {
|
|
89
99
|
private readonly opts;
|
|
90
100
|
private interrupted;
|
|
101
|
+
/** #2-B-ii: aborts the in-flight provider stream on interrupt/preempt. Recreated per `execute`. */
|
|
102
|
+
private abortController;
|
|
91
103
|
private pendingObservations;
|
|
92
104
|
private activeKernel;
|
|
93
105
|
private currentSessionId;
|
|
94
106
|
private nextArchiveStart;
|
|
95
107
|
private localPageOutCache;
|
|
108
|
+
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
109
|
+
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
110
|
+
private pendingAuthoredWorkflows;
|
|
96
111
|
private pendingSpoolOutputs;
|
|
97
112
|
constructor(opts: RuntimeOptions);
|
|
98
113
|
get hostOptions(): RuntimeOptions;
|
|
@@ -140,7 +155,42 @@ export declare class RuntimeRunner {
|
|
|
140
155
|
}): Promise<{
|
|
141
156
|
completed: string[];
|
|
142
157
|
failed: string[];
|
|
158
|
+
outputs: Record<string, string>;
|
|
143
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Routes the
|
|
162
|
+
* spec through the agent-reachable `Syscall::LoadWorkflow` (`submit_workflow`): with no workflow
|
|
163
|
+
* active the kernel bootstraps the DAG, else it flattens onto the running one (bootstrap-or-flatten —
|
|
164
|
+
* one kernel, one quota). The same shared driver runs the resulting batches.
|
|
165
|
+
*/
|
|
166
|
+
bootstrapWorkflow(spec: WorkflowSpec, opts?: {
|
|
167
|
+
submitterAgentId?: string;
|
|
168
|
+
}): Promise<{
|
|
169
|
+
completed: string[];
|
|
170
|
+
failed: string[];
|
|
171
|
+
outputs: Record<string, string>;
|
|
172
|
+
}>;
|
|
173
|
+
/**
|
|
174
|
+
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
|
|
175
|
+
* point (tool turn resolved → kernel in Reason). Each runs in THIS kernel (the kernel resumes the
|
|
176
|
+
* reason loop on `workflow_completed`), then the outcome is injected as a user message and a fresh
|
|
177
|
+
* `call_provider` is synthesized from the updated context (the workflow drive consumed its own
|
|
178
|
+
* kernel actions — same re-render pattern as the reactive-compact retry path).
|
|
179
|
+
*/
|
|
180
|
+
private driveAuthoredWorkflows;
|
|
181
|
+
/**
|
|
182
|
+
* #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
|
|
183
|
+
* routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
|
|
184
|
+
* `WorkflowRun` down), and we abort the matching children's in-flight LLM calls. Returns the
|
|
185
|
+
* torn-down outcome on preemption, else `null`. No-op without a signal source.
|
|
186
|
+
*/
|
|
187
|
+
private monitorWorkflowPreemption;
|
|
188
|
+
/**
|
|
189
|
+
* Shared workflow driver for `runWorkflow` (host `load_workflow`) and `bootstrapWorkflow` (agent
|
|
190
|
+
* `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
|
|
191
|
+
* agent-submitted nodes first), and loop until the kernel reports the workflow complete.
|
|
192
|
+
*/
|
|
193
|
+
private driveWorkflow;
|
|
144
194
|
/**
|
|
145
195
|
* Resume a workflow from the parent session's completed nodes.
|
|
146
196
|
* Reads the session log, extracts completed workflow node agent_ids, and
|
package/dist/runtime/runner.js
CHANGED
|
@@ -5,27 +5,33 @@ import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-rep
|
|
|
5
5
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
6
|
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, buildWorkflowNodesSubmittedEvent, recoverCompletedWorkflowNodes, recoverSubmittedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
7
7
|
import { forceCompact, kernelAction, kernelApply, kernelMaybeAction, messageToKernelMessage, skillMetadataToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
8
|
-
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
8
|
+
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, submitWorkflowToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
9
9
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
10
10
|
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
11
11
|
import { resolveReducer } from "./reducers.js";
|
|
12
|
+
import { loopInstruction, classifyInstruction, judgeGoal, extractLoopContinue, extractClassifyBranch, extractJudgeWinner, } from "./workflow-control-flow.js";
|
|
12
13
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
13
14
|
import { assertNativeProfile } from "./os-profile.js";
|
|
14
15
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
15
16
|
export class RuntimeRunner {
|
|
16
17
|
opts;
|
|
17
18
|
interrupted = false;
|
|
19
|
+
/** #2-B-ii: aborts the in-flight provider stream on interrupt/preempt. Recreated per `execute`. */
|
|
20
|
+
abortController = null;
|
|
18
21
|
pendingObservations = [];
|
|
19
22
|
activeKernel = null;
|
|
20
23
|
currentSessionId = null;
|
|
21
24
|
nextArchiveStart = 0;
|
|
22
25
|
localPageOutCache = [];
|
|
26
|
+
/** M5 v2.1: sub-workflow specs a top-level agent authored via `start_workflow`, awaiting auto-drive
|
|
27
|
+
* at the next safe point (after the tool turn resolves, kernel back in Reason). */
|
|
28
|
+
pendingAuthoredWorkflows = [];
|
|
23
29
|
pendingSpoolOutputs = new Map();
|
|
24
30
|
constructor(opts) {
|
|
25
31
|
this.opts = opts;
|
|
26
32
|
}
|
|
27
33
|
get hostOptions() { return this.opts; }
|
|
28
|
-
interrupt() { this.interrupted = true; }
|
|
34
|
+
interrupt() { this.interrupted = true; this.abortController?.abort(); }
|
|
29
35
|
async *run(req) {
|
|
30
36
|
const prior = req.inheritEvents ?? await this.opts.sessionLog.read(req.sessionId);
|
|
31
37
|
const midRun = isMidRun(prior);
|
|
@@ -245,6 +251,7 @@ export class RuntimeRunner {
|
|
|
245
251
|
}
|
|
246
252
|
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
|
|
247
253
|
this.interrupted = false;
|
|
254
|
+
this.abortController = new AbortController();
|
|
248
255
|
this.pendingObservations = [];
|
|
249
256
|
this.pendingSpoolOutputs.clear();
|
|
250
257
|
this.currentSessionId = sessionId;
|
|
@@ -258,6 +265,8 @@ export class RuntimeRunner {
|
|
|
258
265
|
const runtime = new kernel.KernelRuntime({
|
|
259
266
|
maxTokens: this.opts.maxTokens,
|
|
260
267
|
maxTurns: effectiveMaxTurns,
|
|
268
|
+
// M4/G5: per-node token cap → child run's cumulative token budget (wasm LoopPolicy.maxTotalTokens is f64).
|
|
269
|
+
...(this.opts.maxTotalTokens !== undefined ? { maxTotalTokens: this.opts.maxTotalTokens } : {}),
|
|
261
270
|
timeoutMs: effectiveTimeoutMs !== undefined ? BigInt(effectiveTimeoutMs) : undefined,
|
|
262
271
|
});
|
|
263
272
|
this.activeKernel = runtime;
|
|
@@ -407,24 +416,7 @@ export class RuntimeRunner {
|
|
|
407
416
|
if (this.opts.signalSource) {
|
|
408
417
|
const sig = await this.opts.signalSource.nextSignal();
|
|
409
418
|
if (sig) {
|
|
410
|
-
const
|
|
411
|
-
const source = sig.source ?? "custom";
|
|
412
|
-
const signalType = sig.signalType ?? "event";
|
|
413
|
-
const urgency = sig.urgency ?? "normal";
|
|
414
|
-
const summary = String(sig.payload?.goal ?? "signal");
|
|
415
|
-
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, {
|
|
416
|
-
kind: "signal",
|
|
417
|
-
signal: {
|
|
418
|
-
id,
|
|
419
|
-
source,
|
|
420
|
-
signal_type: signalType,
|
|
421
|
-
urgency,
|
|
422
|
-
summary,
|
|
423
|
-
payload: sig.payload ?? {},
|
|
424
|
-
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
425
|
-
timestamp_ms: Date.now(),
|
|
426
|
-
},
|
|
427
|
-
});
|
|
419
|
+
const sigAction = kernelMaybeAction(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
428
420
|
if (sigAction)
|
|
429
421
|
action = sigAction;
|
|
430
422
|
}
|
|
@@ -432,6 +424,12 @@ export class RuntimeRunner {
|
|
|
432
424
|
if (runtime.isTerminal())
|
|
433
425
|
break;
|
|
434
426
|
if (action.kind === "call_provider") {
|
|
427
|
+
// M5 v2.1: top-level auto-pivot at the safe point (kernel in Reason, not suspended). Loop-top
|
|
428
|
+
// placement catches every path to `call_provider` (incl. post-approval-resume), so a queued
|
|
429
|
+
// authored spec is never stranded. Drains the queue; fires once per authored batch.
|
|
430
|
+
if (this.pendingAuthoredWorkflows.length > 0) {
|
|
431
|
+
action = await this.driveAuthoredWorkflows(runtime, action);
|
|
432
|
+
}
|
|
435
433
|
const finalToolCalls = [];
|
|
436
434
|
let finalText = "";
|
|
437
435
|
const context = action.context;
|
|
@@ -440,8 +438,12 @@ export class RuntimeRunner {
|
|
|
440
438
|
let turnInputTokens = 0;
|
|
441
439
|
let turnOutputTokens = 0;
|
|
442
440
|
let shouldRetry = false;
|
|
441
|
+
const abortSignal = this.abortController?.signal;
|
|
443
442
|
try {
|
|
444
|
-
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState)) {
|
|
443
|
+
for await (const evt of this.opts.provider.stream(context, tools, Object.keys(ext).length ? ext : undefined, providerState, abortSignal)) {
|
|
444
|
+
// #2-B-ii: a preempting interrupt fires abortController — stop consuming the live stream.
|
|
445
|
+
if (abortSignal?.aborted)
|
|
446
|
+
break;
|
|
445
447
|
if (evt.type === "usage") {
|
|
446
448
|
const usageEvt = evt;
|
|
447
449
|
turnTokens = usageEvt.totalTokens;
|
|
@@ -459,6 +461,10 @@ export class RuntimeRunner {
|
|
|
459
461
|
}
|
|
460
462
|
}
|
|
461
463
|
catch (err) {
|
|
464
|
+
// #2-B-ii: an aborted in-flight request surfaces as an AbortError — treat as an interrupt.
|
|
465
|
+
if (abortSignal?.aborted) {
|
|
466
|
+
this.interrupted = true;
|
|
467
|
+
}
|
|
462
468
|
const errMsg = String(err).toLowerCase();
|
|
463
469
|
if ((errMsg.includes("413") || errMsg.includes("too long") || errMsg.includes("context length exceeded") || errMsg.includes("context_length_exceeded")) &&
|
|
464
470
|
!hasAttemptedReactiveCompact) {
|
|
@@ -474,6 +480,11 @@ export class RuntimeRunner {
|
|
|
474
480
|
break;
|
|
475
481
|
}
|
|
476
482
|
}
|
|
483
|
+
// #2-B-ii: stream aborted (preempt/interrupt) via the break path — end the turn now.
|
|
484
|
+
if (abortSignal?.aborted) {
|
|
485
|
+
action = kernelAction(runtime, this.pendingObservations, { kind: "timeout" });
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
477
488
|
if (shouldRetry) {
|
|
478
489
|
action = {
|
|
479
490
|
kind: "call_provider",
|
|
@@ -532,10 +543,26 @@ export class RuntimeRunner {
|
|
|
532
543
|
// R3-1: intercept `submit_workflow_nodes` — it can't apply to this runner's kernel (when this
|
|
533
544
|
// runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
|
|
534
545
|
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
535
|
-
|
|
536
|
-
const
|
|
546
|
+
// M5 v1: `start_workflow` (author a sub-workflow) flattens to the same append path.
|
|
547
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes" || c.name === "start_workflow");
|
|
548
|
+
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes" && c.name !== "start_workflow");
|
|
537
549
|
for (const call of submitCalls) {
|
|
538
|
-
|
|
550
|
+
// M5 v2.1: a TOP-LEVEL agent authoring a whole sub-workflow via `start_workflow` — record the
|
|
551
|
+
// spec and AUTO-PIVOT once this tool turn resolves. A workflow-NODE's `start_workflow` (and
|
|
552
|
+
// every `submit_workflow_nodes`) instead FLATTENS for the parent `runWorkflow` to append.
|
|
553
|
+
if (call.name === "start_workflow" && !this.opts.isWorkflowNode) {
|
|
554
|
+
const spec = parseStartWorkflowSpec(call.arguments);
|
|
555
|
+
if (spec) {
|
|
556
|
+
this.pendingAuthoredWorkflows.push(spec);
|
|
557
|
+
const out = "workflow authored; executing now";
|
|
558
|
+
toolResults.push({ callId: call.id, output: out, isError: false });
|
|
559
|
+
yield { type: "tool_result", callId: call.id, content: out, isError: false };
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const nodes = call.name === "start_workflow"
|
|
564
|
+
? parseStartWorkflowArgs(call.arguments)
|
|
565
|
+
: parseSubmitWorkflowNodesArgs(call.arguments);
|
|
539
566
|
yield { type: "workflow_nodes_submitted", nodes };
|
|
540
567
|
toolResults.push({ callId: call.id, output: "submitted", isError: false });
|
|
541
568
|
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
@@ -725,7 +752,7 @@ export class RuntimeRunner {
|
|
|
725
752
|
* fed back on mismatch. If it still does not conform, the node is failed with the validation
|
|
726
753
|
* reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
|
|
727
754
|
*/
|
|
728
|
-
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
|
|
755
|
+
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs, abortSignal) {
|
|
729
756
|
// G2: a reduce node runs no LLM — execute the registered pure function over its dependency
|
|
730
757
|
// outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
|
|
731
758
|
if (node.reducer) {
|
|
@@ -742,7 +769,40 @@ export class RuntimeRunner {
|
|
|
742
769
|
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
743
770
|
manifest,
|
|
744
771
|
sessionLog: this.opts.sessionLog,
|
|
772
|
+
// M5 v2.1: this child IS a workflow node — its `start_workflow` flattens to this kernel.
|
|
773
|
+
isWorkflowNode: true,
|
|
774
|
+
// #2-B-ii: the per-node abort signal the driver fires when the kernel preempts this node.
|
|
775
|
+
...(abortSignal ? { abortSignal } : {}),
|
|
745
776
|
});
|
|
777
|
+
const textOf = (r) => {
|
|
778
|
+
const c = r.result.finalMessage?.content;
|
|
779
|
+
return typeof c === "string" ? c : c != null ? JSON.stringify(c) : "";
|
|
780
|
+
};
|
|
781
|
+
const withSignal = (r, patch) => ({ ...r, result: { ...r.result, ...patch } });
|
|
782
|
+
// A#2 tournament judge: compare two entrants' produced outputs rather than running the node's own
|
|
783
|
+
// goal. Look up both candidates, judge over the controller's criterion, and report the winner's id.
|
|
784
|
+
if (node.judge_match) {
|
|
785
|
+
const out = outputs ?? new Map();
|
|
786
|
+
const left = out.get(node.judge_match.left) ?? "";
|
|
787
|
+
const right = out.get(node.judge_match.right) ?? "";
|
|
788
|
+
const result = await orchestrator.run(mkCtx(judgeGoal(baseSpec.goal, left, right)));
|
|
789
|
+
const winner = extractJudgeWinner(textOf(result));
|
|
790
|
+
const winnerId = winner === "right" ? node.judge_match.right : node.judge_match.left;
|
|
791
|
+
return withSignal(result, { tournamentWinner: winnerId });
|
|
792
|
+
}
|
|
793
|
+
// A#2 v2 loop iteration: run the increment, then extract a stop signal. No signal ⇒ run to cap.
|
|
794
|
+
if (node.loop_max_iters != null) {
|
|
795
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${loopInstruction(node.loop_max_iters)}`));
|
|
796
|
+
const cont = extractLoopContinue(textOf(result));
|
|
797
|
+
return cont === undefined ? result : withSignal(result, { loopContinue: cont });
|
|
798
|
+
}
|
|
799
|
+
// A#2 classify: run the classifier, then extract the chosen branch label (kernel prunes the rest).
|
|
800
|
+
if (node.classify_labels && node.classify_labels.length) {
|
|
801
|
+
const labels = node.classify_labels;
|
|
802
|
+
const result = await orchestrator.run(mkCtx(`${baseSpec.goal}\n\n${classifyInstruction(labels)}`));
|
|
803
|
+
const branch = extractClassifyBranch(textOf(result), labels);
|
|
804
|
+
return branch === undefined ? result : withSignal(result, { classifyBranch: branch });
|
|
805
|
+
}
|
|
746
806
|
const schema = node.output_schema;
|
|
747
807
|
if (!schema)
|
|
748
808
|
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
@@ -805,7 +865,6 @@ export class RuntimeRunner {
|
|
|
805
865
|
}
|
|
806
866
|
const parentSessionId = this.currentSessionId;
|
|
807
867
|
const runtime = this.activeKernel;
|
|
808
|
-
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
809
868
|
const observations = kernelApply(runtime, this.pendingObservations, {
|
|
810
869
|
kind: "load_workflow",
|
|
811
870
|
spec: workflowSpecToKernel(spec),
|
|
@@ -815,22 +874,103 @@ export class RuntimeRunner {
|
|
|
815
874
|
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
816
875
|
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
817
876
|
});
|
|
877
|
+
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* M5/G1: bootstrap an **agent-authored** workflow ("the model writes its own harness"). Routes the
|
|
881
|
+
* spec through the agent-reachable `Syscall::LoadWorkflow` (`submit_workflow`): with no workflow
|
|
882
|
+
* active the kernel bootstraps the DAG, else it flattens onto the running one (bootstrap-or-flatten —
|
|
883
|
+
* one kernel, one quota). The same shared driver runs the resulting batches.
|
|
884
|
+
*/
|
|
885
|
+
async bootstrapWorkflow(spec, opts) {
|
|
886
|
+
if (!this.activeKernel || !this.currentSessionId) {
|
|
887
|
+
throw new Error("bootstrapWorkflow requires an active parent run");
|
|
888
|
+
}
|
|
889
|
+
const parentSessionId = this.currentSessionId;
|
|
890
|
+
const runtime = this.activeKernel;
|
|
891
|
+
const observations = kernelApply(runtime, this.pendingObservations, submitWorkflowToKernel(spec, parentSessionId, opts?.submitterAgentId));
|
|
892
|
+
return this.driveWorkflow(observations, parentSessionId, runtime);
|
|
893
|
+
}
|
|
894
|
+
/**
|
|
895
|
+
* M5 v2.1: drive the sub-workflow(s) a top-level agent authored via `start_workflow`, at the safe
|
|
896
|
+
* point (tool turn resolved → kernel in Reason). Each runs in THIS kernel (the kernel resumes the
|
|
897
|
+
* reason loop on `workflow_completed`), then the outcome is injected as a user message and a fresh
|
|
898
|
+
* `call_provider` is synthesized from the updated context (the workflow drive consumed its own
|
|
899
|
+
* kernel actions — same re-render pattern as the reactive-compact retry path).
|
|
900
|
+
*/
|
|
901
|
+
async driveAuthoredWorkflows(runtime, action) {
|
|
902
|
+
const specs = this.pendingAuthoredWorkflows;
|
|
903
|
+
this.pendingAuthoredWorkflows = [];
|
|
904
|
+
for (const spec of specs) {
|
|
905
|
+
const outcome = await this.bootstrapWorkflow(spec);
|
|
906
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
907
|
+
kind: "add_history_message",
|
|
908
|
+
message: messageToKernelMessage({ role: "user", content: authoredWorkflowOutcomeNote(outcome) }),
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
return { kind: "call_provider", context: runtime.render(), tools: action.tools };
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* #2-B-ii: while a workflow batch is in flight, poll the signal source; a Critical `InterruptNow`
|
|
915
|
+
* routes through the kernel (root in `SubAgentAwait` → preempt → `AgentPreempted` + tears the
|
|
916
|
+
* `WorkflowRun` down), and we abort the matching children's in-flight LLM calls. Returns the
|
|
917
|
+
* torn-down outcome on preemption, else `null`. No-op without a signal source.
|
|
918
|
+
*/
|
|
919
|
+
async monitorWorkflowPreemption(runtime, controllers, batchState) {
|
|
920
|
+
const source = this.opts.signalSource;
|
|
921
|
+
if (!source)
|
|
922
|
+
return null;
|
|
923
|
+
while (!batchState.settled) {
|
|
924
|
+
const sig = await source.nextSignal();
|
|
925
|
+
if (batchState.settled)
|
|
926
|
+
break;
|
|
927
|
+
if (!sig) {
|
|
928
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
const obs = kernelApply(runtime, this.pendingObservations, signalToKernelEvent(sig));
|
|
932
|
+
const preempted = obs.find(o => o.kind === "agent_preempted");
|
|
933
|
+
if (preempted) {
|
|
934
|
+
for (const id of preempted.agent_ids ?? [])
|
|
935
|
+
controllers.get(id)?.abort();
|
|
936
|
+
const wc = obs.find(o => o.kind === "workflow_completed");
|
|
937
|
+
return { completed: wc?.completed ?? [], failed: wc?.failed ?? [] };
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Shared workflow driver for `runWorkflow` (host `load_workflow`) and `bootstrapWorkflow` (agent
|
|
944
|
+
* `submit_workflow`): run each kernel-emitted batch in parallel, feed completions back (appending any
|
|
945
|
+
* agent-submitted nodes first), and loop until the kernel reports the workflow complete.
|
|
946
|
+
*/
|
|
947
|
+
async driveWorkflow(initial, parentSessionId, runtime) {
|
|
948
|
+
const observations = initial;
|
|
949
|
+
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
818
950
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
|
|
819
951
|
// G4: the batch observation carries the workflow's remaining budget; track the latest.
|
|
820
952
|
const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
|
|
821
953
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
822
954
|
let done = findDone(observations);
|
|
823
955
|
if (done)
|
|
824
|
-
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
956
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: {} };
|
|
825
957
|
let nodes = collectNodes(observations);
|
|
826
958
|
let budget = collectBudget(observations);
|
|
827
959
|
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
828
960
|
const outputs = new Map();
|
|
829
961
|
for (;;) {
|
|
830
962
|
if (nodes.length === 0)
|
|
831
|
-
return { completed: [], failed: [] };
|
|
963
|
+
return { completed: [], failed: [], outputs: Object.fromEntries(outputs) };
|
|
832
964
|
const roundBudget = budget;
|
|
833
|
-
|
|
965
|
+
// #2-B-ii: per-node abort controllers + a concurrent preemption monitor (see node runner).
|
|
966
|
+
const controllers = new Map(nodes.map(n => [n.agent_id, new AbortController()]));
|
|
967
|
+
const batchState = { settled: false };
|
|
968
|
+
const monitor = this.monitorWorkflowPreemption(runtime, controllers, batchState);
|
|
969
|
+
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs, controllers.get(node.agent_id)?.signal)));
|
|
970
|
+
batchState.settled = true;
|
|
971
|
+
const preempted = await monitor;
|
|
972
|
+
if (preempted)
|
|
973
|
+
return { ...preempted, outputs: Object.fromEntries(outputs) };
|
|
834
974
|
// Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
|
|
835
975
|
const nextNodes = [];
|
|
836
976
|
done = undefined;
|
|
@@ -871,7 +1011,7 @@ export class RuntimeRunner {
|
|
|
871
1011
|
}));
|
|
872
1012
|
}
|
|
873
1013
|
if (done && nextNodes.length === 0) {
|
|
874
|
-
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
1014
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [], outputs: Object.fromEntries(outputs) };
|
|
875
1015
|
}
|
|
876
1016
|
nodes = nextNodes;
|
|
877
1017
|
}
|
|
@@ -1090,3 +1230,61 @@ function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
|
1090
1230
|
}
|
|
1091
1231
|
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
1092
1232
|
}
|
|
1233
|
+
/** M5 v1: parse `start_workflow` tool args (`{ spec: { nodes: WorkflowNodeSpec[] } }`) into the
|
|
1234
|
+
* spec's node batch — flattened onto the running workflow via the same append path. */
|
|
1235
|
+
function parseStartWorkflowArgs(argsStr) {
|
|
1236
|
+
let parsed = {};
|
|
1237
|
+
try {
|
|
1238
|
+
parsed = JSON.parse(argsStr);
|
|
1239
|
+
}
|
|
1240
|
+
catch {
|
|
1241
|
+
// Ignore parse error → no nodes.
|
|
1242
|
+
}
|
|
1243
|
+
const spec = parsed.spec;
|
|
1244
|
+
return Array.isArray(spec?.nodes) ? spec.nodes : [];
|
|
1245
|
+
}
|
|
1246
|
+
/** M5 v2.1: parse the full `WorkflowSpec` from a top-level `start_workflow` call for the auto-pivot
|
|
1247
|
+
* drive. Returns `undefined` on a malformed / empty payload (caller falls back to the flatten path). */
|
|
1248
|
+
function parseStartWorkflowSpec(argsStr) {
|
|
1249
|
+
try {
|
|
1250
|
+
const parsed = JSON.parse(argsStr);
|
|
1251
|
+
if (Array.isArray(parsed.spec?.nodes) && parsed.spec.nodes.length > 0) {
|
|
1252
|
+
return { nodes: parsed.spec.nodes };
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
// Ignore parse error → undefined (fall back to flatten).
|
|
1257
|
+
}
|
|
1258
|
+
return undefined;
|
|
1259
|
+
}
|
|
1260
|
+
/** M5 v2.1: render an authored-workflow outcome into a user-message note injected back into the
|
|
1261
|
+
* agent's context, so its next turn continues with the sub-workflow's results in view. */
|
|
1262
|
+
function authoredWorkflowOutcomeNote(outcome) {
|
|
1263
|
+
const lines = [
|
|
1264
|
+
`[authored workflow result] ${outcome.completed.length} node(s) completed` +
|
|
1265
|
+
(outcome.failed.length ? `, ${outcome.failed.length} failed` : "") + ".",
|
|
1266
|
+
];
|
|
1267
|
+
for (const id of outcome.completed) {
|
|
1268
|
+
const out = outcome.outputs[id];
|
|
1269
|
+
if (out)
|
|
1270
|
+
lines.push(`- ${id}: ${out.length > 500 ? out.slice(0, 500) + "…" : out}`);
|
|
1271
|
+
}
|
|
1272
|
+
return lines.join("\n");
|
|
1273
|
+
}
|
|
1274
|
+
/** Lower a host `RuntimeSignal` to the kernel's snake_case `signal` input event. Shared by the main
|
|
1275
|
+
* loop's per-turn poll and #2-B-ii's workflow-batch preemption monitor (so the two never drift). */
|
|
1276
|
+
function signalToKernelEvent(sig) {
|
|
1277
|
+
return {
|
|
1278
|
+
kind: "signal",
|
|
1279
|
+
signal: {
|
|
1280
|
+
id: crypto.randomUUID(),
|
|
1281
|
+
source: sig.source ?? "custom",
|
|
1282
|
+
signal_type: sig.signalType ?? "event",
|
|
1283
|
+
urgency: sig.urgency ?? "normal",
|
|
1284
|
+
summary: String(sig.payload?.goal ?? "signal"),
|
|
1285
|
+
payload: sig.payload ?? {},
|
|
1286
|
+
...(sig.dedupeKey ? { dedupe_key: sig.dedupeKey } : {}),
|
|
1287
|
+
timestamp_ms: Date.now(),
|
|
1288
|
+
},
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
@@ -7,7 +7,16 @@ export interface SubAgentRunContext {
|
|
|
7
7
|
spec: AgentRunSpec;
|
|
8
8
|
manifest: AgentProcessChangedObservation;
|
|
9
9
|
sessionLog: SessionLog;
|
|
10
|
+
/** M5 v2.1: set when this child is a workflow node — propagated so a nested `start_workflow`
|
|
11
|
+
* FLATTENS to the parent kernel rather than auto-pivoting into its own bootstrap. */
|
|
12
|
+
isWorkflowNode?: boolean;
|
|
13
|
+
/** #2-B-ii: parent-controlled abort — when the kernel preempts this node (`AgentPreempted`), the
|
|
14
|
+
* orchestrator interrupts the child runner, cancelling its in-flight LLM call. */
|
|
15
|
+
abortSignal?: AbortSignal;
|
|
10
16
|
}
|
|
17
|
+
/** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
|
|
18
|
+
* Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
|
|
19
|
+
export declare function resolveProvider(opts: RuntimeOptions, modelHint?: string): RuntimeOptions["provider"];
|
|
11
20
|
/** Host-side driver for kernel-isolated sub-agent runs. */
|
|
12
21
|
export declare class SubAgentOrchestrator {
|
|
13
22
|
run(ctx: SubAgentRunContext): Promise<SubAgentResult>;
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { agentRunSpecToKernel, findSpawnProcessObservation, spawnObservationToManifest } from "./types/agent.js";
|
|
2
2
|
import { FilteredExecutionPlane } from "./filtered-plane.js";
|
|
3
3
|
import { kernelApply } from "./kernel-step.js";
|
|
4
|
+
/** #2-B-ii: bridge a parent AbortSignal to a child runner's `interrupt()` (fires now if already aborted). */
|
|
5
|
+
function linkAbort(signal, runner) {
|
|
6
|
+
if (!signal)
|
|
7
|
+
return;
|
|
8
|
+
if (signal.aborted) {
|
|
9
|
+
runner.interrupt();
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
signal.addEventListener("abort", () => runner.interrupt(), { once: true });
|
|
13
|
+
}
|
|
4
14
|
function terminationFromStatus(status) {
|
|
5
15
|
const normalized = status.toLowerCase();
|
|
6
16
|
if (normalized === "completed" ||
|
|
@@ -14,6 +24,16 @@ function terminationFromStatus(status) {
|
|
|
14
24
|
}
|
|
15
25
|
return status;
|
|
16
26
|
}
|
|
27
|
+
/** M1/G3 intelligence routing: resolve the provider for a sub-agent from its spec's `modelHint`.
|
|
28
|
+
* Falls back to the parent provider when there is no hint or no `providerFor` hook resolves it. */
|
|
29
|
+
export function resolveProvider(opts, modelHint) {
|
|
30
|
+
if (modelHint && opts.providerFor) {
|
|
31
|
+
const routed = opts.providerFor(modelHint);
|
|
32
|
+
if (routed)
|
|
33
|
+
return routed;
|
|
34
|
+
}
|
|
35
|
+
return opts.provider;
|
|
36
|
+
}
|
|
17
37
|
/** Derive which meta-tools a child runner should expose based on permitted IDs and available sources. */
|
|
18
38
|
function deriveMetaTools(permitted, opts) {
|
|
19
39
|
const metaTools = new Set();
|
|
@@ -48,6 +68,10 @@ export class SubAgentOrchestrator {
|
|
|
48
68
|
const { RuntimeRunner } = await import("./runner.js");
|
|
49
69
|
const childRunner = new RuntimeRunner({
|
|
50
70
|
...ctx.parentOpts,
|
|
71
|
+
// M1/G3: route to the node's hinted model (falls back to the parent provider).
|
|
72
|
+
provider: resolveProvider(ctx.parentOpts, ctx.spec.modelHint),
|
|
73
|
+
// M4/G5: cap the child run at the node's token budget (falls back to the inherited cap).
|
|
74
|
+
maxTotalTokens: ctx.spec.tokenBudget ?? ctx.parentOpts.maxTotalTokens,
|
|
51
75
|
executionPlane: filteredPlane,
|
|
52
76
|
agentId: ctx.spec.identity.agentId,
|
|
53
77
|
systemPrompt,
|
|
@@ -56,7 +80,11 @@ export class SubAgentOrchestrator {
|
|
|
56
80
|
dreamStore: metaTools.has("memory") ? ctx.parentOpts.dreamStore : undefined,
|
|
57
81
|
knowledgeSource: metaTools.has("knowledge") ? ctx.parentOpts.knowledgeSource : undefined,
|
|
58
82
|
enablePlanTool: metaTools.has("update_plan") ? ctx.parentOpts.enablePlanTool : undefined,
|
|
83
|
+
// M5 v2.1: a workflow node's `start_workflow` flattens to the parent kernel (no nested pivot).
|
|
84
|
+
isWorkflowNode: ctx.isWorkflowNode,
|
|
59
85
|
});
|
|
86
|
+
// #2-B-ii: parent preempt → interrupt the child (cancels its in-flight LLM call).
|
|
87
|
+
linkAbort(ctx.abortSignal, childRunner);
|
|
60
88
|
let done;
|
|
61
89
|
let finalText = "";
|
|
62
90
|
// R3-1: collect any nodes this node's agent submitted via the `submit_workflow_nodes` tool.
|
|
@@ -23,6 +23,11 @@ export interface AgentRunSpec {
|
|
|
23
23
|
capabilityFilter?: AgentCapabilityFilter;
|
|
24
24
|
milestones?: MilestoneContract;
|
|
25
25
|
metadata?: Record<string, unknown>;
|
|
26
|
+
/** M1/G3: per-agent model preference; the host resolves it via `RuntimeOptions.providerFor`.
|
|
27
|
+
* Host-side routing only — not sent to the kernel. */
|
|
28
|
+
modelHint?: string;
|
|
29
|
+
/** M4/G5: cumulative token cap for this sub-agent's run (sets the child kernel's `maxTotalTokens`). */
|
|
30
|
+
tokenBudget?: number;
|
|
26
31
|
}
|
|
27
32
|
/** Kernel process-table observation (Phase 3 canonical spawn signal). */
|
|
28
33
|
export interface AgentProcessChangedObservation {
|
|
@@ -48,6 +53,14 @@ export interface LoopResult {
|
|
|
48
53
|
finalMessage?: Message;
|
|
49
54
|
turnsUsed: number;
|
|
50
55
|
totalTokensUsed: number;
|
|
56
|
+
/** A#2 v2 loop stop signal: a loop iteration sets `false` to end the loop before `max_iters`.
|
|
57
|
+
* `undefined` (every non-loop result) ⇒ no opinion → run to the cap. Sent only when set. */
|
|
58
|
+
loopContinue?: boolean;
|
|
59
|
+
/** A#2 classify routing: a classifier node reports the chosen branch label here; the kernel runs
|
|
60
|
+
* that branch and prunes the rest. Sent only when set. */
|
|
61
|
+
classifyBranch?: string;
|
|
62
|
+
/** A#2 tournament verdict: a judge reports the winning entrant's agent id here. Sent only when set. */
|
|
63
|
+
tournamentWinner?: string;
|
|
51
64
|
}
|
|
52
65
|
export interface SubAgentResult {
|
|
53
66
|
agentId: string;
|
|
@@ -99,6 +112,26 @@ export interface WorkflowNodeSpec {
|
|
|
99
112
|
/** G2: make this a deterministic reduce node — runs no LLM; the runner routes it to the registered
|
|
100
113
|
* reducer of this name over its `dependsOn` nodes' outputs. */
|
|
101
114
|
reducer?: string;
|
|
115
|
+
/** A#2 v2: make this a *loop* node — re-run its agent up to `maxIters` times. An iteration may end
|
|
116
|
+
* the loop early by reporting `loopContinue: false` (the runner solicits this from the agent). */
|
|
117
|
+
loop?: {
|
|
118
|
+
maxIters: number;
|
|
119
|
+
};
|
|
120
|
+
/** A#2: make this a *classify* node — its agent picks exactly one branch `label`; that branch's
|
|
121
|
+
* nodes run and the others are pruned. Each branch node must list this node's index in `dependsOn`. */
|
|
122
|
+
classify?: {
|
|
123
|
+
branches: Array<{
|
|
124
|
+
label: string;
|
|
125
|
+
nodes: number[];
|
|
126
|
+
}>;
|
|
127
|
+
};
|
|
128
|
+
/** A#2: make this a *tournament controller* — generate each `entrants` candidate in parallel, then
|
|
129
|
+
* pairwise-judge them to one winner (this node's `task.goal` is the judging criterion). ≥2 entrants. */
|
|
130
|
+
tournament?: {
|
|
131
|
+
entrants: WorkflowTaskSpec[];
|
|
132
|
+
};
|
|
133
|
+
/** M4/G5: cap this node's child run at `tokenBudget` cumulative tokens (the per-node "use N tokens"). */
|
|
134
|
+
tokenBudget?: number;
|
|
102
135
|
/** Indices of nodes this node depends on. */
|
|
103
136
|
dependsOn?: number[];
|
|
104
137
|
}
|
|
@@ -120,6 +153,20 @@ export interface WorkflowSpawnInfo {
|
|
|
120
153
|
reducer?: string;
|
|
121
154
|
/** G2: the dependency agent ids whose outputs a reduce node consumes. */
|
|
122
155
|
input_agent_ids?: string[];
|
|
156
|
+
/** A#2: present only for a tournament *judge* spawn — the two entrant agent ids whose produced
|
|
157
|
+
* outputs this judge compares. The runner looks them up and reports the winner as `tournamentWinner`. */
|
|
158
|
+
judge_match?: {
|
|
159
|
+
left: string;
|
|
160
|
+
right: string;
|
|
161
|
+
};
|
|
162
|
+
/** A#2 v2: present only for a *loop* iteration spawn — the loop's `max_iters`. Marks the spawn as a
|
|
163
|
+
* loop iteration so the runner solicits + reports a `loopContinue` stop signal. */
|
|
164
|
+
loop_max_iters?: number;
|
|
165
|
+
/** A#2: present only for a *classify* spawn — the branch labels the classifier must choose among.
|
|
166
|
+
* Non-empty marks the spawn as a classifier so the runner instructs the agent + reports `classifyBranch`. */
|
|
167
|
+
classify_labels?: string[];
|
|
168
|
+
/** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
|
|
169
|
+
token_budget?: number;
|
|
123
170
|
}
|
|
124
171
|
/** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
|
|
125
172
|
* `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
|
|
@@ -130,10 +177,14 @@ export interface WorkflowBudget {
|
|
|
130
177
|
running_subagents: number;
|
|
131
178
|
max_concurrent_subagents?: number;
|
|
132
179
|
concurrency_remaining?: number;
|
|
180
|
+
/** M4/G5 token headroom: tokens used / run-level cap / tokens remaining, so a coordinator can scale
|
|
181
|
+
* a submission to "use N tokens". */
|
|
182
|
+
tokens_used?: number;
|
|
183
|
+
tokens_max?: number;
|
|
184
|
+
tokens_remaining?: number;
|
|
133
185
|
}
|
|
134
186
|
/** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
|
|
135
187
|
export declare function workflowBudgetNote(budget: WorkflowBudget | undefined): string;
|
|
136
|
-
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
137
188
|
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
|
|
138
189
|
* `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
|
|
139
190
|
export declare function workflowNodeSpecToKernel(n: WorkflowNodeSpec): Record<string, unknown>;
|
|
@@ -142,8 +193,16 @@ export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string,
|
|
|
142
193
|
* `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
|
|
143
194
|
* nodes coerced to quarantined). Omitted ⇒ no coercion. */
|
|
144
195
|
export declare function submitWorkflowNodesToKernel(nodes: WorkflowNodeSpec[], submitterAgentId?: string): Record<string, unknown>;
|
|
145
|
-
/**
|
|
196
|
+
/** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
|
|
197
|
+
* `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
|
|
198
|
+
* `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
|
|
199
|
+
* on the flatten case. */
|
|
200
|
+
export declare function submitWorkflowToKernel(spec: WorkflowSpec, parentSessionId: string, submitterAgentId?: string): Record<string, unknown>;
|
|
146
201
|
export declare const submitWorkflowNodesTool: ToolSchema;
|
|
202
|
+
/** M5 v1 (flatten): the tool an agent calls to author a sub-workflow — a cohesive DAG of nodes
|
|
203
|
+
* composed onto the running workflow. Lowers to the same append path as `submit_workflow_nodes`
|
|
204
|
+
* (a `WorkflowSpec` is a node batch). v2 adds top-level bootstrap (the `LoadWorkflow` syscall). */
|
|
205
|
+
export declare const startWorkflowTool: ToolSchema;
|
|
147
206
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
148
207
|
export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
|
|
149
208
|
/** Build the host manifest for a kernel-generated workflow node. */
|
|
@@ -85,6 +85,11 @@ export function subAgentResultToKernel(result) {
|
|
|
85
85
|
: null,
|
|
86
86
|
turns_used: result.result.turnsUsed,
|
|
87
87
|
total_tokens_used: result.result.totalTokensUsed,
|
|
88
|
+
// A#2: control-flow signals — additive, omitted on the wire when unset so a plain spawn's
|
|
89
|
+
// result is byte-identical to before. The kernel reads each only for the matching node kind.
|
|
90
|
+
...(result.result.loopContinue !== undefined ? { loop_continue: result.result.loopContinue } : {}),
|
|
91
|
+
...(result.result.classifyBranch !== undefined ? { classify_branch: result.result.classifyBranch } : {}),
|
|
92
|
+
...(result.result.tournamentWinner !== undefined ? { tournament_winner: result.result.tournamentWinner } : {}),
|
|
88
93
|
},
|
|
89
94
|
};
|
|
90
95
|
}
|
|
@@ -105,31 +110,58 @@ export function workflowBudgetNote(budget) {
|
|
|
105
110
|
if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
|
|
106
111
|
parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
|
|
107
112
|
}
|
|
113
|
+
if (budget.tokens_remaining != null && budget.tokens_max != null) {
|
|
114
|
+
parts.push(`tokens ${budget.tokens_used ?? 0}/${budget.tokens_max} used, ${budget.tokens_remaining} remaining`);
|
|
115
|
+
}
|
|
108
116
|
if (parts.length === 0)
|
|
109
117
|
return "";
|
|
110
118
|
return (`[workflow budget] ${parts.join(" · ")}. ` +
|
|
111
|
-
"If you submit more workflow nodes, keep the batch within the remaining node budget.");
|
|
119
|
+
"If you submit more workflow nodes, keep the batch within the remaining node and token budget.");
|
|
120
|
+
}
|
|
121
|
+
/** Normalize a `WorkflowTaskSpec` (object or bare goal string) to the kernel's `RuntimeTask` JSON. */
|
|
122
|
+
function workflowTaskToKernel(t) {
|
|
123
|
+
const task = typeof t === "string" ? { goal: t } : t;
|
|
124
|
+
return {
|
|
125
|
+
goal: task.goal,
|
|
126
|
+
// `criteria` is required by the kernel's RuntimeTask serde (no default).
|
|
127
|
+
criteria: task.criteria ?? [],
|
|
128
|
+
...(task.lane ? { lane: task.lane } : {}),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Lower a node's control-flow kind to the kernel's serde-tagged `NodeKind` JSON, or `undefined` for
|
|
132
|
+
* a plain spawn. `reducer` / `loop` / `classify` / `tournament` are mutually exclusive. */
|
|
133
|
+
function nodeKindToKernel(n) {
|
|
134
|
+
const declared = [n.reducer != null, n.loop != null, n.classify != null, n.tournament != null].filter(Boolean).length;
|
|
135
|
+
if (declared > 1) {
|
|
136
|
+
throw new Error("a workflow node may declare at most one of: reducer, loop, classify, tournament");
|
|
137
|
+
}
|
|
138
|
+
if (n.reducer != null)
|
|
139
|
+
return { type: "reduce", reducer: n.reducer };
|
|
140
|
+
if (n.loop != null)
|
|
141
|
+
return { type: "loop", max_iters: n.loop.maxIters };
|
|
142
|
+
if (n.classify != null) {
|
|
143
|
+
return { type: "classify", branches: n.classify.branches.map(b => ({ label: b.label, nodes: b.nodes })) };
|
|
144
|
+
}
|
|
145
|
+
if (n.tournament != null)
|
|
146
|
+
return { type: "tournament", entrants: n.tournament.entrants.map(workflowTaskToKernel) };
|
|
147
|
+
return undefined;
|
|
112
148
|
}
|
|
113
|
-
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
114
149
|
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
|
|
115
150
|
* `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
|
|
116
151
|
export function workflowNodeSpecToKernel(n) {
|
|
117
|
-
const
|
|
152
|
+
const kind = nodeKindToKernel(n);
|
|
118
153
|
return {
|
|
119
|
-
task:
|
|
120
|
-
goal: task.goal,
|
|
121
|
-
// `criteria` is required by the kernel's RuntimeTask serde (no default).
|
|
122
|
-
criteria: task.criteria ?? [],
|
|
123
|
-
...(task.lane ? { lane: task.lane } : {}),
|
|
124
|
-
},
|
|
154
|
+
task: workflowTaskToKernel(n.task),
|
|
125
155
|
role: n.role,
|
|
126
156
|
isolation: n.isolation ?? "shared",
|
|
127
157
|
context_inheritance: n.contextInheritance ?? "none",
|
|
128
158
|
...(n.modelHint ? { model_hint: n.modelHint } : {}),
|
|
129
159
|
...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
|
|
130
160
|
...(n.outputSchema ? { output_schema: n.outputSchema } : {}),
|
|
131
|
-
// G2:
|
|
132
|
-
...(
|
|
161
|
+
// A#2/G2: loop / classify / tournament / reduce lower to a serde-tagged `NodeKind`; spawn omits it.
|
|
162
|
+
...(kind ? { kind } : {}),
|
|
163
|
+
// M4/G5: per-node token cap (additive; omitted when unset).
|
|
164
|
+
...(n.tokenBudget != null ? { token_budget: n.tokenBudget } : {}),
|
|
133
165
|
...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
|
|
134
166
|
};
|
|
135
167
|
}
|
|
@@ -146,33 +178,113 @@ export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
|
|
|
146
178
|
...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
|
|
147
179
|
};
|
|
148
180
|
}
|
|
181
|
+
/** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
|
|
182
|
+
* `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
|
|
183
|
+
* `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
|
|
184
|
+
* on the flatten case. */
|
|
185
|
+
export function submitWorkflowToKernel(spec, parentSessionId, submitterAgentId) {
|
|
186
|
+
return {
|
|
187
|
+
kind: "submit_workflow",
|
|
188
|
+
spec: workflowSpecToKernel(spec),
|
|
189
|
+
parent_session_id: parentSessionId,
|
|
190
|
+
...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
149
193
|
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
|
|
194
|
+
/** Shared JSON-Schema for a workflow-node batch (a DAG). Used by both `submit_workflow_nodes`
|
|
195
|
+
* (append) and `start_workflow` (M5 v1: author a sub-workflow), so the two tools never drift. */
|
|
196
|
+
const workflowNodesArraySchema = {
|
|
197
|
+
type: "array",
|
|
198
|
+
items: {
|
|
199
|
+
type: "object",
|
|
200
|
+
properties: {
|
|
201
|
+
task: { description: "The node's goal: a string, or { goal, criteria?, lane? }." },
|
|
202
|
+
role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
|
|
203
|
+
isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
|
|
204
|
+
contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
|
|
205
|
+
trust: { type: "string", enum: ["trusted", "quarantined"] },
|
|
206
|
+
outputSchema: { type: "object", description: "Optional JSON Schema the node's output must conform to." },
|
|
207
|
+
modelHint: { type: "string", description: "Preferred model for this node (e.g. \"opus\"/\"sonnet\"); the host routes it." },
|
|
208
|
+
reducer: { type: "string", description: "Make this a deterministic reduce node (no LLM); names a registered reducer." },
|
|
209
|
+
loop: {
|
|
210
|
+
type: "object",
|
|
211
|
+
description: "Make this a loop node: re-run up to maxIters times, ending early when the agent reports done.",
|
|
212
|
+
properties: { maxIters: { type: "integer", description: "Hard iteration cap." } },
|
|
213
|
+
required: ["maxIters"],
|
|
214
|
+
},
|
|
215
|
+
classify: {
|
|
216
|
+
type: "object",
|
|
217
|
+
description: "Make this a classify node: pick one branch label; that branch's nodes run, the rest are pruned.",
|
|
218
|
+
properties: {
|
|
219
|
+
branches: {
|
|
220
|
+
type: "array",
|
|
221
|
+
items: {
|
|
222
|
+
type: "object",
|
|
223
|
+
properties: {
|
|
224
|
+
label: { type: "string" },
|
|
225
|
+
nodes: { type: "array", items: { type: "integer" }, description: "Batch-relative node indices for this branch." },
|
|
226
|
+
},
|
|
227
|
+
required: ["label", "nodes"],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
required: ["branches"],
|
|
232
|
+
},
|
|
233
|
+
tournament: {
|
|
234
|
+
type: "object",
|
|
235
|
+
description: "Make this a tournament controller: generate each entrant, then pairwise-judge to one winner.",
|
|
236
|
+
properties: {
|
|
237
|
+
entrants: {
|
|
238
|
+
type: "array",
|
|
239
|
+
description: "≥2 candidate tasks to generate and judge.",
|
|
240
|
+
items: {
|
|
241
|
+
oneOf: [
|
|
242
|
+
{ type: "string" },
|
|
243
|
+
{ type: "object", properties: { goal: { type: "string" }, criteria: { type: "array", items: { type: "string" } } }, required: ["goal"] },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
required: ["entrants"],
|
|
249
|
+
},
|
|
250
|
+
tokenBudget: { type: "integer", description: "Cap this node's child run at this many cumulative tokens." },
|
|
251
|
+
dependsOn: { type: "array", items: { type: "integer" } },
|
|
252
|
+
},
|
|
253
|
+
required: ["task", "role"],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
150
256
|
export const submitWorkflowNodesTool = {
|
|
151
257
|
name: "submit_workflow_nodes",
|
|
152
258
|
description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
|
|
153
|
-
"spawns as a gated sub-agent. Use when you discover more work that should run as its own node."
|
|
259
|
+
"spawns as a gated sub-agent. Use when you discover more work that should run as its own node. " +
|
|
260
|
+
"A node may declare ONE control-flow kind — `loop` / `classify` / `tournament` / `reducer` — " +
|
|
261
|
+
"otherwise it is a plain spawn. Within a submission, `dependsOn` and `classify.branches[].nodes` " +
|
|
262
|
+
"are batch-relative (index 0 = this batch's first node).",
|
|
263
|
+
parameters: JSON.stringify({
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: { nodes: workflowNodesArraySchema },
|
|
266
|
+
required: ["nodes"],
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
/** M5 v1 (flatten): the tool an agent calls to author a sub-workflow — a cohesive DAG of nodes
|
|
270
|
+
* composed onto the running workflow. Lowers to the same append path as `submit_workflow_nodes`
|
|
271
|
+
* (a `WorkflowSpec` is a node batch). v2 adds top-level bootstrap (the `LoadWorkflow` syscall). */
|
|
272
|
+
export const startWorkflowTool = {
|
|
273
|
+
name: "start_workflow",
|
|
274
|
+
description: "Author and run a sub-workflow: a DAG of nodes (fan-out / classify / tournament / loop / reduce) " +
|
|
275
|
+
"composed onto the current run. Use to structure a multi-step task as its own harness. The nodes " +
|
|
276
|
+
"spawn as gated sub-agents; `dependsOn` / `classify.branches[].nodes` are spec-relative.",
|
|
154
277
|
parameters: JSON.stringify({
|
|
155
278
|
type: "object",
|
|
156
279
|
properties: {
|
|
157
|
-
|
|
158
|
-
type: "
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
task: { description: "The node's goal: a string, or { goal, criteria?, lane? }." },
|
|
163
|
-
role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
|
|
164
|
-
isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
|
|
165
|
-
contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
|
|
166
|
-
trust: { type: "string", enum: ["trusted", "quarantined"] },
|
|
167
|
-
outputSchema: { type: "object", description: "Optional JSON Schema the node's output must conform to." },
|
|
168
|
-
reducer: { type: "string", description: "Make this a deterministic reduce node (no LLM); names a registered reducer." },
|
|
169
|
-
dependsOn: { type: "array", items: { type: "integer" } },
|
|
170
|
-
},
|
|
171
|
-
required: ["task", "role"],
|
|
172
|
-
},
|
|
280
|
+
spec: {
|
|
281
|
+
type: "object",
|
|
282
|
+
description: "The workflow specification.",
|
|
283
|
+
properties: { nodes: workflowNodesArraySchema },
|
|
284
|
+
required: ["nodes"],
|
|
173
285
|
},
|
|
174
286
|
},
|
|
175
|
-
required: ["
|
|
287
|
+
required: ["spec"],
|
|
176
288
|
}),
|
|
177
289
|
};
|
|
178
290
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
@@ -187,6 +299,10 @@ export function workflowNodeToSpec(node, parentSessionId) {
|
|
|
187
299
|
role: node.role,
|
|
188
300
|
isolation: node.isolation,
|
|
189
301
|
goal: node.goal,
|
|
302
|
+
// M1/G3: carry the node's model preference so the orchestrator can route to a provider.
|
|
303
|
+
...(node.model_hint ? { modelHint: node.model_hint } : {}),
|
|
304
|
+
// M4/G5: carry the node's token cap so the orchestrator can bound the child run.
|
|
305
|
+
...(node.token_budget != null ? { tokenBudget: node.token_budget } : {}),
|
|
190
306
|
};
|
|
191
307
|
}
|
|
192
308
|
/** Build the host manifest for a kernel-generated workflow node. */
|
|
@@ -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";
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//! A#2: SDK-side execution of the kernel's control-flow workflow node kinds (Loop / Classify /
|
|
2
|
+
//! Tournament). The kernel owns the scheduling — it re-arms loops, prunes classify branches, and
|
|
3
|
+
//! runs the tournament bracket — and tells the SDK *which* kind a spawn is via the spawn descriptor
|
|
4
|
+
//! (`loop_max_iters` / `classify_labels` / `judge_match`). This module is the SDK half of the "one
|
|
5
|
+
//! agent per node + one additive result field" contract: it builds the prompt that solicits the
|
|
6
|
+
//! decision from the node's agent and extracts the matching result signal (`loopContinue` /
|
|
7
|
+
//! `classifyBranch` / `tournamentWinner`) the kernel reads back.
|
|
8
|
+
import { extractJsonValue } from "./output-schema.js";
|
|
9
|
+
/** Instruction appended to a loop node's goal: do the next increment, and signal when done. */
|
|
10
|
+
export function loopInstruction(maxIters) {
|
|
11
|
+
return (`This task runs as a LOOP (up to ${maxIters} iterations total). Do the next increment of work now. ` +
|
|
12
|
+
`When you judge the overall task COMPLETE and no further iterations are needed, end your response ` +
|
|
13
|
+
`with a JSON object {"loop_continue": false}. To request another iteration, omit it or return ` +
|
|
14
|
+
`{"loop_continue": true}.`);
|
|
15
|
+
}
|
|
16
|
+
/** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
|
|
17
|
+
export function classifyInstruction(labels) {
|
|
18
|
+
return (`Classify the input and choose EXACTLY ONE label from: ${labels.map(l => JSON.stringify(l)).join(", ")}. ` +
|
|
19
|
+
`Respond with ONLY a JSON object: {"branch": "<one of the labels>"}.`);
|
|
20
|
+
}
|
|
21
|
+
/** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */
|
|
22
|
+
export function judgeGoal(criterion, leftOutput, rightOutput) {
|
|
23
|
+
return (`${criterion}\n\nCompare the two candidate outputs below and decide which one better satisfies the ` +
|
|
24
|
+
`criterion above.\n\n[CANDIDATE left]\n${leftOutput}\n\n[CANDIDATE right]\n${rightOutput}\n\n` +
|
|
25
|
+
`Respond with ONLY a JSON object: {"winner": "left"} or {"winner": "right"}.`);
|
|
26
|
+
}
|
|
27
|
+
/** Extract a loop stop signal from a loop iteration's output. Returns the `loopContinue` value, or
|
|
28
|
+
* `undefined` when the agent gave no clear signal (⇒ the kernel runs the loop to `max_iters`).
|
|
29
|
+
* Accepts `{loop_continue: bool}` or, leniently, `{done: bool}` (continue = !done). */
|
|
30
|
+
export function extractLoopContinue(text) {
|
|
31
|
+
const v = extractJsonValue(text);
|
|
32
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
33
|
+
const o = v;
|
|
34
|
+
if (typeof o.loop_continue === "boolean")
|
|
35
|
+
return o.loop_continue;
|
|
36
|
+
if (typeof o.loopContinue === "boolean")
|
|
37
|
+
return o.loopContinue;
|
|
38
|
+
if (typeof o.done === "boolean")
|
|
39
|
+
return !o.done;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Extract the chosen branch label from a classifier's output. Prefers `{branch: "..."}`; falls back
|
|
44
|
+
* to a bare label string that exactly matches one of the valid labels. Returns `undefined` when no
|
|
45
|
+
* recognizable choice was made (the kernel then prunes every branch — a safe "none matched"). */
|
|
46
|
+
export function extractClassifyBranch(text, labels) {
|
|
47
|
+
const v = extractJsonValue(text);
|
|
48
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
49
|
+
const o = v;
|
|
50
|
+
if (typeof o.branch === "string")
|
|
51
|
+
return o.branch;
|
|
52
|
+
if (typeof o.label === "string")
|
|
53
|
+
return o.label;
|
|
54
|
+
}
|
|
55
|
+
if (typeof v === "string" && labels.includes(v))
|
|
56
|
+
return v;
|
|
57
|
+
const trimmed = (text ?? "").trim();
|
|
58
|
+
if (labels.includes(trimmed))
|
|
59
|
+
return trimmed;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Extract a tournament judge's verdict ("left" or "right"). Defaults to "left" when the verdict is
|
|
63
|
+
* unparseable, so the bracket always advances to a champion rather than stalling with no winner. */
|
|
64
|
+
export function extractJudgeWinner(text) {
|
|
65
|
+
const v = extractJsonValue(text);
|
|
66
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
67
|
+
const w = v.winner;
|
|
68
|
+
if (w === "right")
|
|
69
|
+
return "right";
|
|
70
|
+
if (w === "left")
|
|
71
|
+
return "left";
|
|
72
|
+
}
|
|
73
|
+
const lowered = (text ?? "").toLowerCase();
|
|
74
|
+
// Last resort: a bare mention. Bias to "left" on ambiguity (both/neither mentioned).
|
|
75
|
+
if (lowered.includes("right") && !lowered.includes("left"))
|
|
76
|
+
return "right";
|
|
77
|
+
return "left";
|
|
78
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import type { ToolSchema, ToolResult } from "../types.js";
|
|
2
|
+
/** M3/G4: the runtime context a tool may read when executing (carries the working directory). A
|
|
3
|
+
* narrow, dependency-free shape; the execution plane's `RunContext` is structurally assignable to it.
|
|
4
|
+
* (WASM has no filesystem, so worktree isolation is N/A here — this keeps the tool ABI in parity
|
|
5
|
+
* with the Node/Python ports so a tool authored once works across all of them.) */
|
|
6
|
+
export interface ToolExecContext {
|
|
7
|
+
cwd?: string;
|
|
8
|
+
}
|
|
2
9
|
export interface RegisteredTool {
|
|
3
10
|
schema: ToolSchema;
|
|
4
|
-
execute(args: Record<string, unknown
|
|
11
|
+
execute(args: Record<string, unknown>, ctx?: ToolExecContext): Promise<string>;
|
|
5
12
|
}
|
|
6
|
-
export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown
|
|
13
|
+
export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>, ctx?: ToolExecContext) => Promise<string> | string): RegisteredTool;
|
|
7
14
|
export declare function executeTools(calls: {
|
|
8
15
|
id: string;
|
|
9
16
|
name: string;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export function tool(name, description, parameters, fn) {
|
|
2
2
|
return {
|
|
3
3
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
4
|
-
async execute(args) { return fn(args); },
|
|
4
|
+
async execute(args, ctx) { return fn(args, ctx); },
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
export async function executeTools(calls, registry) {
|
package/dist/types.d.ts
CHANGED
|
@@ -179,7 +179,11 @@ export interface LLMProvider {
|
|
|
179
179
|
peekProviderReplay?(message: Pick<Message, "content" | "toolCalls">): ProviderReplay | undefined;
|
|
180
180
|
seedProviderReplay?(message: Pick<Message, "content" | "toolCalls">, replay: ProviderReplay): void;
|
|
181
181
|
complete(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>): Promise<Message>;
|
|
182
|
-
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState
|
|
182
|
+
stream(context: RenderedContext, tools: ToolSchema[], extensions?: Record<string, unknown>, state?: ProviderRunState,
|
|
183
|
+
/** #2-B-ii: when provided, a preempt (`interrupt()`) aborts the in-flight request. SDK-client
|
|
184
|
+
* providers forward it via `{ signal }`; the runner also breaks the consume loop on abort, so
|
|
185
|
+
* providers that ignore it still stop processing immediately. Optional ⇒ backward-compatible. */
|
|
186
|
+
signal?: AbortSignal): AsyncIterable<StreamEvent>;
|
|
183
187
|
}
|
|
184
188
|
export interface DreamSummarizer {
|
|
185
189
|
summarize(archived: Message[], context: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "DeepStrike WASM SDK — browser, Cloudflare Workers, Deno Deploy",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@deepstrike/wasm-kernel": "0.2.
|
|
18
|
+
"@deepstrike/wasm-kernel": "0.2.17"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/jest": "^30.0.0",
|