@deepstrike/wasm 0.2.10 → 0.2.11
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 +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/output-schema.d.ts +14 -0
- package/dist/runtime/output-schema.js +113 -0
- package/dist/runtime/reducers.d.ts +15 -0
- package/dist/runtime/reducers.js +57 -0
- package/dist/runtime/runner.d.ts +17 -0
- package/dist/runtime/runner.js +153 -19
- package/dist/runtime/session-log.d.ts +7 -0
- package/dist/runtime/session-repair.d.ts +14 -0
- package/dist/runtime/session-repair.js +15 -0
- package/dist/runtime/sub-agent-orchestrator.js +9 -1
- package/dist/runtime/types/agent.d.ts +39 -1
- package/dist/runtime/types/agent.js +76 -16
- package/dist/types.d.ts +6 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ 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, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
7
|
+
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
8
8
|
export { Governance } from "./governance.js";
|
|
9
9
|
export type { GovernanceVerdict } from "./governance.js";
|
|
10
10
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
4
|
+
export { workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules } from "./runtime/types/agent.js";
|
|
5
5
|
export { Governance } from "./governance.js";
|
|
6
6
|
export { AnthropicProvider } from "./providers/anthropic.js";
|
|
7
7
|
export { OpenAIProvider, QwenProvider, DeepSeekProvider, MiniMaxProvider, KimiProvider } from "./providers/openai.js";
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type { RunContext, ExecutionPlane } from "./execution-plane.js";
|
|
|
4
4
|
export { LocalExecutionPlane } from "./execution-plane.js";
|
|
5
5
|
export type { MemoryPolicy, MemoryWriteRateLimit, ResourceQuota, RuntimeOptions, SchedulerBudget } from "./runner.js";
|
|
6
6
|
export { RuntimeRunner, collectText } from "./runner.js";
|
|
7
|
+
export { builtinReducers, resolveReducer } from "./reducers.js";
|
|
8
|
+
export type { Reducer, ReducerRegistry, ReducerInput } from "./reducers.js";
|
|
7
9
|
export { getKernel } from "./kernel.js";
|
|
8
10
|
export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./os-profile.js";
|
|
9
11
|
export type { NativeOsProfile, OsProfileId } from "./os-profile.js";
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { InMemorySessionLog } from "./session-log.js";
|
|
2
2
|
export { LocalExecutionPlane } from "./execution-plane.js";
|
|
3
3
|
export { RuntimeRunner, collectText } from "./runner.js";
|
|
4
|
+
export { builtinReducers, resolveReducer } from "./reducers.js";
|
|
4
5
|
export { getKernel } from "./kernel.js";
|
|
5
6
|
export { DEFAULT_NATIVE_ATTENTION_POLICY, DEFAULT_NATIVE_GOVERNANCE_POLICY, DEFAULT_SANDBOX_POLICY, assertNativeProfile, osProfile, validateDeclarativePolicy, } from "./os-profile.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface SchemaValidation {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
errors: string[];
|
|
4
|
+
}
|
|
5
|
+
type JsonSchema = Record<string, unknown>;
|
|
6
|
+
/** Validate `value` against `schema` (the supported subset). `path` is for error messages. */
|
|
7
|
+
export declare function validateAgainstSchema(value: unknown, schema: JsonSchema, path?: string): SchemaValidation;
|
|
8
|
+
/** The instruction appended to a node's goal so its agent produces schema-conforming JSON. */
|
|
9
|
+
export declare function schemaInstruction(schema: JsonSchema): string;
|
|
10
|
+
/** A stronger re-prompt for a retry after a validation failure. */
|
|
11
|
+
export declare function schemaRetryInstruction(schema: JsonSchema, errors: string[]): string;
|
|
12
|
+
/** Best-effort extraction of a JSON value from agent output (raw, fenced, or embedded). */
|
|
13
|
+
export declare function extractJsonValue(text: string): unknown;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// G3 structured output: a small, dependency-free JSON-Schema subset validator + helpers used by the
|
|
2
|
+
// workflow runner to enforce a node's `output_schema`. The kernel carries the schema verbatim (it is
|
|
3
|
+
// zero-I/O and never validates); enforcement lives here, SDK-side, where the agent output exists.
|
|
4
|
+
//
|
|
5
|
+
// Supported keywords (the common structured-output subset): `type` (object | array | string |
|
|
6
|
+
// number | integer | boolean | null), `required`, `properties` (recursive), `items` (recursive),
|
|
7
|
+
// `enum`. Unknown keywords are ignored rather than rejected — a permissive superset of these specs
|
|
8
|
+
// still validates, matching "instruct the model, then check the shape" rather than full JSON Schema.
|
|
9
|
+
function typeOfValue(v) {
|
|
10
|
+
if (v === null)
|
|
11
|
+
return "null";
|
|
12
|
+
if (Array.isArray(v))
|
|
13
|
+
return "array";
|
|
14
|
+
return typeof v; // "object" | "string" | "number" | "boolean"
|
|
15
|
+
}
|
|
16
|
+
function matchesType(v, t) {
|
|
17
|
+
switch (t) {
|
|
18
|
+
case "integer":
|
|
19
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
20
|
+
case "number":
|
|
21
|
+
return typeof v === "number";
|
|
22
|
+
case "object":
|
|
23
|
+
return typeOfValue(v) === "object";
|
|
24
|
+
default:
|
|
25
|
+
return typeOfValue(v) === t;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Validate `value` against `schema` (the supported subset). `path` is for error messages. */
|
|
29
|
+
export function validateAgainstSchema(value, schema, path = "$") {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const type = schema.type;
|
|
32
|
+
if (typeof type === "string" && !matchesType(value, type)) {
|
|
33
|
+
errors.push(`${path}: expected ${type}, got ${typeOfValue(value)}`);
|
|
34
|
+
return { ok: false, errors }; // type mismatch ⇒ stop; deeper checks are meaningless
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(type) && !type.some(t => typeof t === "string" && matchesType(value, t))) {
|
|
37
|
+
errors.push(`${path}: expected one of [${type.join(", ")}], got ${typeOfValue(value)}`);
|
|
38
|
+
return { ok: false, errors };
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(schema.enum) && !schema.enum.some(e => e === value)) {
|
|
41
|
+
errors.push(`${path}: value not in enum`);
|
|
42
|
+
}
|
|
43
|
+
if (typeOfValue(value) === "object") {
|
|
44
|
+
const obj = value;
|
|
45
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
46
|
+
for (const key of required) {
|
|
47
|
+
if (!(key in obj))
|
|
48
|
+
errors.push(`${path}.${key}: required property missing`);
|
|
49
|
+
}
|
|
50
|
+
const properties = schema.properties ?? {};
|
|
51
|
+
for (const [key, sub] of Object.entries(properties)) {
|
|
52
|
+
if (key in obj) {
|
|
53
|
+
const r = validateAgainstSchema(obj[key], sub, `${path}.${key}`);
|
|
54
|
+
if (!r.ok)
|
|
55
|
+
errors.push(...r.errors);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (typeOfValue(value) === "array" && schema.items && typeof schema.items === "object") {
|
|
60
|
+
const items = schema.items;
|
|
61
|
+
value.forEach((el, i) => {
|
|
62
|
+
const r = validateAgainstSchema(el, items, `${path}[${i}]`);
|
|
63
|
+
if (!r.ok)
|
|
64
|
+
errors.push(...r.errors);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { ok: errors.length === 0, errors };
|
|
68
|
+
}
|
|
69
|
+
/** The instruction appended to a node's goal so its agent produces schema-conforming JSON. */
|
|
70
|
+
export function schemaInstruction(schema) {
|
|
71
|
+
return ("You MUST return ONLY a single JSON value that conforms to this JSON Schema, with no prose, " +
|
|
72
|
+
"no markdown, and no code fences:\n" +
|
|
73
|
+
JSON.stringify(schema));
|
|
74
|
+
}
|
|
75
|
+
/** A stronger re-prompt for a retry after a validation failure. */
|
|
76
|
+
export function schemaRetryInstruction(schema, errors) {
|
|
77
|
+
return (`${schemaInstruction(schema)}\n\nYour previous output did NOT conform: ${errors.join("; ")}. ` +
|
|
78
|
+
"Return ONLY the corrected JSON value.");
|
|
79
|
+
}
|
|
80
|
+
/** Best-effort extraction of a JSON value from agent output (raw, fenced, or embedded). */
|
|
81
|
+
export function extractJsonValue(text) {
|
|
82
|
+
const trimmed = (text ?? "").trim();
|
|
83
|
+
if (!trimmed)
|
|
84
|
+
return undefined;
|
|
85
|
+
const tryParse = (s) => {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(s);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const whole = tryParse(trimmed);
|
|
94
|
+
if (whole !== undefined)
|
|
95
|
+
return whole;
|
|
96
|
+
const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
97
|
+
if (fence) {
|
|
98
|
+
const fenced = tryParse(fence[1].trim());
|
|
99
|
+
if (fenced !== undefined)
|
|
100
|
+
return fenced;
|
|
101
|
+
}
|
|
102
|
+
// Fall back to the first balanced {...} or [...] slice.
|
|
103
|
+
for (const [open, close] of [["{", "}"], ["[", "]"]]) {
|
|
104
|
+
const start = trimmed.indexOf(open);
|
|
105
|
+
const end = trimmed.lastIndexOf(close);
|
|
106
|
+
if (start !== -1 && end > start) {
|
|
107
|
+
const slice = tryParse(trimmed.slice(start, end + 1));
|
|
108
|
+
if (slice !== undefined)
|
|
109
|
+
return slice;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** One dependency's contribution to a reduce: the producing node's agent id and its output text. */
|
|
2
|
+
export interface ReducerInput {
|
|
3
|
+
agentId: string;
|
|
4
|
+
output: string;
|
|
5
|
+
}
|
|
6
|
+
/** A pure function over a reduce node's dependency outputs → the reduce node's output string. */
|
|
7
|
+
export type Reducer = (inputs: ReducerInput[]) => string;
|
|
8
|
+
export type ReducerRegistry = Record<string, Reducer>;
|
|
9
|
+
/**
|
|
10
|
+
* Built-in reducers, available to every workflow without registration. A user-supplied registry is
|
|
11
|
+
* merged over these (so a custom reducer can shadow a built-in of the same name).
|
|
12
|
+
*/
|
|
13
|
+
export declare const builtinReducers: ReducerRegistry;
|
|
14
|
+
/** Resolve a reducer by name from the built-ins overlaid with a user registry. */
|
|
15
|
+
export declare function resolveReducer(name: string, user?: ReducerRegistry): Reducer | undefined;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// G2 deterministic compute: the host-side reducer registry. A `NodeKind::Reduce` workflow node runs
|
|
2
|
+
// no LLM agent — the kernel hands the SDK a reducer name + its dependency outputs, and the SDK runs
|
|
3
|
+
// the named pure function here. This is the "ordinary code between stages" (dedupe / filter / merge /
|
|
4
|
+
// early-exit) of the code-orchestration model, expressed deterministically as a DAG node.
|
|
5
|
+
import { extractJsonValue } from "./output-schema.js";
|
|
6
|
+
/** Non-empty, trimmed lines of a string. */
|
|
7
|
+
function lines(s) {
|
|
8
|
+
return s
|
|
9
|
+
.split("\n")
|
|
10
|
+
.map(l => l.trim())
|
|
11
|
+
.filter(l => l.length > 0);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Built-in reducers, available to every workflow without registration. A user-supplied registry is
|
|
15
|
+
* merged over these (so a custom reducer can shadow a built-in of the same name).
|
|
16
|
+
*/
|
|
17
|
+
export const builtinReducers = {
|
|
18
|
+
/** Concatenate every input's output, separated by blank lines, in dependency order. */
|
|
19
|
+
concat: inputs => inputs.map(i => i.output).join("\n\n"),
|
|
20
|
+
/** Union of non-empty lines across all inputs, first-seen order preserved (dedupe a fan-out). */
|
|
21
|
+
dedupe_lines: inputs => {
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const out = [];
|
|
24
|
+
for (const i of inputs) {
|
|
25
|
+
for (const line of lines(i.output)) {
|
|
26
|
+
if (!seen.has(line)) {
|
|
27
|
+
seen.add(line);
|
|
28
|
+
out.push(line);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return out.join("\n");
|
|
33
|
+
},
|
|
34
|
+
/** Parse each input as a JSON array, concatenate, dedupe by canonical JSON → a JSON array string. */
|
|
35
|
+
merge_json_arrays: inputs => {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const merged = [];
|
|
38
|
+
for (const i of inputs) {
|
|
39
|
+
const v = extractJsonValue(i.output);
|
|
40
|
+
const arr = Array.isArray(v) ? v : v !== undefined ? [v] : [];
|
|
41
|
+
for (const el of arr) {
|
|
42
|
+
const key = JSON.stringify(el);
|
|
43
|
+
if (!seen.has(key)) {
|
|
44
|
+
seen.add(key);
|
|
45
|
+
merged.push(el);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return JSON.stringify(merged);
|
|
50
|
+
},
|
|
51
|
+
/** The number of inputs that produced any non-empty output — handy for early-exit/branch gates. */
|
|
52
|
+
count: inputs => String(inputs.filter(i => i.output.trim().length > 0).length),
|
|
53
|
+
};
|
|
54
|
+
/** Resolve a reducer by name from the built-ins overlaid with a user registry. */
|
|
55
|
+
export function resolveReducer(name, user) {
|
|
56
|
+
return user?.[name] ?? builtinReducers[name];
|
|
57
|
+
}
|
package/dist/runtime/runner.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { ExecutionPlane } from "./execution-plane.js";
|
|
|
8
8
|
import { type GovernancePolicy } from "../governance.js";
|
|
9
9
|
import type { AgentRunSpec, SubAgentResult, MilestonePolicy, MilestoneContract, MilestoneCheckResult, WorkflowSpec } from "./types/agent.js";
|
|
10
10
|
import { type SubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
11
|
+
import { type ReducerRegistry } from "./reducers.js";
|
|
11
12
|
import { type NativeOsProfile, type OsProfileId } from "./os-profile.js";
|
|
12
13
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
13
14
|
export interface MemoryWriteRateLimit {
|
|
@@ -69,6 +70,8 @@ export interface RuntimeOptions {
|
|
|
69
70
|
onToolSuspend?: (event: ToolSuspendEvent) => Promise<unknown> | unknown;
|
|
70
71
|
onPermissionRequest?: (event: PermissionRequestEvent) => Promise<PermissionResponse | boolean> | PermissionResponse | boolean;
|
|
71
72
|
subAgentOrchestrator?: SubAgentOrchestrator;
|
|
73
|
+
/** G2: custom reducers for `NodeKind::Reduce` workflow nodes, merged over the built-ins. */
|
|
74
|
+
reducers?: ReducerRegistry;
|
|
72
75
|
milestonePolicy?: MilestonePolicy;
|
|
73
76
|
milestoneContract?: MilestoneContract;
|
|
74
77
|
onMilestoneEvaluate?: (ctx: {
|
|
@@ -113,6 +116,19 @@ export declare class RuntimeRunner {
|
|
|
113
116
|
dream(agentId: string, nowMs?: number): Promise<DreamResult>;
|
|
114
117
|
private execute;
|
|
115
118
|
spawnSubAgent(spec: AgentRunSpec): Promise<SubAgentResult>;
|
|
119
|
+
/**
|
|
120
|
+
* G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
|
|
121
|
+
* validating its output (the supported JSON-Schema subset), and re-running once with the errors
|
|
122
|
+
* fed back on mismatch. If it still does not conform, the node is failed with the validation
|
|
123
|
+
* reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
|
|
124
|
+
*/
|
|
125
|
+
private runWorkflowNode;
|
|
126
|
+
/**
|
|
127
|
+
* G2: execute a deterministic reduce node — run the named reducer (built-ins overlaid with
|
|
128
|
+
* `opts.reducers`) over its dependency outputs and return a synthetic completion. No LLM, zero
|
|
129
|
+
* tokens. An unknown reducer or a thrown reducer fails the node (`Error` → starves dependents).
|
|
130
|
+
*/
|
|
131
|
+
private runReduceNode;
|
|
116
132
|
/**
|
|
117
133
|
* W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
|
|
118
134
|
* through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
|
|
@@ -120,6 +136,7 @@ export declare class RuntimeRunner {
|
|
|
120
136
|
*/
|
|
121
137
|
runWorkflow(spec: WorkflowSpec, opts?: {
|
|
122
138
|
resumedCompleted?: string[];
|
|
139
|
+
resumedSubmissions?: Record<string, unknown>[][];
|
|
123
140
|
}): Promise<{
|
|
124
141
|
completed: string[];
|
|
125
142
|
failed: string[];
|
package/dist/runtime/runner.js
CHANGED
|
@@ -3,10 +3,12 @@ import { governancePolicyToKernelEvent } from "../governance.js";
|
|
|
3
3
|
import { getKernel } from "./kernel.js";
|
|
4
4
|
import { peekProviderReplay, seedProviderReplayFromEvents } from "./provider-replay.js";
|
|
5
5
|
import { sanitizeReplayText } from "./replay-sanitize.js";
|
|
6
|
-
import { buildLlmCompletedEvent, buildRunTerminalEvent, buildWorkflowNodeCompletedEvent, recoverCompletedWorkflowNodes, repairEventsForRecovery, } from "./session-repair.js";
|
|
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, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
8
|
+
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "./types/agent.js";
|
|
9
9
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
10
|
+
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
11
|
+
import { resolveReducer } from "./reducers.js";
|
|
10
12
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
11
13
|
import { assertNativeProfile } from "./os-profile.js";
|
|
12
14
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
@@ -527,7 +529,18 @@ export class RuntimeRunner {
|
|
|
527
529
|
onPermissionRequest: this.opts.onPermissionRequest,
|
|
528
530
|
};
|
|
529
531
|
const toolResults = [];
|
|
530
|
-
|
|
532
|
+
// R3-1: intercept `submit_workflow_nodes` — it can't apply to this runner's kernel (when this
|
|
533
|
+
// runner is a workflow node, the workflow lives in the parent). Surface the nodes as an event;
|
|
534
|
+
// the orchestrator collects them and `runWorkflow` sends them to the parent kernel.
|
|
535
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes");
|
|
536
|
+
const normalCalls = allCalls.filter(c => c.name !== "submit_workflow_nodes");
|
|
537
|
+
for (const call of submitCalls) {
|
|
538
|
+
const nodes = parseSubmitWorkflowNodesArgs(call.arguments);
|
|
539
|
+
yield { type: "workflow_nodes_submitted", nodes };
|
|
540
|
+
toolResults.push({ callId: call.id, output: "submitted", isError: false });
|
|
541
|
+
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
542
|
+
}
|
|
543
|
+
for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
|
|
531
544
|
yield evt;
|
|
532
545
|
if (evt.type === "tool_result") {
|
|
533
546
|
const tre = evt;
|
|
@@ -706,6 +719,81 @@ export class RuntimeRunner {
|
|
|
706
719
|
});
|
|
707
720
|
return result;
|
|
708
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* G3: run one workflow node, enforcing its `output_schema` (if any) by instructing the agent,
|
|
724
|
+
* validating its output (the supported JSON-Schema subset), and re-running once with the errors
|
|
725
|
+
* fed back on mismatch. If it still does not conform, the node is failed with the validation
|
|
726
|
+
* reason (an `Error`-terminated result fails the node in-kernel, starving its dependents).
|
|
727
|
+
*/
|
|
728
|
+
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
|
|
729
|
+
// G2: a reduce node runs no LLM — execute the registered pure function over its dependency
|
|
730
|
+
// outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
|
|
731
|
+
if (node.reducer) {
|
|
732
|
+
return this.runReduceNode(node, outputs ?? new Map());
|
|
733
|
+
}
|
|
734
|
+
const baseSpec = workflowNodeToSpec(node, parentSessionId);
|
|
735
|
+
const manifest = workflowNodeToManifest(node, parentSessionId);
|
|
736
|
+
// G4: surface remaining workflow budget so a coordinator node can size its submission.
|
|
737
|
+
const budgetNote = workflowBudgetNote(budget);
|
|
738
|
+
const withBudget = (goal) => (budgetNote ? `${goal}\n\n${budgetNote}` : goal);
|
|
739
|
+
const mkCtx = (goal) => ({
|
|
740
|
+
parentOpts: this.opts,
|
|
741
|
+
parentSessionId,
|
|
742
|
+
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
743
|
+
manifest,
|
|
744
|
+
sessionLog: this.opts.sessionLog,
|
|
745
|
+
});
|
|
746
|
+
const schema = node.output_schema;
|
|
747
|
+
if (!schema)
|
|
748
|
+
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
749
|
+
const MAX_ATTEMPTS = 2;
|
|
750
|
+
let last;
|
|
751
|
+
let lastErrors = [];
|
|
752
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
753
|
+
const goal = attempt === 1
|
|
754
|
+
? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
|
|
755
|
+
: `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
|
|
756
|
+
const result = await orchestrator.run(mkCtx(goal));
|
|
757
|
+
const content = result.result.finalMessage?.content;
|
|
758
|
+
const text = typeof content === "string" ? content : content != null ? JSON.stringify(content) : "";
|
|
759
|
+
const v = validateAgainstSchema(extractJsonValue(text), schema);
|
|
760
|
+
if (v.ok)
|
|
761
|
+
return result;
|
|
762
|
+
last = result;
|
|
763
|
+
lastErrors = v.errors;
|
|
764
|
+
}
|
|
765
|
+
const reason = `output_schema validation failed after ${MAX_ATTEMPTS} attempts: ${lastErrors.join("; ")}`;
|
|
766
|
+
const fallback = last;
|
|
767
|
+
return {
|
|
768
|
+
...fallback,
|
|
769
|
+
result: {
|
|
770
|
+
...fallback.result,
|
|
771
|
+
termination: "error",
|
|
772
|
+
finalMessage: { role: "assistant", content: reason, toolCalls: [] },
|
|
773
|
+
},
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* G2: execute a deterministic reduce node — run the named reducer (built-ins overlaid with
|
|
778
|
+
* `opts.reducers`) over its dependency outputs and return a synthetic completion. No LLM, zero
|
|
779
|
+
* tokens. An unknown reducer or a thrown reducer fails the node (`Error` → starves dependents).
|
|
780
|
+
*/
|
|
781
|
+
runReduceNode(node, outputs) {
|
|
782
|
+
const ok = (content, termination) => ({
|
|
783
|
+
agentId: node.agent_id,
|
|
784
|
+
result: { termination, finalMessage: { role: "assistant", content, toolCalls: [] }, turnsUsed: 0, totalTokensUsed: 0 },
|
|
785
|
+
});
|
|
786
|
+
const reducer = resolveReducer(node.reducer, this.opts.reducers);
|
|
787
|
+
if (!reducer)
|
|
788
|
+
return ok(`unknown reducer "${node.reducer}"`, "error");
|
|
789
|
+
const inputs = (node.input_agent_ids ?? []).map(agentId => ({ agentId, output: outputs.get(agentId) ?? "" }));
|
|
790
|
+
try {
|
|
791
|
+
return ok(reducer(inputs), "completed");
|
|
792
|
+
}
|
|
793
|
+
catch (err) {
|
|
794
|
+
return ok(`reducer "${node.reducer}" threw: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
795
|
+
}
|
|
796
|
+
}
|
|
709
797
|
/**
|
|
710
798
|
* W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
|
|
711
799
|
* through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
|
|
@@ -718,34 +806,63 @@ export class RuntimeRunner {
|
|
|
718
806
|
const parentSessionId = this.currentSessionId;
|
|
719
807
|
const runtime = this.activeKernel;
|
|
720
808
|
const orchestrator = this.opts.subAgentOrchestrator ?? defaultSubAgentOrchestrator;
|
|
721
|
-
|
|
809
|
+
const observations = kernelApply(runtime, this.pendingObservations, {
|
|
722
810
|
kind: "load_workflow",
|
|
723
811
|
spec: workflowSpecToKernel(spec),
|
|
724
812
|
parent_session_id: parentSessionId,
|
|
725
813
|
// W0-ABI resume: skip nodes already completed before an interruption.
|
|
726
814
|
...(opts?.resumedCompleted && opts.resumedCompleted.length ? { resumed_completed: opts.resumedCompleted } : {}),
|
|
815
|
+
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
816
|
+
...(opts?.resumedSubmissions && opts.resumedSubmissions.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
727
817
|
});
|
|
818
|
+
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.nodes ?? [];
|
|
819
|
+
// G4: the batch observation carries the workflow's remaining budget; track the latest.
|
|
820
|
+
const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
|
|
821
|
+
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
822
|
+
let done = findDone(observations);
|
|
823
|
+
if (done)
|
|
824
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
825
|
+
let nodes = collectNodes(observations);
|
|
826
|
+
let budget = collectBudget(observations);
|
|
827
|
+
// G2: each completed node's output, keyed by agent id — a reduce node reads its deps' outputs.
|
|
828
|
+
const outputs = new Map();
|
|
728
829
|
for (;;) {
|
|
729
|
-
const done = observations.find(o => o.kind === "workflow_completed");
|
|
730
|
-
if (done)
|
|
731
|
-
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
732
|
-
const batch = observations.find(o => o.kind === "workflow_batch_spawned");
|
|
733
|
-
const nodes = batch?.nodes ?? [];
|
|
734
830
|
if (nodes.length === 0)
|
|
735
831
|
return { completed: [], failed: [] };
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
sessionLog: this.opts.sessionLog,
|
|
742
|
-
})));
|
|
743
|
-
observations = [];
|
|
832
|
+
const roundBudget = budget;
|
|
833
|
+
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs)));
|
|
834
|
+
// Accumulate next-batch nodes across feeds (per-node unblock can spawn dependents per feed).
|
|
835
|
+
const nextNodes = [];
|
|
836
|
+
done = undefined;
|
|
744
837
|
for (const result of results) {
|
|
745
|
-
|
|
838
|
+
// G2: record this node's output so a downstream reduce node can consume it.
|
|
839
|
+
const outContent = result.result.finalMessage?.content;
|
|
840
|
+
outputs.set(result.agentId, typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "");
|
|
841
|
+
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
842
|
+
// reporting the node's completion — the workflow is still active, so even a last-node
|
|
843
|
+
// submission keeps the DAG alive.
|
|
844
|
+
if (result.submittedNodes?.length) {
|
|
845
|
+
// G1: stamp the submitting node's agent id so the kernel coerces a quarantined submitter's
|
|
846
|
+
// nodes to quarantined (no topological privilege escalation).
|
|
847
|
+
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
848
|
+
const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
|
|
849
|
+
nextNodes.push(...collectNodes(subObs));
|
|
850
|
+
budget = collectBudget(subObs) ?? budget;
|
|
851
|
+
// R3-1: persist the submission (kernel-shape nodes) so resume can re-apply it.
|
|
852
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
853
|
+
turn: runtime.turn(),
|
|
854
|
+
nodes: submitEvent.nodes ?? [],
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
const obs = kernelApply(runtime, this.pendingObservations, {
|
|
746
858
|
kind: "sub_agent_completed",
|
|
747
859
|
result: subAgentResultToKernel(result),
|
|
748
860
|
});
|
|
861
|
+
nextNodes.push(...collectNodes(obs));
|
|
862
|
+
budget = collectBudget(obs) ?? budget;
|
|
863
|
+
const d = findDone(obs);
|
|
864
|
+
if (d)
|
|
865
|
+
done = d;
|
|
749
866
|
// Persist node completion for resume recovery.
|
|
750
867
|
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodeCompletedEvent({
|
|
751
868
|
turn: runtime.turn(),
|
|
@@ -753,6 +870,10 @@ export class RuntimeRunner {
|
|
|
753
870
|
termination: result.result.termination,
|
|
754
871
|
}));
|
|
755
872
|
}
|
|
873
|
+
if (done && nextNodes.length === 0) {
|
|
874
|
+
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
875
|
+
}
|
|
876
|
+
nodes = nextNodes;
|
|
756
877
|
}
|
|
757
878
|
}
|
|
758
879
|
/**
|
|
@@ -766,7 +887,8 @@ export class RuntimeRunner {
|
|
|
766
887
|
}
|
|
767
888
|
const events = await this.opts.sessionLog.read(this.currentSessionId);
|
|
768
889
|
const resumedCompleted = recoverCompletedWorkflowNodes(events);
|
|
769
|
-
|
|
890
|
+
const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
|
|
891
|
+
return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions });
|
|
770
892
|
}
|
|
771
893
|
async appendObservations(sessionId, runtime, nextArchiveStart) {
|
|
772
894
|
const turn = runtime.turn();
|
|
@@ -956,3 +1078,15 @@ export async function collectText(stream) {
|
|
|
956
1078
|
}
|
|
957
1079
|
return text;
|
|
958
1080
|
}
|
|
1081
|
+
/** R3-1: parse `submit_workflow_nodes` tool args (`{ nodes: WorkflowNodeSpec[] }`). Node shapes are
|
|
1082
|
+
* trusted structurally; the kernel validates them on append. Malformed payload → no nodes. */
|
|
1083
|
+
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
1084
|
+
let parsed = {};
|
|
1085
|
+
try {
|
|
1086
|
+
parsed = JSON.parse(argsStr);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
// Ignore parse error → no nodes submitted.
|
|
1090
|
+
}
|
|
1091
|
+
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
1092
|
+
}
|
|
@@ -239,6 +239,13 @@ export type SessionEvent = {
|
|
|
239
239
|
primitive?: KernelPrimitive;
|
|
240
240
|
agent_id: string;
|
|
241
241
|
termination: string;
|
|
242
|
+
} | {
|
|
243
|
+
kind: "workflow_nodes_submitted";
|
|
244
|
+
turn: number;
|
|
245
|
+
category?: KernelEventCategory;
|
|
246
|
+
primitive?: KernelPrimitive;
|
|
247
|
+
/** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
|
|
248
|
+
nodes: Record<string, unknown>[];
|
|
242
249
|
} | {
|
|
243
250
|
kind: "workflow_batch_spawned";
|
|
244
251
|
turn: number;
|
|
@@ -52,3 +52,17 @@ export declare function recoverCompletedWorkflowNodes(events: Array<{
|
|
|
52
52
|
seq: number;
|
|
53
53
|
event: SessionEvent;
|
|
54
54
|
}>): string[];
|
|
55
|
+
/** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
|
|
56
|
+
* re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
|
|
57
|
+
export declare function buildWorkflowNodesSubmittedEvent(input: {
|
|
58
|
+
turn: number;
|
|
59
|
+
nodes: Record<string, unknown>[];
|
|
60
|
+
}): Extract<SessionEvent, {
|
|
61
|
+
kind: "workflow_nodes_submitted";
|
|
62
|
+
}>;
|
|
63
|
+
/** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
|
|
64
|
+
* resumeWorkflow, so dynamically-appended nodes are reconstructed. */
|
|
65
|
+
export declare function recoverSubmittedWorkflowNodes(events: Array<{
|
|
66
|
+
seq: number;
|
|
67
|
+
event: SessionEvent;
|
|
68
|
+
}>): Record<string, unknown>[][];
|
|
@@ -70,3 +70,18 @@ export function recoverCompletedWorkflowNodes(events) {
|
|
|
70
70
|
}
|
|
71
71
|
return completed;
|
|
72
72
|
}
|
|
73
|
+
/** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
|
|
74
|
+
* re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
|
|
75
|
+
export function buildWorkflowNodesSubmittedEvent(input) {
|
|
76
|
+
return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
|
|
77
|
+
}
|
|
78
|
+
/** R3-1: recover the runtime submission batches (in order) to rebuild `resumed_submissions` for
|
|
79
|
+
* resumeWorkflow, so dynamically-appended nodes are reconstructed. */
|
|
80
|
+
export function recoverSubmittedWorkflowNodes(events) {
|
|
81
|
+
const submissions = [];
|
|
82
|
+
for (const { event } of events) {
|
|
83
|
+
if (event.kind === "workflow_nodes_submitted")
|
|
84
|
+
submissions.push(event.nodes);
|
|
85
|
+
}
|
|
86
|
+
return submissions;
|
|
87
|
+
}
|
|
@@ -41,6 +41,8 @@ export class SubAgentOrchestrator {
|
|
|
41
41
|
});
|
|
42
42
|
let done;
|
|
43
43
|
let finalText = "";
|
|
44
|
+
// R3-1: collect any nodes this node's agent submitted via the `submit_workflow_nodes` tool.
|
|
45
|
+
const submittedNodes = [];
|
|
44
46
|
for await (const evt of childRunner.run({
|
|
45
47
|
sessionId: ctx.spec.identity.sessionId,
|
|
46
48
|
goal: ctx.spec.goal,
|
|
@@ -49,6 +51,8 @@ export class SubAgentOrchestrator {
|
|
|
49
51
|
finalText += evt.delta;
|
|
50
52
|
if (evt.type === "done")
|
|
51
53
|
done = evt;
|
|
54
|
+
if (evt.type === "workflow_nodes_submitted")
|
|
55
|
+
submittedNodes.push(...evt.nodes);
|
|
52
56
|
}
|
|
53
57
|
const loopResult = {
|
|
54
58
|
termination: terminationFromStatus(done?.status ?? "error"),
|
|
@@ -58,7 +62,11 @@ export class SubAgentOrchestrator {
|
|
|
58
62
|
? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } }
|
|
59
63
|
: {}),
|
|
60
64
|
};
|
|
61
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
agentId: ctx.spec.identity.agentId,
|
|
67
|
+
result: loopResult,
|
|
68
|
+
...(submittedNodes.length ? { submittedNodes } : {}),
|
|
69
|
+
};
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
export const defaultSubAgentOrchestrator = new SubAgentOrchestrator();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Message } from "../../types.js";
|
|
1
|
+
import type { Message, ToolSchema } from "../../types.js";
|
|
2
2
|
export type KernelAgentRole = "explore" | "plan" | "implement" | "verify" | "custom";
|
|
3
3
|
export type AgentIsolation = "shared" | "read_only" | "worktree" | "remote";
|
|
4
4
|
export type ContextInheritance = "none" | "system_only" | "full";
|
|
@@ -52,6 +52,9 @@ export interface LoopResult {
|
|
|
52
52
|
export interface SubAgentResult {
|
|
53
53
|
agentId: string;
|
|
54
54
|
result: LoopResult;
|
|
55
|
+
/** R3-1: nodes this node's agent asked to append to the parent workflow DAG. Surfaced by the
|
|
56
|
+
* orchestrator; `runWorkflow` sends them to the parent kernel before completion. SDK-internal. */
|
|
57
|
+
submittedNodes?: WorkflowNodeSpec[];
|
|
55
58
|
}
|
|
56
59
|
export interface MilestoneCheckResult {
|
|
57
60
|
phaseId: string;
|
|
@@ -82,12 +85,20 @@ export type WorkflowTaskSpec = {
|
|
|
82
85
|
lane?: string;
|
|
83
86
|
} | string;
|
|
84
87
|
/** One node in a declarative workflow DAG (camelCase host shape). */
|
|
88
|
+
export type NodeTrust = "trusted" | "quarantined";
|
|
85
89
|
export interface WorkflowNodeSpec {
|
|
86
90
|
task: WorkflowTaskSpec;
|
|
87
91
|
role: KernelAgentRole;
|
|
88
92
|
isolation?: AgentIsolation;
|
|
89
93
|
contextInheritance?: ContextInheritance;
|
|
90
94
|
modelHint?: string;
|
|
95
|
+
/** W3: `quarantined` nodes read untrusted content and must run without privileges (read-only). */
|
|
96
|
+
trust?: NodeTrust;
|
|
97
|
+
/** G3: JSON Schema the node's output must conform to (validated + retried SDK-side). */
|
|
98
|
+
outputSchema?: Record<string, unknown>;
|
|
99
|
+
/** G2: make this a deterministic reduce node — runs no LLM; the runner routes it to the registered
|
|
100
|
+
* reducer of this name over its `dependsOn` nodes' outputs. */
|
|
101
|
+
reducer?: string;
|
|
91
102
|
/** Indices of nodes this node depends on. */
|
|
92
103
|
dependsOn?: number[];
|
|
93
104
|
}
|
|
@@ -103,9 +114,36 @@ export interface WorkflowSpawnInfo {
|
|
|
103
114
|
isolation: string;
|
|
104
115
|
context_inheritance: string;
|
|
105
116
|
model_hint?: string;
|
|
117
|
+
/** G3: JSON Schema the node's output must conform to (carried verbatim from the spec). */
|
|
118
|
+
output_schema?: Record<string, unknown>;
|
|
119
|
+
/** G2: for a reduce node, the registered reducer name to run (no LLM). */
|
|
120
|
+
reducer?: string;
|
|
121
|
+
/** G2: the dependency agent ids whose outputs a reduce node consumes. */
|
|
122
|
+
input_agent_ids?: string[];
|
|
106
123
|
}
|
|
124
|
+
/** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
|
|
125
|
+
* `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
|
|
126
|
+
export interface WorkflowBudget {
|
|
127
|
+
nodes_used: number;
|
|
128
|
+
nodes_max?: number;
|
|
129
|
+
nodes_remaining?: number;
|
|
130
|
+
running_subagents: number;
|
|
131
|
+
max_concurrent_subagents?: number;
|
|
132
|
+
concurrency_remaining?: number;
|
|
133
|
+
}
|
|
134
|
+
/** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
|
|
135
|
+
export declare function workflowBudgetNote(budget: WorkflowBudget | undefined): string;
|
|
107
136
|
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
137
|
+
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
|
|
138
|
+
* `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
|
|
139
|
+
export declare function workflowNodeSpecToKernel(n: WorkflowNodeSpec): Record<string, unknown>;
|
|
108
140
|
export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string, unknown>;
|
|
141
|
+
/** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
|
|
142
|
+
* `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
|
|
143
|
+
* nodes coerced to quarantined). Omitted ⇒ no coercion. */
|
|
144
|
+
export declare function submitWorkflowNodesToKernel(nodes: WorkflowNodeSpec[], submitterAgentId?: string): Record<string, unknown>;
|
|
145
|
+
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
|
|
146
|
+
export declare const submitWorkflowNodesTool: ToolSchema;
|
|
109
147
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
110
148
|
export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
|
|
111
149
|
/** Build the host manifest for a kernel-generated workflow node. */
|
|
@@ -94,27 +94,87 @@ export function milestoneCheckPass(phaseId) {
|
|
|
94
94
|
export function milestoneCheckFail(phaseId, reason) {
|
|
95
95
|
return { phaseId, passed: false, reason };
|
|
96
96
|
}
|
|
97
|
+
/** G4: a concise budget note appended to a coordinator node's goal. "" when nothing is bounded. */
|
|
98
|
+
export function workflowBudgetNote(budget) {
|
|
99
|
+
if (!budget)
|
|
100
|
+
return "";
|
|
101
|
+
const parts = [];
|
|
102
|
+
if (budget.nodes_remaining != null && budget.nodes_max != null) {
|
|
103
|
+
parts.push(`nodes ${budget.nodes_used}/${budget.nodes_max} used, ${budget.nodes_remaining} remaining`);
|
|
104
|
+
}
|
|
105
|
+
if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
|
|
106
|
+
parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
|
|
107
|
+
}
|
|
108
|
+
if (parts.length === 0)
|
|
109
|
+
return "";
|
|
110
|
+
return (`[workflow budget] ${parts.join(" · ")}. ` +
|
|
111
|
+
"If you submit more workflow nodes, keep the batch within the remaining node budget.");
|
|
112
|
+
}
|
|
97
113
|
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
114
|
+
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` and
|
|
115
|
+
* `submit_workflow_nodes` (R3-1) so the two encodings never drift. */
|
|
116
|
+
export function workflowNodeSpecToKernel(n) {
|
|
117
|
+
const task = typeof n.task === "string" ? { goal: n.task } : n.task;
|
|
118
|
+
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
|
+
},
|
|
125
|
+
role: n.role,
|
|
126
|
+
isolation: n.isolation ?? "shared",
|
|
127
|
+
context_inheritance: n.contextInheritance ?? "none",
|
|
128
|
+
...(n.modelHint ? { model_hint: n.modelHint } : {}),
|
|
129
|
+
...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
|
|
130
|
+
...(n.outputSchema ? { output_schema: n.outputSchema } : {}),
|
|
131
|
+
// G2: a reducer name lowers to the kernel's `NodeKind::Reduce` (serde-tagged by `type`).
|
|
132
|
+
...(n.reducer ? { kind: { type: "reduce", reducer: n.reducer } } : {}),
|
|
133
|
+
...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
98
136
|
export function workflowSpecToKernel(spec) {
|
|
137
|
+
return { nodes: spec.nodes.map(workflowNodeSpecToKernel) };
|
|
138
|
+
}
|
|
139
|
+
/** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
|
|
140
|
+
* `submitterAgentId` so the kernel enforces no-privilege-escalation (quarantined submitter ⇒ its
|
|
141
|
+
* nodes coerced to quarantined). Omitted ⇒ no coercion. */
|
|
142
|
+
export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
|
|
99
143
|
return {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
task: {
|
|
104
|
-
goal: task.goal,
|
|
105
|
-
// `criteria` is required by the kernel's RuntimeTask serde (no default).
|
|
106
|
-
criteria: task.criteria ?? [],
|
|
107
|
-
...(task.lane ? { lane: task.lane } : {}),
|
|
108
|
-
},
|
|
109
|
-
role: n.role,
|
|
110
|
-
isolation: n.isolation ?? "shared",
|
|
111
|
-
context_inheritance: n.contextInheritance ?? "none",
|
|
112
|
-
...(n.modelHint ? { model_hint: n.modelHint } : {}),
|
|
113
|
-
...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
|
|
114
|
-
};
|
|
115
|
-
}),
|
|
144
|
+
kind: "submit_workflow_nodes",
|
|
145
|
+
nodes: nodes.map(workflowNodeSpecToKernel),
|
|
146
|
+
...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
|
|
116
147
|
};
|
|
117
148
|
}
|
|
149
|
+
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG. */
|
|
150
|
+
export const submitWorkflowNodesTool = {
|
|
151
|
+
name: "submit_workflow_nodes",
|
|
152
|
+
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.",
|
|
154
|
+
parameters: JSON.stringify({
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
nodes: {
|
|
158
|
+
type: "array",
|
|
159
|
+
items: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
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
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ["nodes"],
|
|
176
|
+
}),
|
|
177
|
+
};
|
|
118
178
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
119
179
|
export function workflowNodeToSpec(node, parentSessionId) {
|
|
120
180
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkflowNodeSpec } from "./runtime/types/agent.js";
|
|
1
2
|
export interface Message {
|
|
2
3
|
role: "system" | "user" | "assistant" | "tool";
|
|
3
4
|
content: string;
|
|
@@ -63,6 +64,11 @@ export interface ToolResultEvent extends StreamEvent {
|
|
|
63
64
|
isFatal?: boolean;
|
|
64
65
|
errorKind?: ToolErrorKind;
|
|
65
66
|
}
|
|
67
|
+
/** R3-1: a workflow node's agent called `submit_workflow_nodes`; the runner surfaces the requested nodes (the workflow lives in the parent kernel) and `runWorkflow` sends them to the parent kernel. */
|
|
68
|
+
export interface WorkflowNodesSubmittedEvent extends StreamEvent {
|
|
69
|
+
type: "workflow_nodes_submitted";
|
|
70
|
+
nodes: WorkflowNodeSpec[];
|
|
71
|
+
}
|
|
66
72
|
export interface DoneEvent extends StreamEvent {
|
|
67
73
|
type: "done";
|
|
68
74
|
iterations: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/wasm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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.11"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/jest": "^30.0.0",
|