pi-subagents 0.54.0 → 0.55.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/agents/reviewer.md +12 -2
  3. package/docs/agents.md +4 -4
  4. package/docs/configuration.md +4 -4
  5. package/docs/extension-api.md +14 -4
  6. package/docs/models.md +24 -3
  7. package/docs/observability.md +14 -3
  8. package/docs/tool-reference.md +21 -6
  9. package/docs/workflows.md +7 -1
  10. package/package.json +1 -1
  11. package/prompts/council.md +9 -0
  12. package/prompts/parallel-review.md +5 -1
  13. package/prompts/review-loop.md +9 -5
  14. package/skills/council-mode/SKILL.md +33 -10
  15. package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
  16. package/skills/pi-subagents/references/execution-controls.md +11 -4
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
  18. package/skills/pi-subagents/references/prompting-and-roles.md +8 -8
  19. package/src/agents/agent-management.ts +30 -9
  20. package/src/agents/agent-serializer.ts +2 -2
  21. package/src/agents/agents.ts +146 -41
  22. package/src/api/external-job-provider.ts +10 -1
  23. package/src/api/preflight.ts +26 -17
  24. package/src/api/project-panes.ts +2 -0
  25. package/src/extension/index.ts +20 -1
  26. package/src/extension/public-execution.ts +12 -9
  27. package/src/extension/rpc.ts +77 -3
  28. package/src/extension/schemas.ts +2 -1
  29. package/src/extension/tool-description.ts +9 -6
  30. package/src/inspectors/herdr/focus.ts +55 -0
  31. package/src/inspectors/herdr/project-panes.ts +228 -44
  32. package/src/integrations/herdr-status.ts +26 -4
  33. package/src/runs/background/async-execution.ts +103 -9
  34. package/src/runs/background/async-job-tracker.ts +33 -14
  35. package/src/runs/background/async-resume.ts +20 -2
  36. package/src/runs/background/async-status.ts +10 -0
  37. package/src/runs/background/control-channel.ts +98 -10
  38. package/src/runs/background/notify.ts +62 -1
  39. package/src/runs/background/run-status.ts +15 -1
  40. package/src/runs/background/subagent-runner.ts +226 -37
  41. package/src/runs/foreground/async-stop-action.ts +23 -2
  42. package/src/runs/foreground/execution.ts +81 -7
  43. package/src/runs/foreground/subagent-executor.ts +348 -40
  44. package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
  45. package/src/runs/shared/acceptance.ts +1 -0
  46. package/src/runs/shared/child-identity.ts +36 -0
  47. package/src/runs/shared/completion-guard.ts +50 -1
  48. package/src/runs/shared/external-job-bridge.ts +53 -37
  49. package/src/runs/shared/external-job-runner.ts +126 -23
  50. package/src/runs/shared/model-fallback.ts +10 -0
  51. package/src/runs/shared/orca-progress-tabs.ts +71 -9
  52. package/src/runs/shared/parallel-utils.ts +12 -0
  53. package/src/runs/shared/pi-args.ts +9 -12
  54. package/src/runs/shared/subagent-prompt-runtime.ts +3 -1
  55. package/src/runs/shared/tool-availability.ts +1 -3
  56. package/src/shared/launch-contract.ts +6 -5
  57. package/src/shared/thinking-ceiling.ts +52 -0
  58. package/src/shared/types.ts +59 -2
  59. package/src/tui/fleet-status.ts +66 -13
  60. package/src/tui/render.ts +5 -4
  61. package/src/workflows/scripted-workflow.ts +107 -10
@@ -43,6 +43,7 @@ import { registerMainWatchdog } from "../watchdog/register-main.ts";
43
43
  import { registerSlashSubagentBridge } from "../slash/slash-bridge.ts";
44
44
  import { createNativeSupervisorChannel } from "../intercom/native-supervisor-channel.ts";
45
45
  import { registerHerdrStatusBridge, type HerdrStatusRun } from "../integrations/herdr-status.ts";
46
+ import { listHerdrProjectPaneRoots, restoreHerdrProjectPaneSnapshots } from "../inspectors/herdr/project-panes.ts";
46
47
  import { registerSubagentRpcBridge } from "./rpc.ts";
47
48
  import { clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails, restoreSlashFinalSnapshots, type SlashMessageDetails } from "../slash/slash-live-state.ts";
