pi-subagents 0.20.1 → 0.21.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 (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +21 -4
  3. package/agent-management.ts +19 -9
  4. package/agent-manager-chain-detail.ts +1 -1
  5. package/agent-manager-detail.ts +2 -0
  6. package/agent-manager-edit.ts +31 -6
  7. package/agent-manager-parallel.ts +2 -2
  8. package/agent-manager.ts +4 -2
  9. package/agent-serializer.ts +2 -0
  10. package/agents/context-builder.md +13 -6
  11. package/agents/delegate.md +2 -0
  12. package/agents/oracle.md +4 -1
  13. package/agents/planner.md +5 -1
  14. package/agents/researcher.md +4 -1
  15. package/agents/reviewer.md +78 -20
  16. package/agents/scout.md +5 -2
  17. package/agents/worker.md +7 -4
  18. package/agents.ts +29 -7
  19. package/async-execution.ts +64 -35
  20. package/async-job-tracker.ts +52 -15
  21. package/async-status.ts +107 -14
  22. package/chain-clarify.ts +1 -1
  23. package/chain-execution.ts +10 -2
  24. package/completion-dedupe.ts +1 -1
  25. package/completion-guard.ts +125 -0
  26. package/doctor.ts +1 -1
  27. package/execution.ts +158 -26
  28. package/fork-context.ts +3 -3
  29. package/index.ts +17 -6
  30. package/intercom-bridge.ts +2 -1
  31. package/jsonl-writer.ts +2 -2
  32. package/long-running-guard.ts +175 -0
  33. package/model-fallback.ts +1 -1
  34. package/package.json +1 -1
  35. package/pi-args.ts +4 -2
  36. package/pi-spawn.ts +1 -1
  37. package/prompt-template-bridge.ts +9 -9
  38. package/prompts/parallel-cleanup.md +10 -3
  39. package/render.ts +239 -49
  40. package/result-intercom.ts +5 -5
  41. package/result-watcher.ts +21 -12
  42. package/run-status.ts +1 -1
  43. package/schemas.ts +24 -13
  44. package/session-tokens.ts +2 -6
  45. package/settings.ts +2 -2
  46. package/single-output.ts +1 -1
  47. package/skills/pi-subagents/SKILL.md +93 -20
  48. package/skills.ts +8 -2
  49. package/slash-bridge.ts +3 -3
  50. package/subagent-control.ts +103 -21
  51. package/subagent-executor.ts +82 -25
  52. package/subagent-prompt-runtime.ts +80 -3
  53. package/subagent-runner.ts +339 -93
  54. package/subagents-status.ts +9 -7
  55. package/text-editor.ts +22 -6
  56. package/top-level-async.ts +1 -1
  57. package/types.ts +63 -8
  58. package/utils.ts +3 -3
  59. package/worktree.ts +5 -5
package/agents/scout.md CHANGED
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff
4
- tools: read, grep, find, ls, bash, write
4
+ tools: read, grep, find, ls, bash, write, intercom
5
5
  model: openai-codex/gpt-5.5
6
- thinking: medium
6
+ thinking: low
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
9
9
  inheritSkills: false
@@ -46,3 +46,6 @@ Explain how the pieces connect.
46
46
 
47
47
  ## Start Here
48
48
  Name the first file another agent should open and why.
49
+
50
+ ## Pi-intercom handoff
51
+ If `intercom` is available and runtime bridge instructions or the task name a safe orchestrator target, send your completed scout findings back with a blocking `intercom({ action: "ask", ... })` before finishing. Keep the message concise, include the output path or top findings, and ask whether the orchestrator wants more context. If no safe target is available, do not guess; return normally.
package/agents/worker.md CHANGED
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  name: worker
3
3
  description: Implementation agent for normal tasks and approved oracle handoffs
4
- model: openai-codex/gpt-5.3-codex
4
+ model: openai-codex/gpt-5.5
5
5
  thinking: high
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
9
+ defaultContext: fork
9
10
  defaultReads: context.md, plan.md
10
11
  defaultProgress: true
11
12
  ---
@@ -18,7 +19,7 @@ Use the provided tools directly. First understand the inherited context, supplie
18
19
 
19
20
  If the task is framed as an approved direction, oracle handoff, or execution plan, treat that direction as the contract. Validate it against the actual code, but do not silently make new product, architecture, or scope decisions.
20
21
 
21
- If the implementation reveals a decision that was not approved and is required to continue safely, pause and escalate. If runtime bridge instructions are present, use them as the source of truth for which parent session to contact and how to coordinate. Use `intercom({ action: "ask", ... })` when a new decision is needed. Use `intercom({ action: "send", ... })` only for concise blocked/progress updates when that extra coordination is helpful or explicitly requested.
22
+ If the implementation reveals a decision that was not approved and is required to continue safely, pause and escalate through the live coordination channel. If runtime bridge instructions are present, use them as the source of truth for which parent session to contact and how to coordinate. Use `intercom({ action: "ask", ... })` when a new decision is needed, and stay alive to receive the reply before continuing. Use `intercom({ action: "send", ... })` only for concise non-blocking progress updates when that extra coordination is helpful or explicitly requested. Do not finish your final response with a question that requires the orchestrator to choose before you can continue.
22
23
 
23
24
  Default responsibilities:
24
25
  - validate the task or approved direction against the actual code
@@ -34,9 +35,11 @@ Working rules:
34
35
  - Do not leave placeholder code, TODOs, or silent scope changes.
35
36
  - Use `bash` for inspection, validation, and relevant tests.
36
37
  - If there is supplied context or a plan, read it first.
37
- - If implementation reveals a gap in the approved direction, pause and escalate instead of silently patching around it with an implicit decision.
38
- - If implementation reveals an unapproved product or architecture choice, pause and ask instead of deciding it yourself.
38
+ - If implementation reveals a gap in the approved direction, pause and escalate with `intercom({ action: "ask", ... })` instead of silently patching around it with an implicit decision.
39
+ - If implementation reveals an unapproved product or architecture choice, use `intercom({ action: "ask", ... })` and wait for the reply instead of deciding it yourself or returning a final choose-one answer.
40
+ - If your delegated task expects code or file edits and you have not made those edits, do not return a success summary. Make the edits, ask the orchestrator if blocked, or explicitly report that no edits were made.
39
41
  - If you send a blocked/progress update through intercom, keep it short and still return the full structured task result normally.
42
+ - If `intercom` is available and runtime bridge instructions or the task name a safe orchestrator target, send your completed implementation summary back with a blocking `intercom({ action: "ask", ... })` before finishing. Stay alive for the reply so you can clarify or handle a small follow-up if requested. If no safe target is available, do not guess; return normally.
40
43
 
41
44
  When running in a chain, expect instructions about:
42
45
  - which files to read first
package/agents.ts CHANGED
@@ -14,7 +14,8 @@ import { parseFrontmatter } from "./frontmatter.ts";
14
14
  export type AgentScope = "user" | "project" | "both";
15
15
 
16
16
  export type AgentSource = "builtin" | "user" | "project";
17
- export type SystemPromptMode = "append" | "replace";
17
+ type SystemPromptMode = "append" | "replace";
18
+ export type AgentDefaultContext = "fresh" | "fork";
18
19
 
19
20
  export function defaultSystemPromptMode(name: string): SystemPromptMode {
20
21
  return name === "delegate" ? "append" : "replace";
@@ -35,6 +36,7 @@ export interface BuiltinAgentOverrideBase {
35
36
  systemPromptMode: SystemPromptMode;
36
37
  inheritProjectContext: boolean;
37
38
  inheritSkills: boolean;
39
+ defaultContext?: AgentDefaultContext;
38
40
  disabled?: boolean;
39
41
  systemPrompt: string;
40
42
  skills?: string[];
@@ -42,20 +44,21 @@ export interface BuiltinAgentOverrideBase {
42
44
  mcpDirectTools?: string[];
43
45
  }
44
46
 
45
- export interface BuiltinAgentOverrideConfig {
47
+ interface BuiltinAgentOverrideConfig {
46
48
  model?: string | false;
47
49
  fallbackModels?: string[] | false;
48
50
  thinking?: string | false;
49
51
  systemPromptMode?: SystemPromptMode;
50
52
  inheritProjectContext?: boolean;
51
53
  inheritSkills?: boolean;
54
+ defaultContext?: AgentDefaultContext | false;
52
55
  disabled?: boolean;
53
56
  systemPrompt?: string;
54
57
  skills?: string[] | false;
55
58
  tools?: string[] | false;
56
59
  }
57
60
 
58
- export interface BuiltinAgentOverrideInfo {
61
+ interface BuiltinAgentOverrideInfo {
59
62
  scope: "user" | "project";
60
63
  path: string;
61
64
  base: BuiltinAgentOverrideBase;
@@ -72,6 +75,7 @@ export interface AgentConfig {
72
75
  systemPromptMode: SystemPromptMode;
73
76
  inheritProjectContext: boolean;
74
77
  inheritSkills: boolean;
78
+ defaultContext?: AgentDefaultContext;
75
79
  systemPrompt: string;
76
80
  source: AgentSource;
77
81
  filePath: string;
@@ -113,7 +117,7 @@ export interface ChainConfig {
113
117
  extraFields?: Record<string, string>;
114
118
  }
115
119
 
116
- export interface AgentDiscoveryResult {
120
+ interface AgentDiscoveryResult {
117
121
  agents: AgentConfig[];
118
122
  projectAgentsDir: string | null;
119
123
  }
@@ -160,6 +164,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
160
164
  systemPromptMode: agent.systemPromptMode,
161
165
  inheritProjectContext: agent.inheritProjectContext,
162
166
  inheritSkills: agent.inheritSkills,
167
+ defaultContext: agent.defaultContext,
163
168
  disabled: agent.disabled,
164
169
  systemPrompt: agent.systemPrompt,
165
170
  skills: agent.skills ? [...agent.skills] : undefined,
@@ -178,6 +183,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
178
183
  ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
179
184
  ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
180
185
  ...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
186
+ ...(override.defaultContext !== undefined ? { defaultContext: override.defaultContext } : {}),
181
187
  ...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
182
188
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
183
189
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
@@ -198,11 +204,11 @@ function findNearestProjectRoot(cwd: string): string | null {
198
204
  }
199
205
  }
200
206
 
201
- export function getUserAgentSettingsPath(): string {
207
+ function getUserAgentSettingsPath(): string {
202
208
  return path.join(os.homedir(), ".pi", "agent", "settings.json");
203
209
  }
204
210
 
205
- export function getProjectAgentSettingsPath(cwd: string): string | null {
211
+ function getProjectAgentSettingsPath(cwd: string): string | null {
206
212
  const projectRoot = findNearestProjectRoot(cwd);
207
213
  return projectRoot ? path.join(projectRoot, ".pi", "settings.json") : null;
208
214
  }
@@ -302,6 +308,14 @@ function parseBuiltinOverrideEntry(
302
308
  }
303
309
  }
304
310
 
311
+ if ("defaultContext" in input) {
312
+ if (input.defaultContext === "fresh" || input.defaultContext === "fork" || input.defaultContext === false) {
313
+ override.defaultContext = input.defaultContext;
314
+ } else {
315
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'defaultContext'; expected 'fresh', 'fork', or false.`);
316
+ }
317
+ }
318
+
305
319
  if ("disabled" in input) {
306
320
  if (typeof input.disabled === "boolean") {
307
321
  override.disabled = input.disabled;
@@ -373,6 +387,7 @@ function applyBuiltinOverride(
373
387
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
374
388
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
375
389
  if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
390
+ if (override.defaultContext !== undefined) next.defaultContext = override.defaultContext === false ? undefined : override.defaultContext;
376
391
  if (override.disabled !== undefined) next.disabled = override.disabled;
377
392
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
378
393
  if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
@@ -420,7 +435,7 @@ function applyBuiltinOverrides(
420
435
 
421
436
  export function buildBuiltinOverrideConfig(
422
437
  base: BuiltinAgentOverrideBase,
423
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
438
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
424
439
  ): BuiltinAgentOverrideConfig | undefined {
425
440
  const override: BuiltinAgentOverrideConfig = {};
426
441
 
@@ -430,6 +445,7 @@ export function buildBuiltinOverrideConfig(
430
445
  if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
431
446
  if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
432
447
  if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
448
+ if (draft.defaultContext !== base.defaultContext) override.defaultContext = draft.defaultContext ?? false;
433
449
  if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false;
434
450
  if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
435
451
  if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
@@ -568,6 +584,11 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
568
584
  : frontmatter.inheritSkills === "false"
569
585
  ? false
570
586
  : defaultInheritSkills();
587
+ const defaultContext = frontmatter.defaultContext === "fork"
588
+ ? "fork" as const
589
+ : frontmatter.defaultContext === "fresh"
590
+ ? "fresh" as const
591
+ : undefined;
571
592
 
572
593
  let extensions: string[] | undefined;
573
594
  if (frontmatter.extensions !== undefined) {
@@ -595,6 +616,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
595
616
  systemPromptMode,
596
617
  inheritProjectContext,
597
618
  inheritSkills,
619
+ defaultContext,
598
620
  systemPrompt: body,
599
621
  source,
600
622
  filePath,
@@ -55,14 +55,14 @@ const jitiCliPath: string | undefined = (() => {
55
55
  return undefined;
56
56
  })();
57
57
 
58
- export interface AsyncExecutionContext {
58
+ interface AsyncExecutionContext {
59
59
  pi: ExtensionAPI;
60
60
  cwd: string;
61
61
  currentSessionId: string;
62
62
  currentModelProvider?: string;
63
63
  }
64
64
 
65
- export interface AsyncChainParams {
65
+ interface AsyncChainParams {
66
66
  chain: ChainStep[];
67
67
  resultMode?: "parallel" | "chain";
68
68
  agents: AgentConfig[];
@@ -84,7 +84,7 @@ export interface AsyncChainParams {
84
84
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
85
85
  }
86
86
 
87
- export interface AsyncSingleParams {
87
+ interface AsyncSingleParams {
88
88
  agent: string;
89
89
  task?: string;
90
90
  agentConfig: AgentConfig;
@@ -108,7 +108,7 @@ export interface AsyncSingleParams {
108
108
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
109
109
  }
110
110
 
111
- export interface AsyncExecutionResult {
111
+ interface AsyncExecutionResult {
112
112
  content: Array<{ type: "text"; text: string }>;
113
113
  details: Details;
114
114
  isError?: boolean;
@@ -167,6 +167,10 @@ function formatAsyncStartError(mode: "single" | "chain", message: string): Async
167
167
  };
168
168
  }
169
169
 
170
+ const UNAVAILABLE_SUBAGENT_SKILL_ERROR = "Skills not found: pi-subagents";
171
+
172
+ class UnavailableSubagentSkillError extends Error {}
173
+
170
174
  /**
171
175
  * Execute a chain asynchronously
172
176
  */
@@ -240,7 +244,8 @@ export function executeAsyncChain(
240
244
  const instructionCwd = behaviorCwd ?? stepCwd;
241
245
  const behavior = resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills);
242
246
  const skillNames = behavior.skills === false ? [] : behavior.skills;
243
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
247
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
248
+ if (missingSkills.includes("pi-subagents")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
244
249
 
245
250
  let systemPrompt = a.systemPrompt?.trim() ?? "";
246
251
  if (resolvedSkills.length > 0) {
@@ -285,36 +290,42 @@ export function executeAsyncChain(
285
290
  return sessionFile;
286
291
  };
287
292
 
288
- const steps: RunnerStep[] = chain.map((s, stepIndex) => {
289
- if (isParallelStep(s)) {
290
- const parallelBehaviors = s.parallel.map((task) => {
291
- const agent = agents.find((candidate) => candidate.name === task.agent)!;
292
- return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
293
- });
294
- const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
295
- if (progressPrecreated) {
296
- if (!s.worktree) writeInitialProgressFile(runnerCwd);
297
- progressInstructionCreated = true;
298
- }
299
- return {
300
- parallel: s.parallel.map((t, taskIndex) => {
301
- let behaviorCwd: string | undefined;
302
- if (s.worktree) {
303
- try {
304
- behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
305
- } catch {
306
- behaviorCwd = undefined;
293
+ let steps: RunnerStep[];
294
+ try {
295
+ steps = chain.map((s, stepIndex) => {
296
+ if (isParallelStep(s)) {
297
+ const parallelBehaviors = s.parallel.map((task) => {
298
+ const agent = agents.find((candidate) => candidate.name === task.agent)!;
299
+ return resolveStepBehavior(agent, buildStepOverrides(task), chainSkills);
300
+ });
301
+ const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
302
+ if (progressPrecreated) {
303
+ if (!s.worktree) writeInitialProgressFile(runnerCwd);
304
+ progressInstructionCreated = true;
305
+ }
306
+ return {
307
+ parallel: s.parallel.map((t, taskIndex) => {
308
+ let behaviorCwd: string | undefined;
309
+ if (s.worktree) {
310
+ try {
311
+ behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
312
+ } catch {
313
+ behaviorCwd = undefined;
314
+ }
307
315
  }
308
- }
309
- return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
310
- }),
311
- concurrency: s.concurrency,
312
- failFast: s.failFast,
313
- worktree: s.worktree,
314
- };
315
- }
316
- return buildSeqStep(s as SequentialStep, nextSessionFile());
317
- });
316
+ return buildSeqStep(t, nextSessionFile(), behaviorCwd, progressPrecreated, parallelBehaviors[taskIndex]);
317
+ }),
318
+ concurrency: s.concurrency,
319
+ failFast: s.failFast,
320
+ worktree: s.worktree,
321
+ };
322
+ }
323
+ return buildSeqStep(s as SequentialStep, nextSessionFile());
324
+ });
325
+ } catch (error) {
326
+ if (error instanceof UnavailableSubagentSkillError) return formatAsyncStartError("chain", error.message);
327
+ throw error;
328
+ }
318
329
  let childTargetIndex = 0;
319
330
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
320
331
  if ("parallel" in step) {
@@ -365,16 +376,33 @@ export function executeAsyncChain(
365
376
  const firstAgents = isParallelStep(firstStep)
366
377
  ? firstStep.parallel.map((t) => t.agent)
367
378
  : [(firstStep as SequentialStep).agent];
379
+ const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
380
+ const flatAgents: string[] = [];
381
+ let flatStepStart = 0;
382
+ for (let stepIndex = 0; stepIndex < chain.length; stepIndex++) {
383
+ const step = chain[stepIndex]!;
384
+ if (isParallelStep(step)) {
385
+ parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
386
+ flatAgents.push(...step.parallel.map((task) => task.agent));
387
+ flatStepStart += step.parallel.length;
388
+ } else {
389
+ flatAgents.push((step as SequentialStep).agent);
390
+ flatStepStart++;
391
+ }
392
+ }
368
393
  ctx.pi.events.emit(SUBAGENT_ASYNC_STARTED_EVENT, {
369
394
  id,
370
395
  pid: spawnResult.pid,
371
396
  agent: firstAgents[0],
397
+ agents: flatAgents,
372
398
  task: isParallelStep(firstStep)
373
399
  ? firstStep.parallel[0]?.task?.slice(0, 50)
374
400
  : (firstStep as SequentialStep).task?.slice(0, 50),
375
401
  chain: chain.map((s) =>
376
402
  isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
377
403
  ),
404
+ chainStepCount: chain.length,
405
+ parallelGroups,
378
406
  cwd: runnerCwd,
379
407
  asyncDir,
380
408
  });
@@ -421,7 +449,8 @@ export function executeAsyncSingle(
421
449
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
422
450
  const skillNames = params.skills ?? agentConfig.skills ?? [];
423
451
  const availableModels = params.availableModels;
424
- const { resolved: resolvedSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
452
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, runnerCwd, ctx.cwd);
453
+ if (missingSkills.includes("pi-subagents")) return formatAsyncStartError("single", UNAVAILABLE_SUBAGENT_SKILL_ERROR);
425
454
  let systemPrompt = agentConfig.systemPrompt?.trim() ?? "";
426
455
  if (resolvedSkills.length > 0) {
427
456
  const injection = buildSkillInjection(resolvedSkills);
@@ -5,6 +5,8 @@ import { renderWidget } from "./render.ts";
5
5
  import { formatControlNoticeMessage } from "./subagent-control.ts";
6
6
  import {
7
7
  type AsyncJobState,
8
+ type AsyncParallelGroupStatus,
9
+ type AsyncStartedEvent,
8
10
  type ControlEvent,
9
11
  type SubagentState,
10
12
  POLL_INTERVAL_MS,
@@ -13,6 +15,23 @@ import {
13
15
  } from "./types.ts";
14
16
  import { readStatus } from "./utils.ts";
15
17
 
18
+
19
+ function isValidParallelGroup(group: AsyncParallelGroupStatus, stepCount: number, chainStepCount: number): boolean {
20
+ return Number.isInteger(group.start)
21
+ && Number.isInteger(group.count)
22
+ && Number.isInteger(group.stepIndex)
23
+ && group.start >= 0
24
+ && group.count > 0
25
+ && group.stepIndex >= 0
26
+ && group.stepIndex < chainStepCount
27
+ && group.start + group.count <= stepCount;
28
+ }
29
+
30
+ function normalizeParallelGroups(groups: AsyncParallelGroupStatus[] | undefined, stepCount: number, chainStepCount: number): AsyncParallelGroupStatus[] {
31
+ if (!groups?.length) return [];
32
+ return groups.filter((group) => isValidParallelGroup(group, stepCount, chainStepCount));
33
+ }
34
+
16
35
  interface AsyncJobTrackerOptions {
17
36
  completionRetentionMs?: number;
18
37
  pollIntervalMs?: number;
@@ -66,8 +85,8 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
66
85
  let parsed: unknown;
67
86
  try {
68
87
  parsed = JSON.parse(line);
69
- } catch {
70
- // Ignore malformed completed records but keep the poller alive for later events.
88
+ } catch (error) {
89
+ console.error(`Ignoring malformed async control event in '${eventsPath}':`, error);
71
90
  continue;
72
91
  }
73
92
  if (!parsed || typeof parsed !== "object" || (parsed as { type?: unknown }).type !== "subagent.control") continue;
@@ -83,7 +102,7 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
83
102
  if (record.channels.includes("event")) {
84
103
  pi.events.emit(SUBAGENT_CONTROL_EVENT, payload);
85
104
  }
86
- if (record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
105
+ if (record.event.type !== "active_long_running" && record.channels.includes("intercom") && record.intercom?.to && record.intercom.message) {
87
106
  pi.events.emit(SUBAGENT_CONTROL_INTERCOM_EVENT, {
88
107
  ...payload,
89
108
  to: record.intercom.to,
@@ -119,15 +138,30 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
119
138
  job.status = status.state;
120
139
  job.activityState = status.activityState;
121
140
  job.lastActivityAt = status.lastActivityAt ?? job.lastActivityAt;
122
- job.currentTool = status.currentTool ?? job.currentTool;
123
- job.currentToolStartedAt = status.currentToolStartedAt ?? job.currentToolStartedAt;
141
+ job.currentTool = status.currentTool;
142
+ job.currentToolStartedAt = status.currentToolStartedAt;
143
+ job.currentPath = status.currentPath;
144
+ job.turnCount = status.turnCount ?? job.turnCount;
145
+ job.toolCount = status.toolCount ?? job.toolCount;
124
146
  job.mode = status.mode;
125
147
  job.currentStep = status.currentStep ?? job.currentStep;
126
- job.stepsTotal = status.steps?.length ?? job.stepsTotal;
127
148
  job.startedAt = status.startedAt ?? job.startedAt;
128
149
  job.updatedAt = status.lastUpdate ?? Date.now();
129
150
  if (status.steps?.length) {
130
- job.agents = status.steps.map((step) => step.agent);
151
+ const groups = normalizeParallelGroups(status.parallelGroups, status.steps.length, status.chainStepCount ?? status.steps.length);
152
+ job.hasParallelGroups = groups.length > 0 || job.hasParallelGroups;
153
+ const activeGroup = status.currentStep !== undefined
154
+ ? groups.find((group) => status.currentStep! >= group.start && status.currentStep! < group.start + group.count)
155
+ : undefined;
156
+ const visibleSteps = activeGroup
157
+ ? status.steps.slice(activeGroup.start, activeGroup.start + activeGroup.count)
158
+ : status.steps;
159
+ job.activeParallelGroup = Boolean(activeGroup);
160
+ job.agents = visibleSteps.map((step) => step.agent);
161
+ job.stepsTotal = visibleSteps.length;
162
+ job.runningSteps = visibleSteps.filter((step) => step.status === "running").length;
163
+ job.completedSteps = visibleSteps.filter((step) => step.status === "complete" || step.status === "completed").length;
164
+ if (status.state === "complete") job.completedSteps = visibleSteps.length;
131
165
  }
132
166
  job.sessionDir = status.sessionDir ?? job.sessionDir;
133
167
  job.outputFile = status.outputFile ?? job.outputFile;
@@ -153,23 +187,26 @@ export function createAsyncJobTracker(pi: Pick<ExtensionAPI, "events">, state: S
153
187
  };
154
188
 
155
189
  const handleStarted = (data: unknown) => {
156
- const info = data as {
157
- id?: string;
158
- asyncDir?: string;
159
- agent?: string;
160
- chain?: string[];
161
- };
190
+ const info = data as AsyncStartedEvent;
162
191
  if (!info.id) return;
163
192
  const now = Date.now();
164
193
  const asyncDir = info.asyncDir ?? path.join(asyncDirRoot, info.id);
165
- const agents = info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
194
+ const rawAgents = info.agents?.length ? info.agents : info.chain && info.chain.length > 0 ? info.chain : info.agent ? [info.agent] : undefined;
195
+ const validParallelGroups = normalizeParallelGroups(info.parallelGroups, Number.MAX_SAFE_INTEGER, info.chainStepCount ?? Number.MAX_SAFE_INTEGER);
196
+ const firstGroup = validParallelGroups.find((group) => group.start === 0);
197
+ const firstGroupCount = firstGroup?.count;
198
+ const agents = firstGroupCount && firstGroupCount > 0
199
+ ? rawAgents?.slice(0, firstGroupCount)
200
+ : rawAgents;
166
201
  state.asyncJobs.set(info.id, {
167
202
  asyncId: info.id,
168
203
  asyncDir,
169
204
  status: "queued",
170
205
  mode: info.chain ? "chain" : "single",
171
206
  agents,
172
- stepsTotal: agents?.length,
207
+ stepsTotal: firstGroupCount ?? agents?.length,
208
+ hasParallelGroups: validParallelGroups.length > 0,
209
+ activeParallelGroup: Boolean(firstGroupCount && firstGroupCount > 0),
173
210
  startedAt: now,
174
211
  updatedAt: now,
175
212
  });