@deepstrike/sdk 0.2.10 → 0.2.12
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/README.md +38 -0
- package/dist/harness/harness.d.ts +5 -0
- package/dist/harness/harness.js +12 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -4
- package/dist/providers/anthropic.d.ts +7 -0
- package/dist/providers/anthropic.js +129 -10
- package/dist/providers/base.d.ts +38 -0
- package/dist/providers/base.js +0 -0
- package/dist/providers/catalog.js +20 -8
- package/dist/providers/deepseek.d.ts +12 -0
- package/dist/providers/deepseek.js +23 -4
- package/dist/providers/gemini.js +23 -4
- package/dist/providers/glm.d.ts +12 -0
- package/dist/providers/glm.js +19 -2
- package/dist/providers/kimi.d.ts +12 -0
- package/dist/providers/kimi.js +19 -2
- package/dist/providers/minimax.js +4 -2
- package/dist/providers/ollama.js +2 -2
- package/dist/providers/openai-chat.js +3 -2
- package/dist/providers/openai-responses.js +13 -0
- package/dist/providers/openai.d.ts +7 -0
- package/dist/providers/openai.js +15 -2
- package/dist/providers/profiles.d.ts +52 -28
- package/dist/providers/profiles.js +52 -28
- package/dist/providers/qwen.d.ts +12 -0
- package/dist/providers/qwen.js +23 -4
- 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 +24 -1
- package/dist/runtime/runner.js +185 -15
- package/dist/runtime/session-log.d.ts +9 -1
- 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 +42 -1
- package/package.json +2 -2
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) {
|
|
@@ -350,9 +458,10 @@ export class RuntimeRunner {
|
|
|
350
458
|
criteria: req.criteria ?? [],
|
|
351
459
|
agent_id: this.opts.agentId,
|
|
352
460
|
system_prompt: this.opts.systemPrompt,
|
|
461
|
+
...(req.attachments?.length ? { attachments: req.attachments } : {}),
|
|
353
462
|
});
|
|
354
463
|
}
|
|
355
|
-
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun);
|
|
464
|
+
yield* this.execute(req.sessionId, req.goal, req.criteria ?? [], req.extensions, prior.length > 0 ? prior : undefined, midRun, req.attachments);
|
|
356
465
|
}
|
|
357
466
|
async *wake(sessionId, extensions) {
|
|
358
467
|
const events = await this.opts.sessionLog.read(sessionId);
|
|
@@ -362,7 +471,7 @@ export class RuntimeRunner {
|
|
|
362
471
|
if (!startEntry)
|
|
363
472
|
throw new Error(`No run_started event for session: ${sessionId}`);
|
|
364
473
|
const start = startEntry.event;
|
|
365
|
-
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true);
|
|
474
|
+
yield* this.execute(sessionId, start.goal, start.criteria, extensions, events, true, start.attachments);
|
|
366
475
|
}
|
|
367
476
|
async *dream(agentId, nowMs = Date.now()) {
|
|
368
477
|
if (!this.opts.dreamStore)
|
|
@@ -515,7 +624,7 @@ export class RuntimeRunner {
|
|
|
515
624
|
}
|
|
516
625
|
return { approved, denied, events };
|
|
517
626
|
}
|
|
518
|
-
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false) {
|
|
627
|
+
async *execute(sessionId, goal, criteria, extensions, priorEvents, resumeMidRun = false, attachments) {
|
|
519
628
|
this.interrupted = false;
|
|
520
629
|
this.pendingObservations = [];
|
|
521
630
|
this.pendingSpoolOutputs.clear();
|
|
@@ -683,6 +792,16 @@ export class RuntimeRunner {
|
|
|
683
792
|
},
|
|
684
793
|
});
|
|
685
794
|
}
|
|
795
|
+
// Multimodal upload: seed the user's attachments (images/audio) as a history
|
|
796
|
+
// message before start_run pushes the "[TASK STATE]" anchor. init_task does not
|
|
797
|
+
// clear history, so order becomes [attachment user msg, "Proceed…"] — both land
|
|
798
|
+
// in the first render. On resume the message is already in the replayed history.
|
|
799
|
+
if (!resumeMidRun && attachments?.length) {
|
|
800
|
+
kernelApply(runtime, this.pendingObservations, {
|
|
801
|
+
kind: "add_history_message",
|
|
802
|
+
message: attachmentsToKernelMessage(attachments),
|
|
803
|
+
});
|
|
804
|
+
}
|
|
686
805
|
let action = resumeMidRun
|
|
687
806
|
? kernelAction(runtime, this.pendingObservations, { kind: "resume" })
|
|
688
807
|
: kernelAction(runtime, this.pendingObservations, startPayload);
|
|
@@ -827,8 +946,9 @@ export class RuntimeRunner {
|
|
|
827
946
|
resultSpool: this.opts.resultSpool ?? new LargeResultSpool(),
|
|
828
947
|
};
|
|
829
948
|
const toolResults = [];
|
|
830
|
-
const normalCalls = allCalls.filter(c => c.name !== "update_plan");
|
|
949
|
+
const normalCalls = allCalls.filter(c => c.name !== "update_plan" && c.name !== "submit_workflow_nodes");
|
|
831
950
|
const planCalls = allCalls.filter(c => c.name === "update_plan");
|
|
951
|
+
const submitCalls = allCalls.filter(c => c.name === "submit_workflow_nodes");
|
|
832
952
|
for (const call of planCalls) {
|
|
833
953
|
const update = parseUpdatePlanArgs(call.arguments);
|
|
834
954
|
kernelApply(runtime, this.pendingObservations, {
|
|
@@ -839,6 +959,18 @@ export class RuntimeRunner {
|
|
|
839
959
|
toolResults.push(result);
|
|
840
960
|
yield { type: "tool_result", callId: call.id, content: "success", isError: false };
|
|
841
961
|
}
|
|
962
|
+
// R3-1: `submit_workflow_nodes` cannot be applied to this runner's kernel — when this runner
|
|
963
|
+
// is a workflow node, the workflow lives in the *parent* kernel. Surface the requested nodes
|
|
964
|
+
// as a stream event; the orchestrator collects them onto the node's result and `runWorkflow`
|
|
965
|
+
// sends `submit_workflow_nodes` to the parent kernel. (When not a workflow node, the event is
|
|
966
|
+
// simply unconsumed — a no-op.)
|
|
967
|
+
for (const call of submitCalls) {
|
|
968
|
+
const nodes = parseSubmitWorkflowNodesArgs(call.arguments);
|
|
969
|
+
yield { type: "workflow_nodes_submitted", nodes };
|
|
970
|
+
const result = { callId: call.id, output: "submitted", isError: false };
|
|
971
|
+
toolResults.push(result);
|
|
972
|
+
yield { type: "tool_result", callId: call.id, content: "submitted", isError: false };
|
|
973
|
+
}
|
|
842
974
|
if (normalCalls.length > 0) {
|
|
843
975
|
for await (const evt of this.opts.executionPlane.executeAll(normalCalls, runCtx)) {
|
|
844
976
|
yield evt;
|
|
@@ -1106,6 +1238,31 @@ export class RuntimeRunner {
|
|
|
1106
1238
|
function isMidRun(events) {
|
|
1107
1239
|
return events.length > 0 && !events.some(e => e.event.kind === "run_terminal");
|
|
1108
1240
|
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Build a kernel `add_history_message` payload from user attachments: a `user`
|
|
1243
|
+
* message whose content is the multimodal parts in the kernel's serde shape
|
|
1244
|
+
* (`Content::Parts`; image `media_type`, not `mediaType`). Lets a caller upload
|
|
1245
|
+
* images/audio with the task — the message lands in history before the first render.
|
|
1246
|
+
*/
|
|
1247
|
+
function attachmentsToKernelMessage(parts) {
|
|
1248
|
+
const content = parts.map(p => {
|
|
1249
|
+
if (p.type === "image") {
|
|
1250
|
+
return {
|
|
1251
|
+
type: "image",
|
|
1252
|
+
...(p.url ? { url: p.url } : {}),
|
|
1253
|
+
...(p.data ? { data: p.data } : {}),
|
|
1254
|
+
...(p.mediaType ? { media_type: p.mediaType } : {}),
|
|
1255
|
+
...(p.detail ? { detail: p.detail } : {}),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (p.type === "audio")
|
|
1259
|
+
return { type: "audio", data: p.data, media_type: p.mediaType };
|
|
1260
|
+
if (p.type === "text")
|
|
1261
|
+
return { type: "text", text: p.text };
|
|
1262
|
+
return { type: "text", text: "" };
|
|
1263
|
+
});
|
|
1264
|
+
return { role: "user", content };
|
|
1265
|
+
}
|
|
1109
1266
|
function compressionAction(action) {
|
|
1110
1267
|
if (action === "snip_compact" ||
|
|
1111
1268
|
action === "micro_compact" ||
|
|
@@ -1317,3 +1474,16 @@ function parseUpdatePlanArgs(argsStr) {
|
|
|
1317
1474
|
: parsed.blocked_on,
|
|
1318
1475
|
};
|
|
1319
1476
|
}
|
|
1477
|
+
/** R3-1: parse the `submit_workflow_nodes` tool arguments (`{ nodes: WorkflowNodeSpec[] }`). Node
|
|
1478
|
+
* shapes are trusted structurally here; the kernel validates them (dep range, quarantine, quota) on
|
|
1479
|
+
* append. A malformed payload yields no nodes rather than throwing. */
|
|
1480
|
+
function parseSubmitWorkflowNodesArgs(argsStr) {
|
|
1481
|
+
let parsed = {};
|
|
1482
|
+
try {
|
|
1483
|
+
parsed = JSON.parse(argsStr);
|
|
1484
|
+
}
|
|
1485
|
+
catch {
|
|
1486
|
+
// Ignore parse error → no nodes submitted.
|
|
1487
|
+
}
|
|
1488
|
+
return Array.isArray(parsed.nodes) ? parsed.nodes : [];
|
|
1489
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
|
|
1
|
+
import type { ContentPart, ProviderReplay, ToolCall, ToolErrorKind } from "../types.js";
|
|
2
2
|
import type { KernelEventCategory, KernelPrimitive } from "./kernel-event-log.js";
|
|
3
3
|
export type RollbackReason = {
|
|
4
4
|
kind: "fatal_tool_error";
|
|
@@ -26,6 +26,7 @@ export type SessionEvent = {
|
|
|
26
26
|
criteria: string[];
|
|
27
27
|
agent_id?: string;
|
|
28
28
|
system_prompt?: string;
|
|
29
|
+
attachments?: ContentPart[];
|
|
29
30
|
} | {
|
|
30
31
|
kind: "llm_completed";
|
|
31
32
|
turn: number;
|
|
@@ -243,6 +244,13 @@ export type SessionEvent = {
|
|
|
243
244
|
primitive?: KernelPrimitive;
|
|
244
245
|
agent_id: string;
|
|
245
246
|
termination: string;
|
|
247
|
+
} | {
|
|
248
|
+
kind: "workflow_nodes_submitted";
|
|
249
|
+
turn: number;
|
|
250
|
+
category?: KernelEventCategory;
|
|
251
|
+
primitive?: KernelPrimitive;
|
|
252
|
+
/** Kernel-shape (snake_case) submitted node specs — persisted so resume can re-apply them. */
|
|
253
|
+
nodes: Record<string, unknown>[];
|
|
246
254
|
} | {
|
|
247
255
|
kind: "workflow_batch_spawned";
|
|
248
256
|
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 {
|