pi-subagents 0.49.0 → 0.51.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 +97 -0
- package/agents/gpt-pro.md +17 -0
- package/agents/oracle.md +7 -5
- package/agents/researcher.md +1 -1
- package/agents/reviewer.md +2 -2
- package/agents/scout.md +1 -1
- package/agents/worker.md +1 -1
- package/async-retention-discovery-worker.mjs +180 -0
- package/docs/agents.md +37 -2
- package/docs/configuration.md +76 -14
- package/docs/extension-api.md +78 -1
- package/docs/missions.md +1 -1
- package/docs/observability.md +20 -4
- package/docs/tool-reference.md +55 -39
- package/docs/workflows.md +171 -5
- package/package.json +4 -2
- package/skills/pi-subagents/SKILL.md +5 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +9 -6
- package/skills/pi-subagents/references/execution-controls.md +22 -18
- package/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/skills/pi-subagents/references/prompting-and-roles.md +33 -17
- package/src/agents/agent-management.ts +100 -345
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +135 -25
- package/src/api/external-job-provider.ts +185 -0
- package/src/api/external-runs.ts +174 -84
- package/src/api/preflight.ts +42 -11
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +36 -3
- package/src/extension/doctor.ts +3 -6
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +210 -90
- package/src/extension/public-execution.ts +31 -2
- package/src/extension/rpc.ts +5 -1
- package/src/extension/schemas.ts +14 -36
- package/src/extension/tool-description.ts +37 -24
- package/src/inspectors/herdr/actions.ts +5 -9
- package/src/inspectors/herdr/inspector-runner.ts +2 -1
- package/src/inspectors/herdr/project-panes.ts +4 -8
- package/src/inspectors/herdr/shell-command.ts +16 -0
- package/src/intercom/intercom-bridge.ts +2 -3
- package/src/intercom/native-supervisor-channel.ts +49 -51
- package/src/missions/goal-driver.ts +3 -1
- package/src/missions/lifecycle.ts +6 -1
- package/src/missions/store.ts +4 -9
- package/src/profiles/profiles.ts +3 -1
- package/src/runs/background/active-run-index.ts +94 -1
- package/src/runs/background/async-execution.ts +98 -38
- package/src/runs/background/async-job-tracker.ts +21 -4
- package/src/runs/background/async-resume.ts +30 -17
- package/src/runs/background/async-retention.ts +888 -0
- package/src/runs/background/async-status-snapshot.ts +277 -0
- package/src/runs/background/async-status.ts +47 -56
- package/src/runs/background/chain-append.ts +3 -33
- package/src/runs/background/chain-root-attachment.ts +2 -2
- package/src/runs/background/completion-replay.ts +11 -1
- package/src/runs/background/control-channel.ts +14 -68
- package/src/runs/background/fleet-view.ts +3 -1
- package/src/runs/background/index-segment.ts +59 -0
- package/src/runs/background/notify.ts +3 -1
- package/src/runs/background/result-files.ts +505 -0
- package/src/runs/background/result-watcher.ts +250 -51
- package/src/runs/background/retained-children.ts +79 -20
- package/src/runs/background/run-id-query.ts +7 -0
- package/src/runs/background/run-id-resolver.ts +37 -29
- package/src/runs/background/run-status.ts +34 -20
- package/src/runs/background/scheduled-runs.ts +71 -27
- package/src/runs/background/stale-run-reconciler.ts +33 -16
- package/src/runs/background/steering.ts +11 -1
- package/src/runs/background/subagent-runner.ts +535 -161
- package/src/runs/background/subagent-wait.ts +9 -7
- package/src/runs/background/terminal-run-index.ts +129 -0
- package/src/runs/background/wait-completions.ts +22 -2
- package/src/runs/background/wait-subscriptions.ts +80 -1
- package/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/src/runs/foreground/async-steering-action.ts +21 -14
- package/src/runs/foreground/execution.ts +221 -15
- package/src/runs/foreground/subagent-executor.ts +529 -1519
- package/src/runs/foreground/workflow-foreground-steering.ts +6 -5
- package/src/runs/shared/chain-outputs.ts +1 -3
- package/src/runs/shared/completion-guard.ts +96 -6
- package/src/runs/shared/external-cli-runner.ts +4 -0
- package/src/runs/shared/external-job-bridge.ts +450 -0
- package/src/runs/shared/external-job-runner.ts +286 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +14 -0
- package/src/runs/shared/model-fallback.ts +34 -3
- package/src/runs/shared/nested-events.ts +66 -62
- package/src/runs/shared/orca-progress-tabs.ts +437 -0
- package/src/runs/shared/parallel-handoff.ts +46 -4
- package/src/runs/shared/parallel-utils.ts +6 -15
- package/src/runs/shared/permissions.ts +5 -1
- package/src/runs/shared/pi-args.ts +8 -1
- package/src/runs/shared/subagent-control.ts +41 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -15
- package/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/src/runs/shared/tool-timeout.ts +93 -0
- package/src/runs/shared/workflow-graph.ts +1 -23
- package/src/runs/shared/worktree.ts +12 -1
- package/src/shared/atomic-json.ts +22 -2
- package/src/shared/capacity-resilient-json.ts +102 -0
- package/src/shared/completion-owner.ts +14 -0
- package/src/shared/file-system-retry.ts +49 -1
- package/src/shared/fork-context.ts +42 -0
- package/src/shared/prompt-resources.ts +0 -40
- package/src/shared/settings.ts +3 -27
- package/src/shared/types.ts +88 -27
- package/src/shared/utils.ts +8 -0
- package/src/shared/watch-strategy.ts +10 -0
- package/src/slash/slash-commands.ts +45 -28
- package/src/slash/slash-live-state.ts +3 -0
- package/src/tui/fleet-status.ts +160 -45
- package/src/tui/fleet.ts +186 -28
- package/src/tui/render.ts +41 -10
- package/src/workflows/chat-progress.ts +7 -5
- package/src/workflows/scripted-workflow.ts +424 -125
- package/src/runs/foreground/chain-clarify.ts +0 -1354
- package/src/runs/foreground/chain-execution.ts +0 -1565
package/src/extension/schemas.ts
CHANGED
|
@@ -93,7 +93,7 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
const AgentContractOverride = Type.Object({
|
|
96
|
-
version: Type.Integer({
|
|
96
|
+
version: Type.Integer({ minimum: 1, maximum: 1, description: "Enable compatibility behavior for this run/child." }),
|
|
97
97
|
}, { additionalProperties: false, description: "Compatibility behavior. Omit for the default behavior." });
|
|
98
98
|
|
|
99
99
|
const ChainGateOverride = Type.String({
|
|
@@ -188,8 +188,6 @@ export const DynamicCollectSchema = Type.Object({
|
|
|
188
188
|
|
|
189
189
|
// Flattened so chain steps do not need an object-shape anyOf/oneOf union.
|
|
190
190
|
export const ChainItem = Type.Object({
|
|
191
|
-
checkpoint: Type.Optional(Type.String({ description: "Approval checkpoint name. Pauses the chain without launching a child until approve-checkpoint or reject-checkpoint is called." })),
|
|
192
|
-
message: Type.Optional(Type.String({ description: "Optional approval message shown while the checkpoint is paused." })),
|
|
193
191
|
agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
|
|
194
192
|
task: Type.Optional(Type.String({
|
|
195
193
|
description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder, {outputs.name}=prior named output. Required for first step, defaults to '{previous}' for subsequent steps."
|
|
@@ -224,7 +222,7 @@ export const ChainItem = Type.Object({
|
|
|
224
222
|
description: "Create isolated git worktrees for each parallel task."
|
|
225
223
|
})),
|
|
226
224
|
}, {
|
|
227
|
-
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, {expand, parallel: {...}, collect} for dynamic fanout
|
|
225
|
+
description: "Chain step: use {agent, task?, ...} for sequential, {parallel: [...]} for static concurrent execution, or {expand, parallel: {...}, collect} for dynamic fanout.",
|
|
228
226
|
additionalProperties: false,
|
|
229
227
|
});
|
|
230
228
|
|
|
@@ -257,17 +255,16 @@ const ControlOverrides = Type.Object({
|
|
|
257
255
|
const SubagentParamProperties = {
|
|
258
256
|
agent: Type.Optional(Type.String({ description: "Agent for one-child execution, or target for agent management actions." })),
|
|
259
257
|
task: Type.Optional(Type.String({ description: "Optional one-child task. Requires agent; cannot combine with action or workflowScript." })),
|
|
260
|
-
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." })),
|
|
261
258
|
// Management action (when present, tool operates in management mode)
|
|
262
259
|
action: Type.Optional(Type.String({ minLength: 1,
|
|
263
260
|
description: "Optional management/control action. Omit this field for structured single-child or workflowScript execution; use it only for management/control actions."
|
|
264
261
|
})),
|
|
265
262
|
name: Type.Optional(Type.String({ description: "Human-readable name for action='schedule.create'." })),
|
|
266
263
|
id: Type.Optional(Type.String({
|
|
267
|
-
description: "Run id/prefix for status/debug.run, interrupt, steer,
|
|
264
|
+
description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
|
|
268
265
|
})),
|
|
269
266
|
runId: Type.Optional(Type.String({
|
|
270
|
-
description: "Target run ID for debug.run, interrupt, steer,
|
|
267
|
+
description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
|
|
271
268
|
})),
|
|
272
269
|
dir: Type.Optional(Type.String({
|
|
273
270
|
description: "Async run directory for status/debug.run, stop, resume, or steer."
|
|
@@ -288,8 +285,6 @@ const SubagentParamProperties = {
|
|
|
288
285
|
target: Type.Optional(Type.String({ enum: ["main", "children", "child"], description: "Target for watchdog actions." })),
|
|
289
286
|
focus: Type.Optional(Type.Boolean({ description: "Focus the new Herdr pane for inspector.open or project.open." })),
|
|
290
287
|
thinking: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "boolean", enum: [false] }], description: "Thinking level for action='watchdog.configure' (off/minimal/low/medium/high/xhigh/max, inherit, or false for off)." })),
|
|
291
|
-
schedule: Type.Optional(Type.String({ deprecated: true, description: "Removed one-shot schedule field. Use action='schedule.create' with at." })),
|
|
292
|
-
scheduleName: Type.Optional(Type.String({ deprecated: true, description: "Removed schedule display field. Use name." })),
|
|
293
288
|
at: Type.Optional(Type.String({ description: "One-shot trigger for action='schedule.create': a relative delay such as '+10m' or an ISO timestamp with timezone." })),
|
|
294
289
|
every: Type.Optional(Type.String({ description: "Fixed recurring interval for action='schedule.create', such as '30m', '6h', '2d', or '2w'." })),
|
|
295
290
|
on: Type.Optional(Type.Unsafe({ anyOf: [{ type: "string" }, { type: "integer" }], description: "Calendar selector reserved for a later schedule slice." })),
|
|
@@ -304,29 +299,26 @@ const SubagentParamProperties = {
|
|
|
304
299
|
runMode: Type.Optional(Type.String({ description: "Attached run mode." })),
|
|
305
300
|
runStatus: Type.Optional(Type.String({ description: "Attached run status." })),
|
|
306
301
|
summary: Type.Optional(Type.String({ description: "Mission close summary." })),
|
|
307
|
-
//
|
|
308
|
-
chainName: Type.Optional(Type.String({
|
|
309
|
-
description: "Chain name for get/update/delete management actions"
|
|
310
|
-
})),
|
|
311
|
-
// Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
|
|
302
|
+
// Agent configuration for create/update (nested to avoid conflicts with execution fields)
|
|
312
303
|
config: Type.Optional(Type.Unsafe({
|
|
313
304
|
anyOf: [
|
|
314
305
|
{ type: "object", additionalProperties: true },
|
|
315
306
|
{ type: "string" },
|
|
316
307
|
],
|
|
317
|
-
description: "Agent
|
|
308
|
+
description: "Agent config for create/update. Object or JSON string."
|
|
318
309
|
})),
|
|
319
|
-
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body.
|
|
320
|
-
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." })),
|
|
310
|
+
workflowScript: Type.Optional(Type.String({ minLength: 1, description: "Trusted inline JavaScript statement body. Normally async unless asyncByDefault:false; set async:true when async matters. Use async:false only when the parent must block until completion, never for reviews or gates. Use explicit return for output. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. Use await runs.run(key, {agent, task, worktree?, gate?}) or runs.run(key, {resume, task}), runs.all([...]), await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}), runs.status(id), runs.ref(s), emit(value), console, and return. For ordinary parallel fanout, use await runs.all([{key, agent, task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout, and each must later be observed with direct await, Promise.race, or Promise.all. runs.steer targets a prior stable child key, never a raw run id, and must be awaited or returned. Mission workflows also have async state.get(key) and state.set(key, JSONValue). Compose 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." })),
|
|
311
|
+
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. Explicit live-card requires same-repository async:false; async workflows should omit chatProgress or use auto/off." })),
|
|
312
|
+
isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
|
|
321
313
|
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." })),
|
|
322
|
-
step: Type.Optional(Type.Unsafe({ ...ChainItem, description: "One chain step for action='append-step' only. Not an execution mode." })),
|
|
323
314
|
context: Type.Optional(Type.String({
|
|
324
315
|
enum: ["fresh", "fork"],
|
|
325
|
-
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child
|
|
316
|
+
description: "'fresh' or 'fork' to branch from parent session. Explicit context overrides every child. If omitted, config defaultSubagentContext wins over each agent defaultContext; implicit fork needs a persisted parent session and leaf, else fresh.",
|
|
326
317
|
})),
|
|
327
|
-
async: Type.Optional(Type.Boolean({ description: "Run in background
|
|
318
|
+
async: Type.Optional(Type.Boolean({ description: "Run in background unless asyncByDefault:false. Set false only when the parent must block until completion." })),
|
|
328
319
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Timeout. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline. Alias maxRuntimeMs." })),
|
|
329
320
|
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias timeoutMs. Foreground and single async runs use config timeoutMs, else 30m; async composites have no default parent deadline." })),
|
|
321
|
+
toolTimeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional hard per-tool-call timeout in milliseconds; known-fast built-in tools have a five-minute default." })),
|
|
330
322
|
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
331
323
|
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
332
324
|
usageBudget: Type.Optional(UsageBudgetOverride),
|
|
@@ -356,26 +348,12 @@ const SubagentParamProperties = {
|
|
|
356
348
|
gate: Type.Optional(Type.String({ minLength: 1, description: "Host gate command. Cannot be combined with acceptance." })),
|
|
357
349
|
};
|
|
358
350
|
|
|
359
|
-
const { step: _legacyChainStep, ...subagentParamPropertiesWithoutStep } = SubagentParamProperties;
|
|
360
|
-
const trimmedSubagentParamProperties = {
|
|
361
|
-
...subagentParamPropertiesWithoutStep,
|
|
362
|
-
id: Type.Optional(Type.String({
|
|
363
|
-
description: "Run id/prefix for status/debug.run, interrupt, steer, or mission.attach-run."
|
|
364
|
-
})),
|
|
365
|
-
runId: Type.Optional(Type.String({
|
|
366
|
-
description: "Target run ID for debug.run, interrupt, steer, or mission.attach-run. Prefer id."
|
|
367
|
-
})),
|
|
368
|
-
};
|
|
369
351
|
const SubagentParamsSchema = Type.Object(SubagentParamProperties);
|
|
370
|
-
const TrimmedSubagentParamsSchema = Type.Object(trimmedSubagentParamProperties);
|
|
371
352
|
|
|
372
353
|
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
373
|
-
export const SubagentParamsWithoutLegacyChainControls = keepTopLevelParameterDescriptions(TrimmedSubagentParamsSchema);
|
|
374
354
|
|
|
375
|
-
export function createSubagentParamsSchema(
|
|
376
|
-
return
|
|
377
|
-
? SubagentParams
|
|
378
|
-
: SubagentParamsWithoutLegacyChainControls;
|
|
355
|
+
export function createSubagentParamsSchema(): typeof SubagentParams {
|
|
356
|
+
return SubagentParams;
|
|
379
357
|
}
|
|
380
358
|
|
|
381
359
|
const SubagentWaitParamsSchema = Type.Object({
|
|
@@ -6,11 +6,25 @@ import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
|
6
6
|
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
7
|
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
8
|
|
|
9
|
+
export const DEFAULT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to configured subagents. For execution, omit action and use {agent, task?} for one child or workflowScript for orchestration. For multi-step or parallel work, make exactly one top-level subagent call with workflowScript and async:true; launch children only inside that script and do not make another top-level call for them. Use runs.run('key',{agent,task}) for one child, await runs.all([{key:'a',agent:'reviewer',task:'...'},{key:'b',agent:'reviewer',task:'...'}]) for ordinary parallel children, and pass completed child results via .output. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
|
|
10
|
+
|
|
11
|
+
export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
|
|
12
|
+
|
|
13
|
+
export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
|
|
14
|
+
"Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
|
|
15
|
+
"Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
|
|
16
|
+
"workflowScript means exactly one top-level subagent tool call with async:true. Inside it, use runs.run/runs.all to launch children; do not make another top-level subagent call for those children.",
|
|
17
|
+
"For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all.",
|
|
18
|
+
"Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
|
|
19
|
+
"Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
|
|
20
|
+
];
|
|
21
|
+
|
|
9
22
|
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
23
|
• Use { action: "list" } before execution and only run executable/non-disabled agents.
|
|
11
24
|
• Keep execution and management separate: omit action for structured single-child or workflowScript execution; use action only for management/control.
|
|
12
|
-
• Async/background runs are the default. Use async:false only when
|
|
25
|
+
• Async/background runs are the normal default unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Use async:false only when the parent must block until completion. Async mode still shows progress. Final reviews and gate checks stay async; needing a result is not a blocking reason. After an async launch, continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll status just to wait; use subagent_wait only when the current request must finish in this turn.
|
|
13
26
|
• Ordinary child subagents are not orchestrators. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
27
|
+
• Oracle/advisor consultations should use supervisor dialogue for material unknowns when available; request one-shot only when desired.
|
|
14
28
|
• 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
29
|
• Async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, status via { action: "status", id }, and lifecycle diagnostics via { action: "debug.run", id }. Include output paths and residual risks when reporting results.`;
|
|
16
30
|
|
|
@@ -19,17 +33,15 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
|
|
|
19
33
|
EXECUTION:
|
|
20
34
|
• Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
|
|
21
35
|
• SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one child through the workflow runtime. Workflow-level fields such as model, context, cwd, worktree, output, budgets, acceptance, and async remain defaults for that child. Do not combine agent/task with action or workflowScript.
|
|
22
|
-
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and runs.all for parallel children;
|
|
36
|
+
• WORKFLOW SCRIPT: { workflowScript: "return runs.run('main', {agent:'worker', task:'...'})" }. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel children; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. 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. Use top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. Scripts normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. Pass async:false only when the parent must block until completion, never for final reviews or gates. Same-repo blocking workflows default to a live in-chat card; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off. Workflow-level child controls default onto each runs.run launch, and explicit child fields override them. Use {action:"children.list"} to list recent retained workflow children with resumable/not-resumable reasons. Resume only rows reported resumable. For a simple follow-up or implementation challenge, use {action:"resume", id:"run-id", message:"..."}. Resume keeps the stored agent/model/tool contract. If no resumable child is listed, launch a same-role fallback challenge and label it as fallback. Inside workflowScript, continue one with runs.run(key, {resume:"run-id", task:"follow-up"}); workflow resumes wait for completed output, and loops must continue from each latest returned runId. Await runs.steer(key, message, {mode?, index?, ackTimeoutMs?}) to guide a prior keyed child without exposing its run id; receipts are queued, delivered, missed, or failed. Always await or return runs.steer. 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.steer, 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.
|
|
23
37
|
• 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" }
|
|
24
38
|
• 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}" }
|
|
25
|
-
• 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.
|
|
39
|
+
• Optional context is "fresh" or "fork". Explicit context wins. When omitted, config defaultSubagentContext wins over agent defaultContext. 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
40
|
• 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. A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
27
41
|
|
|
28
42
|
MANAGEMENT / CONTROL (use action; omit execution fields):
|
|
29
43
|
• list, get, models, guide, children.list, create, update, delete, eject, disable, enable, reset, status, debug.run, doctor, grant-spawn-budget, worktree.discard, refine/refine.show/refine.rollback, mission.create/list/show/update/resolve-decision/attach-run/close, inspector.open/status/close, project.open/status/close, and watchdog actions remain available. Use {action:"guide", topic:"overview"} for packaged current-version help; topics are overview, workflows, agents, missions, observability, tool-reference, configuration, models, watchdog, and extension-api.
|
|
30
44
|
• 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
|
-
• { 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
|
-
• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.
|
|
33
45
|
• 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
46
|
|
|
35
47
|
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
@@ -39,18 +51,18 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
|
|
|
39
51
|
EXECUTE:
|
|
40
52
|
• Call { action:"list" } first and use only executable/non-disabled agents.
|
|
41
53
|
• SINGLE {agent:"worker",task:"..."} starts exactly one child through the workflow runtime. Workflow-level fields remain child defaults. Do not combine agent/task with action or workflowScript.
|
|
42
|
-
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and runs.all for parallel work.
|
|
54
|
+
• SCRIPT {workflowScript:"return runs.run('main', {agent:'worker', task:'...'})"}. Use stable-key runs.run for one child and await runs.all([{key,agent,task}, ...]) for ordinary parallel work; do not read .output from unawaited runs.run launches. Stored runs.run promises are only for advanced rolling fanout and each must later be observed with direct await, Promise.race, or Promise.all. Await runs.steer(key,message,options?) to guide a prior keyed child; it returns queued, delivered, missed, or failed and never accepts a raw run id. Always await or return steering calls. Use {action:"children.list"} for recent retained workflow children and resume only rows reported resumable. Use {action:"resume",id:"run-id",message:"..."} for a simple follow-up or challenge; resume keeps the stored agent/model/tool contract. If none is resumable, launch a same-role fallback challenge and label it as fallback. Inside workflowScript use runs.run(key,{resume:"run-id",task:"follow-up"}) when the script must wait for completion and continue from the latest returned runId. 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 top-level await, plain helper functions, or explicit Promise chains; nested async function, arrow, and method helpers are rejected. For task text with Markdown fences or shell blocks, build quoted lines instead of nesting raw template literals: \`const task=["Run:","\`\`\`bash","npm test","\`\`\`"].join("\\n")\`. 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 normally start async unless config sets asyncByDefault:false; set async:true explicitly when async behavior matters. async:false blocks the parent until completion and auto-enables a same-repo live chat card unless chatProgress is off; explicit live-card requires same-repository async:false, so async workflows should omit chatProgress or use auto/off.
|
|
43
55
|
• 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]"}
|
|
44
|
-
• 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.
|
|
56
|
+
• context can be fresh or fork. Explicit context wins; omitted context follows defaultSubagentContext before agent defaultContext. 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.
|
|
45
57
|
|
|
46
58
|
MANAGE / CONTROL:
|
|
47
59
|
• Use action without execution fields for list/get/models/guide/authoring, refine/refine.show/refine.rollback, mission, watchdog, status, interrupt, stop, resume, steer, script-only scheduling, diagnostics, and other management actions. guide reads shipped current-version docs by topic.
|
|
48
|
-
• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.
|
|
49
60
|
• A mission object needs exactly one non-empty title or summary; objective and labels are optional. goal may only be true and requires budget:{tokens}.
|
|
50
61
|
|
|
51
62
|
ASYNC / SAFETY:
|
|
52
|
-
• Omitted async
|
|
63
|
+
• Omitted async follows asyncByDefault config; set async:true explicitly when async behavior matters. Continue independent work only until its next dependency barrier; consume the result before work that depends on it. Do not sleep or poll merely to wait; use subagent_wait only when this turn must receive results.
|
|
53
64
|
• Ordinary children are not orchestrators. Keep one writer per cwd/worktree and use fresh read-only reviewers for independent checks.
|
|
65
|
+
• Oracle/advisor consultations use available supervisor dialogue for material unknowns; request one-shot when desired.
|
|
54
66
|
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, and {action:"status",id:"..."}.`;
|
|
55
67
|
|
|
56
68
|
|
|
@@ -68,6 +80,19 @@ export interface ToolDescriptionOptions {
|
|
|
68
80
|
warn?: (message: string) => void;
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
export interface SubagentToolPromptMetadata {
|
|
84
|
+
promptSnippet?: string;
|
|
85
|
+
promptGuidelines?: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildSubagentToolPromptMetadata(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}): SubagentToolPromptMetadata {
|
|
89
|
+
if (config.toolDescriptionMode !== undefined) return {};
|
|
90
|
+
return {
|
|
91
|
+
promptSnippet: SUBAGENT_TOOL_PROMPT_SNIPPET,
|
|
92
|
+
promptGuidelines: SUBAGENT_TOOL_PROMPT_GUIDELINES,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
export function resolveToolDescriptionMode(config: Pick<ExtensionConfig, "toolDescriptionMode">, options?: ToolDescriptionOptions): ToolDescriptionMode {
|
|
72
97
|
const mode = config.toolDescriptionMode;
|
|
73
98
|
if (mode === undefined) return "full";
|
|
@@ -155,20 +180,8 @@ function withMandatorySafetyGuidance(description: string): string {
|
|
|
155
180
|
: SUBAGENT_SAFETY_GUIDANCE;
|
|
156
181
|
}
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"• approve-checkpoint and reject-checkpoint decide a paused durable legacy chain checkpoint.",
|
|
161
|
-
"• append-step uses step:{...} only for an already-running durable legacy chain; step is not an execution mode.",
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
function withoutLegacyChainControlGuidance(description: string): string {
|
|
165
|
-
return description
|
|
166
|
-
.split("\n")
|
|
167
|
-
.filter((line) => !LEGACY_CHAIN_CONTROL_GUIDANCE_LINES.has(line.trim()))
|
|
168
|
-
.join("\n");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode" | "legacyChainControls"> = {}, options?: ToolDescriptionOptions): string {
|
|
183
|
+
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}, options?: ToolDescriptionOptions): string {
|
|
184
|
+
if (config.toolDescriptionMode === undefined) return DEFAULT_SUBAGENT_TOOL_DESCRIPTION;
|
|
172
185
|
const mode = resolveToolDescriptionMode(config, options);
|
|
173
186
|
let description: string;
|
|
174
187
|
if (mode === "compact") description = COMPACT_SUBAGENT_TOOL_DESCRIPTION;
|
|
@@ -180,5 +193,5 @@ export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "tool
|
|
|
180
193
|
description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
181
194
|
}
|
|
182
195
|
} else description = FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
183
|
-
return
|
|
196
|
+
return description;
|
|
184
197
|
}
|
|
@@ -12,6 +12,7 @@ import { readStatus } from "../../shared/utils.ts";
|
|
|
12
12
|
import { resolveSubagentRunId } from "../../runs/background/run-id-resolver.ts";
|
|
13
13
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
14
14
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode, type HerdrResult } from "./client.ts";
|
|
15
|
+
import { formatShellCommand } from "./shell-command.ts";
|
|
15
16
|
|
|
16
17
|
export const HERDR_INSPECTOR_ACTIONS = ["inspector.open", "inspector.status", "inspector.close"] as const;
|
|
17
18
|
export type HerdrInspectorAction = typeof HERDR_INSPECTOR_ACTIONS[number];
|
|
@@ -86,16 +87,11 @@ function extractPaneId(value: unknown): string | undefined {
|
|
|
86
87
|
return undefined;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
function shellQuote(value: string): string {
|
|
90
|
-
if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
91
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
90
|
function inspectorCommand(input: { runnerPath: string; asyncDir: string; runId: string; index?: number; missionPath?: string; allowSteer: boolean; allowStop: boolean; sessionRoots: string[] }): string {
|
|
95
|
-
const args = [
|
|
91
|
+
const args = [input.runnerPath, "--async-dir", input.asyncDir, "--run-id", input.runId, "--allow-steer", String(input.allowSteer), "--allow-stop", String(input.allowStop), "--session-roots", JSON.stringify(input.sessionRoots)];
|
|
96
92
|
if (input.index !== undefined) args.push("--index", String(input.index));
|
|
97
93
|
if (input.missionPath) args.push("--mission-path", input.missionPath);
|
|
98
|
-
return
|
|
94
|
+
return formatShellCommand(resolveNodeExecutable(), args);
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
function missionForRun(asyncDir: string, cwd: string, config: MissionStoreConfig | undefined, runId: string): { id: string; path: string } | undefined {
|
|
@@ -197,7 +193,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
197
193
|
if (live.ok) return result(`Herdr inspector pane ${existing.paneId} is already open for async run ${target.runId}.${params.focus ? " Herdr cannot refocus an arbitrary raw pane id; select it in the Herdr UI." : ""}`);
|
|
198
194
|
}
|
|
199
195
|
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", status.cwd ?? deps.cwd];
|
|
200
|
-
|
|
196
|
+
splitArgs.push(params.focus === true ? "--focus" : "--no-focus");
|
|
201
197
|
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: deps.signal });
|
|
202
198
|
if (split.ok === false) return result(formatHerdrError(split.error), true);
|
|
203
199
|
const paneId = extractPaneId(split.data);
|
|
@@ -229,7 +225,7 @@ export async function handleHerdrInspectorAction(action: HerdrInspectorAction, p
|
|
|
229
225
|
...(mission ? { missionId: mission.id, missionPath: mission.path } : {}),
|
|
230
226
|
paneId,
|
|
231
227
|
openedAt: now,
|
|
232
|
-
...(params.focus
|
|
228
|
+
...(params.focus === true ? { lastFocusedAt: now } : {}),
|
|
233
229
|
herdrVersion: detected.data.versionText,
|
|
234
230
|
command,
|
|
235
231
|
};
|
|
@@ -6,6 +6,7 @@ import { parseMissionRecord } from "../../missions/store.ts";
|
|
|
6
6
|
import type { MissionRecord } from "../../missions/types.ts";
|
|
7
7
|
import { requestAsyncSteer, requestAsyncStop } from "../../runs/background/control-channel.ts";
|
|
8
8
|
import { formatAsyncRunTranscript } from "../../runs/background/fleet-view.ts";
|
|
9
|
+
import { steeringReceipt } from "../../runs/background/steering.ts";
|
|
9
10
|
import type { AsyncStatus } from "../../shared/types.ts";
|
|
10
11
|
import { readStatus } from "../../shared/utils.ts";
|
|
11
12
|
|
|
@@ -112,7 +113,7 @@ export function submitInspectorControl(options: RunnerOptions, line: string): st
|
|
|
112
113
|
...(targetIndex !== undefined ? { targetIndex } : { targetIndexes: runningIndexes }),
|
|
113
114
|
source: "herdr-inspector",
|
|
114
115
|
});
|
|
115
|
-
return `Steering queued for run ${options.runId}
|
|
116
|
+
return steeringReceipt(message, `Steering queued for run ${options.runId}.`);
|
|
116
117
|
}
|
|
117
118
|
if (command.startsWith("reply ")) throw new Error("Supervisor replies are owned by the parent Pi session; use subagent_supervisor/intercom there.");
|
|
118
119
|
throw new Error("Unknown control. Use steer <message>, stop, or status.");
|
|
@@ -6,6 +6,7 @@ import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
|
6
6
|
import { writeAtomicJson } from "../../shared/atomic-json.ts";
|
|
7
7
|
import type { Details } from "../../shared/types.ts";
|
|
8
8
|
import { createHerdrClient, detectHerdr, type HerdrClient, type HerdrErrorCode } from "./client.ts";
|
|
9
|
+
import { formatShellCommand } from "./shell-command.ts";
|
|
9
10
|
|
|
10
11
|
export const HERDR_PROJECT_PANE_ACTIONS = ["project.open", "project.status", "project.close"] as const;
|
|
11
12
|
export type HerdrProjectPaneAction = typeof HERDR_PROJECT_PANE_ACTIONS[number];
|
|
@@ -257,11 +258,6 @@ function projectPaneRuntime(value: unknown): ProjectPaneRuntime | undefined {
|
|
|
257
258
|
};
|
|
258
259
|
}
|
|
259
260
|
|
|
260
|
-
function shellQuote(value: string): string {
|
|
261
|
-
if (process.platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
262
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
261
|
function resolveProjectRoot(requested: string): ProjectPaneResult<string> {
|
|
266
262
|
const resolved = path.resolve(requested);
|
|
267
263
|
try {
|
|
@@ -284,7 +280,7 @@ async function inspectPane(client: HerdrClient, paneId: string, signal?: AbortSi
|
|
|
284
280
|
function projectPaneCommand(message: string | undefined): string {
|
|
285
281
|
const args = message?.trim() ? [message.trim()] : [];
|
|
286
282
|
const command = getPiSpawnCommand(args);
|
|
287
|
-
return
|
|
283
|
+
return formatShellCommand(command.command, command.args);
|
|
288
284
|
}
|
|
289
285
|
|
|
290
286
|
function canonicalRuntimePath(value: string | undefined): string | undefined {
|
|
@@ -415,7 +411,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
415
411
|
}
|
|
416
412
|
}
|
|
417
413
|
const splitArgs = ["pane", "split", "--current", "--direction", "right", "--cwd", projectRoot];
|
|
418
|
-
|
|
414
|
+
splitArgs.push(input.focus === true ? "--focus" : "--no-focus");
|
|
419
415
|
const split = await client.run(splitArgs, { timeoutMs: 15_000, signal: input.signal });
|
|
420
416
|
if (!split.ok) return projectPaneError(split.error.code, split.error.message, { projectRoot, details: split.error.details });
|
|
421
417
|
const paneId = extractPaneId(split.data);
|
|
@@ -434,7 +430,7 @@ function createProjectPaneManagerInternal(options: InternalProjectPaneManagerOpt
|
|
|
434
430
|
projectRoot,
|
|
435
431
|
paneId,
|
|
436
432
|
openedAt: now,
|
|
437
|
-
...(input.focus
|
|
433
|
+
...(input.focus === true ? { lastFocusedAt: now } : {}),
|
|
438
434
|
herdrVersion: detected.data.versionText,
|
|
439
435
|
command,
|
|
440
436
|
...(startupMessage ? { startupMessage } : {}),
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function shellQuote(value: string, platform: NodeJS.Platform): string {
|
|
2
|
+
if (platform === "win32") return `"${value.replaceAll('"', '\\"')}"`;
|
|
3
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function isBareExecutable(value: string): boolean {
|
|
7
|
+
return /^[\w./@:-]+$/.test(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function formatShellCommand(exe: string, args: readonly string[], platform: NodeJS.Platform = process.platform): string {
|
|
11
|
+
const quotedArgs = args.map((arg) => shellQuote(arg, platform));
|
|
12
|
+
if (platform === "win32") return `& ${[shellQuote(exe, platform), ...quotedArgs].join(" ")}`;
|
|
13
|
+
// Nushell treats a leading quoted token as a string, so use a bare invoker for paths that need quoting.
|
|
14
|
+
const invocation = isBareExecutable(exe) ? exe : `sh -c 'exec "$0" "$@"' ${shellQuote(exe, platform)}`;
|
|
15
|
+
return [invocation, ...quotedArgs].join(" ");
|
|
16
|
+
}
|
|
@@ -26,9 +26,8 @@ Use contact_supervisor first. It resolves the supervisor session "{orchestratorT
|
|
|
26
26
|
- After contact_supervisor with reason "need_decision" or "interview_request", stay alive and continue only after the reply arrives. Do not finish your final response with a choose-one question.
|
|
27
27
|
- Do not ask for clarification when the only conflict is review-only/no-edit versus progress-writing or artifact-writing instructions. If an output path is configured but no write-capable tool is available, return the complete artifact in your final response; the runtime will persist it. Do not contact the supervisor merely because you cannot write that output path directly.
|
|
28
28
|
- Meaningful progress or unexpected discoveries that change the plan: contact_supervisor({ reason: "progress_update", message: "UPDATE: <summary>" })
|
|
29
|
-
- Generic intercom is lower-level plumbing/fallback only: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
|
|
30
29
|
|
|
31
|
-
Do not use contact_supervisor
|
|
30
|
+
Do not use contact_supervisor for routine completion handoffs. If no coordination is needed, return a focused task result.`;
|
|
32
31
|
|
|
33
32
|
export interface IntercomBridgeState {
|
|
34
33
|
active: boolean;
|
|
@@ -171,7 +170,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
|
|
|
171
170
|
export function applyIntercomBridgeToAgent(agent: AgentConfig, bridge: IntercomBridgeState): AgentConfig {
|
|
172
171
|
if (!bridge.active || !bridge.orchestratorTarget) return agent;
|
|
173
172
|
|
|
174
|
-
const bridgeTools = ["
|
|
173
|
+
const bridgeTools = ["contact_supervisor"];
|
|
175
174
|
const tools = agent.tools && agent.tools.length > 0
|
|
176
175
|
? [...agent.tools, ...bridgeTools.filter((tool) => !agent.tools?.includes(tool))]
|
|
177
176
|
: agent.tools;
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from "../runs/shared/pi-args.ts";
|
|
15
15
|
import { INTERCOM_DETACH_REQUEST_EVENT, POLL_INTERVAL_MS, TEMP_ROOT_DIR, type IntercomEventBus, type SubagentState } from "../shared/types.ts";
|
|
16
16
|
import { writeAtomicJson } from "../shared/atomic-json.ts";
|
|
17
|
+
import { shouldUseNativeFsWatch } from "../shared/watch-strategy.ts";
|
|
17
18
|
|
|
18
19
|
const SUPERVISOR_CHANNEL_ROOT = path.join(TEMP_ROOT_DIR, "supervisor-channels");
|
|
19
20
|
const REQUESTS_DIR = "requests";
|
|
@@ -75,6 +76,7 @@ type SupervisorWatch = (filename: fs.PathLike, listener: fs.WatchListener<string
|
|
|
75
76
|
interface NativeSupervisorChannelDeps {
|
|
76
77
|
platform?: NodeJS.Platform;
|
|
77
78
|
watch?: SupervisorWatch;
|
|
79
|
+
timers?: Pick<typeof globalThis, "setInterval" | "clearInterval" | "setImmediate" | "clearImmediate">;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
const ContactSupervisorParamsSchema = Type.Object({
|
|
@@ -295,38 +297,18 @@ function hasTool(pi: ExtensionAPI, name: string): boolean {
|
|
|
295
297
|
}
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
export function registerNativeSupervisorClient(pi: ExtensionAPI
|
|
299
|
-
if (!readChildMetadata()) return;
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
};
|
|
311
|
-
pi.registerTool(tool);
|
|
312
|
-
}
|
|
313
|
-
if (includeIntercomFallback && !hasTool(pi, "intercom")) {
|
|
314
|
-
const tool: ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> = {
|
|
315
|
-
name: "intercom",
|
|
316
|
-
label: "Intercom",
|
|
317
|
-
description: "Native supervisor-channel intercom fallback for subagents. Prefer contact_supervisor when available.",
|
|
318
|
-
parameters: IntercomParamsSchema,
|
|
319
|
-
async execute(_id, params, signal) {
|
|
320
|
-
const action = (params as IntercomParams).action;
|
|
321
|
-
if (action === "status") return { content: [{ type: "text", text: "Native supervisor channel is active." }], details: { active: true } };
|
|
322
|
-
if (action === "list") return { content: [{ type: "text", text: "Supervisor session available through contact_supervisor." }], details: { sessions: [] } };
|
|
323
|
-
if (action === "send") return sendSupervisorRequest({ reason: "progress_update", message: (params as IntercomParams).message ?? "" }, signal);
|
|
324
|
-
if (action === "ask") return sendSupervisorRequest({ reason: "need_decision", message: (params as IntercomParams).message ?? "" }, signal);
|
|
325
|
-
throw new Error("Native child intercom supports status, list, send, and ask. Use parent intercom reply from the supervisor session.");
|
|
326
|
-
},
|
|
327
|
-
};
|
|
328
|
-
pi.registerTool(tool);
|
|
329
|
-
}
|
|
300
|
+
export function registerNativeSupervisorClient(pi: ExtensionAPI): void {
|
|
301
|
+
if (!readChildMetadata() || hasTool(pi, "contact_supervisor")) return;
|
|
302
|
+
const tool: ToolDefinition<typeof ContactSupervisorParamsSchema, Record<string, unknown>> = {
|
|
303
|
+
name: "contact_supervisor",
|
|
304
|
+
label: "Contact Supervisor",
|
|
305
|
+
description: "Contact the parent/supervisor session for a blocking decision, structured interview, or progress update.",
|
|
306
|
+
parameters: ContactSupervisorParamsSchema,
|
|
307
|
+
execute(_id, params, signal) {
|
|
308
|
+
return sendSupervisorRequest(params as ContactSupervisorParams, signal);
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
pi.registerTool(tool);
|
|
330
312
|
}
|
|
331
313
|
|
|
332
314
|
function parseRequestFile(file: string, channelDir: string): PendingSupervisorRequest | undefined {
|
|
@@ -601,13 +583,11 @@ function publicPendingRequests(pending: Map<string, PendingSupervisorRequest>):
|
|
|
601
583
|
}));
|
|
602
584
|
}
|
|
603
585
|
|
|
604
|
-
function
|
|
586
|
+
function buildParentSupervisorTool(pending: Map<string, PendingSupervisorRequest>, state: SubagentState): ToolDefinition<typeof IntercomParamsSchema, Record<string, unknown>> {
|
|
605
587
|
return {
|
|
606
|
-
name,
|
|
607
|
-
label:
|
|
608
|
-
description:
|
|
609
|
-
? "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests."
|
|
610
|
-
: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
588
|
+
name: NATIVE_SUPERVISOR_TOOL_NAME,
|
|
589
|
+
label: "Subagent Supervisor",
|
|
590
|
+
description: "Native pi-subagents supervisor channel. Use reply/pending/status to answer child subagent requests without overriding pi-intercom.",
|
|
611
591
|
parameters: IntercomParamsSchema,
|
|
612
592
|
async execute(_id, params) {
|
|
613
593
|
refreshPendingRequests(pending, state, state.lastUiContext ?? undefined);
|
|
@@ -627,15 +607,16 @@ function buildParentIntercomTool(pending: Map<string, PendingSupervisorRequest>,
|
|
|
627
607
|
return { content: [{ type: "text", text: `Replied to supervisor request ${request.id}.` }], details: { replyTo: request.id, runId: request.runId, agent: request.agent } };
|
|
628
608
|
}
|
|
629
609
|
if (input.action === "send" || input.action === "ask") {
|
|
630
|
-
throw new Error("
|
|
610
|
+
throw new Error("The native subagent supervisor handles replies only. Child agents initiate asks with contact_supervisor.");
|
|
631
611
|
}
|
|
632
|
-
throw new Error(`Unsupported
|
|
612
|
+
throw new Error(`Unsupported supervisor action: ${input.action}`);
|
|
633
613
|
},
|
|
634
614
|
};
|
|
635
615
|
}
|
|
636
616
|
|
|
637
|
-
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
617
|
+
export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentState, deps: NativeSupervisorChannelDeps = {}): { start: () => void; activateTransport: () => void; dispose: () => void; pending: Map<string, PendingSupervisorRequest> } {
|
|
638
618
|
const watch = deps.watch ?? fs.watch;
|
|
619
|
+
const timers = deps.timers ?? globalThis;
|
|
639
620
|
const pending = new Map<string, PendingSupervisorRequest>();
|
|
640
621
|
const seenFiles = new Set<string>();
|
|
641
622
|
const requestWatchers = new Map<string, fs.FSWatcher>();
|
|
@@ -645,10 +626,16 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
645
626
|
let deferredWatcherRefresh: ReturnType<typeof setImmediate> | undefined;
|
|
646
627
|
let started = false;
|
|
647
628
|
let lastStaleCleanupAt = 0;
|
|
629
|
+
const platform = deps.platform ?? process.platform;
|
|
630
|
+
const useNativeWatcher = () => shouldUseNativeFsWatch("supervisor-channel", platform) && platform !== "win32";
|
|
631
|
+
const hasTransportDemand = () => {
|
|
632
|
+
if (pending.size > 0) return true;
|
|
633
|
+
if (state.foregroundControls.size > 0) return true;
|
|
634
|
+
return [...state.asyncJobs.values()].some((job) => job.status === "queued" || job.status === "running");
|
|
635
|
+
};
|
|
648
636
|
|
|
649
637
|
const registerParentTools = (): void => {
|
|
650
|
-
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(
|
|
651
|
-
if (!hasTool(pi, "intercom")) pi.registerTool(buildParentIntercomTool(pending, state));
|
|
638
|
+
if (!hasTool(pi, NATIVE_SUPERVISOR_TOOL_NAME)) pi.registerTool(buildParentSupervisorTool(pending, state));
|
|
652
639
|
};
|
|
653
640
|
|
|
654
641
|
const cleanupStaleChannelsIfDue = (): void => {
|
|
@@ -712,12 +699,18 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
712
699
|
|
|
713
700
|
const startPolling = (): void => {
|
|
714
701
|
if (poller) return;
|
|
715
|
-
poller = setInterval(
|
|
702
|
+
poller = timers.setInterval(() => {
|
|
703
|
+
poll();
|
|
704
|
+
if (!useNativeWatcher() && platform === "darwin" && !hasTransportDemand()) {
|
|
705
|
+
if (poller) timers.clearInterval(poller);
|
|
706
|
+
poller = undefined;
|
|
707
|
+
}
|
|
708
|
+
}, CHANNEL_POLL_MS);
|
|
716
709
|
poller.unref?.();
|
|
717
710
|
};
|
|
718
711
|
const startSafetyPolling = (): void => {
|
|
719
712
|
if (safetyPoller) return;
|
|
720
|
-
safetyPoller = setInterval(() => {
|
|
713
|
+
safetyPoller = timers.setInterval(() => {
|
|
721
714
|
watchExistingRequestDirs();
|
|
722
715
|
poll();
|
|
723
716
|
}, CHANNEL_SAFETY_POLL_MS);
|
|
@@ -753,7 +746,7 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
753
746
|
};
|
|
754
747
|
const scheduleWatcherRefresh = (): void => {
|
|
755
748
|
if (deferredWatcherRefresh) return;
|
|
756
|
-
deferredWatcherRefresh = setImmediate(() => {
|
|
749
|
+
deferredWatcherRefresh = timers.setImmediate(() => {
|
|
757
750
|
deferredWatcherRefresh = undefined;
|
|
758
751
|
if (!started) return;
|
|
759
752
|
watchExistingRequestDirs();
|
|
@@ -763,6 +756,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
763
756
|
};
|
|
764
757
|
|
|
765
758
|
return {
|
|
759
|
+
activateTransport: () => {
|
|
760
|
+
if (!started) return;
|
|
761
|
+
poll();
|
|
762
|
+
if (!useNativeWatcher() && hasTransportDemand()) startPolling();
|
|
763
|
+
},
|
|
766
764
|
start: () => {
|
|
767
765
|
if (started) return;
|
|
768
766
|
started = true;
|
|
@@ -770,8 +768,8 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
770
768
|
poll();
|
|
771
769
|
try {
|
|
772
770
|
fs.mkdirSync(SUPERVISOR_CHANNEL_ROOT, { recursive: true });
|
|
773
|
-
if ((
|
|
774
|
-
startPolling();
|
|
771
|
+
if (!useNativeWatcher()) {
|
|
772
|
+
if (platform === "win32") startPolling();
|
|
775
773
|
return;
|
|
776
774
|
}
|
|
777
775
|
watchExistingRequestDirs();
|
|
@@ -799,11 +797,11 @@ export function createNativeSupervisorChannel(pi: ExtensionAPI, state: SubagentS
|
|
|
799
797
|
try { watcher.close(); } catch {}
|
|
800
798
|
}
|
|
801
799
|
requestWatchers.clear();
|
|
802
|
-
if (poller) clearInterval(poller);
|
|
800
|
+
if (poller) timers.clearInterval(poller);
|
|
803
801
|
poller = undefined;
|
|
804
|
-
if (safetyPoller) clearInterval(safetyPoller);
|
|
802
|
+
if (safetyPoller) timers.clearInterval(safetyPoller);
|
|
805
803
|
safetyPoller = undefined;
|
|
806
|
-
if (deferredWatcherRefresh) clearImmediate(deferredWatcherRefresh);
|
|
804
|
+
if (deferredWatcherRefresh) timers.clearImmediate(deferredWatcherRefresh);
|
|
807
805
|
deferredWatcherRefresh = undefined;
|
|
808
806
|
pending.clear();
|
|
809
807
|
seenFiles.clear();
|