@deepstrike/sdk 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +7 -5
- package/dist/providers/openai.d.ts +2 -2
- package/dist/providers/openai.js +3 -2
- package/dist/runtime/execution-plane.d.ts +5 -0
- package/dist/runtime/execution-plane.js +3 -1
- package/dist/runtime/kernel-step.js +8 -1
- package/dist/runtime/process-sandbox-plane.js +14 -8
- package/dist/runtime/runner.d.ts +69 -0
- package/dist/runtime/runner.js +261 -35
- package/dist/runtime/sub-agent-orchestrator.d.ts +11 -0
- package/dist/runtime/sub-agent-orchestrator.js +78 -13
- package/dist/runtime/workflow-control-flow.d.ts +17 -0
- package/dist/runtime/workflow-control-flow.js +78 -0
- package/dist/runtime/workflow-store.d.ts +15 -0
- package/dist/runtime/workflow-store.js +47 -0
- package/dist/runtime/worktree-plane.d.ts +43 -0
- package/dist/runtime/worktree-plane.js +81 -0
- package/dist/tools/index.d.ts +9 -3
- package/dist/tools/index.js +2 -2
- package/dist/types/agent.d.ts +63 -0
- package/dist/types/agent.js +184 -44
- package/dist/types.d.ts +6 -1
- package/package.json +2 -2
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//! A#2: SDK-side execution of the kernel's control-flow workflow node kinds (Loop / Classify /
|
|
2
|
+
//! Tournament). The kernel owns the scheduling — it re-arms loops, prunes classify branches, and
|
|
3
|
+
//! runs the tournament bracket — and tells the SDK *which* kind a spawn is via the spawn descriptor
|
|
4
|
+
//! (`loop_max_iters` / `classify_labels` / `judge_match`). This module is the SDK half of the "one
|
|
5
|
+
//! agent per node + one additive result field" contract: it builds the prompt that solicits the
|
|
6
|
+
//! decision from the node's agent and extracts the matching result signal (`loopContinue` /
|
|
7
|
+
//! `classifyBranch` / `tournamentWinner`) the kernel reads back.
|
|
8
|
+
import { extractJsonValue } from "./output-schema.js";
|
|
9
|
+
/** Instruction appended to a loop node's goal: do the next increment, and signal when done. */
|
|
10
|
+
export function loopInstruction(maxIters) {
|
|
11
|
+
return (`This task runs as a LOOP (up to ${maxIters} iterations total). Do the next increment of work now. ` +
|
|
12
|
+
`When you judge the overall task COMPLETE and no further iterations are needed, end your response ` +
|
|
13
|
+
`with a JSON object {"loop_continue": false}. To request another iteration, omit it or return ` +
|
|
14
|
+
`{"loop_continue": true}.`);
|
|
15
|
+
}
|
|
16
|
+
/** Instruction appended to a classify node's goal: pick exactly one of the kernel's branch labels. */
|
|
17
|
+
export function classifyInstruction(labels) {
|
|
18
|
+
return (`Classify the input and choose EXACTLY ONE label from: ${labels.map(l => JSON.stringify(l)).join(", ")}. ` +
|
|
19
|
+
`Respond with ONLY a JSON object: {"branch": "<one of the labels>"}.`);
|
|
20
|
+
}
|
|
21
|
+
/** Build a tournament judge's goal: the controller's criterion + the two candidates to compare. */
|
|
22
|
+
export function judgeGoal(criterion, leftOutput, rightOutput) {
|
|
23
|
+
return (`${criterion}\n\nCompare the two candidate outputs below and decide which one better satisfies the ` +
|
|
24
|
+
`criterion above.\n\n[CANDIDATE left]\n${leftOutput}\n\n[CANDIDATE right]\n${rightOutput}\n\n` +
|
|
25
|
+
`Respond with ONLY a JSON object: {"winner": "left"} or {"winner": "right"}.`);
|
|
26
|
+
}
|
|
27
|
+
/** Extract a loop stop signal from a loop iteration's output. Returns the `loopContinue` value, or
|
|
28
|
+
* `undefined` when the agent gave no clear signal (⇒ the kernel runs the loop to `max_iters`).
|
|
29
|
+
* Accepts `{loop_continue: bool}` or, leniently, `{done: bool}` (continue = !done). */
|
|
30
|
+
export function extractLoopContinue(text) {
|
|
31
|
+
const v = extractJsonValue(text);
|
|
32
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
33
|
+
const o = v;
|
|
34
|
+
if (typeof o.loop_continue === "boolean")
|
|
35
|
+
return o.loop_continue;
|
|
36
|
+
if (typeof o.loopContinue === "boolean")
|
|
37
|
+
return o.loopContinue;
|
|
38
|
+
if (typeof o.done === "boolean")
|
|
39
|
+
return !o.done;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
/** Extract the chosen branch label from a classifier's output. Prefers `{branch: "..."}`; falls back
|
|
44
|
+
* to a bare label string that exactly matches one of the valid labels. Returns `undefined` when no
|
|
45
|
+
* recognizable choice was made (the kernel then prunes every branch — a safe "none matched"). */
|
|
46
|
+
export function extractClassifyBranch(text, labels) {
|
|
47
|
+
const v = extractJsonValue(text);
|
|
48
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
49
|
+
const o = v;
|
|
50
|
+
if (typeof o.branch === "string")
|
|
51
|
+
return o.branch;
|
|
52
|
+
if (typeof o.label === "string")
|
|
53
|
+
return o.label;
|
|
54
|
+
}
|
|
55
|
+
if (typeof v === "string" && labels.includes(v))
|
|
56
|
+
return v;
|
|
57
|
+
const trimmed = (text ?? "").trim();
|
|
58
|
+
if (labels.includes(trimmed))
|
|
59
|
+
return trimmed;
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** Extract a tournament judge's verdict ("left" or "right"). Defaults to "left" when the verdict is
|
|
63
|
+
* unparseable, so the bracket always advances to a champion rather than stalling with no winner. */
|
|
64
|
+
export function extractJudgeWinner(text) {
|
|
65
|
+
const v = extractJsonValue(text);
|
|
66
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
67
|
+
const w = v.winner;
|
|
68
|
+
if (w === "right")
|
|
69
|
+
return "right";
|
|
70
|
+
if (w === "left")
|
|
71
|
+
return "left";
|
|
72
|
+
}
|
|
73
|
+
const lowered = (text ?? "").toLowerCase();
|
|
74
|
+
// Last resort: a bare mention. Bias to "left" on ambiguity (both/neither mentioned).
|
|
75
|
+
if (lowered.includes("right") && !lowered.includes("left"))
|
|
76
|
+
return "right";
|
|
77
|
+
return "left";
|
|
78
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { WorkflowSpec } from "../types/agent.js";
|
|
2
|
+
/** File-backed `WorkflowSpec` store. Default root `~/.deepstrike/workflows`; override via `rootDir`
|
|
3
|
+
* (e.g. a skill folder for distribution). One spec per `<name>.json`. */
|
|
4
|
+
export declare class FileWorkflowStore {
|
|
5
|
+
private readonly root;
|
|
6
|
+
constructor(opts?: {
|
|
7
|
+
rootDir?: string;
|
|
8
|
+
});
|
|
9
|
+
/** Persist `spec` under `name`; returns the file path written. */
|
|
10
|
+
save(name: string, spec: WorkflowSpec): Promise<string>;
|
|
11
|
+
/** Load the spec saved under `name`. Throws if it does not exist. */
|
|
12
|
+
load(name: string): Promise<WorkflowSpec>;
|
|
13
|
+
/** The names of all saved workflows (sorted); `[]` when the store dir does not exist yet. */
|
|
14
|
+
list(): Promise<string[]>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//! M6: file-backed persistence for declarative `WorkflowSpec`s — the SDK side of "save & share
|
|
2
|
+
//! workflows". A spec is pure data, so a saved workflow is plain JSON that round-trips exactly. Check
|
|
3
|
+
//! the files into `~/.deepstrike/workflows/`, or ship them inside a skill as templates: put the JSON
|
|
4
|
+
//! in the skill folder and have the agent `load()` + (optionally) tweak the spec before `runWorkflow`.
|
|
5
|
+
import { mkdir, writeFile, readFile, readdir } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
function defaultRoot() {
|
|
9
|
+
return join(homedir(), ".deepstrike", "workflows");
|
|
10
|
+
}
|
|
11
|
+
/** Reject names that could escape the store directory; allow a safe slug only. */
|
|
12
|
+
function safeName(name) {
|
|
13
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
14
|
+
throw new Error(`invalid workflow name "${name}": use only letters, digits, "-", "_"`);
|
|
15
|
+
}
|
|
16
|
+
return name;
|
|
17
|
+
}
|
|
18
|
+
/** File-backed `WorkflowSpec` store. Default root `~/.deepstrike/workflows`; override via `rootDir`
|
|
19
|
+
* (e.g. a skill folder for distribution). One spec per `<name>.json`. */
|
|
20
|
+
export class FileWorkflowStore {
|
|
21
|
+
root;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.root = opts?.rootDir ?? defaultRoot();
|
|
24
|
+
}
|
|
25
|
+
/** Persist `spec` under `name`; returns the file path written. */
|
|
26
|
+
async save(name, spec) {
|
|
27
|
+
const path = join(this.root, `${safeName(name)}.json`);
|
|
28
|
+
await mkdir(this.root, { recursive: true });
|
|
29
|
+
await writeFile(path, JSON.stringify(spec, null, 2), "utf8");
|
|
30
|
+
return path;
|
|
31
|
+
}
|
|
32
|
+
/** Load the spec saved under `name`. Throws if it does not exist. */
|
|
33
|
+
async load(name) {
|
|
34
|
+
const path = join(this.root, `${safeName(name)}.json`);
|
|
35
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
36
|
+
}
|
|
37
|
+
/** The names of all saved workflows (sorted); `[]` when the store dir does not exist yet. */
|
|
38
|
+
async list() {
|
|
39
|
+
try {
|
|
40
|
+
const files = await readdir(this.root);
|
|
41
|
+
return files.filter(f => f.endsWith(".json")).map(f => f.slice(0, -".json".length)).sort();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ExecutionPlane, RunContext } from "./execution-plane.js";
|
|
2
|
+
import type { RegisteredTool } from "../tools/index.js";
|
|
3
|
+
import type { ToolCall, ToolSchema, StreamEvent } from "../types.js";
|
|
4
|
+
/** Creates and removes the worktree directory for one sub-agent. Injectable so the plane can be
|
|
5
|
+
* unit-tested without a real git repo (the default is git-backed). */
|
|
6
|
+
export interface WorktreeManager {
|
|
7
|
+
/** Create the working directory for sub-agent `id`; returns its absolute path. */
|
|
8
|
+
create(id: string): Promise<string>;
|
|
9
|
+
/** Remove the working directory previously created at `path`. Must not throw on a missing path. */
|
|
10
|
+
remove(path: string): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** Default git-backed manager: `git worktree add --detach <root>/<id> <ref>` then `git worktree
|
|
13
|
+
* remove --force`. Falls back to a plain recursive delete if `worktree remove` fails (e.g. the dir
|
|
14
|
+
* was already detached), so cleanup is best-effort and never throws. */
|
|
15
|
+
export declare class GitWorktreeManager implements WorktreeManager {
|
|
16
|
+
private readonly opts;
|
|
17
|
+
constructor(opts?: {
|
|
18
|
+
repoRoot?: string;
|
|
19
|
+
ref?: string;
|
|
20
|
+
rootDir?: string;
|
|
21
|
+
});
|
|
22
|
+
create(id: string): Promise<string>;
|
|
23
|
+
remove(path: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Decorator plane: lazily creates a worktree on first execution, injects it as `RunContext.cwd` for
|
|
26
|
+
* every delegated call, and removes it on [`cleanup`]. Tool registration/schemas pass straight
|
|
27
|
+
* through to the inner plane. The worktree only *isolates* to the extent the inner plane honors
|
|
28
|
+
* `ctx.cwd` (e.g. a subprocess plane rooting commands there). */
|
|
29
|
+
export declare class WorktreeExecutionPlane implements ExecutionPlane {
|
|
30
|
+
private readonly inner;
|
|
31
|
+
private readonly manager;
|
|
32
|
+
private readonly id;
|
|
33
|
+
private path;
|
|
34
|
+
constructor(inner: ExecutionPlane, manager: WorktreeManager, id: string);
|
|
35
|
+
register(...tools: RegisteredTool[]): this;
|
|
36
|
+
unregister(name: string): this;
|
|
37
|
+
schemas(): ToolSchema[];
|
|
38
|
+
/** The created worktree path, or undefined before the first `executeAll` / after `cleanup`. */
|
|
39
|
+
worktreePath(): string | undefined;
|
|
40
|
+
executeAll(calls: ToolCall[], ctx: RunContext): AsyncIterable<StreamEvent>;
|
|
41
|
+
/** Remove the worktree (idempotent — safe to call when none was created). */
|
|
42
|
+
cleanup(): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//! M3/G4: per-sub-agent git-worktree isolation as an execution-plane decorator.
|
|
2
|
+
//!
|
|
3
|
+
//! An `isolation: "worktree"` workflow node should run its tools in its own working tree so parallel
|
|
4
|
+
//! write-capable nodes (the migration / refactor / evals patterns) don't clobber each other. This
|
|
5
|
+
//! module owns the *worktree lifecycle*: create one git worktree per sub-agent, inject its path as
|
|
6
|
+
//! `RunContext.cwd` so a cwd-aware inner plane scopes its work there, and remove it when the
|
|
7
|
+
//! sub-agent finishes. The git operations are behind an injectable [`WorktreeManager`] so the plane
|
|
8
|
+
//! is testable without mutating a real repository.
|
|
9
|
+
import { execFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
/** Default git-backed manager: `git worktree add --detach <root>/<id> <ref>` then `git worktree
|
|
16
|
+
* remove --force`. Falls back to a plain recursive delete if `worktree remove` fails (e.g. the dir
|
|
17
|
+
* was already detached), so cleanup is best-effort and never throws. */
|
|
18
|
+
export class GitWorktreeManager {
|
|
19
|
+
opts;
|
|
20
|
+
constructor(opts = {}) {
|
|
21
|
+
this.opts = opts;
|
|
22
|
+
}
|
|
23
|
+
async create(id) {
|
|
24
|
+
const root = this.opts.rootDir ?? (await mkdtemp(join(tmpdir(), "deepstrike-wt-")));
|
|
25
|
+
const path = join(root, id);
|
|
26
|
+
const ref = this.opts.ref ?? "HEAD";
|
|
27
|
+
await execFileAsync("git", ["worktree", "add", "--detach", path, ref], { cwd: this.opts.repoRoot });
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
async remove(path) {
|
|
31
|
+
try {
|
|
32
|
+
await execFileAsync("git", ["worktree", "remove", "--force", path], { cwd: this.opts.repoRoot });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
await rm(path, { recursive: true, force: true }).catch(() => { });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Decorator plane: lazily creates a worktree on first execution, injects it as `RunContext.cwd` for
|
|
40
|
+
* every delegated call, and removes it on [`cleanup`]. Tool registration/schemas pass straight
|
|
41
|
+
* through to the inner plane. The worktree only *isolates* to the extent the inner plane honors
|
|
42
|
+
* `ctx.cwd` (e.g. a subprocess plane rooting commands there). */
|
|
43
|
+
export class WorktreeExecutionPlane {
|
|
44
|
+
inner;
|
|
45
|
+
manager;
|
|
46
|
+
id;
|
|
47
|
+
path;
|
|
48
|
+
constructor(inner, manager, id) {
|
|
49
|
+
this.inner = inner;
|
|
50
|
+
this.manager = manager;
|
|
51
|
+
this.id = id;
|
|
52
|
+
}
|
|
53
|
+
register(...tools) {
|
|
54
|
+
this.inner.register(...tools);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
unregister(name) {
|
|
58
|
+
this.inner.unregister(name);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
schemas() {
|
|
62
|
+
return this.inner.schemas();
|
|
63
|
+
}
|
|
64
|
+
/** The created worktree path, or undefined before the first `executeAll` / after `cleanup`. */
|
|
65
|
+
worktreePath() {
|
|
66
|
+
return this.path;
|
|
67
|
+
}
|
|
68
|
+
async *executeAll(calls, ctx) {
|
|
69
|
+
if (this.path === undefined)
|
|
70
|
+
this.path = await this.manager.create(this.id);
|
|
71
|
+
yield* this.inner.executeAll(calls, { ...ctx, cwd: this.path });
|
|
72
|
+
}
|
|
73
|
+
/** Remove the worktree (idempotent — safe to call when none was created). */
|
|
74
|
+
async cleanup() {
|
|
75
|
+
if (this.path === undefined)
|
|
76
|
+
return;
|
|
77
|
+
const p = this.path;
|
|
78
|
+
this.path = undefined;
|
|
79
|
+
await this.manager.remove(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ToolChunk, ToolSchema, ToolResult } from "../types.js";
|
|
2
|
+
/** M3/G4: the runtime context a tool may read when executing. Carries the working directory the tool
|
|
3
|
+
* should operate in — set to a sub-agent's git worktree for `isolation: "worktree"` nodes. A narrow,
|
|
4
|
+
* dependency-free shape; the execution plane's `RunContext` is structurally assignable to it. */
|
|
5
|
+
export interface ToolExecContext {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}
|
|
2
8
|
export interface RegisteredTool {
|
|
3
9
|
schema: ToolSchema;
|
|
4
|
-
execute(args: Record<string, unknown
|
|
10
|
+
execute(args: Record<string, unknown>, ctx?: ToolExecContext): Promise<string> | AsyncIterable<ToolChunk>;
|
|
5
11
|
}
|
|
6
|
-
export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown
|
|
7
|
-
export declare function streamingTool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown
|
|
12
|
+
export declare function tool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>, ctx?: ToolExecContext) => Promise<string> | string): RegisteredTool;
|
|
13
|
+
export declare function streamingTool(name: string, description: string, parameters: Record<string, unknown>, fn: (args: Record<string, unknown>, ctx?: ToolExecContext) => AsyncIterable<ToolChunk>): RegisteredTool;
|
|
8
14
|
export declare function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T>;
|
|
9
15
|
export declare function normalizeToolChunk(chunk: ToolChunk): Exclude<ToolChunk, string>;
|
|
10
16
|
export declare function toolChunkText(chunk: ToolChunk): string;
|
package/dist/tools/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export function tool(name, description, parameters, fn) {
|
|
2
2
|
return {
|
|
3
3
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
4
|
-
async execute(args) { return fn(args); },
|
|
4
|
+
async execute(args, ctx) { return fn(args, ctx); },
|
|
5
5
|
};
|
|
6
6
|
}
|
|
7
7
|
export function streamingTool(name, description, parameters, fn) {
|
|
8
8
|
return {
|
|
9
9
|
schema: { name, description, parameters: JSON.stringify(parameters) },
|
|
10
|
-
execute(args) { return fn(args); },
|
|
10
|
+
execute(args, ctx) { return fn(args, ctx); },
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
export function isAsyncIterable(value) {
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface AgentRunSpec {
|
|
|
23
23
|
capabilityFilter?: AgentCapabilityFilter;
|
|
24
24
|
milestones?: MilestoneContract;
|
|
25
25
|
metadata?: Record<string, unknown>;
|
|
26
|
+
/** M1/G3: per-agent model preference (e.g. "opus"/"sonnet"/"haiku"); the host resolves it to a
|
|
27
|
+
* provider via `RuntimeOptions.providerFor`. Host-side routing only — not sent to the kernel. */
|
|
28
|
+
modelHint?: string;
|
|
29
|
+
/** M4/G5: cumulative token cap for this sub-agent's run (sets the child kernel's `maxTotalTokens`). */
|
|
30
|
+
tokenBudget?: number;
|
|
26
31
|
}
|
|
27
32
|
/** Kernel process-table observation (Phase 3 canonical spawn signal). */
|
|
28
33
|
export interface AgentProcessChangedObservation {
|
|
@@ -48,6 +53,14 @@ export interface LoopResult {
|
|
|
48
53
|
finalMessage?: Message;
|
|
49
54
|
turnsUsed: number;
|
|
50
55
|
totalTokensUsed: number;
|
|
56
|
+
/** A#2 v2 loop stop signal: a loop iteration sets `false` to end the loop before `max_iters`.
|
|
57
|
+
* `undefined` (every non-loop result) ⇒ no opinion → run to the cap. Sent only when set. */
|
|
58
|
+
loopContinue?: boolean;
|
|
59
|
+
/** A#2 classify routing: a classifier node reports the chosen branch label here; the kernel runs
|
|
60
|
+
* that branch and prunes the rest. Sent only when set. */
|
|
61
|
+
classifyBranch?: string;
|
|
62
|
+
/** A#2 tournament verdict: a judge reports the winning entrant's agent id here. Sent only when set. */
|
|
63
|
+
tournamentWinner?: string;
|
|
51
64
|
}
|
|
52
65
|
export interface SubAgentResult {
|
|
53
66
|
agentId: string;
|
|
@@ -103,6 +116,26 @@ export interface WorkflowNodeSpec {
|
|
|
103
116
|
/** G2: make this a deterministic *reduce* node — it runs no LLM agent. The runner routes it to the
|
|
104
117
|
* registered reducer of this name, over its `dependsOn` nodes' outputs (dedupe / filter / merge). */
|
|
105
118
|
reducer?: string;
|
|
119
|
+
/** A#2 v2: make this a *loop* node — re-run its agent up to `maxIters` times. An iteration may end
|
|
120
|
+
* the loop early by reporting `loopContinue: false` (the runner solicits this from the agent). */
|
|
121
|
+
loop?: {
|
|
122
|
+
maxIters: number;
|
|
123
|
+
};
|
|
124
|
+
/** A#2: make this a *classify* node — its agent picks exactly one branch `label`; that branch's
|
|
125
|
+
* nodes run and the others are pruned. Each branch node must list this node's index in `dependsOn`. */
|
|
126
|
+
classify?: {
|
|
127
|
+
branches: Array<{
|
|
128
|
+
label: string;
|
|
129
|
+
nodes: number[];
|
|
130
|
+
}>;
|
|
131
|
+
};
|
|
132
|
+
/** A#2: make this a *tournament controller* — generate each `entrants` candidate in parallel, then
|
|
133
|
+
* pairwise-judge them to one winner (this node's `task.goal` is the judging criterion). ≥2 entrants. */
|
|
134
|
+
tournament?: {
|
|
135
|
+
entrants: WorkflowTaskSpec[];
|
|
136
|
+
};
|
|
137
|
+
/** M4/G5: cap this node's child run at `tokenBudget` cumulative tokens (the per-node "use N tokens"). */
|
|
138
|
+
tokenBudget?: number;
|
|
106
139
|
/** Indices of nodes this node depends on. */
|
|
107
140
|
dependsOn?: number[];
|
|
108
141
|
}
|
|
@@ -126,6 +159,20 @@ export interface WorkflowSpawnInfo {
|
|
|
126
159
|
reducer?: string;
|
|
127
160
|
/** G2: the dependency agent ids whose outputs a reduce node consumes. */
|
|
128
161
|
input_agent_ids?: string[];
|
|
162
|
+
/** A#2: present only for a tournament *judge* spawn — the two entrant agent ids whose produced
|
|
163
|
+
* outputs this judge compares. The runner looks them up and reports the winner as `tournamentWinner`. */
|
|
164
|
+
judge_match?: {
|
|
165
|
+
left: string;
|
|
166
|
+
right: string;
|
|
167
|
+
};
|
|
168
|
+
/** A#2 v2: present only for a *loop* iteration spawn — the loop's `max_iters`. Marks the spawn as a
|
|
169
|
+
* loop iteration so the runner solicits + reports a `loopContinue` stop signal. */
|
|
170
|
+
loop_max_iters?: number;
|
|
171
|
+
/** A#2: present only for a *classify* spawn — the branch labels the classifier must choose among.
|
|
172
|
+
* Non-empty marks the spawn as a classifier so the runner instructs the agent + reports `classifyBranch`. */
|
|
173
|
+
classify_labels?: string[];
|
|
174
|
+
/** M4/G5: the node's per-node cumulative token cap, if set — the runner caps the child run here. */
|
|
175
|
+
token_budget?: number;
|
|
129
176
|
}
|
|
130
177
|
/** G4 budget-as-signal: the workflow's remaining headroom under the active quota, carried on the
|
|
131
178
|
* `workflow_batch_spawned` observation so a coordinator node can scale its next submission. */
|
|
@@ -136,6 +183,11 @@ export interface WorkflowBudget {
|
|
|
136
183
|
running_subagents: number;
|
|
137
184
|
max_concurrent_subagents?: number;
|
|
138
185
|
concurrency_remaining?: number;
|
|
186
|
+
/** M4/G5 token headroom: cumulative tokens used, the run-level cap, and tokens remaining before the
|
|
187
|
+
* token budget terminates the run — so a coordinator can scale a submission to "use N tokens". */
|
|
188
|
+
tokens_used?: number;
|
|
189
|
+
tokens_max?: number;
|
|
190
|
+
tokens_remaining?: number;
|
|
139
191
|
}
|
|
140
192
|
/** G4: a concise, human-readable budget note appended to a coordinator node's goal, so its agent can
|
|
141
193
|
* size a `submit_workflow_nodes` batch to what is actually available. Returns "" when nothing is
|
|
@@ -150,10 +202,21 @@ export declare function workflowSpecToKernel(spec: WorkflowSpec): Record<string,
|
|
|
150
202
|
* `submitterAgentId` (the node that requested the append) so the kernel can enforce no-privilege-
|
|
151
203
|
* escalation — a quarantined submitter's nodes are coerced to quarantined. Omitted ⇒ no coercion. */
|
|
152
204
|
export declare function submitWorkflowNodesToKernel(nodes: WorkflowNodeSpec[], submitterAgentId?: string): Record<string, unknown>;
|
|
205
|
+
/** M5/G1: map an agent-authored spec to the `submit_workflow` kernel event body (the agent-reachable
|
|
206
|
+
* `Syscall::LoadWorkflow`). The kernel bootstraps the DAG when none is active, else flattens onto it.
|
|
207
|
+
* `parentSessionId` seeds child session ids on bootstrap; `submitterAgentId` carries G1 trust coercion
|
|
208
|
+
* on the flatten case (a quarantined author's nodes are coerced quarantined). */
|
|
209
|
+
export declare function submitWorkflowToKernel(spec: WorkflowSpec, parentSessionId: string, submitterAgentId?: string): Record<string, unknown>;
|
|
153
210
|
/** R3-1: the tool a workflow-coordinator node's agent calls to append work to the running DAG
|
|
154
211
|
* (true loop-until-done / dynamic fan-out). Give it to nodes meant to fan out; the runner intercepts
|
|
155
212
|
* the call and routes the nodes to the parent kernel (the child's own kernel holds no workflow). */
|
|
156
213
|
export declare const submitWorkflowNodesTool: ToolSchema;
|
|
214
|
+
/** M5 v1 (flatten): the tool an agent calls to **author a sub-workflow** — a cohesive DAG of nodes
|
|
215
|
+
* (incl. loop/classify/tournament/reduce) composed onto the running workflow. Mechanically it lowers
|
|
216
|
+
* to the same append path as `submit_workflow_nodes` (a `WorkflowSpec` is a node batch), but reads as
|
|
217
|
+
* "write a harness" rather than "append nodes". v2 adds top-level bootstrap (the `LoadWorkflow`
|
|
218
|
+
* kernel syscall) so a plain run can start a workflow from scratch. */
|
|
219
|
+
export declare const startWorkflowTool: ToolSchema;
|
|
157
220
|
/** Build a sub-agent run spec for a kernel-generated workflow node. */
|
|
158
221
|
export declare function workflowNodeToSpec(node: WorkflowSpawnInfo, parentSessionId: string): AgentRunSpec;
|
|
159
222
|
/** Build the host manifest for a kernel-generated workflow node. */
|