pi-subagents 0.21.0 → 0.21.2

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 (73) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +49 -18
  3. package/agents/context-builder.md +6 -3
  4. package/package.json +3 -4
  5. package/prompts/parallel-context-build.md +53 -0
  6. package/prompts/parallel-handoff-plan.md +59 -0
  7. package/prompts/parallel-review.md +4 -0
  8. package/skills/pi-subagents/SKILL.md +71 -3
  9. package/{agent-management.ts → src/agents/agent-management.ts} +1 -5
  10. package/{agents.ts → src/agents/agents.ts} +1 -1
  11. package/{doctor.ts → src/extension/doctor.ts} +5 -5
  12. package/{index.ts → src/extension/index.ts} +21 -20
  13. package/{schemas.ts → src/extension/schemas.ts} +25 -39
  14. package/{intercom-bridge.ts → src/intercom/intercom-bridge.ts} +19 -10
  15. package/{result-intercom.ts → src/intercom/result-intercom.ts} +33 -5
  16. package/{agent-manager-chain-detail.ts → src/manager-ui/agent-manager-chain-detail.ts} +3 -3
  17. package/{agent-manager-detail.ts → src/manager-ui/agent-manager-detail.ts} +7 -7
  18. package/{agent-manager-edit.ts → src/manager-ui/agent-manager-edit.ts} +4 -4
  19. package/{agent-manager-list.ts → src/manager-ui/agent-manager-list.ts} +12 -5
  20. package/{agent-manager-parallel.ts → src/manager-ui/agent-manager-parallel.ts} +3 -3
  21. package/{agent-manager.ts → src/manager-ui/agent-manager.ts} +19 -16
  22. package/{async-execution.ts → src/runs/background/async-execution.ts} +11 -11
  23. package/{async-job-tracker.ts → src/runs/background/async-job-tracker.ts} +29 -6
  24. package/src/runs/background/async-resume.ts +305 -0
  25. package/{async-status.ts → src/runs/background/async-status.ts} +14 -12
  26. package/{notify.ts → src/runs/background/notify.ts} +1 -1
  27. package/{result-watcher.ts → src/runs/background/result-watcher.ts} +93 -23
  28. package/{run-status.ts → src/runs/background/run-status.ts} +63 -28
  29. package/src/runs/background/stale-run-reconciler.ts +275 -0
  30. package/{subagent-runner.ts → src/runs/background/subagent-runner.ts} +43 -55
  31. package/{chain-clarify.ts → src/runs/foreground/chain-clarify.ts} +7 -7
  32. package/{chain-execution.ts → src/runs/foreground/chain-execution.ts} +10 -10
  33. package/{execution.ts → src/runs/foreground/execution.ts} +26 -22
  34. package/{subagent-executor.ts → src/runs/foreground/subagent-executor.ts} +155 -24
  35. package/{long-running-guard.ts → src/runs/shared/long-running-guard.ts} +3 -3
  36. package/{model-fallback.ts → src/runs/shared/model-fallback.ts} +1 -1
  37. package/{subagent-control.ts → src/runs/shared/subagent-control.ts} +4 -8
  38. package/src/shared/atomic-json.ts +16 -0
  39. package/{settings.ts → src/shared/settings.ts} +21 -14
  40. package/{types.ts → src/shared/types.ts} +10 -2
  41. package/{utils.ts → src/shared/utils.ts} +1 -15
  42. package/{slash-bridge.ts → src/slash/slash-bridge.ts} +2 -2
  43. package/{slash-commands.ts → src/slash/slash-commands.ts} +13 -10
  44. package/{slash-live-state.ts → src/slash/slash-live-state.ts} +2 -2
  45. package/{render.ts → src/tui/render.ts} +3 -3
  46. package/{subagents-status.ts → src/tui/subagents-status.ts} +34 -14
  47. /package/{agent-scope.ts → src/agents/agent-scope.ts} +0 -0
  48. /package/{agent-selection.ts → src/agents/agent-selection.ts} +0 -0
  49. /package/{agent-serializer.ts → src/agents/agent-serializer.ts} +0 -0
  50. /package/{agent-templates.ts → src/agents/agent-templates.ts} +0 -0
  51. /package/{chain-serializer.ts → src/agents/chain-serializer.ts} +0 -0
  52. /package/{frontmatter.ts → src/agents/frontmatter.ts} +0 -0
  53. /package/{skills.ts → src/agents/skills.ts} +0 -0
  54. /package/{completion-dedupe.ts → src/runs/background/completion-dedupe.ts} +0 -0
  55. /package/{top-level-async.ts → src/runs/background/top-level-async.ts} +0 -0
  56. /package/{completion-guard.ts → src/runs/shared/completion-guard.ts} +0 -0
  57. /package/{parallel-utils.ts → src/runs/shared/parallel-utils.ts} +0 -0
  58. /package/{pi-args.ts → src/runs/shared/pi-args.ts} +0 -0
  59. /package/{pi-spawn.ts → src/runs/shared/pi-spawn.ts} +0 -0
  60. /package/{run-history.ts → src/runs/shared/run-history.ts} +0 -0
  61. /package/{single-output.ts → src/runs/shared/single-output.ts} +0 -0
  62. /package/{subagent-prompt-runtime.ts → src/runs/shared/subagent-prompt-runtime.ts} +0 -0
  63. /package/{worktree.ts → src/runs/shared/worktree.ts} +0 -0
  64. /package/{artifacts.ts → src/shared/artifacts.ts} +0 -0
  65. /package/{file-coalescer.ts → src/shared/file-coalescer.ts} +0 -0
  66. /package/{fork-context.ts → src/shared/fork-context.ts} +0 -0
  67. /package/{formatters.ts → src/shared/formatters.ts} +0 -0
  68. /package/{jsonl-writer.ts → src/shared/jsonl-writer.ts} +0 -0
  69. /package/{post-exit-stdio-guard.ts → src/shared/post-exit-stdio-guard.ts} +0 -0
  70. /package/{session-tokens.ts → src/shared/session-tokens.ts} +0 -0
  71. /package/{prompt-template-bridge.ts → src/slash/prompt-template-bridge.ts} +0 -0
  72. /package/{render-helpers.ts → src/tui/render-helpers.ts} +0 -0
  73. /package/{text-editor.ts → src/tui/text-editor.ts} +0 -0
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  import { Type } from "typebox";
6
+ import { SUBAGENT_ACTIONS } from "../shared/types.ts";
6
7
 
