@deepstrike/sdk 0.2.9 → 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/harness/harness.d.ts +5 -0
- package/dist/harness/harness.js +12 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- 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 +21 -0
- package/dist/runtime/runner.js +146 -12
- 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 +14 -1
- package/dist/types/agent.d.ts +43 -1
- package/dist/types/agent.js +105 -18
- package/dist/types.d.ts +8 -0
- package/package.json +2 -2
|
@@ -26,6 +26,8 @@ export interface HarnessOutcome {
|
|
|
26
26
|
overallScore?: number;
|
|
27
27
|
feedback?: string;
|
|
28
28
|
details?: CriterionResult[];
|
|
29
|
+
/** R3-1: nodes the agent submitted via `submit_workflow_nodes` while running under the harness. */
|
|
30
|
+
submittedNodes?: import("../types/agent.js").WorkflowNodeSpec[];
|
|
29
31
|
}
|
|
30
32
|
export interface Verdict {
|
|
31
33
|
passed: boolean;
|
|
@@ -55,6 +57,9 @@ export type HarnessEvent = {
|
|
|
55
57
|
callId: string;
|
|
56
58
|
content: string;
|
|
57
59
|
isError: boolean;
|
|
60
|
+
} | {
|
|
61
|
+
type: "workflow_nodes_submitted";
|
|
62
|
+
nodes: import("../types/agent.js").WorkflowNodeSpec[];
|
|
58
63
|
} | {
|
|
59
64
|
type: "supervising";
|
|
60
65
|
} | {
|
package/dist/harness/harness.js
CHANGED
|
@@ -61,8 +61,14 @@ export class HarnessLoop {
|
|
|
61
61
|
}
|
|
62
62
|
async run(request) {
|
|
63
63
|
let last;
|
|
64
|
-
|
|
64
|
+
// R3-1: collect nodes the agent submitted while running under the harness, so dynamic fan-out
|
|
65
|
+
// works in harness mode too (not just the plain streaming path).
|
|
66
|
+
const submittedNodes = [];
|
|
67
|
+
for await (const evt of this.stream(request)) {
|
|
65
68
|
last = evt;
|
|
69
|
+
if (evt.type === "workflow_nodes_submitted")
|
|
70
|
+
submittedNodes.push(...evt.nodes);
|
|
71
|
+
}
|
|
66
72
|
const done = last?.type === "done" ? last : undefined;
|
|
67
73
|
return {
|
|
68
74
|
result: "",
|
|
@@ -73,6 +79,7 @@ export class HarnessLoop {
|
|
|
73
79
|
overallScore: done?.verdict.overallScore,
|
|
74
80
|
feedback: done?.verdict.feedback,
|
|
75
81
|
details: done?.verdict.details,
|
|
82
|
+
...(submittedNodes.length ? { submittedNodes } : {}),
|
|
76
83
|
};
|
|
77
84
|
}
|
|
78
85
|
async *stream(request) {
|
|
@@ -107,6 +114,10 @@ export class HarnessLoop {
|
|
|
107
114
|
const tr = evt;
|
|
108
115
|
yield { type: "tool_result", callId: tr.callId, content: tr.content, isError: tr.isError };
|
|
109
116
|
}
|
|
117
|
+
else if (evt.type === "workflow_nodes_submitted") {
|
|
118
|
+
const ws = evt;
|
|
119
|
+
yield { type: "workflow_nodes_submitted", nodes: ws.nodes };
|
|
120
|
+
}
|
|
110
121
|
else if (evt.type === "done") {
|
|
111
122
|
const d = evt;
|
|
112
123
|
lastIterations = d.iterations;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { RuntimeRunner, collectText } from "./runtime/runner.js";
|
|
2
2
|
export type { RuntimeOptions, SchedulerBudget } from "./runtime/runner.js";
|
|
3
|
+
export { builtinReducers, resolveReducer } from "./runtime/reducers.js";
|
|
4
|
+
export type { Reducer, ReducerRegistry, ReducerInput } from "./runtime/reducers.js";
|
|
3
5
|
export type { MemoryPolicy, MemoryWriteRateLimit, ResourceQuota } from "./kernel.js";
|
|
4
6
|
export { KernelPrimitivesDashboard } from "./runtime/kernel-primitives-dashboard.js";
|
|
5
7
|
export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
|
|
@@ -61,7 +63,7 @@ export { SinglePassHarness, EvalLoopHarness, HarnessLoop } from "./harness/harne
|
|
|
61
63
|
export type { HarnessRequest, HarnessOutcome, HarnessLoopOptions, QualityGate } from "./harness/harness.js";
|
|
62
64
|
export type { Message, ToolCall, ToolResult, ToolSchema, ContentPart, TextPart, ImagePart, AudioPart, StreamEvent, TextDelta, ThinkingDelta, ToolCallEvent, ToolChunk, ToolDeltaEvent, ToolSuspendEvent, ToolResultEvent, DoneEvent, ErrorEvent, PermissionRequestEvent, PermissionResolvedEvent, PermissionResponse, LLMProvider, RetryConfig, TokenUsage, ProviderToolSpec, ProviderRunState, ProviderReplay, RenderedContext, ReplayabilityAssessment, } from "./types.js";
|
|
63
65
|
export type { AgentCapabilityFilter, AgentIdentity, AgentIsolation, AgentRunSpec, AgentProcessChangedObservation, ContextInheritance, KernelAgentRole, LoopResult, MilestoneCheckResult, MilestoneContract, MilestonePhase, MilestonePolicy, SubAgentResult, TerminationReason, WorkflowSpec, WorkflowNodeSpec, WorkflowTaskSpec, WorkflowSpawnInfo, } from "./types/agent.js";
|
|
64
|
-
export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, fanoutSynthesize, generateAndFilter, verifyRules, } from "./types/agent.js";
|
|
66
|
+
export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules, } from "./types/agent.js";
|
|
65
67
|
export type { AcceptanceCriterion, VerificationContract, ContractCheckResult, } from "./collaboration/contract.js";
|
|
66
68
|
export { ContractBuilder, formatContractForSystemPrompt, contractToCriteriaStrings, } from "./collaboration/contract.js";
|
|
67
69
|
export { AgentPool } from "./collaboration/pool.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// ── Runtime (Layer 1.5) ────────────────────────────────────────────────────
|
|
2
2
|
export { RuntimeRunner, collectText } from "./runtime/runner.js";
|
|
3
|
+
export { builtinReducers, resolveReducer } from "./runtime/reducers.js";
|
|
3
4
|
export { KernelPrimitivesDashboard } from "./runtime/kernel-primitives-dashboard.js";
|
|
4
5
|
export { FilteredExecutionPlane } from "./runtime/filtered-plane.js";
|
|
5
6
|
export { SubAgentOrchestrator, defaultSubAgentOrchestrator, spawnStandalone } from "./runtime/sub-agent-orchestrator.js";
|
|
@@ -41,7 +42,7 @@ export { PermissionManager, PermissionMode } from "./safety/permissions.js";
|
|
|
41
42
|
export { Governance, governancePolicyToKernelEvent } from "./governance.js";
|
|
42
43
|
// ── Harness ────────────────────────────────────────────────────────────────
|
|
43
44
|
export { SinglePassHarness, EvalLoopHarness, HarnessLoop } from "./harness/harness.js";
|
|
44
|
-
export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, fanoutSynthesize, generateAndFilter, verifyRules, } from "./types/agent.js";
|
|
45
|
+
export { agentIdentitySub, agentRunSpecToKernel, milestoneCheckFail, milestoneCheckPass, milestoneCheckResultToKernel, subAgentResultToKernel, workflowSpecToKernel, workflowNodeSpecToKernel, submitWorkflowNodesToKernel, submitWorkflowNodesTool, fanoutSynthesize, generateAndFilter, verifyRules, } from "./types/agent.js";
|
|
45
46
|
export { ContractBuilder, formatContractForSystemPrompt, contractToCriteriaStrings, } from "./collaboration/contract.js";
|
|
46
47
|
export { AgentPool } from "./collaboration/pool.js";
|
|
47
48
|
export { KERNEL_ROLE_MAP } from "./collaboration/pool.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 MemoryPolicy, type ResourceQuota } from "../kernel.js";
|
|
9
9
|
import type { AgentRunSpec, MilestoneCheckResult, MilestoneContract, MilestonePolicy, 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 GovernancePolicy } from "../governance.js";
|
|
12
13
|
import { type NativeOsProfile, type OsProfileId } from "./os-profile.js";
|
|
13
14
|
import { LargeResultSpool } from "./large-result-spool.js";
|
|
@@ -97,6 +98,9 @@ export interface RuntimeOptions {
|
|
|
97
98
|
evalProvider: LLMProvider;
|
|
98
99
|
maxAttempts?: number;
|
|
99
100
|
};
|
|
101
|
+
/** G2: custom reducers for `NodeKind::Reduce` workflow nodes, merged over the built-ins
|
|
102
|
+
* (`concat` / `dedupe_lines` / `merge_json_arrays` / `count`). A reduce node runs no LLM. */
|
|
103
|
+
reducers?: ReducerRegistry;
|
|
100
104
|
/** Optional system prompt injected into the dream synthesis call. */
|
|
101
105
|
dreamSystemPrompt?: string;
|
|
102
106
|
/** Custom LLM provider used for background memory consolidation (dream loop). */
|
|
@@ -159,6 +163,22 @@ export declare class RuntimeRunner {
|
|
|
159
163
|
* Requires an active parent run (`run()` / `wake()` in progress or paused at milestone).
|
|
160
164
|
*/
|
|
161
165
|
spawnSubAgent(spec: AgentRunSpec): AsyncIterable<StreamEvent>;
|
|
166
|
+
/**
|
|
167
|
+
* G3: run one workflow node, enforcing its `output_schema` (if any). Without a schema this is a
|
|
168
|
+
* plain `orchestrator.run`. With one, the node's agent is instructed to emit conforming JSON, its
|
|
169
|
+
* output is validated (the supported JSON-Schema subset), and on mismatch the node is re-run once
|
|
170
|
+
* with the validation errors fed back. If it still does not conform, the node is failed with the
|
|
171
|
+
* validation reason — a node that cannot meet its declared output contract starves its dependents,
|
|
172
|
+
* exactly as a denied spawn does.
|
|
173
|
+
*/
|
|
174
|
+
private runWorkflowNode;
|
|
175
|
+
/**
|
|
176
|
+
* G2: execute a deterministic reduce node. Looks up the named reducer (built-ins overlaid with
|
|
177
|
+
* `opts.reducers`), runs it over the node's dependency outputs (gathered from `outputs`), and
|
|
178
|
+
* returns a synthetic completion carrying the reducer's output — no LLM, zero tokens. An unknown
|
|
179
|
+
* reducer or a thrown reducer fails the node (`Error` termination → the kernel starves dependents).
|
|
180
|
+
*/
|
|
181
|
+
private runReduceNode;
|
|
162
182
|
/**
|
|
163
183
|
* W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
|
|
164
184
|
* through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
|
|
@@ -167,6 +187,7 @@ export declare class RuntimeRunner {
|
|
|
167
187
|
*/
|
|
168
188
|
runWorkflow(spec: WorkflowSpec, opts?: {
|
|
169
189
|
resumedCompleted?: string[];
|
|
190
|
+
resumedSubmissions?: Record<string, unknown>[][];
|
|
170
191
|
}): Promise<{
|
|
171
192
|
completed: string[];
|
|
172
193
|
failed: string[];
|
package/dist/runtime/runner.js
CHANGED
|
@@ -3,11 +3,13 @@ import { resolvePermissionRequest } from "./execution-plane.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 { KernelPrimitivesDashboard } from "./kernel-primitives-dashboard.js";
|
|
8
8
|
import { capabilityMarker, capabilitySkill, capabilityTool, capabilityCommandMount, capabilityCommandUnmount, kernelAction, kernelApply, kernelMaybeAction, forceCompact, messageToKernelMessage, skillMetadataToKernel, taskUpdateToKernel, toolResultToKernel, toolSchemaToKernel, } from "./kernel-step.js";
|
|
9
|
-
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
9
|
+
import { agentRunSpecToKernel, findSpawnProcessObservation, milestoneCheckPass, milestoneCheckResultToKernel, spawnObservationToManifest, subAgentResultToKernel, submitWorkflowNodesToKernel, workflowBudgetNote, workflowNodeToManifest, workflowNodeToSpec, workflowSpecToKernel, } from "../types/agent.js";
|
|
10
10
|
import { defaultSubAgentOrchestrator } from "./sub-agent-orchestrator.js";
|
|
11
|
+
import { extractJsonValue, schemaInstruction, schemaRetryInstruction, validateAgainstSchema, } from "./output-schema.js";
|
|
12
|
+
import { resolveReducer } from "./reducers.js";
|
|
11
13
|
import { governancePolicyToKernelEvent } from "../governance.js";
|
|
12
14
|
import { kernelObservationToSessionEvent, withCategory } from "./kernel-event-log.js";
|
|
13
15
|
import { assertNativeProfile } from "./os-profile.js";
|
|
@@ -256,6 +258,86 @@ export class RuntimeRunner {
|
|
|
256
258
|
});
|
|
257
259
|
yield { type: "done", iterations: result.result.turnsUsed, totalTokens: result.result.totalTokensUsed, status: result.result.termination };
|
|
258
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* G3: run one workflow node, enforcing its `output_schema` (if any). Without a schema this is a
|
|
263
|
+
* plain `orchestrator.run`. With one, the node's agent is instructed to emit conforming JSON, its
|
|
264
|
+
* output is validated (the supported JSON-Schema subset), and on mismatch the node is re-run once
|
|
265
|
+
* with the validation errors fed back. If it still does not conform, the node is failed with the
|
|
266
|
+
* validation reason — a node that cannot meet its declared output contract starves its dependents,
|
|
267
|
+
* exactly as a denied spawn does.
|
|
268
|
+
*/
|
|
269
|
+
async runWorkflowNode(node, parentSessionId, orchestrator, budget, outputs) {
|
|
270
|
+
// G2: a reduce node runs no LLM — execute the registered pure function over its dependency
|
|
271
|
+
// outputs and feed the result back as an ordinary completion. Deterministic; no agent burned.
|
|
272
|
+
if (node.reducer) {
|
|
273
|
+
return this.runReduceNode(node, outputs ?? new Map());
|
|
274
|
+
}
|
|
275
|
+
const baseSpec = workflowNodeToSpec(node, parentSessionId);
|
|
276
|
+
const manifest = workflowNodeToManifest(node, parentSessionId);
|
|
277
|
+
// G4: surface the workflow's remaining budget to the node's agent so a coordinator can size its
|
|
278
|
+
// `submit_workflow_nodes` batch to what is available (empty string ⇒ unbounded, no note).
|
|
279
|
+
const budgetNote = workflowBudgetNote(budget);
|
|
280
|
+
const withBudget = (goal) => (budgetNote ? `${goal}\n\n${budgetNote}` : goal);
|
|
281
|
+
const mkCtx = (goal) => ({
|
|
282
|
+
parentOpts: this.opts,
|
|
283
|
+
parentSessionId,
|
|
284
|
+
spec: { ...baseSpec, goal: withBudget(goal) },
|
|
285
|
+
manifest,
|
|
286
|
+
sessionLog: this.opts.sessionLog,
|
|
287
|
+
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
288
|
+
});
|
|
289
|
+
const schema = node.output_schema;
|
|
290
|
+
if (!schema)
|
|
291
|
+
return orchestrator.run(mkCtx(baseSpec.goal));
|
|
292
|
+
const MAX_ATTEMPTS = 2;
|
|
293
|
+
let last;
|
|
294
|
+
let lastErrors = [];
|
|
295
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
296
|
+
const goal = attempt === 1
|
|
297
|
+
? `${baseSpec.goal}\n\n${schemaInstruction(schema)}`
|
|
298
|
+
: `${baseSpec.goal}\n\n${schemaRetryInstruction(schema, lastErrors)}`;
|
|
299
|
+
const result = await orchestrator.run(mkCtx(goal));
|
|
300
|
+
const content = result.result.finalMessage?.content;
|
|
301
|
+
const text = typeof content === "string" ? content : content != null ? JSON.stringify(content) : "";
|
|
302
|
+
const v = validateAgainstSchema(extractJsonValue(text), schema);
|
|
303
|
+
if (v.ok)
|
|
304
|
+
return result;
|
|
305
|
+
last = result;
|
|
306
|
+
lastErrors = v.errors;
|
|
307
|
+
}
|
|
308
|
+
const reason = `output_schema validation failed after ${MAX_ATTEMPTS} attempts: ${lastErrors.join("; ")}`;
|
|
309
|
+
const fallback = last;
|
|
310
|
+
return {
|
|
311
|
+
...fallback,
|
|
312
|
+
result: {
|
|
313
|
+
...fallback.result,
|
|
314
|
+
termination: "error",
|
|
315
|
+
finalMessage: { role: "assistant", content: reason, toolCalls: [] },
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* G2: execute a deterministic reduce node. Looks up the named reducer (built-ins overlaid with
|
|
321
|
+
* `opts.reducers`), runs it over the node's dependency outputs (gathered from `outputs`), and
|
|
322
|
+
* returns a synthetic completion carrying the reducer's output — no LLM, zero tokens. An unknown
|
|
323
|
+
* reducer or a thrown reducer fails the node (`Error` termination → the kernel starves dependents).
|
|
324
|
+
*/
|
|
325
|
+
runReduceNode(node, outputs) {
|
|
326
|
+
const ok = (content, termination) => ({
|
|
327
|
+
agentId: node.agent_id,
|
|
328
|
+
result: { termination, finalMessage: { role: "assistant", content, toolCalls: [] }, turnsUsed: 0, totalTokensUsed: 0 },
|
|
329
|
+
});
|
|
330
|
+
const reducer = resolveReducer(node.reducer, this.opts.reducers);
|
|
331
|
+
if (!reducer)
|
|
332
|
+
return ok(`unknown reducer "${node.reducer}"`, "error");
|
|
333
|
+
const inputs = (node.input_agent_ids ?? []).map(agentId => ({ agentId, output: outputs.get(agentId) ?? "" }));
|
|
334
|
+
try {
|
|
335
|
+
return ok(reducer(inputs), "completed");
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
return ok(`reducer "${node.reducer}" threw: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
259
341
|
/**
|
|
260
342
|
* W0-ABI: run a declarative workflow DAG. The kernel owns the DAG and gates every node spawn
|
|
261
343
|
* through the syscall trap; this driver runs each kernel-emitted batch of nodes in parallel,
|
|
@@ -275,26 +357,30 @@ export class RuntimeRunner {
|
|
|
275
357
|
parent_session_id: parentSessionId,
|
|
276
358
|
// W0-ABI resume: skip nodes already completed before an interruption.
|
|
277
359
|
...(opts?.resumedCompleted?.length ? { resumed_completed: opts.resumedCompleted } : {}),
|
|
360
|
+
// R3-1: re-apply recorded runtime submissions so dynamically-appended nodes are reconstructed.
|
|
361
|
+
...(opts?.resumedSubmissions?.length ? { resumed_submissions: opts.resumedSubmissions } : {}),
|
|
278
362
|
});
|
|
279
363
|
const collectNodes = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")
|
|
280
364
|
?.nodes ?? [];
|
|
365
|
+
// G4: the batch observation also carries the workflow's remaining budget; track the latest so a
|
|
366
|
+
// coordinator node's prompt reflects current headroom when it decides how much to submit.
|
|
367
|
+
const collectBudget = (obs) => obs.find(o => o.kind === "workflow_batch_spawned")?.budget;
|
|
281
368
|
const findDone = (obs) => obs.find(o => o.kind === "workflow_completed");
|
|
282
369
|
let done = findDone(observations);
|
|
283
370
|
if (done)
|
|
284
371
|
return { completed: done.completed ?? [], failed: done.failed ?? [] };
|
|
285
372
|
let nodes = collectNodes(observations);
|
|
373
|
+
let budget = collectBudget(observations);
|
|
374
|
+
// G2: each completed node's output, keyed by agent id — a reduce node reads its dependencies'
|
|
375
|
+
// outputs from here. Deps always complete in an earlier round than the reduce node that needs
|
|
376
|
+
// them (the kernel keeps the reduce node un-ready until its deps finish), so this is populated.
|
|
377
|
+
const outputs = new Map();
|
|
286
378
|
for (;;) {
|
|
287
379
|
if (nodes.length === 0)
|
|
288
380
|
return { completed: [], failed: [] }; // nothing to run (e.g. all gated)
|
|
289
381
|
// Run the currently-runnable nodes in parallel — each is independent within a round.
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
parentSessionId,
|
|
293
|
-
spec: workflowNodeToSpec(node, parentSessionId),
|
|
294
|
-
manifest: workflowNodeToManifest(node, parentSessionId),
|
|
295
|
-
sessionLog: this.opts.sessionLog,
|
|
296
|
-
...(this.opts.subAgentHarness ? { harness: this.opts.subAgentHarness } : {}),
|
|
297
|
-
})));
|
|
382
|
+
const roundBudget = budget;
|
|
383
|
+
const results = await Promise.all(nodes.map(node => this.runWorkflowNode(node, parentSessionId, orchestrator, roundBudget, outputs)));
|
|
298
384
|
// Feed completions back one at a time. The kernel's run-queue executor may spawn a node's
|
|
299
385
|
// dependents the moment *that* node completes (per-node unblock), so each feed can emit its
|
|
300
386
|
// own `workflow_batch_spawned`; ACCUMULATE them across the round rather than keeping only the
|
|
@@ -304,11 +390,32 @@ export class RuntimeRunner {
|
|
|
304
390
|
const nextNodes = [];
|
|
305
391
|
done = undefined;
|
|
306
392
|
for (const result of results) {
|
|
393
|
+
// G2: record this node's output so a downstream reduce node can consume it.
|
|
394
|
+
const outContent = result.result.finalMessage?.content;
|
|
395
|
+
outputs.set(result.agentId, typeof outContent === "string" ? outContent : outContent != null ? JSON.stringify(outContent) : "");
|
|
396
|
+
// R3-1: if this node's agent submitted more nodes, append them to the parent DAG BEFORE
|
|
397
|
+
// reporting the node's completion — the workflow is still active (the kernel hasn't seen this
|
|
398
|
+
// node finish), so even a submission from the last running node keeps the DAG alive. The
|
|
399
|
+
// appended nodes' `workflow_batch_spawned` is collected into this round like any other.
|
|
400
|
+
if (result.submittedNodes?.length) {
|
|
401
|
+
// G1: stamp the submitting node's agent id so the kernel can coerce a quarantined
|
|
402
|
+
// submitter's nodes to quarantined (no topological privilege escalation).
|
|
403
|
+
const submitEvent = submitWorkflowNodesToKernel(result.submittedNodes, result.agentId);
|
|
404
|
+
const subObs = kernelApply(runtime, this.pendingObservations, submitEvent);
|
|
405
|
+
nextNodes.push(...collectNodes(subObs));
|
|
406
|
+
budget = collectBudget(subObs) ?? budget;
|
|
407
|
+
// R3-1: persist the submission (kernel-shape nodes) so resume can re-apply it.
|
|
408
|
+
await this.opts.sessionLog.append(parentSessionId, buildWorkflowNodesSubmittedEvent({
|
|
409
|
+
turn: runtime.turn(),
|
|
410
|
+
nodes: submitEvent.nodes ?? [],
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
307
413
|
const obs = kernelApply(runtime, this.pendingObservations, {
|
|
308
414
|
kind: "sub_agent_completed",
|
|
309
415
|
result: subAgentResultToKernel(result),
|
|
310
416
|
});
|
|
311
417
|
nextNodes.push(...collectNodes(obs));
|
|
418
|
+
budget = collectBudget(obs) ?? budget;
|
|
312
419
|
const d = findDone(obs);
|
|
313
420
|
if (d)
|
|
314
421
|
done = d;
|
|
@@ -336,7 +443,8 @@ export class RuntimeRunner {
|
|
|
336
443
|
}
|
|
337
444
|
const events = await this.opts.sessionLog.read(this.currentSessionId);
|
|
338
445
|
const resumedCompleted = recoverCompletedWorkflowNodes(events);
|
|
339
|
-
|
|
446
|
+
const resumedSubmissions = recoverSubmittedWorkflowNodes(events);
|
|
447
|
+
return this.runWorkflow(spec, { resumedCompleted, resumedSubmissions });
|
|
340
448
|
}
|
|
341
449
|
interrupt() { this.interrupted = true; }
|
|
342
450
|
async *run(req) {
|
|
@@ -827,8 +935,9 @@ export class RuntimeRunner {
|
|
|
827
935
|
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
828
936
|
};
|
|
829
937
|
const toolResults = [];
|
|
830
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan");
|
|
938
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes");
|
|
831
939
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
940
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes");
|
|
832
941
|
for (const call of planCalls) {
|
|
833
942
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
834
943
|
kernelApply(runtime, this.pendingObservations, {
|
|
@@ -839,6 +948,18 @@ export class RuntimeRunner {
|
|
|
839
948
|
toolResults.push(result);
|
|
840
949
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
841
950
|
}
|
|
951
|
+
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
952
|
+
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
953
|
+
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
954
|
+
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
955
|
+
// simply unconsumed — a no-op.)
|
|
956
|
+
for (const call of submitCalls) {
|
|
957
|
+
const nodes = parseSubmitWorkflowNodesArgs(call.arguments);
|
|
958
|
+
yield { type: "workflow_nodes_submitted", nodes };
|
|
959
|
+
const result = { callId: call.id, output: "submitted", isError: false };
|
|
960
|
+
toolResults.push(result);
|
|
961
|
+
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
962
|
+
}
|
|
842
963
|
if (normalCalls.length > 0) {
|
|
843
964
|
for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
|
|
844
965
|
yield evt;
|
|
@@ -1317,3 +1438,16 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
1317
1438
|
: parsed.blocked_on,
|
|
1318
1439
|
};
|
|
1319
1440
|
}
|
|
1441
|
+
/** R3-1: parse the `submit_workflow_nodes` tool arguments (`{ nodes: WorkflowNodeSpec[] }`). Node
|
|
1442
|
+
* shapes are trusted structurally here; the kernel validates them (dep range, quarantine, quota) on
|
|
1443
|
+
* append. A malformed payload yields no nodes rather than throwing. */
|
|
1444
|
+
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
1445
|
+
let parsed = {};
|
|
1446
|
+
try {
|
|
1447
|
+
parsed = JSON.parse(argsStr);
|
|
1448
|
+
}
|
|
1449
|
+
catch {
|
|
1450
|
+
// Ignore parse error → no nodes submitted.
|
|
1451
|
+
}
|
|
1452
|
+
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
1453
|
+
}
|
|
@@ -243,6 +243,13 @@ export type SessionEvent = {
|
|
|
243
243
|
primitive?: KernelPrimitive;
|
|
244
244
|
agent_id: string;
|
|
245
245
|
termination: string;
|
|
246
|
+
} | {
|
|
247
|
+
kind: "workflow_nodes_submitted";
|
|
248
|
+
turn: number;
|
|
249
|
+
category?: KernelEventCategory;
|
|
250
|
+
primitive?: KernelPrimitive;
|
|
251
|
+
/** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
|
|
252
|
+
nodes: Record<string, unknown>[];
|
|
246
253
|
} | {
|
|
247
254
|
kind: "workflow_batch_spawned";
|
|
248
255
|
turn: number;
|
|
@@ -58,3 +58,17 @@ export declare function recoverCompletedWorkflowNodes(events: Array<{
|
|
|
58
58
|
seq: number;
|
|
59
59
|
event: SessionEvent;
|
|
60
60
|
}>): string[];
|
|
61
|
+
/** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
|
|
62
|
+
* re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
|
|
63
|
+
export declare function buildWorkflowNodesSubmittedEvent(input: {
|
|
64
|
+
turn: number;
|
|
65
|
+
nodes: Record<string, unknown>[];
|
|
66
|
+
}): Extract<SessionEvent, {
|
|
67
|
+
kind: "workflow_nodes_submitted";
|
|
68
|
+
}>;
|
|
69
|
+
/** R3-1: recover the runtime submission batches (in order) from a session event stream, to rebuild
|
|
70
|
+
* `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed. */
|
|
71
|
+
export declare function recoverSubmittedWorkflowNodes(events: Array<{
|
|
72
|
+
seq: number;
|
|
73
|
+
event: SessionEvent;
|
|
74
|
+
}>): Record<string, unknown>[][];
|
|
@@ -76,3 +76,18 @@ export function recoverCompletedWorkflowNodes(events) {
|
|
|
76
76
|
}
|
|
77
77
|
return completed;
|
|
78
78
|
}
|
|
79
|
+
/** R3-1: build workflow_nodes_submitted for persistence after a runtime submission, so resume can
|
|
80
|
+
* re-apply it. `nodes` is the kernel-shape (snake_case) submitted node array. */
|
|
81
|
+
export function buildWorkflowNodesSubmittedEvent(input) {
|
|
82
|
+
return { kind: "workflow_nodes_submitted", turn: input.turn, nodes: input.nodes };
|
|
83
|
+
}
|
|
84
|
+
/** R3-1: recover the runtime submission batches (in order) from a session event stream, to rebuild
|
|
85
|
+
* `resumed_submissions` for resumeWorkflow so dynamically-appended nodes are reconstructed. */
|
|
86
|
+
export function recoverSubmittedWorkflowNodes(events) {
|
|
87
|
+
const submissions = [];
|
|
88
|
+
for (const { event } of events) {
|
|
89
|
+
if (event.kind === "workflow_nodes_submitted")
|
|
90
|
+
submissions.push(event.nodes);
|
|
91
|
+
}
|
|
92
|
+
return submissions;
|
|
93
|
+
}
|
|
@@ -74,15 +74,24 @@ export class SubAgentOrchestrator {
|
|
|
74
74
|
totalTokensUsed: outcome.totalTokens,
|
|
75
75
|
...(outcome.result ? { finalMessage: { role: "assistant", content: outcome.result, toolCalls: [] } } : {}),
|
|
76
76
|
},
|
|
77
|
+
// R3-1: surface nodes the agent submitted under the harness so `runWorkflow` appends them.
|
|
78
|
+
...(outcome.submittedNodes?.length ? { submittedNodes: outcome.submittedNodes } : {}),
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
let done;
|
|
80
82
|
let finalText = "";
|
|
83
|
+
// R3-1: collect any nodes this node's agent submitted via the `submit_workflow_nodes` tool (the
|
|
84
|
+
// runner surfaces them as `workflow_nodes_submitted` because the workflow lives in the parent
|
|
85
|
+
// kernel, not this child's). `runWorkflow` sends them to the parent kernel.
|
|
86
|
+
const submittedNodes = [];
|
|
81
87
|
for await (const evt of this.stream(ctx)) {
|
|
82
88
|
if (evt.type === "text_delta")
|
|
83
89
|
finalText += evt.delta;
|
|
84
90
|
if (evt.type === "done")
|
|
85
91
|
done = evt;
|
|
92
|
+
if (evt.type === "workflow_nodes_submitted") {
|
|
93
|
+
submittedNodes.push(...evt.nodes);
|
|
94
|
+
}
|
|
86
95
|
}
|
|
87
96
|
const loopResult = {
|
|
88
97
|
termination: terminationFromStatus(done?.status ?? "error"),
|
|
@@ -90,7 +99,11 @@ export class SubAgentOrchestrator {
|
|
|
90
99
|
totalTokensUsed: done?.totalTokens ?? 0,
|
|
91
100
|
...(finalText ? { finalMessage: { role: "assistant", content: finalText, toolCalls: [] } } : {}),
|
|
92
101
|
};
|
|
93
|
-
return {
|
|
102
|
+
return {
|
|
103
|
+
agentId: ctx.spec.identity.agentId,
|
|
104
|
+
result: loopResult,
|
|
105
|
+
...(submittedNodes.length ? { submittedNodes } : {}),
|
|
106
|
+
};
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
export const defaultSubAgentOrchestrator = new SubAgentOrchestrator();
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -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,11 @@ 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 (via the
|
|
56
|
+
* `submit_workflow_nodes` tool). Surfaced by the orchestrator; the `runWorkflow` driver sends
|
|
57
|
+
* them to the parent kernel before this node's completion. SDK-internal — not sent over the wire
|
|
58
|
+
* on the kernel `SubAgentResult` (see `subAgentResultToKernel`). */
|
|
59
|
+
submittedNodes?: WorkflowNodeSpec[];
|
|
55
60
|
}
|
|
56
61
|
export interface MilestoneCheckResult {
|
|
57
62
|
phaseId: string;
|
|
@@ -92,6 +97,12 @@ export interface WorkflowNodeSpec {
|
|
|
92
97
|
modelHint?: string;
|
|
93
98
|
/** W3: `quarantined` nodes read untrusted content and must run without privileges. */
|
|
94
99
|
trust?: NodeTrust;
|
|
100
|
+
/** G3: JSON Schema the node's output must conform to. The kernel carries it to the spawn
|
|
101
|
+
* descriptor; the runner instructs the agent and validates + retries once on mismatch. */
|
|
102
|
+
outputSchema?: Record<string, unknown>;
|
|
103
|
+
/** G2: make this a deterministic *reduce* node — it runs no LLM agent. The runner routes it to the
|
|
104
|
+
* registered reducer of this name, over its `dependsOn` nodes' outputs (dedupe / filter / merge). */
|
|
105
|
+
reducer?: string;
|
|
95
106
|
/** Indices of nodes this node depends on. */
|
|
96
107
|
dependsOn?: number[];
|
|
97
108
|
}
|
|
@@ -109,9 +120,40 @@ export interface WorkflowSpawnInfo {
|
|
|
109
120
|
model_hint?: string;
|
|
110
121
|
/** W3 trust level: `"trusted"` | `"quarantined"`. */
|
|
111
122
|
trust?: string;
|
|
123
|
+
/** G3: JSON Schema the node's output must conform to (carried verbatim from the spec). */
|
|
124
|
+
output_schema?: Record<string, unknown>;
|
|
125
|
+
/** G2: for a reduce node, the name of the registered host function to run (no LLM). */
|
|
126
|
+
reducer?: string;
|
|
127
|
+
/** G2: the dependency agent ids whose outputs a reduce node consumes. */
|
|
128
|
+
input_agent_ids?: string[];
|
|
112
129
|
}
|
|
130
|
+
/** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
|
|
131
|
+
* `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
|
|
132
|
+
export interface WorkflowBudget {
|
|
133
|
+
nodes_used: number;
|
|
134
|
+
nodes_max?: number;
|
|
135
|
+
nodes_remaining?: number;
|
|
136
|
+
running_subagents: number;
|
|
137
|
+
max_concurrent_subagents?: number;
|
|
138
|
+
concurrency_remaining?: number;
|
|
139
|
+
}
|
|
140
|
+
/** G4: a concise, human-readable budget note appended to a coordinator node's goal, so its agent can
|
|
141
|
+
* size a `submit_workflow_nodes` batch to what is actually available. Returns "" when nothing is
|
|
142
|
+
* bounded (no quota ⇒ no signal). */
|
|
143
|
+
export declare function workflowBudgetNote(budget: WorkflowBudget | undefined): string;
|
|
144
|
+
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` (the
|
|
145
|
+
* whole spec) and `submit_workflow_nodes` (R3-1 runtime append) so the two encodings never drift. */
|
|
146
|
+
export declare function workflowNodeSpecToKernel(n: WorkflowNodeSpec): Record<string, unknown>;
|
|
113
147
|
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
114
148
|
export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string, unknown>;
|
|
149
|
+
/** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
|
|
150
|
+
* `submitterAgentId` (the node that requested the append) so the kernel can enforce no-privilege-
|
|
151
|
+
* escalation — a quarantined submitter's nodes are coerced to quarantined. Omitted ⇒ no coercion. */
|
|
152
|
+
export declare function submitWorkflowNodesToKernel(nodes: WorkflowNodeSpec[], submitterAgentId?: string): Record<string, unknown>;
|
|
153
|
+
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
|
|
154
|
+
* (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
|
|
155
|
+
* the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
|
|
156
|
+
export declare const submitWorkflowNodesTool: ToolSchema;
|
|
115
157
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
116
158
|
export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
|
|
117
159
|
/** Build the host manifest for a kernel-generated workflow node. */
|
package/dist/types/agent.js
CHANGED
|
@@ -94,29 +94,116 @@ export function milestoneCheckPass(phaseId) {
|
|
|
94
94
|
export function milestoneCheckFail(phaseId, reason) {
|
|
95
95
|
return { phaseId, passed: false, reason };
|
|
96
96
|
}
|
|
97
|
+
/** G4: a concise, human-readable budget note appended to a coordinator node's goal, so its agent can
|
|
98
|
+
* size a `submit_workflow_nodes` batch to what is actually available. Returns "" when nothing is
|
|
99
|
+
* bounded (no quota ⇒ no signal). */
|
|
100
|
+
export function workflowBudgetNote(budget) {
|
|
101
|
+
if (!budget)
|
|
102
|
+
return "";
|
|
103
|
+
const parts = [];
|
|
104
|
+
if (budget.nodes_remaining != null && budget.nodes_max != null) {
|
|
105
|
+
parts.push(`nodes ${budget.nodes_used}/${budget.nodes_max} used, ${budget.nodes_remaining} remaining`);
|
|
106
|
+
}
|
|
107
|
+
if (budget.concurrency_remaining != null && budget.max_concurrent_subagents != null) {
|
|
108
|
+
parts.push(`concurrency ${budget.running_subagents}/${budget.max_concurrent_subagents} running, ${budget.concurrency_remaining} free`);
|
|
109
|
+
}
|
|
110
|
+
if (parts.length === 0)
|
|
111
|
+
return "";
|
|
112
|
+
return (`[workflow budget] ${parts.join(" · ")}. ` +
|
|
113
|
+
"If you submit more workflow nodes, keep the batch within the remaining node budget.");
|
|
114
|
+
}
|
|
115
|
+
/** Map one host `WorkflowNodeSpec` to its snake_case kernel JSON. Shared by `load_workflow` (the
|
|
116
|
+
* whole spec) and `submit_workflow_nodes` (R3-1 runtime append) so the two encodings never drift. */
|
|
117
|
+
export function workflowNodeSpecToKernel(n) {
|
|
118
|
+
const task = typeof n.task === "string" ? { goal: n.task } : n.task;
|
|
119
|
+
return {
|
|
120
|
+
task: {
|
|
121
|
+
goal: task.goal,
|
|
122
|
+
// `criteria` is required by the kernel's RuntimeTask serde (no default).
|
|
123
|
+
criteria: task.criteria ?? [],
|
|
124
|
+
...(task.lane ? { lane: task.lane } : {}),
|
|
125
|
+
},
|
|
126
|
+
role: n.role,
|
|
127
|
+
// role/isolation/context_inheritance have no serde default in the kernel — always emit.
|
|
128
|
+
isolation: n.isolation ?? "shared",
|
|
129
|
+
context_inheritance: n.contextInheritance ?? "none",
|
|
130
|
+
...(n.modelHint ? { model_hint: n.modelHint } : {}),
|
|
131
|
+
...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
|
|
132
|
+
...(n.outputSchema ? { output_schema: n.outputSchema } : {}),
|
|
133
|
+
// G2: a reducer name lowers to the kernel's `NodeKind::Reduce` (serde-tagged by `type`).
|
|
134
|
+
...(n.reducer ? { kind: { type: "reduce", reducer: n.reducer } } : {}),
|
|
135
|
+
...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
97
138
|
/** Map a host `WorkflowSpec` to the snake_case kernel JSON (`load_workflow.spec`). */
|
|
98
139
|
export function workflowSpecToKernel(spec) {
|
|
140
|
+
return { nodes: spec.nodes.map(workflowNodeSpecToKernel) };
|
|
141
|
+
}
|
|
142
|
+
/** R3-1: map a batch of host nodes to the `submit_workflow_nodes` kernel event body. G1: pass
|
|
143
|
+
* `submitterAgentId` (the node that requested the append) so the kernel can enforce no-privilege-
|
|
144
|
+
* escalation — a quarantined submitter's nodes are coerced to quarantined. Omitted ⇒ no coercion. */
|
|
145
|
+
export function submitWorkflowNodesToKernel(nodes, submitterAgentId) {
|
|
99
146
|
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
|
-
// role/isolation/context_inheritance have no serde default in the kernel — always emit.
|
|
111
|
-
isolation: n.isolation ?? "shared",
|
|
112
|
-
context_inheritance: n.contextInheritance ?? "none",
|
|
113
|
-
...(n.modelHint ? { model_hint: n.modelHint } : {}),
|
|
114
|
-
...(n.trust && n.trust !== "trusted" ? { trust: n.trust } : {}),
|
|
115
|
-
...(n.dependsOn && n.dependsOn.length ? { depends_on: n.dependsOn } : {}),
|
|
116
|
-
};
|
|
117
|
-
}),
|
|
147
|
+
kind: "submit_workflow_nodes",
|
|
148
|
+
nodes: nodes.map(workflowNodeSpecToKernel),
|
|
149
|
+
...(submitterAgentId ? { submitter_agent_id: submitterAgentId } : {}),
|
|
118
150
|
};
|
|
119
151
|
}
|
|
152
|
+
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
|
|
153
|
+
* (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
|
|
154
|
+
* the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
|
|
155
|
+
export const submitWorkflowNodesTool = {
|
|
156
|
+
name: "submit_workflow_nodes",
|
|
157
|
+
description: "Append new nodes to the running workflow DAG (dynamic fan-out / loop-until-done). Each node " +
|
|
158
|
+
"spawns as a gated sub-agent. Use when you discover more work that should run as its own node.",
|
|
159
|
+
parameters: JSON.stringify({
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
nodes: {
|
|
163
|
+
type: "array",
|
|
164
|
+
description: "Workflow nodes to append; each runs as a gated sub-agent.",
|
|
165
|
+
items: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
task: {
|
|
169
|
+
description: "The node's goal: a string, or an object { goal, criteria?, lane? }.",
|
|
170
|
+
oneOf: [
|
|
171
|
+
{ type: "string" },
|
|
172
|
+
{
|
|
173
|
+
type: "object",
|
|
174
|
+
properties: {
|
|
175
|
+
goal: { type: "string" },
|
|
176
|
+
criteria: { type: "array", items: { type: "string" } },
|
|
177
|
+
},
|
|
178
|
+
required: ["goal"],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
role: { type: "string", enum: ["explore", "plan", "implement", "verify", "custom"] },
|
|
183
|
+
isolation: { type: "string", enum: ["shared", "read_only", "worktree", "remote"] },
|
|
184
|
+
contextInheritance: { type: "string", enum: ["none", "system_only", "full"] },
|
|
185
|
+
trust: { type: "string", enum: ["trusted", "quarantined"] },
|
|
186
|
+
outputSchema: {
|
|
187
|
+
type: "object",
|
|
188
|
+
description: "Optional JSON Schema the node's output must conform to (validated + retried SDK-side).",
|
|
189
|
+
},
|
|
190
|
+
reducer: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Make this a deterministic reduce node (no LLM); names a registered reducer.",
|
|
193
|
+
},
|
|
194
|
+
dependsOn: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "integer" },
|
|
197
|
+
description: "Batch-relative, backward-only dependency indices within this submission.",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: ["task", "role"],
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
required: ["nodes"],
|
|
205
|
+
}),
|
|
206
|
+
};
|
|
120
207
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
121
208
|
export function workflowNodeToSpec(node, parentSessionId) {
|
|
122
209
|
return {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkflowNodeSpec } from "./types/agent.js";
|
|
1
2
|
export interface TextPart {
|
|
2
3
|
type: "text";
|
|
3
4
|
text: string;
|
|
@@ -116,6 +117,13 @@ export interface ToolResultEvent extends StreamEvent {
|
|
|
116
117
|
isFatal?: boolean;
|
|
117
118
|
errorKind?: ToolErrorKind;
|
|
118
119
|
}
|
|
120
|
+
/** R3-1: a workflow node's agent called the `submit_workflow_nodes` tool. The runner intercepts it
|
|
121
|
+
* (it cannot apply to the child's own kernel — the workflow lives in the parent) and surfaces the
|
|
122
|
+
* requested nodes as this event; the `runWorkflow` driver sends them to the parent kernel. */
|
|
123
|
+
export interface WorkflowNodesSubmittedEvent extends StreamEvent {
|
|
124
|
+
type: "workflow_nodes_submitted";
|
|
125
|
+
nodes: WorkflowNodeSpec[];
|
|
126
|
+
}
|
|
119
127
|
export interface DoneEvent extends StreamEvent {
|
|
120
128
|
type: "done";
|
|
121
129
|
iterations: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepstrike/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "DeepStrike Node.js SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@anthropic-ai/sdk": "^0.99.0",
|
|
23
|
-
"@deepstrike/core": "0.2.
|
|
23
|
+
"@deepstrike/core": "0.2.11",
|
|
24
24
|
"@google/generative-ai": "^0.24.1",
|
|
25
25
|
"openai": "^5.23.2"
|
|
26
26
|
},
|