pi-subagents 0.42.1 → 0.44.0
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/CHANGELOG.md +50 -0
- package/README.md +3 -5
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -2
- package/skills/pi-subagents/references/constraints-and-recipes.md +35 -32
- package/skills/pi-subagents/references/execution-controls.md +30 -44
- package/skills/pi-subagents/references/management-authoring-rpc.md +20 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +16 -49
- package/src/agents/agent-refinements.ts +624 -0
- package/src/agents/agents.ts +0 -2
- package/src/agents/proactive-skills.ts +1 -1
- package/src/api/delegation.ts +1 -2
- package/src/extension/config.ts +28 -7
- package/src/extension/control-notices.ts +2 -2
- package/src/extension/fanout-child.ts +46 -22
- package/src/extension/index.ts +32 -13
- package/src/extension/public-execution.ts +71 -0
- package/src/extension/rpc.ts +7 -8
- package/src/extension/schemas.ts +14 -14
- package/src/extension/tool-description.ts +14 -14
- package/src/missions/actions.ts +47 -11
- package/src/missions/goal-driver.ts +162 -0
- package/src/missions/lifecycle.ts +44 -12
- package/src/missions/store.ts +68 -3
- package/src/missions/types.ts +25 -3
- package/src/missions/workflow-state.ts +250 -0
- package/src/profiles/profiles.ts +1 -3
- package/src/runs/background/async-execution.ts +5 -1
- package/src/runs/background/async-job-tracker.ts +2 -17
- package/src/runs/background/async-status.ts +2 -0
- package/src/runs/background/control-channel.ts +50 -6
- package/src/runs/background/retained-children.ts +68 -0
- package/src/runs/background/run-id-resolver.ts +71 -0
- package/src/runs/background/run-status.ts +3 -2
- package/src/runs/background/scheduled-runs.ts +17 -13
- package/src/runs/background/steering.ts +7 -5
- package/src/runs/background/subagent-runner.ts +50 -23
- package/src/runs/foreground/async-steering-action.ts +36 -10
- package/src/runs/foreground/chain-clarify.ts +7 -3
- package/src/runs/foreground/execution.ts +29 -19
- package/src/runs/foreground/subagent-executor.ts +282 -77
- package/src/runs/shared/acceptance.ts +154 -9
- package/src/runs/shared/subagent-prompt-runtime.ts +97 -25
- package/src/shared/settings.ts +10 -6
- package/src/shared/types.ts +22 -2
- package/src/slash/delegation-adapters.ts +0 -13
- package/src/slash/prompt-template-bridge.ts +14 -12
- package/src/slash/prompt-workflows.ts +10 -4
- package/src/slash/slash-bridge.ts +8 -6
- package/src/slash/slash-commands.ts +24 -9
- package/src/slash/slash-live-state.ts +4 -2
- package/src/tui/fleet-status.ts +8 -4
- package/src/tui/fleet.ts +15 -5
- package/src/tui/render.ts +10 -29
- package/src/workflows/scripted-workflow.ts +91 -6
- package/agents/context-builder.md +0 -46
- package/agents/planner.md +0 -56
- package/prompts/parallel-context-build.md +0 -55
- package/prompts/parallel-handoff-plan.md +0 -61
package/src/extension/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { ArtifactDirPreference, ExtensionConfig } from "../shared/types.ts";
|
|
4
5
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
@@ -8,6 +9,31 @@ import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
|
8
9
|
|
|
9
10
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
10
11
|
|
|
12
|
+
export function resolveScheduledStoreRoot(value: string): string {
|
|
13
|
+
const expanded = value.startsWith("~/") ? path.join(os.homedir(), value.slice(2)) : value;
|
|
14
|
+
if (!path.isAbsolute(expanded)) throw new Error(`config.scheduledRuns.storeRoot must be an absolute path or "~/...", got ${JSON.stringify(value)}`);
|
|
15
|
+
return path.normalize(expanded);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function validateScheduledRunsConfig(value: unknown): void {
|
|
19
|
+
if (value === undefined) return;
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.scheduledRuns must be a JSON object");
|
|
21
|
+
const storeRoot = (value as Record<string, unknown>).storeRoot;
|
|
22
|
+
if (storeRoot === undefined) return;
|
|
23
|
+
if (typeof storeRoot !== "string" || !storeRoot.trim()) throw new Error("config.scheduledRuns.storeRoot must be a non-empty string");
|
|
24
|
+
resolveScheduledStoreRoot(storeRoot);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function validateConfig(config: Record<string, unknown>): void {
|
|
28
|
+
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
29
|
+
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
30
|
+
}
|
|
31
|
+
validateMissionStoreConfig(config.missions);
|
|
32
|
+
validateAuthorityPolicy(config.authorityPolicy);
|
|
33
|
+
validatePermissionConfig(config.permissions);
|
|
34
|
+
validateScheduledRunsConfig(config.scheduledRuns);
|
|
35
|
+
}
|
|
36
|
+
|
|
11
37
|
export function getConfigPath(): string {
|
|
12
38
|
return path.join(getAgentDir(), "extensions", "subagent", "config.json");
|
|
13
39
|
}
|
|
@@ -18,13 +44,7 @@ function readConfigForUpdate(configPath = getConfigPath()): ExtensionConfig {
|
|
|
18
44
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
19
45
|
throw new Error(`Subagent config at '${configPath}' must be a JSON object`);
|
|
20
46
|
}
|
|
21
|
-
|
|
22
|
-
if (config.artifactDir !== undefined && !ARTIFACT_DIR_PREFERENCES.has(config.artifactDir as ArtifactDirPreference)) {
|
|
23
|
-
throw new Error(`config.artifactDir must be "project", "session", or "temp"`);
|
|
24
|
-
}
|
|
25
|
-
validateMissionStoreConfig(config.missions);
|
|
26
|
-
validateAuthorityPolicy(config.authorityPolicy);
|
|
27
|
-
validatePermissionConfig(config.permissions);
|
|
47
|
+
validateConfig(parsed as Record<string, unknown>);
|
|
28
48
|
return parsed as ExtensionConfig;
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -36,6 +56,7 @@ export function saveConfig(config: ExtensionConfig, configPath = getConfigPath()
|
|
|
36
56
|
export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConfig): ExtensionConfig {
|
|
37
57
|
const configPath = getConfigPath();
|
|
38
58
|
const next = updater(readConfigForUpdate(configPath));
|
|
59
|
+
validateConfig(next as Record<string, unknown>);
|
|
39
60
|
saveConfig(next, configPath);
|
|
40
61
|
return next;
|
|
41
62
|
}
|
|
@@ -6,7 +6,7 @@ export const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
|
|
|
6
6
|
|
|
7
7
|
export interface SubagentControlMessageDetails {
|
|
8
8
|
event: ControlEvent;
|
|
9
|
-
source?: "foreground" | "async";
|
|
9
|
+
source?: "foreground" | "async" | "goal";
|
|
10
10
|
asyncDir?: string;
|
|
11
11
|
childIntercomTarget?: string;
|
|
12
12
|
noticeText?: string;
|
|
@@ -37,7 +37,7 @@ function deliverControlNotice(input: {
|
|
|
37
37
|
display: true,
|
|
38
38
|
details: { ...input.details, childIntercomTarget, noticeText },
|
|
39
39
|
},
|
|
40
|
-
{ triggerTurn: input.details.source
|
|
40
|
+
{ triggerTurn: input.details.source === "async" },
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -7,7 +7,7 @@ import { getArtifactsDir } from "../shared/artifacts.ts";
|
|
|
7
7
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
8
8
|
import { resolveWaitToolConfig } from "../runs/background/wait-config.ts";
|
|
9
9
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
10
|
-
import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
10
|
+
import { readNestedControlRequests, resolveNestedRouteFromEnv, type NestedRoute, writeNestedControlResult } from "../runs/shared/nested-events.ts";
|
|
11
11
|
import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
|
|
12
12
|
import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
|
|
13
13
|
import { SubagentParams } from "./schemas.ts";
|
|
@@ -50,25 +50,42 @@ function createChildSafeState(): SubagentState {
|
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
function
|
|
54
|
-
let route;
|
|
53
|
+
function resolveNestedControlRoute(): NestedRoute | undefined {
|
|
55
54
|
try {
|
|
56
|
-
|
|
55
|
+
return resolveNestedRouteFromEnv();
|
|
57
56
|
} catch {
|
|
58
57
|
return undefined;
|
|
59
58
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function nestedControlRouteKey(route: NestedRoute): string {
|
|
62
|
+
return route.controlInbox;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface NestedControlInboxState {
|
|
66
|
+
seen: Set<string>;
|
|
67
|
+
inFlight: Set<string>;
|
|
68
|
+
pendingResults: Map<string, Parameters<typeof writeNestedControlResult>[1]>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface NestedControlListenerEntry {
|
|
72
|
+
cleanup: () => void;
|
|
73
|
+
state: NestedControlInboxState;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createNestedControlInboxState(): NestedControlInboxState {
|
|
77
|
+
return { seen: new Set(), inFlight: new Set(), pendingResults: new Map() };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState, route: NestedRoute, inboxState: NestedControlInboxState): () => void {
|
|
64
81
|
const timer = setInterval(() => {
|
|
65
82
|
try {
|
|
66
83
|
for (const request of readNestedControlRequests(route)) {
|
|
67
|
-
if (seen.has(request.requestId) || inFlight.has(request.requestId)) continue;
|
|
68
|
-
inFlight.add(request.requestId);
|
|
84
|
+
if (inboxState.seen.has(request.requestId) || inboxState.inFlight.has(request.requestId)) continue;
|
|
85
|
+
inboxState.inFlight.add(request.requestId);
|
|
69
86
|
void (async () => {
|
|
70
87
|
try {
|
|
71
|
-
let result = pendingResults.get(request.requestId);
|
|
88
|
+
let result = inboxState.pendingResults.get(request.requestId);
|
|
72
89
|
if (!result) {
|
|
73
90
|
let ok = false;
|
|
74
91
|
let message = "Control request failed.";
|
|
@@ -107,15 +124,15 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState)
|
|
|
107
124
|
try {
|
|
108
125
|
writeNestedControlResult(route, result);
|
|
109
126
|
} catch (error) {
|
|
110
|
-
pendingResults.set(request.requestId, result);
|
|
127
|
+
inboxState.pendingResults.set(request.requestId, result);
|
|
111
128
|
console.error(`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`, error);
|
|
112
129
|
return;
|
|
113
130
|
}
|
|
114
|
-
pendingResults.delete(request.requestId);
|
|
115
|
-
seen.add(request.requestId);
|
|
131
|
+
inboxState.pendingResults.delete(request.requestId);
|
|
132
|
+
inboxState.seen.add(request.requestId);
|
|
116
133
|
try { fs.unlinkSync(request.filePath); } catch {}
|
|
117
134
|
} finally {
|
|
118
|
-
inFlight.delete(request.requestId);
|
|
135
|
+
inboxState.inFlight.delete(request.requestId);
|
|
119
136
|
}
|
|
120
137
|
})();
|
|
121
138
|
}
|
|
@@ -124,7 +141,7 @@ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState)
|
|
|
124
141
|
}
|
|
125
142
|
}, 200);
|
|
126
143
|
timer.unref?.();
|
|
127
|
-
return timer;
|
|
144
|
+
return () => clearInterval(timer);
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
|
|
@@ -164,14 +181,21 @@ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI):
|
|
|
164
181
|
].join("\n"),
|
|
165
182
|
parameters: SubagentParams,
|
|
166
183
|
execute(id, params, signal, onUpdate, ctx) {
|
|
167
|
-
|
|
168
|
-
if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
|
|
169
|
-
return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
|
|
170
|
-
}
|
|
171
|
-
return executor.execute(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
184
|
+
return executor.executePublic(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
172
185
|
},
|
|
173
186
|
};
|
|
174
187
|
|
|
175
188
|
pi.registerTool(tool);
|
|
176
|
-
|
|
189
|
+
const route = resolveNestedControlRoute();
|
|
190
|
+
if (!route) return;
|
|
191
|
+
const listenerCleanupKey = "__piSubagentFanoutChildNestedControlInboxCleanups";
|
|
192
|
+
const listenerCleanups = globalStore[listenerCleanupKey] instanceof Map
|
|
193
|
+
? globalStore[listenerCleanupKey] as Map<string, NestedControlListenerEntry>
|
|
194
|
+
: new Map<string, NestedControlListenerEntry>();
|
|
195
|
+
globalStore[listenerCleanupKey] = listenerCleanups;
|
|
196
|
+
const routeKey = nestedControlRouteKey(route);
|
|
197
|
+
const previous = listenerCleanups.get(routeKey);
|
|
198
|
+
previous?.cleanup();
|
|
199
|
+
const inboxState = previous?.state ?? createNestedControlInboxState();
|
|
200
|
+
listenerCleanups.set(routeKey, { state: inboxState, cleanup: startNestedControlInboxListener(pi, state, route, inboxState) });
|
|
177
201
|
}
|
package/src/extension/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - Sync (default): Streams output, renders markdown, tracks usage
|
|
6
6
|
* - Async: Background execution, emits events when done
|
|
7
7
|
*
|
|
8
|
-
* Public execution
|
|
8
|
+
* Public execution mode: workflow (workflowScript)
|
|
9
9
|
* Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
@@ -50,9 +50,12 @@ import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_M
|
|
|
50
50
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
|
|
51
51
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
52
52
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
53
|
-
import { loadConfig, resolveAsyncByDefault } from "./config.ts";
|
|
53
|
+
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
54
54
|
import { buildSubagentToolDescription } from "./tool-description.ts";
|
|
55
|
+
import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
|
|
55
56
|
import { syncMissionFromAsyncCompletion } from "../missions/lifecycle.ts";
|
|
57
|
+
import { resolveMissionStoreLocation } from "../missions/store.ts";
|
|
58
|
+
import { listRetainedChildren } from "../runs/background/retained-children.ts";
|
|
56
59
|
import {
|
|
57
60
|
type Details,
|
|
58
61
|
type SubagentState,
|
|
@@ -379,8 +382,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
379
382
|
}, { placement: fleetViewPlacement })
|
|
380
383
|
: undefined;
|
|
381
384
|
let executorScheduled: ((id: string, params: SubagentParamsLike, signal: AbortSignal, ctx: ExtensionContext) => Promise<AgentToolResult<Details>>) | undefined;
|
|
385
|
+
let goalTurnId = 0;
|
|
386
|
+
const scheduledStoreRoot = config.scheduledRuns?.storeRoot === undefined ? undefined : resolveScheduledStoreRoot(config.scheduledRuns.storeRoot);
|
|
382
387
|
const scheduledRunManager = createScheduledRunManager({
|
|
383
388
|
config,
|
|
389
|
+
storeRoot: scheduledStoreRoot,
|
|
384
390
|
launch: (params, ctx, signal) => {
|
|
385
391
|
if (!executorScheduled) {
|
|
386
392
|
return Promise.resolve({
|
|
@@ -501,7 +507,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
501
507
|
|
|
502
508
|
const executeSubagentCollapsed = (id: string, params: SubagentParamsLike, signal: AbortSignal, onUpdate: ((result: AgentToolResult<Details>) => void) | undefined, ctx: ExtensionContext) => {
|
|
503
509
|
if (ctx.hasUI) ctx.ui.setToolsExpanded(false);
|
|
504
|
-
return executor.
|
|
510
|
+
return executor.executePublic(id, params, signal, onUpdate, ctx);
|
|
505
511
|
};
|
|
506
512
|
|
|
507
513
|
const slashBridge = registerSlashSubagentBridge({
|
|
@@ -525,7 +531,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
525
531
|
const rpcBridge = registerSubagentRpcBridge({
|
|
526
532
|
events: pi.events,
|
|
527
533
|
getContext: () => state.lastUiContext,
|
|
528
|
-
execute: (id, params, signal, onUpdate, ctx) => executor.
|
|
534
|
+
execute: (id, params, signal, onUpdate, ctx) => executor.executePublic(id, params, signal, onUpdate, ctx),
|
|
529
535
|
state,
|
|
530
536
|
});
|
|
531
537
|
|
|
@@ -537,11 +543,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
537
543
|
parameters: SubagentParams,
|
|
538
544
|
|
|
539
545
|
execute(id, params, signal, onUpdate, ctx) {
|
|
540
|
-
|
|
541
|
-
if (input.tasks !== undefined || input.chain !== undefined || input.concurrency !== undefined || input.chainDir !== undefined || (input.worktree !== undefined && !(input.worktree === true && input.agent))) {
|
|
542
|
-
return Promise.resolve({ content: [{ type: "text", text: "Legacy top-level chain and parallel inputs were removed; use workflowScript." }], isError: true, details: { mode: "management", results: [] } });
|
|
543
|
-
}
|
|
544
|
-
return executeSubagentCollapsed(id, input, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
546
|
+
return executeSubagentCollapsed(id, params as SubagentParamsLike, signal ?? new AbortController().signal, onUpdate, ctx);
|
|
545
547
|
},
|
|
546
548
|
|
|
547
549
|
renderCall(args, theme) {
|
|
@@ -554,11 +556,11 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
554
556
|
}
|
|
555
557
|
if (args.workflowScript)
|
|
556
558
|
return new Text(
|
|
557
|
-
`${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async,
|
|
559
|
+
`${theme.fg("toolTitle", theme.bold("subagent "))}${formatWorkflowManifest(args.workflowScript, args.async, false)}`,
|
|
558
560
|
0,
|
|
559
561
|
0,
|
|
560
562
|
);
|
|
561
|
-
const asyncLabel = args.async === true
|
|
563
|
+
const asyncLabel = args.async === true ? theme.fg("warning", " [async]") : "";
|
|
562
564
|
return new Text(
|
|
563
565
|
`${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
564
566
|
0,
|
|
@@ -581,8 +583,24 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
581
583
|
registerWaitTool(pi, state, waitToolConfig.enabled, waitSubscriptionManager);
|
|
582
584
|
|
|
583
585
|
pi.on("agent_end", async (_event, ctx) => {
|
|
584
|
-
if (ctx.hasUI)
|
|
585
|
-
|
|
586
|
+
if (!ctx.hasUI) await drainOutstandingWork({ state, events: pi.events });
|
|
587
|
+
const ownerSessionId = state.currentSessionId;
|
|
588
|
+
if (!ownerSessionId) return;
|
|
589
|
+
goalTurnId += 1;
|
|
590
|
+
try {
|
|
591
|
+
const location = resolveMissionStoreLocation({ projectRoot: state.baseCwd, ...(config.missions ? { config: config.missions } : {}) });
|
|
592
|
+
const retainedChildren = listRetainedChildren(DIRS.async, ownerSessionId);
|
|
593
|
+
for (const notice of collectGoalContinuationNotices({ location, ownerSessionId, retainedChildren, turnId: goalTurnId })) {
|
|
594
|
+
handleSubagentControlNotice({
|
|
595
|
+
pi,
|
|
596
|
+
state,
|
|
597
|
+
visibleControlNotices: new Set(),
|
|
598
|
+
details: { source: "goal", event: notice.event, noticeText: notice.message },
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
} catch (error) {
|
|
602
|
+
console.error("Failed to evaluate goal missions:", error);
|
|
603
|
+
}
|
|
586
604
|
});
|
|
587
605
|
|
|
588
606
|
registerSlashCommands(pi, state);
|
|
@@ -678,6 +696,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
678
696
|
|
|
679
697
|
const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
|
|
680
698
|
state.baseCwd = ctx.cwd;
|
|
699
|
+
goalTurnId = 0;
|
|
681
700
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
682
701
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
683
702
|
state.subagentSpawns = {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface PublicSubagentExecutionParams {
|
|
2
|
+
action?: unknown;
|
|
3
|
+
agent?: unknown;
|
|
4
|
+
task?: unknown;
|
|
5
|
+
step?: unknown;
|
|
6
|
+
tasks?: unknown;
|
|
7
|
+
chain?: unknown;
|
|
8
|
+
parallel?: unknown;
|
|
9
|
+
concurrency?: unknown;
|
|
10
|
+
chainDir?: unknown;
|
|
11
|
+
workflowScript?: unknown;
|
|
12
|
+
resume?: unknown;
|
|
13
|
+
clarify?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PublicSubagentExecutionMode = "workflow" | "management";
|
|
17
|
+
|
|
18
|
+
export type PublicSubagentExecutionNormalization<T> =
|
|
19
|
+
| { ok: true; params: T }
|
|
20
|
+
| { ok: false; error: string; mode: PublicSubagentExecutionMode };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enforce the public execution cutover before requests reach the executor.
|
|
24
|
+
* Internal runs.run children and structured owned delegation bypass this boundary.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizePublicSubagentExecution<T extends PublicSubagentExecutionParams>(params: T): PublicSubagentExecutionNormalization<T> {
|
|
27
|
+
const action = params.action;
|
|
28
|
+
if (action !== undefined && (typeof action !== "string" || !action.trim())) {
|
|
29
|
+
return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
|
|
30
|
+
}
|
|
31
|
+
const normalizedAction = typeof action === "string" ? action.trim() : undefined;
|
|
32
|
+
if (params.clarify !== undefined) {
|
|
33
|
+
return { ok: false, error: "Public workflowScript execution does not support clarify UI.", mode: "workflow" };
|
|
34
|
+
}
|
|
35
|
+
if (params.resume !== undefined) {
|
|
36
|
+
return { ok: false, error: "Top-level resume execution is not available. Put resume on a workflowScript runs.run/runs.all item.", mode: "workflow" };
|
|
37
|
+
}
|
|
38
|
+
const hasLegacyOrchestration = params.tasks !== undefined || params.chain !== undefined || params.parallel !== undefined || params.concurrency !== undefined || params.chainDir !== undefined;
|
|
39
|
+
if (hasLegacyOrchestration) {
|
|
40
|
+
return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: normalizedAction ? "management" : "workflow" };
|
|
41
|
+
}
|
|
42
|
+
if (normalizedAction !== undefined) {
|
|
43
|
+
const legacyAction = normalizedAction.toLowerCase();
|
|
44
|
+
if (legacyAction === "single") {
|
|
45
|
+
return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
|
|
46
|
+
}
|
|
47
|
+
if (legacyAction === "parallel" || legacyAction === "tasks" || legacyAction === "chain") {
|
|
48
|
+
return { ok: false, error: "Legacy top-level chain and parallel inputs were removed; use workflowScript.", mode: "workflow" };
|
|
49
|
+
}
|
|
50
|
+
if (normalizedAction === "schedule.create") {
|
|
51
|
+
if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
|
|
52
|
+
return { ok: false, error: "schedule.create requires workflowScript and does not accept direct agent, task, or step execution fields.", mode: "management" };
|
|
53
|
+
}
|
|
54
|
+
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
|
|
55
|
+
return { ok: false, error: "schedule.create requires a non-empty workflowScript.", mode: "management" };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, params: { ...params, action: normalizedAction } };
|
|
58
|
+
}
|
|
59
|
+
if (params.workflowScript !== undefined) {
|
|
60
|
+
return { ok: false, error: "workflowScript execution must omit action; only schedule.create accepts action with workflowScript.", mode: "management" };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, params: { ...params, action: normalizedAction } };
|
|
63
|
+
}
|
|
64
|
+
if (params.agent !== undefined || params.task !== undefined || params.step !== undefined) {
|
|
65
|
+
return { ok: false, error: "Direct execution was removed. Use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
|
|
66
|
+
}
|
|
67
|
+
if (typeof params.workflowScript !== "string" || !params.workflowScript.trim()) {
|
|
68
|
+
return { ok: false, error: "Execution requires a non-empty workflowScript. Direct execution was removed; use workflowScript: \"return runs.run('main', { agent, task })\".", mode: "workflow" };
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, params };
|
|
71
|
+
}
|
package/src/extension/rpc.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { readStatus } from "../shared/utils.ts";
|
|
20
20
|
import { SubagentParams } from "./schemas.ts";
|
|
21
21
|
import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
|
|
22
|
+
import { normalizePublicSubagentExecution } from "./public-execution.ts";
|
|
22
23
|
|
|
23
24
|
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
24
25
|
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
@@ -420,19 +421,15 @@ async function executeChecked(
|
|
|
420
421
|
|
|
421
422
|
function spawnParams(params: unknown): SubagentParamsLike {
|
|
422
423
|
const input = assertRecordParams(params, "spawn");
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
if (input.action !== undefined) {
|
|
424
|
+
const normalized = normalizePublicSubagentExecution(input);
|
|
425
|
+
if (!normalized.ok) throw new SubagentRpcError("invalid_params", normalized.error);
|
|
426
|
+
if (normalized.params.action !== undefined) {
|
|
427
427
|
throw new SubagentRpcError("invalid_params", "RPC spawn does not accept management/control actions. Use status or interrupt RPC methods instead.");
|
|
428
428
|
}
|
|
429
429
|
if (input.async === false) {
|
|
430
430
|
throw new SubagentRpcError("invalid_params", "RPC spawn only supports detached async launches; omit async or set async: true.");
|
|
431
431
|
}
|
|
432
|
-
|
|
433
|
-
throw new SubagentRpcError("invalid_params", "RPC spawn cannot open the clarify UI; omit clarify or set clarify: false.");
|
|
434
|
-
}
|
|
435
|
-
return { ...(input as SubagentParamsLike), async: true, clarify: false };
|
|
432
|
+
return { ...(normalized.params as SubagentParamsLike), async: true };
|
|
436
433
|
}
|
|
437
434
|
|
|
438
435
|
function steerParams(params: unknown): SubagentParamsLike {
|
|
@@ -441,10 +438,12 @@ function steerParams(params: unknown): SubagentParamsLike {
|
|
|
441
438
|
throw new SubagentRpcError("invalid_params", "RPC steer requires a non-empty message.");
|
|
442
439
|
const target = normalizeTargetParams(input, "steer");
|
|
443
440
|
if (!target.id && !target.runId && !target.dir) throw new SubagentRpcError("invalid_params", "RPC steer requires id, runId, or dir.");
|
|
441
|
+
if (input.mode !== undefined && input.mode !== "steer" && input.mode !== "follow_up" && input.mode !== "auto") throw new SubagentRpcError("invalid_params", "RPC steer mode must be steer, follow_up, or auto.");
|
|
444
442
|
return {
|
|
445
443
|
action: "steer",
|
|
446
444
|
...target,
|
|
447
445
|
message: input.message.trim(),
|
|
446
|
+
...(typeof input.mode === "string" ? { mode: input.mode as "steer" | "follow_up" | "auto" } : {}),
|
|
448
447
|
steeringRecovery: false,
|
|
449
448
|
};
|
|
450
449
|
}
|
package/src/extension/schemas.ts
CHANGED
|
@@ -255,11 +255,11 @@ const ControlOverrides = Type.Object({
|
|
|
255
255
|
});
|
|
256
256
|
|
|
257
257
|
const SubagentParamsSchema = Type.Object({
|
|
258
|
-
agent: Type.Optional(Type.String({ description: "Agent
|
|
259
|
-
|
|
258
|
+
agent: Type.Optional(Type.String({ description: "Agent target for management actions such as get, update, delete, and models." })),
|
|
259
|
+
resume: Type.Optional(Type.String({ description: "Retained child run id for a workflowScript runs.run/runs.all item. Mutually exclusive with agent; task supplies the follow-up." })),
|
|
260
260
|
// Management action (when present, tool operates in management mode)
|
|
261
|
-
action: Type.Optional(Type.String({
|
|
262
|
-
description: "Optional management/control action. Omit this field
|
|
261
|
+
action: Type.Optional(Type.String({ minLength: 1,
|
|
262
|
+
description: "Optional management/control action. Omit this field for workflowScript execution; use it only for management/control actions."
|
|
263
263
|
})),
|
|
264
264
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
265
265
|
id: Type.Optional(Type.String({
|
|
@@ -279,7 +279,8 @@ const SubagentParamsSchema = Type.Object({
|
|
|
279
279
|
})),
|
|
280
280
|
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
|
|
281
281
|
message: Type.Optional(Type.String({ description: "Follow-up message for resume, live guidance for steer, or optional startup prompt for project.open." })),
|
|
282
|
-
|
|
282
|
+
mode: Type.Optional(Type.String({ enum: ["steer", "follow_up", "auto"], description: "Delivery mode for action='steer'. steer interrupts at the next safe point (default), follow_up waits for the next turn boundary, and auto follows up mid-turn but delivers immediately between turns." })),
|
|
283
|
+
steeringRecovery: Type.Optional(Type.Boolean({ description: "For action='steer', allow pause-and-revive recovery after a missed acknowledgment. Defaults true for direct tool calls in steer mode; extension RPC steering forces false so callers retain exact child ownership." })),
|
|
283
284
|
additional: Type.Optional(Type.Integer({ minimum: 1, description: "Positive launches to add with action='grant-spawn-budget'. Root interactive parent with native user confirmation only; total grants cannot exceed the original configured cap." })),
|
|
284
285
|
scope: Type.Optional(Type.String({ enum: ["session", "user", "project"], description: "Scope for action='watchdog.configure'. Defaults to session to avoid persistent settings writes unless user/project is explicit." })),
|
|
285
286
|
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
|
|
@@ -294,8 +295,8 @@ const SubagentParamsSchema = Type.Object({
|
|
|
294
295
|
overlap: Type.Optional(Type.String({ enum: ["skip"], description: "Overlap policy. This slice supports skip only." })),
|
|
295
296
|
catchUp: Type.Optional(Type.String({ enum: ["none", "latest"], description: "Missed occurrence policy for recurring schedules. Defaults to latest." })),
|
|
296
297
|
missionId: Type.Optional(Type.String({ description: "Mission id." })),
|
|
297
|
-
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission." })),
|
|
298
|
-
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
298
|
+
mission: Type.Optional(Type.Unsafe({ ...MissionLaunchOverride, description: "Mission object, or false for no mission. Use objective for intent; goal:true with budget.tokens enables turn-end continuation notices." })),
|
|
299
|
+
missionUpdate: Type.Optional(Type.Unsafe({ ...MissionUpdateOverride, description: "Mission update: objective, goal false or {paused:boolean}, budget, summary, labels, decisions, artifacts, or delivery receipts." })),
|
|
299
300
|
missionStatus: Type.Optional(Type.String({ description: "Mission status." })),
|
|
300
301
|
missionScope: Type.Optional(Type.String({ description: "Mission list scope: project (default) or global pointer index." })),
|
|
301
302
|
runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
|
|
@@ -313,9 +314,9 @@ const SubagentParamsSchema = Type.Object({
|
|
|
313
314
|
],
|
|
314
315
|
description: "Agent/chain config for create/update. Object or JSON string; presence of steps creates a chain."
|
|
315
316
|
})),
|
|
316
|
-
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript
|
|
317
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Starts async by default; pass async:false for a small foreground run. Use explicit return for output. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), runs.status(id), runs.ref(s), emit(value), console, and return. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Use ordinary JavaScript loops, branches, awaits, and arrays to mix sequential and parallel phases dynamically. Set worktree:true at workflow or child level for a separate managed worktree; child fields override workflow defaults. gate is one host-run command and cannot be combined with acceptance. runs.run accepts one child only. No filesystem, shell, Pi tools, or host globals." })),
|
|
317
318
|
chatProgress: Type.Optional(Type.String({ enum: ["auto", "off", "live-card"], description: "WorkflowScript chat progress projection. auto shows a live in-chat card only for watched foreground workflows in the same Git repository; it is off otherwise." })),
|
|
318
|
-
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives
|
|
319
|
+
worktree: Type.Optional(Type.Boolean({ description: "Managed child isolation. true gives each workflow child a separate git worktree; an individual runs.run/runs.all item can override a workflow default with worktree:false." })),
|
|
319
320
|
step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
|
|
320
321
|
context: Type.Optional(Type.String({
|
|
321
322
|
enum: ["fresh", "fork"],
|
|
@@ -335,23 +336,22 @@ const SubagentParamsSchema = Type.Object({
|
|
|
335
336
|
sessionDir: Type.Optional(
|
|
336
337
|
Type.String({ description: "Directory to store session logs (default: temp; enables sessions even if share=false)" }),
|
|
337
338
|
),
|
|
338
|
-
// Clarification TUI
|
|
339
|
-
clarify: Type.Optional(Type.Boolean({ description: "Show TUI to preview/edit before execution. Explicit clarify: true keeps the run foreground for the clarify UI; omitted clarify can still run in the background when async: true is set." })),
|
|
340
339
|
control: Type.Optional(ControlOverrides),
|
|
341
|
-
//
|
|
340
|
+
// Workflow defaults forwarded to each runs.run/runs.all child unless overridden there.
|
|
342
341
|
output: Type.Optional(Type.Unsafe({
|
|
343
342
|
anyOf: [
|
|
344
343
|
{ type: "string" },
|
|
345
344
|
{ type: "boolean" },
|
|
346
345
|
],
|
|
347
|
-
description: "
|
|
346
|
+
description: "Default child output file (string), or false to disable. Relative paths resolve against cwd.",
|
|
348
347
|
})),
|
|
349
348
|
outputMode: Type.Optional(OutputModeOverride),
|
|
350
349
|
skill: Type.Optional(SkillOverride),
|
|
351
|
-
model: Type.Optional(Type.String({ description: "
|
|
350
|
+
model: Type.Optional(Type.String({ description: "Default child model override (e.g. 'anthropic/claude-sonnet-4')" })),
|
|
352
351
|
outputSchema: Type.Optional(JsonSchemaObject),
|
|
353
352
|
agentContract: Type.Optional(AgentContractOverride),
|
|
354
353
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
354
|
+
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
|
|
355
355
|
});
|
|
356
356
|
|
|
357
357
|
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
@@ -8,42 +8,41 @@ const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
|
8
8
|
|
|
9
9
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
10
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
|
-
• Keep execution and management separate: omit action for
|
|
11
|
+
• Keep execution and management separate: omit action for workflowScript execution; use action only for management/control.
|
|
12
12
|
• Async/background runs are the default. Use async:false only when a blocking foreground result is needed. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
13
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
14
|
• Keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers for independent review, then have the parent synthesize and apply fixes.
|
|
15
15
|
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
|
|
16
16
|
|
|
17
|
-
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `
|
|
17
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
|
|
18
18
|
|
|
19
|
-
EXECUTION
|
|
19
|
+
EXECUTION:
|
|
20
20
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
|
-
•
|
|
22
|
-
•
|
|
23
|
-
•
|
|
24
|
-
•
|
|
25
|
-
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async runs; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
21
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Every execution is a workflow. Use stable-key runs.run for one child and runs.all for parallel children; ordinary JavaScript provides sequence, branching, filtering, retries, and aggregation. workflowScript is an ordinary JavaScript statement body, so use an explicit return for a useful result. Scripts start asynchronously by default; pass async:false only for a small foreground run. Same-repo foreground workflows default to a live in-chat card; set chatProgress to auto, off, or live-card to control that projection. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list up to 10 completed retained children from this parent session, then continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); resume and agent are mutually exclusive, and resume keeps the stored agent/model/tool contract. For repository mutation lanes, set worktree:true on the workflow or individual runs.run/runs.all item for managed isolation; each parallel child gets a separate worktree and handoff artifact. A workflow usageBudget is enforced once across the workflow. Available globals are runs.run, runs.all, runs.status, runs.ref/refs, emit, console, and standard JavaScript only. Workflows get async state.get(key) and state.set(key, JSONValue) through their automatic or explicit mission; mission:false workflows do not have a state global. Scripts cannot access filesystem, shell, arbitrary Pi tools, or host globals.
|
|
22
|
+
• Sequential example: { workflowScript: "const a = await runs.run('analyze', {agent:'agent-a', task:'Analyze the request'}); return (await runs.run('plan', {agent:'agent-b', task:'Plan from: '+a.output})).output" }
|
|
23
|
+
• Parallel example: { workflowScript: "const [a,b] = await runs.all([{key:'correctness',agent:'agent-a',task:'Review correctness'},{key:'tests',agent:'agent-b',task:'Review tests'}]); return {correctness:a.output,tests:b.output}" }
|
|
24
|
+
• Optional context is "fresh" or "fork". timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls; evidence levels end at verified, and acceptance.review.required requests independent writer review.
|
|
26
25
|
• Durable mission attachment is automatic by default. Use missionId to attach an existing mission, mission:{...} to override auto-create, or mission:false for ephemeral work.
|
|
27
26
|
|
|
28
27
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
29
|
-
• list, get, models, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, mission.create/list/show/update/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available.
|
|
28
|
+
• list, get, models, children.list, create, update, delete, eject, disable, enable, reset, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available.
|
|
30
29
|
• status, interrupt, stop, resume, and steer manage live or persisted runs. Use status view:"fleet" for an overview or view:"transcript" with id and optional index to tail output.
|
|
31
30
|
• { action: "append-step", id: "...", step: {agent:"agent-c", task:"Use {previous}"} } appends one step to an already-running durable legacy chain. step is control-only, not an execution mode.
|
|
32
31
|
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
33
|
-
• Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, agent, task
|
|
32
|
+
• Create durable project schedules with { action:"schedule.create", id?, name?, at:"+10m" | ISO, workflowScript:"return runs.run('main', {agent:'worker', task:'...'})" } or { every:"6h", workflowScript:"..." }. Manage them with schedule.list/show/history/pause/resume/run/run-due/delete. This first slice supports fixed intervals; calendar schedules and schedule mission attachment are deferred.
|
|
34
33
|
|
|
35
34
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
36
35
|
|
|
37
|
-
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `
|
|
36
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run subagents only through { workflowScript }; omit action. Use action only for management/control actions.
|
|
38
37
|
|
|
39
38
|
EXECUTE:
|
|
40
39
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
|
-
•
|
|
40
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work. Use {action:"children.list"} for the last 10 retained children in this parent session, then runs.run(key,{resume:"run-id",task:"follow-up"}) to continue one with its stored contract. Workflows get async state.get/state.set through their automatic or explicit mission; mission:false does not. Scripts are ordinary JavaScript statement bodies; use explicit return for a useful result. Use JavaScript for sequence, branching, retries, and aggregation. For repository mutation lanes, use worktree:true on the workflow or runs.run/runs.all item for managed isolation. Scripts start async by default; async:false is the foreground escape hatch and auto-enables a same-repo live chat card unless chatProgress is off.
|
|
42
41
|
• Example: {workflowScript:"const [a,b]=await runs.all([{key:'a',agent:'agent-a',task:'Implement A',worktree:true},{key:'b',agent:'agent-b',task:'Implement B',worktree:true}]); return [a.output,b.output]"}
|
|
43
|
-
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async
|
|
42
|
+
• context can be fresh or fork. timeoutMs/maxRuntimeMs apply to foreground and async workflows; foreground workflows default to 30 minutes and async workflows have no default timeout. Omit acceptance for reviewer/read-only calls.
|
|
44
43
|
|
|
45
44
|
MANAGE / CONTROL:
|
|
46
|
-
• Use action without execution fields for list/get/models/authoring, mission, watchdog, status, interrupt, stop, resume, steer, scheduling, diagnostics, and other management actions.
|
|
45
|
+
• Use action without execution fields for list/get/models/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions.
|
|
47
46
|
• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
|
|
48
47
|
|
|
49
48
|
ASYNC / SAFETY:
|
|
@@ -51,6 +50,7 @@ ASYNC / SAFETY:
|
|
|
51
50
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
52
51
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
53
52
|
|
|
53
|
+
|
|
54
54
|
function isToolDescriptionMode(value: unknown): value is ToolDescriptionMode {
|
|
55
55
|
return value === "full" || value === "compact" || value === "custom";
|
|
56
56
|
}
|