7
8
  const SkillOverride = Type.Unsafe({
8
- type: ["string", "array", "boolean"],
9
9
  anyOf: [
10
10
  { type: "array", items: { type: "string" } },
11
11
  { type: "boolean" },
@@ -15,12 +15,14 @@ const SkillOverride = Type.Unsafe({
15
15
  });
16
16
 
17
17
  const OutputOverride = Type.Unsafe({
18
- type: ["string", "boolean"],
18
+ anyOf: [
19
+ { type: "string" },
20
+ { type: "boolean" },
21
+ ],
19
22
  description: "Output filename/path (string), or false to disable file output",
20
23
  });
21
24
 
22
25
  const ReadsOverride = Type.Unsafe({
23
- type: ["array", "boolean"],
24
26
  anyOf: [
25
27
  { type: "array", items: { type: "string" } },
26
28
  { type: "boolean" },
@@ -40,20 +42,6 @@ const TaskItem = Type.Object({
40
42
  skill: Type.Optional(SkillOverride),
41
43
  });
42
44
 
43
- // Sequential chain step (single agent)
44
- const SequentialStepSchema = Type.Object({
45
- agent: Type.String(),
46
- task: Type.Optional(Type.String({
47
- description: "Task template with variables: {task}=original request, {previous}=prior step's text response, {chain_dir}=shared folder. Required for first step, defaults to '{previous}' for subsequent steps."
48
- })),
49
- cwd: Type.Optional(Type.String()),
50
- output: Type.Optional(OutputOverride),
51
- reads: Type.Optional(ReadsOverride),
52
- progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
53
- skill: Type.Optional(SkillOverride),
54
- model: Type.Optional(Type.String({ description: "Override model for this step" })),
55
- });
56
-
57
45
  // Parallel task item (within a parallel step)
58
46
  const ParallelTaskSchema = Type.Object({
59
47
  agent: Type.String(),
@@ -67,17 +55,7 @@ const ParallelTaskSchema = Type.Object({
67
55
  model: Type.Optional(Type.String({ description: "Override model for this task" })),
68
56
  });
69
57
 
70
- // Parallel chain step (multiple agents running concurrently)
71
- const ParallelStepSchema = Type.Object({
72
- parallel: Type.Array(ParallelTaskSchema, { minItems: 1, description: "Tasks to run in parallel" }),
73
- concurrency: Type.Optional(Type.Number({ description: "Max concurrent tasks (default: 4)" })),
74
- failFast: Type.Optional(Type.Boolean({ description: "Stop on first failure (default: false)" })),
75
- worktree: Type.Optional(Type.Boolean({
76
- description: "Create isolated git worktrees for each parallel task."
77
- })),
78
- });
79
-
80
- // Flattened so providers that reject anyOf/oneOf can still accept either sequential or parallel steps.
58
+ // Flattened so chain steps do not need an object-shape anyOf/oneOf union.
81
59
  const ChainItem = Type.Object({
82
60
  agent: Type.Optional(Type.String({ description: "Sequential step agent name" })),
83
61
  task: Type.Optional(Type.String({
@@ -100,9 +78,9 @@ const ChainItem = Type.Object({
100
78
  const ControlOverrides = Type.Object({
101
79
  enabled: Type.Optional(Type.Boolean({ description: "Enable/disable subagent control attention tracking for this run" })),
102
80
  needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "No-observed-activity window before a run needs attention" })),
103
- activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 300000)" })),
104
- activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by assistant turns (default: 15)" })),
105
- activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by total tokens (default: 150000)" })),
81
+ activeNoticeAfterMs: Type.Optional(Type.Integer({ minimum: 1, description: "Active-long-running notice threshold by elapsed ms (default: 240000)" })),
82
+ activeNoticeAfterTurns: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by assistant turns (disabled by default)" })),
83
+ activeNoticeAfterTokens: Type.Optional(Type.Integer({ minimum: 1, description: "Optional active-long-running notice threshold by total tokens (disabled by default)" })),
106
84
  failedToolAttemptsBeforeAttention: Type.Optional(Type.Integer({ minimum: 1, description: "Consecutive mutating-tool failures before escalating to needs_attention (default: 3)" })),
107
85
  notifyOn: Type.Optional(Type.Array(Type.String({ enum: ["active_long_running", "needs_attention"] }), {
108
86
  description: "Control event types that should notify the parent/orchestrator. Defaults to active_long_running and needs_attention.",
@@ -117,26 +95,31 @@ export const SubagentParams = Type.Object({
117
95
  task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
118
96
  // Management action (when present, tool operates in management mode)
119
97
  action: Type.Optional(Type.String({
120
- description: "Action: 'list', 'get', 'create', 'update', 'delete', 'status', 'interrupt', or 'doctor' diagnostics. Omit for execution mode."
98
+ enum: [...SUBAGENT_ACTIONS],
99
+ description: "Management/control action. Omit for execution mode."
121
100
  })),
122
101
  id: Type.Optional(Type.String({
123
- description: "Run id or prefix for action='status' or action='interrupt'."
102
+ description: "Run id or prefix for action='status', action='interrupt', or action='resume'."
124
103
  })),
125
104
  runId: Type.Optional(Type.String({
126
- description: "Target run ID for action='interrupt'. Defaults to the most recently active controllable run in this session. Prefer id for new calls."
105
+ description: "Target run ID for action='interrupt' or action='resume'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
127
106
  })),
128
107
  dir: Type.Optional(Type.String({
129
- description: "Async run directory for action='status'."
108
+ description: "Async run directory for action='status' or action='resume'."
130
109
  })),
110
+ index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child." })),
111
+ message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'." })),
131
112
  // Chain identifier for management (can't reuse 'chain' — that's the execution array)
132
113
  chainName: Type.Optional(Type.String({
133
114
  description: "Chain name for get/update/delete management actions"
134
115
  })),
135
116
  // Agent/chain configuration for create/update (nested to avoid conflicts with execution fields)
136
117
  config: Type.Optional(Type.Unsafe({
137
- type: ["object", "string"],
138
- additionalProperties: true,
139
- description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skills?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
118
+ anyOf: [
119
+ { type: "object", additionalProperties: true },
120
+ { type: "string" },
121
+ ],
122
+ description: "Agent or chain config for create/update. Agent: name, description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, description, scope, steps (array of {agent, task?, output?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
140
123
  })),
141
124
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, reads?, progress?}, ...]" })),
142
125
  concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
@@ -165,7 +148,10 @@ export const SubagentParams = Type.Object({
165
148
  control: Type.Optional(ControlOverrides),
166
149
  // Solo agent overrides
167
150
  output: Type.Optional(Type.Unsafe({
168
- type: ["string", "boolean"],
151
+ anyOf: [
152
+ { type: "string" },
153
+ { type: "boolean" },
154
+ ],
169
155
  description: "Output file for single agent (string), or false to disable. Relative paths resolve against cwd.",
170
156
  })),
171
157
  skill: Type.Optional(SkillOverride),
@@ -1,12 +1,21 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
- import type { AgentConfig } from "./agents.ts";
5
- import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "./types.ts";
4
+ import type { AgentConfig } from "../agents/agents.ts";
5
+ import type { ExtensionConfig, IntercomBridgeConfig, IntercomBridgeMode } from "../shared/types.ts";
6
+
7
+ function defaultIntercomExtensionDir(): string {
8
+ return path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
9
+ }
10
+
11
+ function defaultIntercomConfigPath(): string {
12
+ return path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
13
+ }
14
+
15
+ function defaultSubagentConfigDir(): string {
16
+ return path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
17
+ }
6
18
 
7
- const DEFAULT_INTERCOM_EXTENSION_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "pi-intercom");
8
- const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "intercom", "config.json");
9
- const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
10
19
  const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
11
20
  export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
12
21
  const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
@@ -135,9 +144,9 @@ ${instruction}`;
135
144
  export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeDiagnostic {
136
145
  const config = resolveIntercomBridgeConfig(input.config);
137
146
  const mode = config.mode;
138
- const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
147
+ const extensionDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir());
139
148
  const orchestratorTarget = input.orchestratorTarget?.trim();
140
- const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
149
+ const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
141
150
  const wantsIntercom = mode !== "off" && !(mode === "fork-only" && input.context !== "fork");
142
151
  const piIntercomAvailable = fs.existsSync(extensionDir);
143
152
  let configStatus: ReturnType<typeof intercomConfigStatus> | undefined;
@@ -173,9 +182,9 @@ export function diagnoseIntercomBridge(input: ResolveIntercomBridgeInput): Inter
173
182
  export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): IntercomBridgeState {
174
183
  const config = resolveIntercomBridgeConfig(input.config);
175
184
  const mode = config.mode;
176
- const extensionDir = path.resolve(input.extensionDir ?? DEFAULT_INTERCOM_EXTENSION_DIR);
185
+ const extensionDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir());
177
186
  const orchestratorTarget = input.orchestratorTarget?.trim();
178
- const settingsDir = path.resolve(input.settingsDir ?? DEFAULT_SUBAGENT_CONFIG_DIR);
187
+ const settingsDir = path.resolve(input.settingsDir ?? defaultSubagentConfigDir());
179
188
  const defaultInstruction = buildIntercomBridgeInstruction(
180
189
  orchestratorTarget || "{orchestratorTarget}",
181
190
  DEFAULT_INTERCOM_BRIDGE_TEMPLATE,
@@ -194,7 +203,7 @@ export function resolveIntercomBridge(input: ResolveIntercomBridgeInput): Interc
194
203
  return { active: false, mode, extensionDir, instruction: defaultInstruction };
195
204
  }
196
205
 
197
- const configPath = path.resolve(input.configPath ?? DEFAULT_INTERCOM_CONFIG_PATH);
206
+ const configPath = path.resolve(input.configPath ?? defaultIntercomConfigPath());
198
207
  const intercomStatus = intercomConfigStatus(configPath);
199
208
  if (intercomStatus.error) console.warn(`Failed to parse intercom config at '${configPath}'. Assuming enabled.`, intercomStatus.error);
200
209
  if (!intercomStatus.enabled) {
@@ -8,7 +8,7 @@ import {
8
8
  type SubagentResultStatus,
9
9
  SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
10
10
  SUBAGENT_RESULT_INTERCOM_EVENT,
11
- } from "./types.ts";
11
+ } from "../shared/types.ts";
12
12
 
13
13
  export function resolveSubagentResultStatus(input: {
14
14
  exitCode?: number;
@@ -69,10 +69,24 @@ interface GroupedResultIntercomMessageInput {
69
69
  chainSteps?: number;
70
70
  }
71
71
 
72
+ function asyncResumeGuidance(input: {
73
+ source: "foreground" | "async";
74
+ children: SubagentResultIntercomChild[];
75
+ asyncId?: string;
76
+ }): string | undefined {
77
+ if (input.source !== "async" || !input.asyncId) return undefined;
78
+ if (input.children.length === 1 && typeof input.children[0]?.sessionPath === "string") {
79
+ return `Revive: subagent({ action: "resume", id: "${input.asyncId}", message: "..." })`;
80
+ }
81
+ if (input.children.length > 1) return "Resume: unsupported for multi-child async runs until per-child session files are persisted.";
82
+ return "Resume: unavailable; no single child session file was persisted.";
83
+ }
84
+
72
85
  function formatSubagentResultIntercomMessage(input: {
73
86
  runId: string;
74
87
  mode: "single" | "parallel" | "chain";
75
88
  status: SubagentResultStatus;
89
+ source: "foreground" | "async";
76
90
  children: SubagentResultIntercomChild[];
77
91
  asyncId?: string;
78
92
  asyncDir?: string;
@@ -92,16 +106,20 @@ function formatSubagentResultIntercomMessage(input: {
92
106
  }
93
107
  if (input.asyncId) lines.push(`Async id: ${input.asyncId}`);
94
108
  if (input.asyncDir) lines.push(`Async dir: ${input.asyncDir}`);
109
+ const resumeGuidance = asyncResumeGuidance(input);
110
+ if (resumeGuidance) lines.push(resumeGuidance);
95
111
  if (input.children.some((child) => child.intercomTarget)) {
96
112
  lines.push("");
97
- lines.push("Intercom targets below identify child sessions used while they were running; completed child sessions may no longer be reachable. Inspect artifacts or session logs for follow-up.");
113
+ lines.push(input.source === "async"
114
+ ? "Previous intercom targets below identify child sessions used while they were running. Inspect artifacts or session logs if resume is unavailable."
115
+ : "Intercom targets below identify child sessions used while they were running; completed child sessions may no longer be reachable. Inspect artifacts or session logs for follow-up.");
98
116
  }
99
117
 
100
118
  for (let index = 0; index < input.children.length; index++) {
101
119
  const child = input.children[index]!;
102
120
  lines.push("");
103
121
  lines.push(`${index + 1}. ${child.agent} — ${child.status}`);
104
- if (child.intercomTarget) lines.push(`Run intercom target: ${child.intercomTarget}`);
122
+ if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
105
123
  if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
106
124
  if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
107
125
  lines.push("Summary:");
@@ -144,9 +162,19 @@ export async function deliverSubagentResultIntercomEvent(
144
162
  events: IntercomEventBus,
145
163
  payload: SubagentResultIntercomPayload,
146
164
  timeoutMs = 500,
165
+ ): Promise<boolean> {
166
+ return deliverSubagentIntercomMessageEvent(events, payload.to, payload.message, timeoutMs, payload);
167
+ }
168
+
169
+ export async function deliverSubagentIntercomMessageEvent(
170
+ events: IntercomEventBus,
171
+ to: string,
172
+ message: string,
173
+ timeoutMs = 500,
174
+ extra: Record<string, unknown> = {},
147
175
  ): Promise<boolean> {
148
176
  if (typeof events.on !== "function" || typeof events.emit !== "function") return false;
149
- const requestId = payload.requestId ?? randomUUID();
177
+ const requestId = typeof extra.requestId === "string" ? extra.requestId : randomUUID();
150
178
  return new Promise((resolve) => {
151
179
  let settled = false;
152
180
  let unsubscribe: (() => void) | undefined;
@@ -166,7 +194,7 @@ export async function deliverSubagentResultIntercomEvent(
166
194
  });
167
195
  timer = setTimeout(() => finish(false), timeoutMs);
168
196
  try {
169
- events.emit(SUBAGENT_RESULT_INTERCOM_EVENT, { ...payload, requestId });
197
+ events.emit(SUBAGENT_RESULT_INTERCOM_EVENT, { ...extra, to, message, requestId });
170
198
  } catch {
171
199
  finish(false);
172
200
  }
@@ -1,8 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { ChainConfig, ChainStepConfig } from "./agents.ts";
4
- import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "./render-helpers.ts";
5
- import { isParallelStep, type ChainStep } from "./settings.ts";
3
+ import type { ChainConfig, ChainStepConfig } from "../agents/agents.ts";
4
+ import { row, renderFooter, renderHeader, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
5
+ import { isParallelStep, type ChainStep } from "../shared/settings.ts";
6
6
 
7
7
  export interface ChainDetailState {
8
8
  scrollOffset: number;
@@ -1,12 +1,12 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import type { AgentConfig } from "./agents.ts";
4
- import { formatDuration } from "./formatters.ts";
5
- import type { RunEntry } from "./run-history.ts";
6
- import { buildSkillInjection, resolveSkills } from "./skills.ts";
7
- import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "./text-editor.ts";
8
- import type { TextEditorState } from "./text-editor.ts";
9
- import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "./render-helpers.ts";
3
+ import type { AgentConfig } from "../agents/agents.ts";
4
+ import { formatDuration } from "../shared/formatters.ts";
5
+ import type { RunEntry } from "../runs/shared/run-history.ts";
6
+ import { buildSkillInjection, resolveSkills } from "../agents/skills.ts";
7
+ import { ensureCursorVisible, getCursorDisplayPos, renderEditor, wrapText } from "../tui/text-editor.ts";
8
+ import type { TextEditorState } from "../tui/text-editor.ts";
9
+ import { pad, row, renderHeader, renderFooter, formatPath, formatScrollInfo } from "../tui/render-helpers.ts";
10
10
 
11
11
  export interface DetailState {
12
12
  resolved: boolean;
@@ -1,9 +1,9 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
3
- import { defaultSystemPromptMode, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "./agents.ts";
4
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
5
- import type { TextEditorState } from "./text-editor.ts";
6
- import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "./render-helpers.ts";
3
+ import { defaultSystemPromptMode, type AgentConfig, type AgentDefaultContext, type BuiltinAgentOverrideBase } from "../agents/agents.ts";
4
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
5
+ import type { TextEditorState } from "../tui/text-editor.ts";
6
+ import { pad, row, renderHeader, renderFooter, formatScrollInfo } from "../tui/render-helpers.ts";
7
7
 
8
8
  export interface ModelInfo { provider: string; id: string; fullId: string; }
9
9
  export interface SkillInfo { name: string; source: string; description?: string; }
@@ -1,7 +1,7 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
- import type { AgentSource } from "./agents.ts";
2
+ import type { AgentSource } from "../agents/agents.ts";
3
3
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
4
- import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "./render-helpers.ts";
4
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "../tui/render-helpers.ts";
5
5
 
6
6
  export interface ListAgent {
7
7
  id: string;
@@ -22,6 +22,12 @@ export interface ListState {
22
22
  selected: string[];
23
23
  }
24
24
 
25
+ export interface ListShortcuts {
26
+ newShortcut: string;
27
+ }
28
+
29
+ export const DEFAULT_AGENT_MANAGER_NEW_SHORTCUT = "shift+ctrl+n";
30
+
25
31
  export type ListAction =
26
32
  | { type: "open-detail"; id: string }
27
33
  | { type: "clone"; id: string }
@@ -57,7 +63,7 @@ function clampCursor(state: ListState, filtered: ListAgent[]): void {
57
63
  }
58
64
  }
59
65
 
60
- export function handleListInput(state: ListState, agents: ListAgent[], data: string): ListAction | undefined {
66
+ export function handleListInput(state: ListState, agents: ListAgent[], data: string, shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT }): ListAction | undefined {
61
67
  const filtered = fuzzyFilter(agents, state.filterQuery);
62
68
 
63
69
  if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
@@ -98,7 +104,7 @@ export function handleListInput(state: ListState, agents: ListAgent[], data: str
98
104
  return;
99
105
  }
100
106
 
101
- if (matchesKey(data, "alt+n")) {
107
+ if (matchesKey(data, shortcuts.newShortcut)) {
102
108
  return { type: "new" };
103
109
  }
104
110
 
@@ -160,6 +166,7 @@ export function renderList(
160
166
  width: number,
161
167
  theme: Theme,
162
168
  statusMessage?: { text: string; type: "error" | "info" },
169
+ shortcuts: ListShortcuts = { newShortcut: DEFAULT_AGENT_MANAGER_NEW_SHORTCUT },
163
170
  ): string[] {
164
171
  const lines: string[] = [];
165
172
  const filtered = fuzzyFilter(agents, state.filterQuery);
@@ -269,7 +276,7 @@ export function renderList(
269
276
  ? ` [ctrl+r] chain [ctrl+p] parallel [tab] add [shift+tab] remove [esc] clear (${selCount}) `
270
277
  : selCount === 1
271
278
  ? " [ctrl+r] run [ctrl+p] parallel [tab] add more [shift+tab] remove [esc] clear "
272
- : " [enter] view [ctrl+r] run [tab] select [alt+n] new [esc] close ";
279
+ : ` [enter] view [ctrl+r] run [tab] select [${shortcuts.newShortcut}] new [esc] close `;
273
280
  lines.push(renderFooter(footerText, width, theme));
274
281
 
275
282
  return lines;
@@ -1,8 +1,8 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
2
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
- import type { TextEditorState } from "./text-editor.ts";
4
- import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "./text-editor.ts";
5
- import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "./render-helpers.ts";
3
+ import type { TextEditorState } from "../tui/text-editor.ts";
4
+ import { createEditorState, handleEditorInput, renderEditor, wrapText, getCursorDisplayPos, ensureCursorVisible } from "../tui/text-editor.ts";
5
+ import { pad, row, renderHeader, renderFooter, fuzzyFilter } from "../tui/render-helpers.ts";
6
6
 
7
7
  interface ParallelSlot {
8
8
  agentName: string;
@@ -14,20 +14,20 @@ import {
14
14
  type AgentConfig,
15
15
  type BuiltinAgentOverrideBase,
16
16
  type ChainConfig,
17
- } from "./agents.ts";
18
- import { serializeAgent } from "./agent-serializer.ts";
19
- import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "./agent-templates.ts";
20
- import { parseChain, serializeChain } from "./chain-serializer.ts";
21
- import { renderList, handleListInput, type ListAgent, type ListState, type ListAction } from "./agent-manager-list.ts";
17
+ } from "../agents/agents.ts";
18
+ import { serializeAgent } from "../agents/agent-serializer.ts";
19
+ import { TEMPLATE_ITEMS, type AgentTemplate, type TemplateItem } from "../agents/agent-templates.ts";
20
+ import { parseChain, serializeChain } from "../agents/chain-serializer.ts";
21
+ import { DEFAULT_AGENT_MANAGER_NEW_SHORTCUT, renderList, handleListInput, type ListAgent, type ListShortcuts, type ListState, type ListAction } from "./agent-manager-list.ts";
22
22
  import { createParallelState, handleParallelInput, renderParallel, formatParallelTitle, type ParallelState, type AgentOption } from "./agent-manager-parallel.ts";
23
23
  import { renderDetail, handleDetailInput, renderTaskInput, type DetailState, type DetailAction, type LaunchToggleState } from "./agent-manager-detail.ts";
24
24
  import { renderChainDetail, handleChainDetailInput, type ChainDetailAction, type ChainDetailState } from "./agent-manager-chain-detail.ts";
25
25
  import { createEditState, handleEditInput, renderEdit, type EditField, type EditScreen, type EditState, type ModelInfo, type SkillInfo } from "./agent-manager-edit.ts";
26
- import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "./text-editor.ts";
27
- import type { TextEditorState } from "./text-editor.ts";
28
- import { loadRunsForAgent } from "./run-history.ts";
29
- import { pad, row, renderHeader, renderFooter } from "./render-helpers.ts";
30
- import { isParallelStep, type ChainStep } from "./settings.ts";
26
+ import { createEditorState, ensureCursorVisible, getCursorDisplayPos, handleEditorInput, renderEditor, wrapText } from "../tui/text-editor.ts";
27
+ import type { TextEditorState } from "../tui/text-editor.ts";
28
+ import { loadRunsForAgent } from "../runs/shared/run-history.ts";
29
+ import { pad, row, renderHeader, renderFooter } from "../tui/render-helpers.ts";
30
+ import { isParallelStep, type ChainStep } from "../shared/settings.ts";
31
31
 
32
32
  export type ManagerResult =
33
33
  | { action: "launch"; agent: string; task: string; skipClarify?: boolean; fork?: boolean; background?: boolean }
@@ -43,6 +43,7 @@ interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
43
43
  interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "new-chain"; editor: TextEditorState; scope: "user" | "project"; allowProject: boolean; sourceId?: string; template?: AgentTemplate; error?: string; }
44
44
  interface StatusMessage { text: string; type: "error" | "info"; }
45
45
  interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
46
+ export interface AgentManagerOptions { newShortcut?: string; }
46
47
 
47
48
  const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "disabled", "tools", "skills", "prompt"];
48
49
 
@@ -131,14 +132,16 @@ export class AgentManagerComponent implements Component {
131
132
  private models: ModelInfo[];
132
133
  private skills: SkillInfo[];
133
134
  private done: (result: ManagerResult) => void;
135
+ private shortcuts: ListShortcuts;
134
136
 
135
- constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void) {
137
+ constructor(tui: TUI, theme: Theme, agentData: AgentData, models: ModelInfo[], skills: SkillInfo[], done: (result: ManagerResult) => void, options: AgentManagerOptions = {}) {
136
138
  this.tui = tui;
137
139
  this.theme = theme;
138
140
  this.agentData = agentData;
139
141
  this.models = models;
140
142
  this.skills = skills;
141
143
  this.done = done;
144
+ this.shortcuts = { newShortcut: options.newShortcut?.trim() || DEFAULT_AGENT_MANAGER_NEW_SHORTCUT };
142
145
  this.loadEntries();
143
146
  }
144
147
 
@@ -518,7 +521,7 @@ export class AgentManagerComponent implements Component {
518
521
  if (this.screen === "list" && this.statusMessage) this.clearStatus();
519
522
  if (this.screen.startsWith("edit") && this.editState?.error) this.editState.error = undefined;
520
523
  switch (this.screen) {
521
- case "list": { const action = handleListInput(this.listState, this.listAgents(), data); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
524
+ case "list": { const action = handleListInput(this.listState, this.listAgents(), data, this.shortcuts); if (action) this.handleListAction(action); this.tui.requestRender(); return; }
522
525
  case "template-select": this.handleTemplateSelectInput(data); return;
523
526
  case "override-scope": this.handleOverrideScopeInput(data); return;
524
527
  case "detail": {
@@ -671,14 +674,14 @@ export class AgentManagerComponent implements Component {
671
674
  render(width: number): string[] {
672
675
  this.overlayWidth = width; const w = this.overlayWidth;
673
676
  switch (this.screen) {
674
- case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
677
+ case "list": return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts);
675
678
  case "template-select": return this.renderTemplateSelect(w);
676
679
  case "override-scope": return this.renderOverrideScope(w);
677
- case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
678
- case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
680
+ case "detail": { const entry = this.getAgentEntry(this.currentAgentId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts); return renderDetail(this.detailState, entry.config, this.agentData.cwd, w, this.theme); }
681
+ case "chain-detail": { const entry = this.getChainEntry(this.currentChainId); if (!entry) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts); return renderChainDetail(this.chainDetailState, entry.config, w, this.theme); }
679
682
  case "edit": case "edit-field": case "edit-prompt": return this.editState ? renderEdit(this.screen as EditScreen, this.editState, w, this.theme) : [];
680
683
  case "parallel-builder": {
681
- if (!this.parallelState) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage);
684
+ if (!this.parallelState) return renderList(this.listState, this.listAgents(), w, this.theme, this.statusMessage, this.shortcuts);
682
685
  const agentOptions: AgentOption[] = this.agents.map((e) => ({ name: e.config.name, description: e.config.description, model: e.config.model }));
683
686
  return renderParallel(this.parallelState, agentOptions, w, this.theme);
684
687
  }
@@ -9,16 +9,16 @@ import * as path from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { createRequire } from "node:module";
11
11
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
- import type { AgentConfig } from "./agents.ts";
13
- import { applyThinkingSuffix } from "./pi-args.ts";
14
- import { injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
15
- import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "./settings.ts";
16
- import type { RunnerStep } from "./parallel-utils.ts";
17
- import { resolvePiPackageRoot } from "./pi-spawn.ts";
18
- import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "./skills.ts";
19
- import { resolveChildCwd } from "./utils.ts";
20
- import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "./model-fallback.ts";
21
- import { resolveExpectedWorktreeAgentCwd } from "./worktree.ts";
12
+ import type { AgentConfig } from "../../agents/agents.ts";
13
+ import { applyThinkingSuffix } from "../shared/pi-args.ts";
14
+ import { injectSingleOutputInstruction, resolveSingleOutputPath } from "../shared/single-output.ts";
15
+ import { buildChainInstructions, isParallelStep, resolveStepBehavior, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
16
+ import type { RunnerStep } from "../shared/parallel-utils.ts";
17
+ import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
+ import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
19
+ import { resolveChildCwd } from "../../shared/utils.ts";
20
+ import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
21
+ import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
22
22
  import {
23
23
  type ArtifactConfig,
24
24
  type Details,
@@ -30,7 +30,7 @@ import {
30
30
  TEMP_ROOT_DIR,
31
31
  getAsyncConfigPath,
32
32
  resolveChildMaxSubagentDepth,
33
- } from "./types.ts";
33
+ } from "../../shared/types.ts";
34
34
 
35
35
  const require = createRequire(import.meta.url);
36
36
  const piPackageRoot = resolvePiPackageRoot();
@@ -1,8 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { renderWidget } from "./render.ts";
5
- import { formatControlNoticeMessage } from "./subagent-control.ts";
4
+ import { renderWidget } from "../../tui/render.ts";
5
+ import { formatControlNoticeMessage } from "../shared/subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
8
8
  type AsyncParallelGroupStatus,
@@ -10,10 +10,12 @@ import {
10
10
  type ControlEvent,
11
11
  type SubagentState,
12
12
  POLL_INTERVAL_MS,
13
+ RESULTS_DIR,
13
14
  SUBAGENT_CONTROL_EVENT,
14
15
  SUBAGENT_CONTROL_INTERCOM_EVENT,
15
- } from "./types.ts";
16
- import { readStatus } from "./utils.ts";
16
+ } from "../../shared/types.ts";
17
+ import { readStatus } from "../../shared/utils.ts";
18
+ import { reconcileAsyncRun } from "./stale-run-reconciler.ts";
17
19
 
18
20
 
19
21
  function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
@@ -35,6 +37,9 @@ function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined,
35
37
  interface AsyncJobTrackerOptions {
36
38
  completionRetentionMs?: number;
37
39
  pollIntervalMs?: number;
40
+ resultsDir?: string;
41
+ kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
42
+ now?: () => number;
38
43
  }
39
44
 
40
45
  export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: SubagentState, asyncDirRoot: string, options: AsyncJobTrackerOptions = {}): {
@@ -45,6 +50,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
45
50
  } {
46
51
  const completionRetentionMs = options.completionRetentionMs ?? 10000;
47
52
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
53
+ const resultsDir = options.resultsDir ?? RESULTS_DIR;
48
54
  const rerenderWidget = (ctx: ExtensionContext, jobs = Array.from(state.asyncJobs.values())) => {
49
55
  renderWidget(ctx, jobs);
50
56
  ctx.ui.requestRender?.();
@@ -132,7 +138,20 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
132
138
  for (const job of state.asyncJobs.values()) {
133
139
  try {
134
140
  emitNewControlEvents(job);
135
- const status = readStatus(job.asyncDir);
141
+ const reconciliation = reconcileAsyncRun(job.asyncDir, {
142
+ resultsDir,
143
+ kill: options.kill,
144
+ now: options.now,
145
+ startedRun: {
146
+ runId: job.asyncId,
147
+ pid: job.pid,
148
+ mode: job.mode,
149
+ agents: job.agents,
150
+ startedAt: job.startedAt,
151
+ sessionFile: job.sessionFile,
152
+ },
153
+ });
154
+ const status = reconciliation.status ?? readStatus(job.asyncDir);
136
155
  if (status) {
137
156
  const previousStatus = job.status;
138
157
  job.status = status.state;
@@ -167,7 +186,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
167
186
  job.outputFile = status.outputFile ?? job.outputFile;
168
187
  job.totalTokens = status.totalTokens ?? job.totalTokens;
169
188
  job.sessionFile = status.sessionFile ?? job.sessionFile;
170
- if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && previousStatus !== job.status) {
189
+ if ((job.status === "complete" || job.status === "failed" || job.status === "paused") && (previousStatus !== job.status || !state.cleanupTimers.has(job.asyncId))) {
171
190
  scheduleCleanup(job.asyncId);
172
191
  }
173
192
  continue;
@@ -178,6 +197,9 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
178
197
  console.error(`Failed to read async status for '${job.asyncDir}':`, error);
179
198
  job.status = "failed";
180
199
  job.updatedAt = Date.now();
200
+ if (!state.cleanupTimers.has(job.asyncId)) {
201
+ scheduleCleanup(job.asyncId);
202
+ }
181
203
  }
182
204
  }
183
205
 
@@ -202,6 +224,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
202
224
  asyncId: info.id,
203
225
  asyncDir,
204
226
  status: "queued",
227
+ pid: typeof info.pid === "number" ? info.pid : undefined,
205
228
  mode: info.chain ? "chain" : "single",
206
229
  agents,
207
230
  stepsTotal: firstGroupCount ?? agents?.length,