48
49
  import { inspectSubagentStatus } from "../runs/background/run-status.ts";
@@ -436,6 +437,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
436
437
  grantHistory: [],
437
438
  },
438
439
  activeAsyncCapacity: { used: 0, limit: resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession) ?? 0 },
440
+ herdrProjectPanes: new Map(),
439
441
  asyncJobs: new Map(),
440
442
  fleetJobs: new Map(),
441
443
  foregroundRuns: new Map(),
@@ -501,7 +503,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
501
503
  ...all.user,
502
504
  ...all.project,
503
505
  ];
504
- return mergeRuntimeAgents(pi, discovered, configuredAgents);
506
+ const merged = mergeRuntimeAgents(pi, discovered, configuredAgents);
507
+ if (discovered.maxThinking === undefined) return merged;
508
+ return {
509
+ ...merged,
510
+ agents: merged.agents.map((agent) => agent.maxThinking === discovered.maxThinking ? agent : { ...agent, maxThinking: discovered.maxThinking }),
511
+ };
505
512
  };
506
513
  const { ensurePoller, refreshWidget, handleStarted, handleComplete, resetJobs, restoreActiveJobs, dispose: disposeAsyncJobTracker } = createAsyncJobTracker(pi, state, DIRS.async, {
507
514
  widgetEnabled: asyncWidgetEnabled,
@@ -600,6 +607,15 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
600
607
  const expandKey = keyText("app.tools.expand");
601
608
  text += `\n ${theme.fg("dim", `${expandKey} full notification`)}`;
602
609
  }
610
+ if (details.workflowRunId) {
611
+ text += `\n ${theme.fg("muted", `workflow: ${details.workflowRunId}`)}`;
612
+ }
613
+ if (details.childRuns?.length) {
614
+ text += `\n ${theme.fg("muted", `children: ${details.childRuns.map((child) => `${child.workflowKey ?? child.agent ?? "child"}=${child.runId}`).join(", ")}`)}`;
615
+ }
616
+ if (details.reconciledFromDetachedChild) {
617
+ text += `\n ${theme.fg("muted", `reconciled child: ${details.reconciledFromDetachedChild}`)}`;
618
+ }
603
619
  if (details.sessionLabel && details.sessionValue) {
604
620
  text += `\n ${theme.fg("muted", `${details.sessionLabel}: ${shortenPath(details.sessionValue)}`)}`;
605
621
  }
@@ -732,6 +748,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
732
748
  const herdrStatusBridge = registerHerdrStatusBridge({
733
749
  events: pi.events,
734
750
  getRuns: activeHerdrRuns,
751
+ getProjectPaneCount: () => [...(state.herdrProjectPanes?.values() ?? [])].filter((pane) => pane.state === "open").length,
735
752
  async runHerdr(args) {
736
753
  await pi.exec(process.env.HERDR_BIN || "herdr", [...args], { timeout: 5_000 });
737
754
  },
@@ -840,6 +857,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
840
857
  granted: 0,
841
858
  grantHistory: [],
842
859
  };
860
+ const projectPaneOwnerRoot = path.resolve(ctx.cwd);
861
+ restoreHerdrProjectPaneSnapshots(state, [...new Set([...(state.herdrProjectPanes?.keys() ?? []), ...listHerdrProjectPaneRoots(projectPaneOwnerRoot), projectPaneOwnerRoot])]);
843
862
  // Set PI_SUBAGENT_PARENT_SESSION for permission-system forwarding.
844
863
  // Only set in the root session (the interactive UI session), not in
845
864
  // child subagent processes — children inherit the parent's value
@@ -17,6 +17,12 @@ export interface PublicSubagentExecutionParams {
17
17
  output?: unknown;
18
18
  resume?: unknown;
19
19
  clarify?: unknown;
20
+ workflowParentRunId?: unknown;
21
+ workflowKey?: unknown;
22
+ workflowChildAsyncId?: unknown;
23
+ workflowAwaitAsync?: unknown;
24
+ workflowParentDeadlineAt?: unknown;
25
+ suppressRoutineResultIntercom?: unknown;
20
26
  runFanoutBudget?: unknown;
21
27
  runFanoutAdmitted?: unknown;
22
28
  }
@@ -46,6 +52,9 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
46
52
  if (params.runFanoutBudget !== undefined || params.runFanoutAdmitted !== undefined) {
47
53
  return { ok: false, error: "Public execution does not accept internal run fan-out fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
48
54
  }
55
+ if (params.workflowParentRunId !== undefined || params.workflowKey !== undefined || params.workflowChildAsyncId !== undefined || params.workflowAwaitAsync !== undefined || params.workflowParentDeadlineAt !== undefined || params.suppressRoutineResultIntercom !== undefined) {
56
+ return { ok: false, error: "Public execution does not accept internal workflow child fields.", mode: params.workflowScript !== undefined ? "workflow" : "management" };
57
+ }
49
58
  const action = params.action;
50
59
  if (action !== undefined && (typeof action !== "string" || !action.trim())) {
51
60
  return { ok: false, error: "action must be a non-empty management/control action, or omit action and use workflowScript.", mode: "management" };
@@ -111,18 +120,12 @@ export function normalizePublicSubagentExecution<T extends PublicSubagentExecuti
111
120
  if (params.task !== undefined && typeof params.task !== "string") {
112
121
  return { ok: false, error: "Structured single-child task must be a string when provided.", mode: "workflow" };
113
122
  }
114
- const { agent: _agent, task: _task, output, ...workflowDefaults } = params;
115
- const child = {
116
- agent: params.agent.trim(),
117
- ...(params.task !== undefined ? { task: params.task } : {}),
118
- output: output === undefined ? true : output,
119
- };
120
123
  return {
121
124
  ok: true,
122
125
  params: {
123
- ...workflowDefaults,
124
- ...(params.async === undefined && options.asyncByDefault === false ? { async: false } : {}),
125
- workflowScript: `return runs.run("main", ${JSON.stringify(child)})`,
126
+ ...params,
127
+ agent: params.agent.trim(),
128
+ output: params.output === undefined ? true : params.output,
126
129
  } as T,
127
130
  };
128
131
  }
@@ -13,14 +13,17 @@ import {
13
13
  type SubagentState,
14
14
  DIRS,
15
15
  SUBAGENT_ASYNC_COMPLETE_EVENT,
16
+ SUBAGENT_CHILD_STATUS_EVENT,
16
17
  SUBAGENT_PROCESS_TERMINAL_EVENT,
17
18
  SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
19
+ type SubagentChildStatusEvent,
18
20
  } from "../shared/types.ts";
19
21
  import { sanitizeDisplayText, truncateDisplayText } from "../shared/display-text.ts";
20
22
  import { readStatus } from "../shared/utils.ts";
21
23
  import { SubagentParams } from "./schemas.ts";
22
24
  import { normalizePublicSubagentExecution } from "./public-execution.ts";
23
25
  import { ASYNC_STATUS_SNAPSHOT_KIND, ASYNC_STATUS_SNAPSHOT_VERSION, buildAsyncStatusSnapshotForState } from "../runs/background/async-status-snapshot.ts";
26
+ import { isStoppableAsyncStatusStep, resolveAsyncStatusChild, type ResolvedAsyncStatusChild } from "../runs/shared/child-identity.ts";
24
27
 
25
28
  export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
26
29
  export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
@@ -405,6 +408,7 @@ function pingData(ctx: ExtensionContext | null) {
405
408
  request: SUBAGENT_RPC_REQUEST_EVENT,
406
409
  replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
407
410
  asyncComplete: SUBAGENT_ASYNC_COMPLETE_EVENT,
411
+ childStatus: SUBAGENT_CHILD_STATUS_EVENT,
408
412
  processTerminal: SUBAGENT_PROCESS_TERMINAL_EVENT,
409
413
  },
410
414
  session: sessionData(ctx),
@@ -500,8 +504,14 @@ function stopAsyncRun(
500
504
  params: unknown,
501
505
  options: RegisterSubagentRpcBridgeOptions,
502
506
  ctx: ExtensionContext,
503
- ): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
504
- const target = normalizeTargetParams(params, "stop");
507
+ ): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string; childId?: string } {
508
+ const input = assertRecordParams(params, "stop");
509
+ const rawChildId = input.childId;
510
+ if (rawChildId !== undefined && (typeof rawChildId !== "string" || !rawChildId.trim() || /[\r\n]/.test(rawChildId) || rawChildId.length > 256)) {
511
+ throw new SubagentRpcError("invalid_params", "RPC stop childId must be a non-empty string without newlines and at most 256 characters.");
512
+ }
513
+ const childId = typeof rawChildId === "string" ? rawChildId : undefined;
514
+ const target = normalizeTargetParams(input, "stop");
505
515
  assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
506
516
  const asyncDirRoot = options.asyncDirRoot ?? DIRS.async;
507
517
  const resultsDir = options.resultsDir ?? DIRS.results;
@@ -523,7 +533,60 @@ function stopAsyncRun(
523
533
  throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
524
534
  }
525
535
 
536
+ let child: ResolvedAsyncStatusChild | undefined;
537
+ const emitChildStopping = (runId: string, asyncDir: string, stoppedChild: ResolvedAsyncStatusChild, ts = options.now?.() ?? Date.now()): void => {
538
+ options.events.emit(SUBAGENT_CHILD_STATUS_EVENT, {
539
+ type: "subagent.child-status",
540
+ version: 1,
541
+ runId,
542
+ childId: stoppedChild.id,
543
+ status: "stopping",
544
+ ts,
545
+ reason: "user",
546
+ source: "rpc",
547
+ asyncDir,
548
+ stepIndex: stoppedChild.index,
549
+ agent: stoppedChild.step.agent,
550
+ ...(stoppedChild.step.runId ? { childRunId: stoppedChild.step.runId } : {}),
551
+ ...(stoppedChild.step.workflowKey ? { workflowKey: stoppedChild.step.workflowKey } : {}),
552
+ ...(stoppedChild.step.phase ? { phase: stoppedChild.step.phase } : {}),
553
+ ...(stoppedChild.step.label ? { label: stoppedChild.step.label } : {}),
554
+ } satisfies SubagentChildStatusEvent);
555
+ };
556
+ if (childId !== undefined) {
557
+ const resolution = resolveAsyncStatusChild(initialStatus, childId);
558
+ if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
559
+ child = resolution.child;
560
+ if (!isStoppableAsyncStatusStep(child.step)) {
561
+ throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${initialRunId}' is ${child.step.status}; stop only supports pending or running children.`);
562
+ }
563
+ }
526
564
  if (initialStatus.mode === "workflow" && initialStatus.state === "running") {
565
+ if (child) {
566
+ const stopChild = options.state?.workflowChildStops?.get(initialRunId);
567
+ if (!stopChild) throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; child stop is unavailable.`);
568
+ if (!stopChild(child.id, `Workflow child '${child.id}' stopped by RPC.`)) throw new SubagentRpcError("invalid_state", `Child '${childId}' in workflow ${initialRunId} is not available to stop.`);
569
+ emitChildStopping(initialRunId, location.asyncDir, child);
570
+ return {
571
+ runId: initialRunId,
572
+ asyncDir: location.asyncDir,
573
+ previousState: initialStatus.state,
574
+ state: "stopping",
575
+ childId: child.id,
576
+ message: `Stop requested for child ${child.id} in async run ${initialRunId}.`,
577
+ };
578
+ }
579
+ const workflowController = options.state?.workflowControllers?.get(initialRunId);
580
+ if (workflowController) {
581
+ workflowController.abort(new Error("Workflow stopped by RPC."));
582
+ return {
583
+ runId: initialRunId,
584
+ asyncDir: location.asyncDir,
585
+ previousState: initialStatus.state,
586
+ state: "stopping",
587
+ message: `Stop requested for async run ${initialRunId}.`,
588
+ };
589
+ }
527
590
  throw new SubagentRpcError("invalid_state", `Workflow ${initialRunId} is not controlled by this extension runtime; reload recovery cannot stop it safely.`);
528
591
  }
529
592
 
@@ -541,6 +604,14 @@ function stopAsyncRun(
541
604
  if (status.state !== "running") {
542
605
  throw new SubagentRpcError("invalid_state", `Async run ${runId} is ${status.state}; stop only supports running async runs.`);
543
606
  }
607
+ if (childId !== undefined) {
608
+ const resolution = resolveAsyncStatusChild(status, childId);
609
+ if (!resolution.ok) throw new SubagentRpcError(resolution.code === "not_found" ? "not_found" : "invalid_params", resolution.message);
610
+ child = resolution.child;
611
+ if (!isStoppableAsyncStatusStep(child.step)) {
612
+ throw new SubagentRpcError("invalid_state", `Child '${childId}' in async run '${runId}' is ${child.step.status}; stop only supports pending or running children.`);
613
+ }
614
+ }
544
615
 
545
616
  try {
546
617
  deliverStopRequest({
@@ -549,17 +620,20 @@ function stopAsyncRun(
549
620
  kill: options.kill,
550
621
  now: options.now,
551
622
  source: "rpc-stop",
623
+ ...(child ? { targetIndex: child.index, childId: child.id } : {}),
552
624
  });
553
625
  } catch (error) {
554
626
  throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
555
627
  }
628
+ if (child) emitChildStopping(runId, location.asyncDir, child);
556
629
 
557
630
  return {
558
631
  runId,
559
632
  asyncDir: location.asyncDir,
560
633
  previousState: status.state,
561
634
  state: "stopping",
562
- message: `Stop requested for async run ${runId}.`,
635
+ ...(child ? { childId: child.id } : {}),
636
+ message: child ? `Stop requested for child ${child.id} in async run ${runId}.` : `Stop requested for async run ${runId}.`,
563
637
  };
564
638
  }
565
639
 
@@ -271,6 +271,7 @@ const SubagentParamProperties = {
271
271
  })),
272
272
  handoffPath: Type.Optional(Type.String({ description: "worktree.discard manifest." })),
273
273
  index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
274
+ childId: Type.Optional(Type.String({ minLength: 1, maxLength: 256, description: "Stable child identity for child-scoped stop requests." })),
274
275
  view: Type.Optional(Type.String({
275
276
  enum: ["fleet", "transcript"],
276
277
  description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
@@ -307,7 +308,7 @@ const SubagentParamProperties = {
307
308
  ],
308
309
  description: "Agent config for create/update. Object or JSON string."
309
310
  })),
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}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use 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
+ 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}), where resume is a retained run id or {workflowRunId,key,latest:true} from a durable async workflow receipt. Use 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}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. 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
312
  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
313
  isolation: Type.Optional(Type.String({ enum: ["none", "worktree"], description: "Workflow child isolation. none runs in the shared cwd; worktree requires managed git worktree isolation." })),
313
314
  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." })),
@@ -6,7 +6,7 @@ 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.`;
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 read its ordered array result with indexes, destructuring, or .map(...), not by key property. Use action only for management/control. Use guide or the pi-subagents skill for advanced workflow details.`;
10
10
 
11
11
  export const SUBAGENT_TOOL_PROMPT_SNIPPET = "Delegate to subagents; orchestrate in one workflowScript call.";
12
12
 
@@ -14,8 +14,9 @@ export const SUBAGENT_TOOL_PROMPT_GUIDELINES = [
14
14
  "Use subagent only when delegation is needed. Before executing, call { action: \"list\" } and run only executable, non-disabled agents.",
15
15
  "Omit action for execution. Use { agent, task? } only for one child; use workflowScript for multi-step or parallel work.",
16
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.",
17
+ "For ordinary parallel work, use await runs.all([{key,agent,task}, ...]); it resolves to an ordered array, not a key map, so use results[0], destructuring, or results.map(...), not results.<key>. 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
18
  "Keep one writer per cwd/worktree unless writers run in isolated worktrees.",
19
+ "To pass an explicit model to a child, first call { action: \"models\" } and copy an exact provider/id (e.g. openai-codex/gpt-5.6-sol); bare ids resolve only when unique in the registry, and agent names (gpt-pro, advisor) are not model ids.",
19
20
  "Use guide or the pi-subagents skill for advanced scheduling, missions, steering, and retention.",
20
21
  ];
21
22
 
@@ -32,8 +33,9 @@ export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, task?
32
33
 
33
34
  EXECUTION:
34
35
  • Before executing, use { action: "list" } and run only executable/non-disabled configured agents.
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.
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.
36
+ • When passing an explicit model to a child (on the call or a runs.run/runs.all item), first call { action: "models" } and copy an exact provider/id; bare ids resolve only when unique in the registry, and agent names (e.g. gpt-pro, advisor) are not model ids.
37
+ • SINGLE CHILD: { agent:"worker", task:"..." }. This structured form starts exactly one direct child. Fields such as model, context, cwd, worktree, output, budgets, acceptance, and async apply to that child. Do not combine agent/task with action or workflowScript.
38
+ • 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. runs.all resolves to an ordered array, not a key map, so use results[0], array destructuring, or results.map((result) => result.output), not results.<key>. 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.
37
39
  • 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" }
38
40
  • 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}" }
39
41
  • Optional context is "fresh", "fork", or "profile". profile requires the selected agent's declared defaultContext and ignores config defaultSubagentContext. Explicit fresh/fork 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.
@@ -50,8 +52,9 @@ export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Run one child with { agent, ta
50
52
 
51
53
  EXECUTE:
52
54
  • Call { action:"list" } first and use only executable/non-disabled agents.
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.
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.
55
+ • Passing an explicit model? Call {action:"models"} first and copy an exact provider/id; bare ids resolve only when unique in the registry; agent names (e.g. gpt-pro, advisor) are not model ids.
56
+ • SINGLE {agent:"worker",task:"..."} starts exactly one direct child. Fields apply to that child. Do not combine agent/task with action or workflowScript.
57
+ • 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. runs.all resolves to an ordered array, not a key map; use results[0], destructuring, or results.map(...), not results.<key>. 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.
55
58
  • 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]"}
56
59
  • context can be fresh, fork, or profile. profile requires the selected agent's declared defaultContext and ignores defaultSubagentContext. Explicit fresh/fork 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.
57
60
 
@@ -0,0 +1,55 @@
1
+ import type { HerdrClient, HerdrErrorCode } from "./client.ts";
2
+
3
+ export type HerdrFocusErrorCode = HerdrErrorCode | "PANE_FOCUS_UNSUPPORTED" | "INVALID_PANE_RESPONSE";
4
+
5
+ export type HerdrPaneFocusResult =
6
+ | { ok: true; data: { paneId: string; tabId?: string; workspaceId?: string } }
7
+ | { ok: false; error: { code: HerdrFocusErrorCode; message: string; details?: unknown } };
8
+
9
+ export function herdrPaneRecord(value: unknown): Record<string, unknown> | undefined {
10
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
11
+ const record = value as Record<string, unknown>;
12
+ return record.pane && typeof record.pane === "object" && !Array.isArray(record.pane)
13
+ ? record.pane as Record<string, unknown>
14
+ : record;
15
+ }
16
+
17
+ function text(record: Record<string, unknown>, ...keys: string[]): string | undefined {
18
+ for (const key of keys) if (typeof record[key] === "string" && record[key]) return record[key] as string;
19
+ return undefined;
20
+ }
21
+
22
+ export function herdrPaneFocusTarget(value: unknown): { paneId?: string; tabId?: string; workspaceId?: string } {
23
+ const pane = herdrPaneRecord(value);
24
+ if (!pane) return {};
25
+ return {
26
+ paneId: text(pane, "pane_id", "paneId", "id"),
27
+ tabId: text(pane, "tab_id", "tabId"),
28
+ workspaceId: text(pane, "workspace_id", "workspaceId"),
29
+ };
30
+ }
31
+
32
+ export async function focusHerdrPane(client: HerdrClient, paneId: string, signal?: AbortSignal): Promise<HerdrPaneFocusResult> {
33
+ const live = await client.run(["pane", "get", paneId], { timeoutMs: 5_000, signal });
34
+ if (live.ok === false) return live;
35
+ const target = herdrPaneFocusTarget(live.data);
36
+ if (!target.paneId) {
37
+ return { ok: false, error: { code: "INVALID_PANE_RESPONSE", message: `Herdr pane get returned no pane id for '${paneId}'.`, details: live.data } };
38
+ }
39
+ if (target.tabId) {
40
+ const focused = await client.run(["tab", "focus", target.tabId], { timeoutMs: 5_000, signal });
41
+ return focused.ok ? { ok: true, data: { paneId: target.paneId, tabId: target.tabId, ...(target.workspaceId ? { workspaceId: target.workspaceId } : {}) } } : focused;
42
+ }
43
+ if (target.workspaceId) {
44
+ const focused = await client.run(["workspace", "focus", target.workspaceId], { timeoutMs: 5_000, signal });
45
+ return focused.ok ? { ok: true, data: { paneId: target.paneId, workspaceId: target.workspaceId } } : focused;
46
+ }
47
+ return {
48
+ ok: false,
49
+ error: {
50
+ code: "PANE_FOCUS_UNSUPPORTED",
51
+ message: `Herdr pane '${paneId}' has no tab_id or workspace_id. Select it in Herdr manually, or upgrade Herdr focus support.`,
52
+ details: live.data,
53
+ },
54
+ };
55
+ }