@astralyn/pi 0.82.3 → 0.82.5

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 (158) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/cli/config-selector.d.ts.map +1 -1
  3. package/dist/cli/config-selector.js +3 -2
  4. package/dist/cli/config-selector.js.map +1 -1
  5. package/dist/cli/startup-ui.d.ts.map +1 -1
  6. package/dist/cli/startup-ui.js +3 -2
  7. package/dist/cli/startup-ui.js.map +1 -1
  8. package/dist/core/agent-session.d.ts +2 -0
  9. package/dist/core/agent-session.d.ts.map +1 -1
  10. package/dist/core/agent-session.js +33 -14
  11. package/dist/core/agent-session.js.map +1 -1
  12. package/dist/core/extensions/index.d.ts +2 -2
  13. package/dist/core/extensions/index.d.ts.map +1 -1
  14. package/dist/core/extensions/index.js +1 -1
  15. package/dist/core/extensions/index.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts +1 -1
  17. package/dist/core/extensions/loader.d.ts.map +1 -1
  18. package/dist/core/extensions/loader.js +2 -3
  19. package/dist/core/extensions/loader.js.map +1 -1
  20. package/dist/core/extensions/types.d.ts +18 -0
  21. package/dist/core/extensions/types.d.ts.map +1 -1
  22. package/dist/core/extensions/types.js +11 -0
  23. package/dist/core/extensions/types.js.map +1 -1
  24. package/dist/extensions/index.d.ts.map +1 -1
  25. package/dist/extensions/index.js +2 -0
  26. package/dist/extensions/index.js.map +1 -1
  27. package/dist/extensions/plan/constants.d.ts +31 -0
  28. package/dist/extensions/plan/constants.d.ts.map +1 -0
  29. package/dist/extensions/plan/constants.js +57 -0
  30. package/dist/extensions/plan/constants.js.map +1 -0
  31. package/dist/extensions/plan/index.d.ts +21 -0
  32. package/dist/extensions/plan/index.d.ts.map +1 -0
  33. package/dist/extensions/plan/index.js +291 -0
  34. package/dist/extensions/plan/index.js.map +1 -0
  35. package/dist/extensions/plan/schema.d.ts +15 -0
  36. package/dist/extensions/plan/schema.d.ts.map +1 -0
  37. package/dist/extensions/plan/schema.js +13 -0
  38. package/dist/extensions/plan/schema.js.map +1 -0
  39. package/dist/extensions/plan/state.d.ts +29 -0
  40. package/dist/extensions/plan/state.d.ts.map +1 -0
  41. package/dist/extensions/plan/state.js +74 -0
  42. package/dist/extensions/plan/state.js.map +1 -0
  43. package/dist/extensions/plan/storage.d.ts +18 -0
  44. package/dist/extensions/plan/storage.d.ts.map +1 -0
  45. package/dist/extensions/plan/storage.js +58 -0
  46. package/dist/extensions/plan/storage.js.map +1 -0
  47. package/dist/extensions/question/dialog.d.ts +3 -1
  48. package/dist/extensions/question/dialog.d.ts.map +1 -1
  49. package/dist/extensions/question/dialog.js +106 -52
  50. package/dist/extensions/question/dialog.js.map +1 -1
  51. package/dist/extensions/question/index.d.ts.map +1 -1
  52. package/dist/extensions/question/index.js +2 -2
  53. package/dist/extensions/question/index.js.map +1 -1
  54. package/dist/extensions/question/render-cache.d.ts +9 -0
  55. package/dist/extensions/question/render-cache.d.ts.map +1 -1
  56. package/dist/extensions/question/render-cache.js +22 -0
  57. package/dist/extensions/question/render-cache.js.map +1 -1
  58. package/dist/extensions/question/results.d.ts.map +1 -1
  59. package/dist/extensions/question/results.js +6 -1
  60. package/dist/extensions/question/results.js.map +1 -1
  61. package/dist/extensions/question/schema.d.ts +4 -0
  62. package/dist/extensions/question/schema.d.ts.map +1 -1
  63. package/dist/extensions/question/schema.js +19 -28
  64. package/dist/extensions/question/schema.js.map +1 -1
  65. package/dist/extensions/question/types.d.ts +1 -1
  66. package/dist/extensions/question/types.d.ts.map +1 -1
  67. package/dist/extensions/question/types.js.map +1 -1
  68. package/dist/extensions/rewind/anchor.d.ts +29 -0
  69. package/dist/extensions/rewind/anchor.d.ts.map +1 -0
  70. package/dist/extensions/rewind/anchor.js +35 -0
  71. package/dist/extensions/rewind/anchor.js.map +1 -0
  72. package/dist/extensions/rewind/engine.d.ts +2 -6
  73. package/dist/extensions/rewind/engine.d.ts.map +1 -1
  74. package/dist/extensions/rewind/engine.js +41 -10
  75. package/dist/extensions/rewind/engine.js.map +1 -1
  76. package/dist/extensions/rewind/index.d.ts.map +1 -1
  77. package/dist/extensions/rewind/index.js +55 -21
  78. package/dist/extensions/rewind/index.js.map +1 -1
  79. package/dist/extensions/rewind/menu.d.ts.map +1 -1
  80. package/dist/extensions/rewind/menu.js +7 -2
  81. package/dist/extensions/rewind/menu.js.map +1 -1
  82. package/dist/extensions/rewind/restore.d.ts +26 -9
  83. package/dist/extensions/rewind/restore.d.ts.map +1 -1
  84. package/dist/extensions/rewind/restore.js +58 -16
  85. package/dist/extensions/rewind/restore.js.map +1 -1
  86. package/dist/extensions/subagent/activity.d.ts +1 -0
  87. package/dist/extensions/subagent/activity.d.ts.map +1 -1
  88. package/dist/extensions/subagent/activity.js +20 -4
  89. package/dist/extensions/subagent/activity.js.map +1 -1
  90. package/dist/extensions/subagent/agents.d.ts.map +1 -1
  91. package/dist/extensions/subagent/agents.js +13 -18
  92. package/dist/extensions/subagent/agents.js.map +1 -1
  93. package/dist/extensions/subagent/constants.d.ts +5 -4
  94. package/dist/extensions/subagent/constants.d.ts.map +1 -1
  95. package/dist/extensions/subagent/constants.js +12 -5
  96. package/dist/extensions/subagent/constants.js.map +1 -1
  97. package/dist/extensions/subagent/index.d.ts.map +1 -1
  98. package/dist/extensions/subagent/index.js +12 -5
  99. package/dist/extensions/subagent/index.js.map +1 -1
  100. package/dist/extensions/subagent/render.d.ts.map +1 -1
  101. package/dist/extensions/subagent/render.js +166 -41
  102. package/dist/extensions/subagent/render.js.map +1 -1
  103. package/dist/extensions/subagent/resolve.d.ts +2 -5
  104. package/dist/extensions/subagent/resolve.d.ts.map +1 -1
  105. package/dist/extensions/subagent/resolve.js +8 -13
  106. package/dist/extensions/subagent/resolve.js.map +1 -1
  107. package/dist/extensions/subagent/runner.d.ts +3 -1
  108. package/dist/extensions/subagent/runner.d.ts.map +1 -1
  109. package/dist/extensions/subagent/runner.js +115 -66
  110. package/dist/extensions/subagent/runner.js.map +1 -1
  111. package/dist/extensions/subagent/schema.d.ts +0 -6
  112. package/dist/extensions/subagent/schema.d.ts.map +1 -1
  113. package/dist/extensions/subagent/schema.js +3 -4
  114. package/dist/extensions/subagent/schema.js.map +1 -1
  115. package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
  116. package/dist/extensions/subagent/sdk-runner.js +37 -16
  117. package/dist/extensions/subagent/sdk-runner.js.map +1 -1
  118. package/dist/extensions/subagent/types.d.ts +10 -2
  119. package/dist/extensions/subagent/types.d.ts.map +1 -1
  120. package/dist/extensions/subagent/types.js.map +1 -1
  121. package/dist/extensions/todo/constants.d.ts +1 -1
  122. package/dist/extensions/todo/constants.d.ts.map +1 -1
  123. package/dist/extensions/todo/constants.js +29 -4
  124. package/dist/extensions/todo/constants.js.map +1 -1
  125. package/dist/extensions/todo/index.d.ts +1 -1
  126. package/dist/extensions/todo/index.d.ts.map +1 -1
  127. package/dist/extensions/todo/index.js +22 -3
  128. package/dist/extensions/todo/index.js.map +1 -1
  129. package/dist/extensions/todo/schema.d.ts +6 -5
  130. package/dist/extensions/todo/schema.d.ts.map +1 -1
  131. package/dist/extensions/todo/schema.js +20 -9
  132. package/dist/extensions/todo/schema.js.map +1 -1
  133. package/dist/extensions/todo/state.d.ts +5 -1
  134. package/dist/extensions/todo/state.d.ts.map +1 -1
  135. package/dist/extensions/todo/state.js +184 -38
  136. package/dist/extensions/todo/state.js.map +1 -1
  137. package/dist/extensions/todo/view.d.ts.map +1 -1
  138. package/dist/extensions/todo/view.js +10 -6
  139. package/dist/extensions/todo/view.js.map +1 -1
  140. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  141. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  142. package/dist/modes/interactive/interactive-mode.js +36 -2
  143. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  144. package/dist/utils/coalescing-terminal.d.ts +116 -0
  145. package/dist/utils/coalescing-terminal.d.ts.map +1 -0
  146. package/dist/utils/coalescing-terminal.js +256 -0
  147. package/dist/utils/coalescing-terminal.js.map +1 -0
  148. package/docs/bundled/extensions/README.md +1 -0
  149. package/docs/bundled/extensions/plan.md +31 -0
  150. package/docs/bundled/extensions/question.md +9 -7
  151. package/docs/bundled/extensions/rewind.md +19 -8
  152. package/docs/bundled/extensions/subagent.md +16 -16
  153. package/docs/bundled/extensions/todo.md +5 -3
  154. package/docs/compaction.md +6 -2
  155. package/docs/docs.json +4 -0
  156. package/docs/extensions.md +4 -2
  157. package/npm-shrinkwrap.json +2 -2
  158. package/package.json +1 -1
@@ -2,15 +2,12 @@ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { type Api, type Model } from "@earendil-works/pi-ai/compat";
3
3
  import type { ModelRegistry } from "../../core/model-registry.ts";
4
4
  import type { SubagentTask } from "./schema.ts";
5
- import { loadSubagentConfig } from "./settings.ts";
6
- import type { AgentDefinition, ResolvedSubagentTask } from "./types.ts";
5
+ import type { AgentDefinition, ResolvedSubagentTask, SubagentConfigFile } from "./types.ts";
7
6
  export interface ParentModelContext {
8
7
  model: Model<Api> | undefined;
9
8
  thinking: ThinkingLevel;
10
9
  modelRegistry: Pick<ModelRegistry, "find" | "getAvailable" | "hasConfiguredAuth">;
11
10
  }
12
11
  export declare function resolveTaskCwd(parentCwd: string, requestedCwd: string | undefined): string;
13
- export declare function resolveSubagentTask(task: SubagentTask, parentCwd: string, agents: readonly AgentDefinition[], parent: ParentModelContext, configAgentDir?: string): Promise<ResolvedSubagentTask>;
14
- export declare function loadProfileOverrides(agentDir?: string): Promise<Awaited<ReturnType<typeof loadSubagentConfig>>>;
15
- export declare function modelLabel(model: Model<Api>): string;
12
+ export declare function resolveSubagentTask(task: SubagentTask, parentCwd: string, agents: readonly AgentDefinition[], parent: ParentModelContext, configAgentDir?: string, preloadedConfig?: SubagentConfigFile): Promise<ResolvedSubagentTask>;
16
13
  //# sourceMappingURL=resolve.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/resolve.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,KAAK,GAAG,EAAsB,KAAK,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAExF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAA2B,MAAM,YAAY,CAAC;AAEjG,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,cAAc,GAAG,mBAAmB,CAAC,CAAC;CAClF;AAoBD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAc1F;AA4CD,wBAAsB,mBAAmB,CACxC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,MAAM,EAAE,kBAAkB,EAC1B,cAAc,SAAgB,GAC5B,OAAO,CAAC,oBAAoB,CAAC,CAqB/B;AAED,wBAAsB,oBAAoB,CACzC,QAAQ,SAAgB,GACtB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAEzD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAEpD","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { type Api, clampThinkingLevel, type Model } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport type { SubagentTask } from \"./schema.ts\";\nimport { loadSubagentConfig } from \"./settings.ts\";\nimport type { AgentDefinition, ResolvedSubagentTask, SubagentProfileOverride } from \"./types.ts\";\n\nexport interface ParentModelContext {\n\tmodel: Model<Api> | undefined;\n\tthinking: ThinkingLevel;\n\tmodelRegistry: Pick<ModelRegistry, \"find\" | \"getAvailable\" | \"hasConfiguredAuth\">;\n}\n\nfunction normalizeCwdInput(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(\"cwd must not be empty.\");\n\treturn trimmed.startsWith(\"@\") ? trimmed.slice(1) : trimmed;\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n\tconst normalizedParent = process.platform === \"win32\" ? parent.toLowerCase() : parent;\n\tconst normalizedChild = process.platform === \"win32\" ? child.toLowerCase() : child;\n\tconst rest = relative(normalizedParent, normalizedChild);\n\treturn rest === \"\" || (rest !== \"..\" && !rest.startsWith(`..${requirePathSeparator()}`) && !isAbsolute(rest));\n}\n\nfunction requirePathSeparator(): string {\n\treturn process.platform === \"win32\" ? \"\\\\\" : \"/\";\n}\n\nexport function resolveTaskCwd(parentCwd: string, requestedCwd: string | undefined): string {\n\tconst value = normalizeCwdInput(requestedCwd);\n\tif (value !== undefined && isAbsolute(value))\n\t\tthrow new Error(\"cwd must be a relative path inside the parent working directory.\");\n\tconst candidate = resolve(parentCwd, value ?? \".\");\n\tconst realParent = realpathSync(parentCwd);\n\tif (!isWithin(realParent, candidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\tif (!existsSync(candidate) || !statSync(candidate).isDirectory())\n\t\tthrow new Error(`Subagent cwd is not a directory: ${candidate}`);\n\tconst realCandidate = realpathSync(candidate);\n\tif (!isWithin(realParent, realCandidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\treturn realCandidate;\n}\n\nfunction formatModel(model: Model<Api>): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction findAvailableModel(spec: string, parent: ParentModelContext): Model<Api> {\n\tconst normalized = spec.trim();\n\tlet model: Model<Api> | undefined;\n\tif (normalized.includes(\"/\")) {\n\t\tconst separator = normalized.indexOf(\"/\");\n\t\tmodel = parent.modelRegistry.find(normalized.slice(0, separator), normalized.slice(separator + 1));\n\t} else {\n\t\tconst matches = parent.modelRegistry.getAvailable().filter((candidate) => candidate.id === normalized);\n\t\tif (matches.length === 1) model = matches[0];\n\t\tif (matches.length > 1) throw new Error(`Model id \"${normalized}\" is ambiguous; use provider/model.`);\n\t}\n\tif (!model) throw new Error(`Model \"${normalized}\" is not available.`);\n\tif (!parent.modelRegistry.hasConfiguredAuth(model))\n\t\tthrow new Error(`Model \"${normalized}\" has no configured authentication.`);\n\treturn model;\n}\n\n// Exactly two layers: a /agents override wins, otherwise the subagent\n// inherits the parent session. Callers and agent files cannot pick models.\nfunction resolveModel(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n): { model: Model<Api>; source: ResolvedSubagentTask[\"modelSource\"] } {\n\tif (override?.model) return { model: findAvailableModel(override.model, parent), source: \"profile\" };\n\tif (!parent.model) throw new Error(\"The parent session has no active model.\");\n\treturn { model: parent.model, source: \"parent\" };\n}\n\nfunction resolveThinking(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n\tmodel: Model<Api>,\n): { thinking: ThinkingLevel; source: ResolvedSubagentTask[\"thinkingSource\"] } {\n\tconst requested = override?.thinking ?? parent.thinking;\n\tconst source: ResolvedSubagentTask[\"thinkingSource\"] = override?.thinking ? \"profile\" : \"parent\";\n\treturn { thinking: clampThinkingLevel(model, requested) as ThinkingLevel, source };\n}\n\nexport async function resolveSubagentTask(\n\ttask: SubagentTask,\n\tparentCwd: string,\n\tagents: readonly AgentDefinition[],\n\tparent: ParentModelContext,\n\tconfigAgentDir = getAgentDir(),\n): Promise<ResolvedSubagentTask> {\n\tconst agentName = task.agent ?? \"general\";\n\tconst agent = agents.find((candidate) => candidate.name === agentName);\n\tif (!agent) {\n\t\tconst available = agents.map((candidate) => candidate.name).join(\", \") || \"none\";\n\t\tthrow new Error(`Unknown agent \"${agentName}\". Available agents: ${available}.`);\n\t}\n\tconst config = await loadSubagentConfig(configAgentDir);\n\tconst override = config.profiles[agent.name];\n\tconst resolvedModel = resolveModel(override, parent);\n\tconst resolvedThinking = resolveThinking(override, parent, resolvedModel.model);\n\treturn {\n\t\tagent,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\tcwd: resolveTaskCwd(parentCwd, task.cwd ?? undefined),\n\t\tmodel: resolvedModel.model,\n\t\tthinking: resolvedThinking.thinking,\n\t\tmodelSource: resolvedModel.source,\n\t\tthinkingSource: resolvedThinking.source,\n\t};\n}\n\nexport async function loadProfileOverrides(\n\tagentDir = getAgentDir(),\n): Promise<Awaited<ReturnType<typeof loadSubagentConfig>>> {\n\treturn loadSubagentConfig(agentDir);\n}\n\nexport function modelLabel(model: Model<Api>): string {\n\treturn formatModel(model);\n}\n"]}
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/resolve.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnE,OAAO,EAAE,KAAK,GAAG,EAAsB,KAAK,KAAK,EAAE,MAAM,8BAA8B,CAAC;AAExF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,KAAK,EAAE,eAAe,EAAE,oBAAoB,EAAE,kBAAkB,EAA2B,MAAM,YAAY,CAAC;AAErH,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;IAC9B,QAAQ,EAAE,aAAa,CAAC;IACxB,aAAa,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,cAAc,GAAG,mBAAmB,CAAC,CAAC;CAClF;AAoBD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAkB1F;AAwCD,wBAAsB,mBAAmB,CACxC,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,MAAM,EAAE,kBAAkB,EAC1B,cAAc,SAAgB,EAC9B,eAAe,CAAC,EAAE,kBAAkB,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAqB/B","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { type Api, clampThinkingLevel, type Model } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport type { SubagentTask } from \"./schema.ts\";\nimport { loadSubagentConfig } from \"./settings.ts\";\nimport type { AgentDefinition, ResolvedSubagentTask, SubagentConfigFile, SubagentProfileOverride } from \"./types.ts\";\n\nexport interface ParentModelContext {\n\tmodel: Model<Api> | undefined;\n\tthinking: ThinkingLevel;\n\tmodelRegistry: Pick<ModelRegistry, \"find\" | \"getAvailable\" | \"hasConfiguredAuth\">;\n}\n\nfunction normalizeCwdInput(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(\"cwd must not be empty.\");\n\treturn trimmed.startsWith(\"@\") ? trimmed.slice(1) : trimmed;\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n\tconst normalizedParent = process.platform === \"win32\" ? parent.toLowerCase() : parent;\n\tconst normalizedChild = process.platform === \"win32\" ? child.toLowerCase() : child;\n\tconst rest = relative(normalizedParent, normalizedChild);\n\treturn rest === \"\" || (rest !== \"..\" && !rest.startsWith(`..${requirePathSeparator()}`) && !isAbsolute(rest));\n}\n\nfunction requirePathSeparator(): string {\n\treturn process.platform === \"win32\" ? \"\\\\\" : \"/\";\n}\n\nexport function resolveTaskCwd(parentCwd: string, requestedCwd: string | undefined): string {\n\tconst value = normalizeCwdInput(requestedCwd);\n\tif (value !== undefined && isAbsolute(value))\n\t\tthrow new Error(\"cwd must be a relative path inside the parent working directory.\");\n\tconst candidate = resolve(parentCwd, value ?? \".\");\n\t// Lexical check first: candidate and parent are both un-realpathed here,\n\t// so a symlinked parent (macOS /tmp, junctions) compares consistently and\n\t// only genuine \"..\" traversal fails. Symlink escapes are caught below by\n\t// comparing the realpathed candidate against the realpathed parent.\n\tif (!isWithin(resolve(parentCwd), candidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\tif (!existsSync(candidate) || !statSync(candidate).isDirectory())\n\t\tthrow new Error(`Subagent cwd is not a directory: ${candidate}`);\n\tconst realParent = realpathSync(parentCwd);\n\tconst realCandidate = realpathSync(candidate);\n\tif (!isWithin(realParent, realCandidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\treturn realCandidate;\n}\n\nfunction findAvailableModel(spec: string, parent: ParentModelContext): Model<Api> {\n\tconst normalized = spec.trim();\n\tlet model: Model<Api> | undefined;\n\tif (normalized.includes(\"/\")) {\n\t\tconst separator = normalized.indexOf(\"/\");\n\t\tmodel = parent.modelRegistry.find(normalized.slice(0, separator), normalized.slice(separator + 1));\n\t} else {\n\t\tconst matches = parent.modelRegistry.getAvailable().filter((candidate) => candidate.id === normalized);\n\t\tif (matches.length === 1) model = matches[0];\n\t\tif (matches.length > 1) throw new Error(`Model id \"${normalized}\" is ambiguous; use provider/model.`);\n\t}\n\tif (!model) throw new Error(`Model \"${normalized}\" is not available.`);\n\tif (!parent.modelRegistry.hasConfiguredAuth(model))\n\t\tthrow new Error(`Model \"${normalized}\" has no configured authentication.`);\n\treturn model;\n}\n\n// Exactly two layers: a /agents override wins, otherwise the subagent\n// inherits the parent session. Callers and agent files cannot pick models.\nfunction resolveModel(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n): { model: Model<Api>; source: ResolvedSubagentTask[\"modelSource\"] } {\n\tif (override?.model) return { model: findAvailableModel(override.model, parent), source: \"profile\" };\n\tif (!parent.model) throw new Error(\"The parent session has no active model.\");\n\treturn { model: parent.model, source: \"parent\" };\n}\n\nfunction resolveThinking(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n\tmodel: Model<Api>,\n): { thinking: ThinkingLevel; source: ResolvedSubagentTask[\"thinkingSource\"] } {\n\tconst requested = override?.thinking ?? parent.thinking;\n\tconst source: ResolvedSubagentTask[\"thinkingSource\"] = override?.thinking ? \"profile\" : \"parent\";\n\treturn { thinking: clampThinkingLevel(model, requested) as ThinkingLevel, source };\n}\n\nexport async function resolveSubagentTask(\n\ttask: SubagentTask,\n\tparentCwd: string,\n\tagents: readonly AgentDefinition[],\n\tparent: ParentModelContext,\n\tconfigAgentDir = getAgentDir(),\n\tpreloadedConfig?: SubagentConfigFile,\n): Promise<ResolvedSubagentTask> {\n\tconst agentName = task.agent ?? \"general\";\n\tconst agent = agents.find((candidate) => candidate.name === agentName);\n\tif (!agent) {\n\t\tconst available = agents.map((candidate) => candidate.name).join(\", \") || \"none\";\n\t\tthrow new Error(`Unknown agent \"${agentName}\". Available agents: ${available}.`);\n\t}\n\tconst config = preloadedConfig ?? (await loadSubagentConfig(configAgentDir));\n\tconst override = config.profiles[agent.name];\n\tconst resolvedModel = resolveModel(override, parent);\n\tconst resolvedThinking = resolveThinking(override, parent, resolvedModel.model);\n\treturn {\n\t\tagent,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\tcwd: resolveTaskCwd(parentCwd, task.cwd ?? undefined),\n\t\tmodel: resolvedModel.model,\n\t\tthinking: resolvedThinking.thinking,\n\t\tmodelSource: resolvedModel.source,\n\t\tthinkingSource: resolvedThinking.source,\n\t};\n}\n"]}
@@ -25,19 +25,20 @@ export function resolveTaskCwd(parentCwd, requestedCwd) {
25
25
  if (value !== undefined && isAbsolute(value))
26
26
  throw new Error("cwd must be a relative path inside the parent working directory.");
27
27
  const candidate = resolve(parentCwd, value ?? ".");
28
- const realParent = realpathSync(parentCwd);
29
- if (!isWithin(realParent, candidate))
28
+ // Lexical check first: candidate and parent are both un-realpathed here,
29
+ // so a symlinked parent (macOS /tmp, junctions) compares consistently and
30
+ // only genuine ".." traversal fails. Symlink escapes are caught below by
31
+ // comparing the realpathed candidate against the realpathed parent.
32
+ if (!isWithin(resolve(parentCwd), candidate))
30
33
  throw new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);
31
34
  if (!existsSync(candidate) || !statSync(candidate).isDirectory())
32
35
  throw new Error(`Subagent cwd is not a directory: ${candidate}`);
36
+ const realParent = realpathSync(parentCwd);
33
37
  const realCandidate = realpathSync(candidate);
34
38
  if (!isWithin(realParent, realCandidate))
35
39
  throw new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);
36
40
  return realCandidate;
37
41
  }
38
- function formatModel(model) {
39
- return `${model.provider}/${model.id}`;
40
- }
41
42
  function findAvailableModel(spec, parent) {
42
43
  const normalized = spec.trim();
43
44
  let model;
@@ -72,14 +73,14 @@ function resolveThinking(override, parent, model) {
72
73
  const source = override?.thinking ? "profile" : "parent";
73
74
  return { thinking: clampThinkingLevel(model, requested), source };
74
75
  }
75
- export async function resolveSubagentTask(task, parentCwd, agents, parent, configAgentDir = getAgentDir()) {
76
+ export async function resolveSubagentTask(task, parentCwd, agents, parent, configAgentDir = getAgentDir(), preloadedConfig) {
76
77
  const agentName = task.agent ?? "general";
77
78
  const agent = agents.find((candidate) => candidate.name === agentName);
78
79
  if (!agent) {
79
80
  const available = agents.map((candidate) => candidate.name).join(", ") || "none";
80
81
  throw new Error(`Unknown agent "${agentName}". Available agents: ${available}.`);
81
82
  }
82
- const config = await loadSubagentConfig(configAgentDir);
83
+ const config = preloadedConfig ?? (await loadSubagentConfig(configAgentDir));
83
84
  const override = config.profiles[agent.name];
84
85
  const resolvedModel = resolveModel(override, parent);
85
86
  const resolvedThinking = resolveThinking(override, parent, resolvedModel.model);
@@ -94,10 +95,4 @@ export async function resolveSubagentTask(task, parentCwd, agents, parent, confi
94
95
  thinkingSource: resolvedThinking.source,
95
96
  };
96
97
  }
97
- export async function loadProfileOverrides(agentDir = getAgentDir()) {
98
- return loadSubagentConfig(agentDir);
99
- }
100
- export function modelLabel(model) {
101
- return formatModel(model);
102
- }
103
98
  //# sourceMappingURL=resolve.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/subagent/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { type Api, clampThinkingLevel, type Model } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport type { SubagentTask } from \"./schema.ts\";\nimport { loadSubagentConfig } from \"./settings.ts\";\nimport type { AgentDefinition, ResolvedSubagentTask, SubagentProfileOverride } from \"./types.ts\";\n\nexport interface ParentModelContext {\n\tmodel: Model<Api> | undefined;\n\tthinking: ThinkingLevel;\n\tmodelRegistry: Pick<ModelRegistry, \"find\" | \"getAvailable\" | \"hasConfiguredAuth\">;\n}\n\nfunction normalizeCwdInput(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(\"cwd must not be empty.\");\n\treturn trimmed.startsWith(\"@\") ? trimmed.slice(1) : trimmed;\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n\tconst normalizedParent = process.platform === \"win32\" ? parent.toLowerCase() : parent;\n\tconst normalizedChild = process.platform === \"win32\" ? child.toLowerCase() : child;\n\tconst rest = relative(normalizedParent, normalizedChild);\n\treturn rest === \"\" || (rest !== \"..\" && !rest.startsWith(`..${requirePathSeparator()}`) && !isAbsolute(rest));\n}\n\nfunction requirePathSeparator(): string {\n\treturn process.platform === \"win32\" ? \"\\\\\" : \"/\";\n}\n\nexport function resolveTaskCwd(parentCwd: string, requestedCwd: string | undefined): string {\n\tconst value = normalizeCwdInput(requestedCwd);\n\tif (value !== undefined && isAbsolute(value))\n\t\tthrow new Error(\"cwd must be a relative path inside the parent working directory.\");\n\tconst candidate = resolve(parentCwd, value ?? \".\");\n\tconst realParent = realpathSync(parentCwd);\n\tif (!isWithin(realParent, candidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\tif (!existsSync(candidate) || !statSync(candidate).isDirectory())\n\t\tthrow new Error(`Subagent cwd is not a directory: ${candidate}`);\n\tconst realCandidate = realpathSync(candidate);\n\tif (!isWithin(realParent, realCandidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\treturn realCandidate;\n}\n\nfunction formatModel(model: Model<Api>): string {\n\treturn `${model.provider}/${model.id}`;\n}\n\nfunction findAvailableModel(spec: string, parent: ParentModelContext): Model<Api> {\n\tconst normalized = spec.trim();\n\tlet model: Model<Api> | undefined;\n\tif (normalized.includes(\"/\")) {\n\t\tconst separator = normalized.indexOf(\"/\");\n\t\tmodel = parent.modelRegistry.find(normalized.slice(0, separator), normalized.slice(separator + 1));\n\t} else {\n\t\tconst matches = parent.modelRegistry.getAvailable().filter((candidate) => candidate.id === normalized);\n\t\tif (matches.length === 1) model = matches[0];\n\t\tif (matches.length > 1) throw new Error(`Model id \"${normalized}\" is ambiguous; use provider/model.`);\n\t}\n\tif (!model) throw new Error(`Model \"${normalized}\" is not available.`);\n\tif (!parent.modelRegistry.hasConfiguredAuth(model))\n\t\tthrow new Error(`Model \"${normalized}\" has no configured authentication.`);\n\treturn model;\n}\n\n// Exactly two layers: a /agents override wins, otherwise the subagent\n// inherits the parent session. Callers and agent files cannot pick models.\nfunction resolveModel(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n): { model: Model<Api>; source: ResolvedSubagentTask[\"modelSource\"] } {\n\tif (override?.model) return { model: findAvailableModel(override.model, parent), source: \"profile\" };\n\tif (!parent.model) throw new Error(\"The parent session has no active model.\");\n\treturn { model: parent.model, source: \"parent\" };\n}\n\nfunction resolveThinking(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n\tmodel: Model<Api>,\n): { thinking: ThinkingLevel; source: ResolvedSubagentTask[\"thinkingSource\"] } {\n\tconst requested = override?.thinking ?? parent.thinking;\n\tconst source: ResolvedSubagentTask[\"thinkingSource\"] = override?.thinking ? \"profile\" : \"parent\";\n\treturn { thinking: clampThinkingLevel(model, requested) as ThinkingLevel, source };\n}\n\nexport async function resolveSubagentTask(\n\ttask: SubagentTask,\n\tparentCwd: string,\n\tagents: readonly AgentDefinition[],\n\tparent: ParentModelContext,\n\tconfigAgentDir = getAgentDir(),\n): Promise<ResolvedSubagentTask> {\n\tconst agentName = task.agent ?? \"general\";\n\tconst agent = agents.find((candidate) => candidate.name === agentName);\n\tif (!agent) {\n\t\tconst available = agents.map((candidate) => candidate.name).join(\", \") || \"none\";\n\t\tthrow new Error(`Unknown agent \"${agentName}\". Available agents: ${available}.`);\n\t}\n\tconst config = await loadSubagentConfig(configAgentDir);\n\tconst override = config.profiles[agent.name];\n\tconst resolvedModel = resolveModel(override, parent);\n\tconst resolvedThinking = resolveThinking(override, parent, resolvedModel.model);\n\treturn {\n\t\tagent,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\tcwd: resolveTaskCwd(parentCwd, task.cwd ?? undefined),\n\t\tmodel: resolvedModel.model,\n\t\tthinking: resolvedThinking.thinking,\n\t\tmodelSource: resolvedModel.source,\n\t\tthinkingSource: resolvedThinking.source,\n\t};\n}\n\nexport async function loadProfileOverrides(\n\tagentDir = getAgentDir(),\n): Promise<Awaited<ReturnType<typeof loadSubagentConfig>>> {\n\treturn loadSubagentConfig(agentDir);\n}\n\nexport function modelLabel(model: Model<Api>): string {\n\treturn formatModel(model);\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../src/extensions/subagent/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, realpathSync, statSync } from \"node:fs\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport type { ThinkingLevel } from \"@earendil-works/pi-agent-core\";\nimport { type Api, clampThinkingLevel, type Model } from \"@earendil-works/pi-ai/compat\";\nimport { getAgentDir } from \"../../config.ts\";\nimport type { ModelRegistry } from \"../../core/model-registry.ts\";\nimport type { SubagentTask } from \"./schema.ts\";\nimport { loadSubagentConfig } from \"./settings.ts\";\nimport type { AgentDefinition, ResolvedSubagentTask, SubagentConfigFile, SubagentProfileOverride } from \"./types.ts\";\n\nexport interface ParentModelContext {\n\tmodel: Model<Api> | undefined;\n\tthinking: ThinkingLevel;\n\tmodelRegistry: Pick<ModelRegistry, \"find\" | \"getAvailable\" | \"hasConfiguredAuth\">;\n}\n\nfunction normalizeCwdInput(value: string | undefined): string | undefined {\n\tif (value === undefined) return undefined;\n\tconst trimmed = value.trim();\n\tif (!trimmed) throw new Error(\"cwd must not be empty.\");\n\treturn trimmed.startsWith(\"@\") ? trimmed.slice(1) : trimmed;\n}\n\nfunction isWithin(parent: string, child: string): boolean {\n\tconst normalizedParent = process.platform === \"win32\" ? parent.toLowerCase() : parent;\n\tconst normalizedChild = process.platform === \"win32\" ? child.toLowerCase() : child;\n\tconst rest = relative(normalizedParent, normalizedChild);\n\treturn rest === \"\" || (rest !== \"..\" && !rest.startsWith(`..${requirePathSeparator()}`) && !isAbsolute(rest));\n}\n\nfunction requirePathSeparator(): string {\n\treturn process.platform === \"win32\" ? \"\\\\\" : \"/\";\n}\n\nexport function resolveTaskCwd(parentCwd: string, requestedCwd: string | undefined): string {\n\tconst value = normalizeCwdInput(requestedCwd);\n\tif (value !== undefined && isAbsolute(value))\n\t\tthrow new Error(\"cwd must be a relative path inside the parent working directory.\");\n\tconst candidate = resolve(parentCwd, value ?? \".\");\n\t// Lexical check first: candidate and parent are both un-realpathed here,\n\t// so a symlinked parent (macOS /tmp, junctions) compares consistently and\n\t// only genuine \"..\" traversal fails. Symlink escapes are caught below by\n\t// comparing the realpathed candidate against the realpathed parent.\n\tif (!isWithin(resolve(parentCwd), candidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\tif (!existsSync(candidate) || !statSync(candidate).isDirectory())\n\t\tthrow new Error(`Subagent cwd is not a directory: ${candidate}`);\n\tconst realParent = realpathSync(parentCwd);\n\tconst realCandidate = realpathSync(candidate);\n\tif (!isWithin(realParent, realCandidate))\n\t\tthrow new Error(`Subagent cwd escapes the parent working directory: ${requestedCwd}`);\n\treturn realCandidate;\n}\n\nfunction findAvailableModel(spec: string, parent: ParentModelContext): Model<Api> {\n\tconst normalized = spec.trim();\n\tlet model: Model<Api> | undefined;\n\tif (normalized.includes(\"/\")) {\n\t\tconst separator = normalized.indexOf(\"/\");\n\t\tmodel = parent.modelRegistry.find(normalized.slice(0, separator), normalized.slice(separator + 1));\n\t} else {\n\t\tconst matches = parent.modelRegistry.getAvailable().filter((candidate) => candidate.id === normalized);\n\t\tif (matches.length === 1) model = matches[0];\n\t\tif (matches.length > 1) throw new Error(`Model id \"${normalized}\" is ambiguous; use provider/model.`);\n\t}\n\tif (!model) throw new Error(`Model \"${normalized}\" is not available.`);\n\tif (!parent.modelRegistry.hasConfiguredAuth(model))\n\t\tthrow new Error(`Model \"${normalized}\" has no configured authentication.`);\n\treturn model;\n}\n\n// Exactly two layers: a /agents override wins, otherwise the subagent\n// inherits the parent session. Callers and agent files cannot pick models.\nfunction resolveModel(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n): { model: Model<Api>; source: ResolvedSubagentTask[\"modelSource\"] } {\n\tif (override?.model) return { model: findAvailableModel(override.model, parent), source: \"profile\" };\n\tif (!parent.model) throw new Error(\"The parent session has no active model.\");\n\treturn { model: parent.model, source: \"parent\" };\n}\n\nfunction resolveThinking(\n\toverride: SubagentProfileOverride | undefined,\n\tparent: ParentModelContext,\n\tmodel: Model<Api>,\n): { thinking: ThinkingLevel; source: ResolvedSubagentTask[\"thinkingSource\"] } {\n\tconst requested = override?.thinking ?? parent.thinking;\n\tconst source: ResolvedSubagentTask[\"thinkingSource\"] = override?.thinking ? \"profile\" : \"parent\";\n\treturn { thinking: clampThinkingLevel(model, requested) as ThinkingLevel, source };\n}\n\nexport async function resolveSubagentTask(\n\ttask: SubagentTask,\n\tparentCwd: string,\n\tagents: readonly AgentDefinition[],\n\tparent: ParentModelContext,\n\tconfigAgentDir = getAgentDir(),\n\tpreloadedConfig?: SubagentConfigFile,\n): Promise<ResolvedSubagentTask> {\n\tconst agentName = task.agent ?? \"general\";\n\tconst agent = agents.find((candidate) => candidate.name === agentName);\n\tif (!agent) {\n\t\tconst available = agents.map((candidate) => candidate.name).join(\", \") || \"none\";\n\t\tthrow new Error(`Unknown agent \"${agentName}\". Available agents: ${available}.`);\n\t}\n\tconst config = preloadedConfig ?? (await loadSubagentConfig(configAgentDir));\n\tconst override = config.profiles[agent.name];\n\tconst resolvedModel = resolveModel(override, parent);\n\tconst resolvedThinking = resolveThinking(override, parent, resolvedModel.model);\n\treturn {\n\t\tagent,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\tcwd: resolveTaskCwd(parentCwd, task.cwd ?? undefined),\n\t\tmodel: resolvedModel.model,\n\t\tthinking: resolvedThinking.thinking,\n\t\tmodelSource: resolvedModel.source,\n\t\tthinkingSource: resolvedThinking.source,\n\t};\n}\n"]}
@@ -17,14 +17,16 @@ export interface SubagentInvocationOptions {
17
17
  parent: ParentModelContext;
18
18
  modelRuntime: ModelRuntime;
19
19
  agentDir: string;
20
- configAgentDir: string;
21
20
  projectTrusted: boolean;
22
21
  signal?: AbortSignal;
23
22
  gate: ConcurrencyGate;
24
23
  onUpdate?: (details: SubagentDetails) => void;
25
24
  registerAbort?: (abort: () => Promise<void>) => () => void;
25
+ /** Test hook: overrides the task-retry backoff base delay. */
26
+ taskRetryBaseDelayMs?: number;
26
27
  }
27
28
  export declare function boundSubagentDetails(details: SubagentDetails): SubagentDetails;
29
+ export declare function isSubagentError(details: Pick<SubagentDetails, "status" | "runs">): boolean;
28
30
  export declare function runSubagentInvocation(options: SubagentInvocationOptions): Promise<SubagentExecutionResult>;
29
31
  export declare function statusSummary(details: SubagentDetails): string;
30
32
  //# sourceMappingURL=runner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAahE,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,cAAc,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,aAAa,CAAC;AAEhE,OAAO,KAAK,EACX,eAAe,EAEf,eAAe,EACf,uBAAuB,EAGvB,MAAM,YAAY,CAAC;AAWpB,qBAAa,eAAe;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B,YAAY,KAAK,SAAkB,EAElC;IAED,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAkBjD;IAED,OAAO,CAAC,OAAO;CAef;AAED,MAAM,WAAW,yBAAyB;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC;CAC3D;AAwCD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAyF9E;AA6KD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA2ChH;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAE9D","sourcesContent":["import type { ModelRuntime } from \"../../core/model-runtime.ts\";\nimport { boundText, emptyUsage, mergeUsage, toNestedUsage } from \"./activity.ts\";\nimport {\n\tCHAIN_HANDOFF_LIMIT,\n\tDETAILS_ACTIVITY_LIMIT,\n\tDETAILS_OUTPUT_LIMIT,\n\tERROR_TEXT_LIMIT,\n\tMAX_CONCURRENCY,\n\tMAX_TASKS,\n\tPARALLEL_OUTPUT_LIMIT,\n\tPARALLEL_TASK_OUTPUT_LIMIT,\n\tSINGLE_OUTPUT_LIMIT,\n} from \"./constants.ts\";\nimport { type ParentModelContext, resolveSubagentTask } from \"./resolve.ts\";\nimport type { SubagentParams, SubagentTask } from \"./schema.ts\";\nimport { runSdkTask } from \"./sdk-runner.ts\";\nimport type {\n\tAgentDefinition,\n\tResolvedSubagentTask,\n\tSubagentDetails,\n\tSubagentExecutionResult,\n\tSubagentRunDetails,\n\tSubagentUsage,\n} from \"./types.ts\";\n\nconst EMPTY_OUTPUT = \"(Subagent completed but returned no output.)\";\n\ninterface Waiter {\n\tresolve: (release: () => void) => void;\n\treject: (error: Error) => void;\n\tsignal?: AbortSignal;\n\tabortListener?: () => void;\n}\n\nexport class ConcurrencyGate {\n\tprivate active = 0;\n\tprivate readonly waiters: Waiter[] = [];\n\tprivate readonly limit: number;\n\n\tconstructor(limit = MAX_CONCURRENCY) {\n\t\tthis.limit = limit;\n\t}\n\n\tacquire(signal?: AbortSignal): Promise<() => void> {\n\t\tif (signal?.aborted) return Promise.reject(new Error(\"Subagent was aborted while queued.\"));\n\t\tif (this.active < this.limit) {\n\t\t\tthis.active++;\n\t\t\treturn Promise.resolve(() => this.release());\n\t\t}\n\t\treturn new Promise<() => void>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = { resolve, reject, signal };\n\t\t\tif (signal) {\n\t\t\t\twaiter.abortListener = () => {\n\t\t\t\t\tconst index = this.waiters.indexOf(waiter);\n\t\t\t\t\tif (index >= 0) this.waiters.splice(index, 1);\n\t\t\t\t\treject(new Error(\"Subagent was aborted while queued.\"));\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.abortListener, { once: true });\n\t\t\t}\n\t\t\tthis.waiters.push(waiter);\n\t\t});\n\t}\n\n\tprivate release(): void {\n\t\tthis.active = Math.max(0, this.active - 1);\n\t\twhile (this.waiters.length > 0) {\n\t\t\tconst waiter = this.waiters.shift();\n\t\t\tif (!waiter) return;\n\t\t\tif (waiter.signal?.aborted) {\n\t\t\t\twaiter.abortListener?.();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twaiter.signal?.removeEventListener(\"abort\", waiter.abortListener!);\n\t\t\tthis.active++;\n\t\t\twaiter.resolve(() => this.release());\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nexport interface SubagentInvocationOptions {\n\tparams: SubagentParams;\n\tparentCwd: string;\n\tagents: readonly AgentDefinition[];\n\tparent: ParentModelContext;\n\tmodelRuntime: ModelRuntime;\n\tagentDir: string;\n\tconfigAgentDir: string;\n\tprojectTrusted: boolean;\n\tsignal?: AbortSignal;\n\tgate: ConcurrencyGate;\n\tonUpdate?: (details: SubagentDetails) => void;\n\tregisterAbort?: (abort: () => Promise<void>) => () => void;\n}\n\nfunction runId(index: number): string {\n\treturn `subagent-${index + 1}`;\n}\n\nfunction createRun(task: ResolvedSubagentTask, index: number, step?: number): SubagentRunDetails {\n\treturn {\n\t\tid: runId(index),\n\t\tagent: task.agent.name,\n\t\tagentSource: task.agent.source,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\tcwd: task.cwd,\n\t\tmodel: `${task.model.provider}/${task.model.id}`,\n\t\tthinking: task.thinking,\n\t\tstatus: \"queued\",\n\t\tactivities: [],\n\t\tliveText: \"\",\n\t\tfinalOutput: \"\",\n\t\tusage: emptyUsage(),\n\t\tstep,\n\t};\n}\n\nfunction aggregateUsage(runs: readonly SubagentRunDetails[]): SubagentUsage {\n\tconst usage = emptyUsage();\n\tfor (const run of runs) mergeUsage(usage, run.usage);\n\treturn usage;\n}\n\ninterface TextField {\n\tget: () => string;\n\tset: (value: string) => void;\n}\n\nfunction detailsSize(details: SubagentDetails): number {\n\treturn Buffer.byteLength(JSON.stringify(details), \"utf8\");\n}\n\nexport function boundSubagentDetails(details: SubagentDetails): SubagentDetails {\n\tconst perRunBudget = Math.max(\n\t\t1_024,\n\t\tMath.floor((DETAILS_OUTPUT_LIMIT - 8 * 1024) / Math.max(1, details.runs.length)),\n\t);\n\tconst outputLimit = Math.min(SINGLE_OUTPUT_LIMIT, Math.max(1_024, perRunBudget - 7 * 1024));\n\tconst bounded: SubagentDetails = {\n\t\t...details,\n\t\truns: details.runs.map((run) => ({\n\t\t\t...run,\n\t\t\tprompt: boundText(run.prompt, 1_024),\n\t\t\tcwd: boundText(run.cwd, 1_024),\n\t\t\tcurrentActivity: run.currentActivity ? boundText(run.currentActivity, 512) : undefined,\n\t\t\tactivities: run.activities.slice(-DETAILS_ACTIVITY_LIMIT).map((activity) => ({\n\t\t\t\t...activity,\n\t\t\t\tsummary: boundText(activity.summary, 256),\n\t\t\t\tresultSummary: activity.resultSummary ? boundText(activity.resultSummary, 256) : undefined,\n\t\t\t})),\n\t\t\tliveText: boundText(run.liveText, 1_024),\n\t\t\tfinalOutput: boundText(run.finalOutput, outputLimit),\n\t\t\terror: run.error ? boundText(run.error, 1_024) : undefined,\n\t\t})),\n\t};\n\tconst fields: TextField[] = [];\n\tfor (const run of bounded.runs) {\n\t\tfields.push(\n\t\t\t{\n\t\t\t\tget: () => run.prompt,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.prompt = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.cwd,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.cwd = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.currentActivity ?? \"\",\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.currentActivity = value || undefined;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.liveText,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.liveText = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.finalOutput,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.finalOutput = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.error ?? \"\",\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.error = value || undefined;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t\tfor (const activity of run.activities) {\n\t\t\tfields.push(\n\t\t\t\t{\n\t\t\t\t\tget: () => activity.summary,\n\t\t\t\t\tset: (value) => {\n\t\t\t\t\t\tactivity.summary = value;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tget: () => activity.resultSummary ?? \"\",\n\t\t\t\t\tset: (value) => {\n\t\t\t\t\t\tactivity.resultSummary = value || undefined;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\twhile (detailsSize(bounded) > DETAILS_OUTPUT_LIMIT) {\n\t\tconst largest = fields\n\t\t\t.map((field) => ({ field, size: Buffer.byteLength(field.get(), \"utf8\") }))\n\t\t\t.sort((left, right) => right.size - left.size)[0];\n\t\tif (!largest || largest.size === 0) break;\n\t\tconst overflow = detailsSize(bounded) - DETAILS_OUTPUT_LIMIT;\n\t\tlargest.field.set(boundText(largest.field.get(), Math.max(0, largest.size - overflow)));\n\t}\n\treturn bounded;\n}\n\nfunction statusOf(runs: readonly SubagentRunDetails[]): SubagentDetails[\"status\"] {\n\tif (runs.some((run) => run.status === \"aborted\")) return \"aborted\";\n\tif (runs.some((run) => run.status === \"failed\")) return \"failed\";\n\tif (runs.some((run) => run.status === \"queued\" || run.status === \"running\")) return \"running\";\n\treturn \"completed\";\n}\n\nfunction statusText(details: SubagentDetails): string {\n\tconst running = details.runs.filter((run) => run.status === \"running\").length;\n\tconst queued = details.runs.filter((run) => run.status === \"queued\").length;\n\tconst completed = details.runs.filter((run) => run.status === \"completed\").length;\n\tconst failed = details.runs.filter((run) => run.status === \"failed\").length;\n\tconst aborted = details.runs.filter((run) => run.status === \"aborted\").length;\n\tif (details.mode === \"single\") {\n\t\tconst run = details.runs[0];\n\t\tif (run?.currentActivity) return run.currentActivity;\n\t\tif (\n\t\t\t!run ||\n\t\t\t(run.status === \"running\" && run.usage.turns === 0 && run.usage.toolUses === 0 && run.liveText.length === 0)\n\t\t) {\n\t\t\treturn \"Initializing…\";\n\t\t}\n\t\treturn run.status === \"running\" ? \"Thinking…\" : run.status;\n\t}\n\tif (details.mode === \"chain\") {\n\t\tconst current = details.runs.find((run) => run.status === \"running\" || run.status === \"queued\");\n\t\treturn current\n\t\t\t? `Step ${current.step ?? 1}/${details.runs.length} · ${current.currentActivity ?? current.agent}`\n\t\t\t: `${completed}/${details.runs.length} steps complete`;\n\t}\n\treturn `${completed}/${details.runs.length} complete · ${running} running · ${queued} queued${failed ? ` · ${failed} failed` : \"\"}${aborted ? ` · ${aborted} aborted` : \"\"}`;\n}\n\nfunction emitDetails(\n\tmode: SubagentDetails[\"mode\"],\n\truns: SubagentRunDetails[],\n\tstartedAt: number,\n\tonUpdate: ((details: SubagentDetails) => void) | undefined,\n): SubagentDetails {\n\tconst details: SubagentDetails = {\n\t\tmode,\n\t\tstatus: statusOf(runs),\n\t\truns: [...runs],\n\t\tstartedAt,\n\t\tusage: aggregateUsage(runs),\n\t};\n\tonUpdate?.(boundSubagentDetails(details));\n\treturn details;\n}\n\nfunction replacePrevious(prompt: string, previous: string): string {\n\treturn prompt.replace(/\\{previous\\}/gu, boundText(previous, CHAIN_HANDOFF_LIMIT));\n}\n\nfunction validateTaskCount(tasks: readonly SubagentTask[]): void {\n\tif (tasks.length === 0) throw new Error(\"Subagent task list must not be empty.\");\n\tif (tasks.length > MAX_TASKS) throw new Error(`Subagent task list is limited to ${MAX_TASKS} tasks.`);\n}\n\nasync function resolveTasks(\n\ttasks: readonly SubagentTask[],\n\toptions: SubagentInvocationOptions,\n): Promise<ResolvedSubagentTask[]> {\n\tvalidateTaskCount(tasks);\n\treturn Promise.all(\n\t\ttasks.map((task) =>\n\t\t\tresolveSubagentTask(task, options.parentCwd, options.agents, options.parent, options.configAgentDir),\n\t\t),\n\t);\n}\n\nasync function runWithGate(\n\ttask: ResolvedSubagentTask,\n\trun: SubagentRunDetails,\n\toptions: SubagentInvocationOptions,\n\tonProgress: () => void,\n): Promise<SubagentRunDetails> {\n\tonProgress();\n\tlet release: (() => void) | undefined;\n\ttry {\n\t\trelease = await options.gate.acquire(options.signal);\n\t\tif (options.signal?.aborted) {\n\t\t\trun.status = \"aborted\";\n\t\t\trun.error = \"Subagent was aborted while queued.\";\n\t\t\treturn run;\n\t\t}\n\t\treturn await runSdkTask({\n\t\t\ttask,\n\t\t\trun,\n\t\t\tmodelRuntime: options.modelRuntime,\n\t\t\tagentDir: options.agentDir,\n\t\t\tprojectTrusted: options.projectTrusted,\n\t\t\tsignal: options.signal,\n\t\t\tonProgress,\n\t\t\tregisterAbort: options.registerAbort,\n\t\t});\n\t} catch (error) {\n\t\trun.status = options.signal?.aborted ? \"aborted\" : \"failed\";\n\t\trun.error = boundText(error instanceof Error ? error.message : String(error), ERROR_TEXT_LIMIT);\n\t\trun.endedAt = Date.now();\n\t\tonProgress();\n\t\treturn run;\n\t} finally {\n\t\trelease?.();\n\t}\n}\n\nfunction resultContent(details: SubagentDetails): string {\n\tif (details.mode === \"single\") {\n\t\tconst run = details.runs[0];\n\t\tif (!run) return \"Subagent produced no run.\";\n\t\tif (run.status === \"failed\" || run.status === \"aborted\") {\n\t\t\treturn boundText(\n\t\t\t\t`${run.status === \"aborted\" ? \"Subagent aborted\" : \"Subagent failed\"}: ${run.error ?? \"unknown error\"}${run.finalOutput ? `\\n\\nPartial response:\\n${run.finalOutput}` : \"\"}`,\n\t\t\t\tSINGLE_OUTPUT_LIMIT,\n\t\t\t);\n\t\t}\n\t\treturn run.finalOutput || EMPTY_OUTPUT;\n\t}\n\tif (details.mode === \"chain\") {\n\t\tconst failed = details.runs.find((run) => run.status === \"failed\" || run.status === \"aborted\");\n\t\tif (failed)\n\t\t\treturn boundText(\n\t\t\t\t`Chain stopped at step ${failed.step ?? \"?\"} (${failed.agent}): ${failed.error ?? \"unknown error\"}`,\n\t\t\t\tERROR_TEXT_LIMIT,\n\t\t\t);\n\t\tconst last = details.runs[details.runs.length - 1];\n\t\treturn last?.finalOutput || EMPTY_OUTPUT;\n\t}\n\tconst sections = details.runs.map((run) => {\n\t\tconst output = run.finalOutput || run.error || EMPTY_OUTPUT;\n\t\treturn `### ${run.agent} · ${run.status}\\n\\n${boundText(output, PARALLEL_TASK_OUTPUT_LIMIT)}`;\n\t});\n\treturn boundText(`Parallel results\\n\\n${sections.join(\"\\n\\n---\\n\\n\")}`, PARALLEL_OUTPUT_LIMIT);\n}\n\nfunction invocationMode(params: SubagentParams): { mode: SubagentDetails[\"mode\"]; tasks: SubagentTask[] } {\n\tconst provided: string[] = [];\n\tif (params.prompt != null) provided.push(\"prompt\");\n\tif (params.tasks != null) provided.push(\"tasks\");\n\tif (params.chain != null) provided.push(\"chain\");\n\tif (provided.length !== 1) {\n\t\tthrow new Error(\n\t\t\tprovided.length === 0\n\t\t\t\t? \"Provide exactly one subagent mode: prompt (single), tasks (parallel), or chain (sequential); none was provided.\"\n\t\t\t\t: `Provide exactly one subagent mode: received ${provided.join(\", \")}. Keep one and set the unused mode fields to null or omit them.`,\n\t\t);\n\t}\n\tif (params.tasks != null) {\n\t\treturn { mode: \"parallel\", tasks: params.tasks };\n\t}\n\tif (params.chain != null) {\n\t\treturn { mode: \"chain\", tasks: params.chain };\n\t}\n\tif (params.prompt == null) {\n\t\tthrow new Error(\"Provide exactly one subagent mode: prompt (single), tasks (parallel), or chain (sequential).\");\n\t}\n\tif (!params.description) throw new Error(\"description is required for single mode.\");\n\treturn {\n\t\tmode: \"single\",\n\t\ttasks: [\n\t\t\t{\n\t\t\t\tagent: params.agent,\n\t\t\t\tdescription: params.description,\n\t\t\t\tprompt: params.prompt,\n\t\t\t\tcwd: params.cwd,\n\t\t\t},\n\t\t],\n\t};\n}\n\nexport async function runSubagentInvocation(options: SubagentInvocationOptions): Promise<SubagentExecutionResult> {\n\tconst { mode, tasks } = invocationMode(options.params);\n\tconst resolved = await resolveTasks(tasks, options);\n\tconst runs = resolved.map((task, index) => createRun(task, index, mode === \"chain\" ? index + 1 : undefined));\n\tconst startedAt = Date.now();\n\tlet latestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);\n\tconst progress = () => {\n\t\tlatestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);\n\t};\n\n\tif (mode === \"single\") {\n\t\tawait runWithGate(resolved[0]!, runs[0]!, options, progress);\n\t} else if (mode === \"parallel\") {\n\t\tawait Promise.all(resolved.map((task, index) => runWithGate(task, runs[index]!, options, progress)));\n\t} else {\n\t\tlet previous = \"\";\n\t\tfor (let index = 0; index < resolved.length; index++) {\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\truns[index]!.status = \"aborted\";\n\t\t\t\truns[index]!.error = \"Skipped because the parent subagent call was aborted.\";\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tconst task = { ...resolved[index]!, prompt: replacePrevious(resolved[index]!.prompt, previous) };\n\t\t\tawait runWithGate(task, runs[index]!, options, progress);\n\t\t\tif (runs[index]!.status !== \"completed\") {\n\t\t\t\tfor (let skipped = index + 1; skipped < runs.length; skipped++) {\n\t\t\t\t\truns[skipped]!.status = options.signal?.aborted ? \"aborted\" : \"failed\";\n\t\t\t\t\truns[skipped]!.error = `Skipped because chain step ${index + 1} did not complete.`;\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tprevious = runs[index]!.finalOutput;\n\t\t}\n\t}\n\tlatestDetails = emitDetails(mode, runs, startedAt, undefined);\n\tlatestDetails.endedAt = Date.now();\n\tconst isError = latestDetails.status === \"failed\" || latestDetails.status === \"aborted\";\n\treturn {\n\t\tcontent: resultContent(latestDetails),\n\t\tdetails: boundSubagentDetails(latestDetails),\n\t\tusage: toNestedUsage(latestDetails.usage),\n\t\tisError,\n\t};\n}\n\nexport function statusSummary(details: SubagentDetails): string {\n\treturn statusText(details);\n}\n"]}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/extensions/subagent/runner.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAehE,OAAO,EAAE,KAAK,kBAAkB,EAAuB,MAAM,cAAc,CAAC;AAC5E,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,aAAa,CAAC;AAGhE,OAAO,KAAK,EACX,eAAe,EAEf,eAAe,EACf,uBAAuB,EAGvB,MAAM,YAAY,CAAC;AAWpB,qBAAa,eAAe;IAC3B,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAgB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAE/B,YAAY,KAAK,SAAkB,EAElC;IAED,OAAO,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAkBjD;IAED,OAAO,CAAC,OAAO;CAef;AAED,MAAM,WAAW,yBAAyB;IACzC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,eAAe,EAAE,CAAC;IACnC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,YAAY,EAAE,YAAY,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAC;IAC9C,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC;IAC3D,8DAA8D;IAC9D,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAyCD,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,eAAe,GAAG,eAAe,CAyF9E;AA6ND,wBAAgB,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,GAAG,MAAM,CAAC,GAAG,OAAO,CAK1F;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CA4BhH;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAE9D","sourcesContent":["import { relative } from \"node:path\";\nimport { isRetryableAssistantError } from \"@earendil-works/pi-ai/compat\";\nimport type { ModelRuntime } from \"../../core/model-runtime.ts\";\nimport { sleep } from \"../../utils/sleep.ts\";\nimport { boundText, emptyUsage, mergeUsage, toNestedUsage } from \"./activity.ts\";\nimport {\n\tDETAILS_ACTIVITY_LIMIT,\n\tDETAILS_OUTPUT_LIMIT,\n\tERROR_TEXT_LIMIT,\n\tMAX_CONCURRENCY,\n\tMAX_TASKS,\n\tPARALLEL_OUTPUT_LIMIT,\n\tPARALLEL_TASK_OUTPUT_LIMIT,\n\tSINGLE_OUTPUT_LIMIT,\n\tTASK_RETRY_BASE_DELAY_MS,\n\tTASK_RETRY_LIMIT,\n} from \"./constants.ts\";\nimport { type ParentModelContext, resolveSubagentTask } from \"./resolve.ts\";\nimport type { SubagentParams, SubagentTask } from \"./schema.ts\";\nimport { runSdkTask } from \"./sdk-runner.ts\";\nimport { loadSubagentConfig } from \"./settings.ts\";\nimport type {\n\tAgentDefinition,\n\tResolvedSubagentTask,\n\tSubagentDetails,\n\tSubagentExecutionResult,\n\tSubagentRunDetails,\n\tSubagentUsage,\n} from \"./types.ts\";\n\nconst EMPTY_OUTPUT = \"(Subagent completed but returned no output.)\";\n\ninterface Waiter {\n\tresolve: (release: () => void) => void;\n\treject: (error: Error) => void;\n\tsignal?: AbortSignal;\n\tabortListener?: () => void;\n}\n\nexport class ConcurrencyGate {\n\tprivate active = 0;\n\tprivate readonly waiters: Waiter[] = [];\n\tprivate readonly limit: number;\n\n\tconstructor(limit = MAX_CONCURRENCY) {\n\t\tthis.limit = limit;\n\t}\n\n\tacquire(signal?: AbortSignal): Promise<() => void> {\n\t\tif (signal?.aborted) return Promise.reject(new Error(\"Subagent was aborted while queued.\"));\n\t\tif (this.active < this.limit) {\n\t\t\tthis.active++;\n\t\t\treturn Promise.resolve(() => this.release());\n\t\t}\n\t\treturn new Promise<() => void>((resolve, reject) => {\n\t\t\tconst waiter: Waiter = { resolve, reject, signal };\n\t\t\tif (signal) {\n\t\t\t\twaiter.abortListener = () => {\n\t\t\t\t\tconst index = this.waiters.indexOf(waiter);\n\t\t\t\t\tif (index >= 0) this.waiters.splice(index, 1);\n\t\t\t\t\treject(new Error(\"Subagent was aborted while queued.\"));\n\t\t\t\t};\n\t\t\t\tsignal.addEventListener(\"abort\", waiter.abortListener, { once: true });\n\t\t\t}\n\t\t\tthis.waiters.push(waiter);\n\t\t});\n\t}\n\n\tprivate release(): void {\n\t\tthis.active = Math.max(0, this.active - 1);\n\t\twhile (this.waiters.length > 0) {\n\t\t\tconst waiter = this.waiters.shift();\n\t\t\tif (!waiter) return;\n\t\t\tif (waiter.signal?.aborted) {\n\t\t\t\twaiter.abortListener?.();\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twaiter.signal?.removeEventListener(\"abort\", waiter.abortListener!);\n\t\t\tthis.active++;\n\t\t\twaiter.resolve(() => this.release());\n\t\t\treturn;\n\t\t}\n\t}\n}\n\nexport interface SubagentInvocationOptions {\n\tparams: SubagentParams;\n\tparentCwd: string;\n\tagents: readonly AgentDefinition[];\n\tparent: ParentModelContext;\n\tmodelRuntime: ModelRuntime;\n\tagentDir: string;\n\tprojectTrusted: boolean;\n\tsignal?: AbortSignal;\n\tgate: ConcurrencyGate;\n\tonUpdate?: (details: SubagentDetails) => void;\n\tregisterAbort?: (abort: () => Promise<void>) => () => void;\n\t/** Test hook: overrides the task-retry backoff base delay. */\n\ttaskRetryBaseDelayMs?: number;\n}\n\nfunction runId(index: number): string {\n\treturn `subagent-${index + 1}`;\n}\n\nfunction createRun(task: ResolvedSubagentTask, index: number, parentCwd: string): SubagentRunDetails {\n\treturn {\n\t\tid: runId(index),\n\t\tagent: task.agent.name,\n\t\tagentSource: task.agent.source,\n\t\tdescription: task.description,\n\t\tprompt: task.prompt,\n\t\t// Display-only relative path; the worker session itself uses the\n\t\t// resolved absolute task.cwd. Empty means \"same as the parent\".\n\t\tcwd: relative(parentCwd, task.cwd),\n\t\tmodel: `${task.model.provider}/${task.model.id}`,\n\t\tthinking: task.thinking,\n\t\tstatus: \"queued\",\n\t\tactivities: [],\n\t\tliveText: \"\",\n\t\tfinalOutput: \"\",\n\t\tusage: emptyUsage(),\n\t};\n}\n\nfunction aggregateUsage(runs: readonly SubagentRunDetails[]): SubagentUsage {\n\tconst usage = emptyUsage();\n\tfor (const run of runs) mergeUsage(usage, run.usage);\n\treturn usage;\n}\n\ninterface TextField {\n\tget: () => string;\n\tset: (value: string) => void;\n}\n\nfunction detailsSize(details: SubagentDetails): number {\n\treturn Buffer.byteLength(JSON.stringify(details), \"utf8\");\n}\n\nexport function boundSubagentDetails(details: SubagentDetails): SubagentDetails {\n\tconst perRunBudget = Math.max(\n\t\t1_024,\n\t\tMath.floor((DETAILS_OUTPUT_LIMIT - 8 * 1024) / Math.max(1, details.runs.length)),\n\t);\n\tconst outputLimit = Math.min(SINGLE_OUTPUT_LIMIT, Math.max(1_024, perRunBudget - 7 * 1024));\n\tconst bounded: SubagentDetails = {\n\t\t...details,\n\t\truns: details.runs.map((run) => ({\n\t\t\t...run,\n\t\t\tprompt: boundText(run.prompt, 1_024),\n\t\t\tcwd: boundText(run.cwd, 1_024),\n\t\t\tcurrentActivity: run.currentActivity ? boundText(run.currentActivity, 512) : undefined,\n\t\t\tactivities: run.activities.slice(-DETAILS_ACTIVITY_LIMIT).map((activity) => ({\n\t\t\t\t...activity,\n\t\t\t\tsummary: boundText(activity.summary, 256),\n\t\t\t\tresultSummary: activity.resultSummary ? boundText(activity.resultSummary, 256) : undefined,\n\t\t\t})),\n\t\t\tliveText: boundText(run.liveText, 1_024),\n\t\t\tfinalOutput: boundText(run.finalOutput, outputLimit),\n\t\t\terror: run.error ? boundText(run.error, 1_024) : undefined,\n\t\t})),\n\t};\n\tconst fields: TextField[] = [];\n\tfor (const run of bounded.runs) {\n\t\tfields.push(\n\t\t\t{\n\t\t\t\tget: () => run.prompt,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.prompt = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.cwd,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.cwd = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.currentActivity ?? \"\",\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.currentActivity = value || undefined;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.liveText,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.liveText = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.finalOutput,\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.finalOutput = value;\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tget: () => run.error ?? \"\",\n\t\t\t\tset: (value) => {\n\t\t\t\t\trun.error = value || undefined;\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t\tfor (const activity of run.activities) {\n\t\t\tfields.push(\n\t\t\t\t{\n\t\t\t\t\tget: () => activity.summary,\n\t\t\t\t\tset: (value) => {\n\t\t\t\t\t\tactivity.summary = value;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tget: () => activity.resultSummary ?? \"\",\n\t\t\t\t\tset: (value) => {\n\t\t\t\t\t\tactivity.resultSummary = value || undefined;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t);\n\t\t}\n\t}\n\twhile (detailsSize(bounded) > DETAILS_OUTPUT_LIMIT) {\n\t\tconst largest = fields\n\t\t\t.map((field) => ({ field, size: Buffer.byteLength(field.get(), \"utf8\") }))\n\t\t\t.sort((left, right) => right.size - left.size)[0];\n\t\tif (!largest || largest.size === 0) break;\n\t\tconst overflow = detailsSize(bounded) - DETAILS_OUTPUT_LIMIT;\n\t\tlargest.field.set(boundText(largest.field.get(), Math.max(0, largest.size - overflow)));\n\t}\n\treturn bounded;\n}\n\nfunction statusOf(runs: readonly SubagentRunDetails[]): SubagentDetails[\"status\"] {\n\tif (runs.some((run) => run.status === \"aborted\")) return \"aborted\";\n\tif (runs.some((run) => run.status === \"failed\")) return \"failed\";\n\tif (runs.some((run) => run.status === \"queued\" || run.status === \"running\")) return \"running\";\n\treturn \"completed\";\n}\n\nfunction statusText(details: SubagentDetails): string {\n\tconst running = details.runs.filter((run) => run.status === \"running\").length;\n\tconst queued = details.runs.filter((run) => run.status === \"queued\").length;\n\tconst completed = details.runs.filter((run) => run.status === \"completed\").length;\n\tconst failed = details.runs.filter((run) => run.status === \"failed\").length;\n\tconst aborted = details.runs.filter((run) => run.status === \"aborted\").length;\n\tif (details.mode === \"single\") {\n\t\tconst run = details.runs[0];\n\t\tif (run?.currentActivity) return run.currentActivity;\n\t\tif (\n\t\t\t!run ||\n\t\t\t(run.status === \"running\" && run.usage.turns === 0 && run.usage.toolUses === 0 && run.liveText.length === 0)\n\t\t) {\n\t\t\treturn \"Initializing…\";\n\t\t}\n\t\treturn run.status === \"running\" ? \"Thinking…\" : run.status;\n\t}\n\tconst parts = [`${completed}/${details.runs.length} complete`];\n\tif (running) parts.push(`${running} running`);\n\tif (queued) parts.push(`${queued} queued`);\n\tif (failed) parts.push(`${failed} failed`);\n\tif (aborted) parts.push(`${aborted} aborted`);\n\treturn parts.join(\" · \");\n}\n\nfunction emitDetails(\n\tmode: SubagentDetails[\"mode\"],\n\truns: SubagentRunDetails[],\n\tstartedAt: number,\n\tonUpdate: ((details: SubagentDetails) => void) | undefined,\n): SubagentDetails {\n\tconst details: SubagentDetails = {\n\t\tmode,\n\t\tstatus: statusOf(runs),\n\t\truns: [...runs],\n\t\tstartedAt,\n\t\tusage: aggregateUsage(runs),\n\t};\n\tonUpdate?.(boundSubagentDetails(details));\n\treturn details;\n}\n\nfunction validateTaskCount(tasks: readonly SubagentTask[]): void {\n\tif (tasks.length === 0) throw new Error(\"Subagent task list must not be empty.\");\n\tif (tasks.length > MAX_TASKS) throw new Error(`Subagent task list is limited to ${MAX_TASKS} tasks.`);\n}\n\nasync function resolveTasks(\n\ttasks: readonly SubagentTask[],\n\toptions: SubagentInvocationOptions,\n): Promise<ResolvedSubagentTask[]> {\n\tvalidateTaskCount(tasks);\n\tconst config = await loadSubagentConfig(options.agentDir);\n\treturn Promise.all(\n\t\ttasks.map((task) =>\n\t\t\tresolveSubagentTask(task, options.parentCwd, options.agents, options.parent, options.agentDir, config),\n\t\t),\n\t);\n}\n\n// Reuses pi-ai's provider-error classification by wrapping the stored error\n// text in the assistant-message shape it inspects.\nfunction isRetryableRunError(error: string | undefined): boolean {\n\tif (!error) return false;\n\tconst probe = { role: \"assistant\", content: [], stopReason: \"error\", errorMessage: error };\n\treturn isRetryableAssistantError(probe as unknown as Parameters<typeof isRetryableAssistantError>[0]);\n}\n\n// Session-level auto-retry already covers errors that happen inside the agent\n// loop; this catches the paths that bypass it (preflight throws such as auth\n// checks). Only a run that produced nothing is retried, so partial work is\n// never discarded.\nfunction shouldRetryTask(run: SubagentRunDetails): boolean {\n\treturn (\n\t\trun.status === \"failed\" && run.usage.turns === 0 && run.usage.toolUses === 0 && isRetryableRunError(run.error)\n\t);\n}\n\nfunction resetRunForRetry(run: SubagentRunDetails, attempt: number, maxAttempts: number): void {\n\trun.status = \"queued\";\n\trun.error = undefined;\n\trun.startedAt = undefined;\n\trun.endedAt = undefined;\n\trun.finalOutput = \"\";\n\trun.liveText = \"\";\n\trun.currentActivity = `Retrying (attempt ${attempt}/${maxAttempts})…`;\n\trun.activities.length = 0;\n\trun.usage = emptyUsage();\n}\n\nasync function runWithGate(\n\ttask: ResolvedSubagentTask,\n\trun: SubagentRunDetails,\n\toptions: SubagentInvocationOptions,\n\tonProgress: () => void,\n): Promise<SubagentRunDetails> {\n\tonProgress();\n\tlet release: (() => void) | undefined;\n\ttry {\n\t\trelease = await options.gate.acquire(options.signal);\n\t\tif (options.signal?.aborted) {\n\t\t\trun.status = \"aborted\";\n\t\t\trun.error = \"Subagent was aborted while queued.\";\n\t\t\treturn run;\n\t\t}\n\t\tconst baseDelayMs = options.taskRetryBaseDelayMs ?? TASK_RETRY_BASE_DELAY_MS;\n\t\tfor (let attempt = 0; ; attempt++) {\n\t\t\tconst result = await runSdkTask({\n\t\t\t\ttask,\n\t\t\t\trun,\n\t\t\t\tmodelRuntime: options.modelRuntime,\n\t\t\t\tagentDir: options.agentDir,\n\t\t\t\tprojectTrusted: options.projectTrusted,\n\t\t\t\tsignal: options.signal,\n\t\t\t\tonProgress,\n\t\t\t\tregisterAbort: options.registerAbort,\n\t\t\t});\n\t\t\tif (attempt >= TASK_RETRY_LIMIT || options.signal?.aborted || !shouldRetryTask(result)) return result;\n\t\t\tresetRunForRetry(run, attempt + 1, TASK_RETRY_LIMIT);\n\t\t\tonProgress();\n\t\t\ttry {\n\t\t\t\tawait sleep(baseDelayMs * 2 ** attempt, options.signal);\n\t\t\t} catch {\n\t\t\t\trun.status = \"aborted\";\n\t\t\t\trun.error = \"Subagent was aborted while waiting to retry.\";\n\t\t\t\trun.endedAt = Date.now();\n\t\t\t\tonProgress();\n\t\t\t\treturn run;\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\trun.status = options.signal?.aborted ? \"aborted\" : \"failed\";\n\t\trun.error = boundText(error instanceof Error ? error.message : String(error), ERROR_TEXT_LIMIT);\n\t\trun.endedAt = Date.now();\n\t\tonProgress();\n\t\treturn run;\n\t} finally {\n\t\trelease?.();\n\t}\n}\n\nfunction resultContent(details: SubagentDetails): string {\n\tif (details.mode === \"single\") {\n\t\tconst run = details.runs[0];\n\t\tif (!run) return \"Subagent produced no run.\";\n\t\tif (run.status === \"failed\" || run.status === \"aborted\") {\n\t\t\treturn boundText(\n\t\t\t\t`${run.status === \"aborted\" ? \"Subagent aborted\" : \"Subagent failed\"}: ${run.error ?? \"unknown error\"}${run.finalOutput ? `\\n\\nPartial response:\\n${run.finalOutput}` : \"\"}`,\n\t\t\t\tSINGLE_OUTPUT_LIMIT,\n\t\t\t);\n\t\t}\n\t\treturn run.finalOutput || EMPTY_OUTPUT;\n\t}\n\tconst sections = details.runs.map((run) => {\n\t\tconst output = run.finalOutput || run.error || EMPTY_OUTPUT;\n\t\treturn `### ${run.description} (${run.agent}) — ${run.status}\\n\\n${boundText(output, PARALLEL_TASK_OUTPUT_LIMIT)}`;\n\t});\n\treturn boundText(sections.join(\"\\n\\n---\\n\\n\"), PARALLEL_OUTPUT_LIMIT);\n}\n\nfunction invocationMode(params: SubagentParams): { mode: SubagentDetails[\"mode\"]; tasks: SubagentTask[] } {\n\tconst provided: string[] = [];\n\tif (params.prompt != null) provided.push(\"prompt\");\n\tif (params.tasks != null) provided.push(\"tasks\");\n\tif (provided.length !== 1) {\n\t\tthrow new Error(\n\t\t\tprovided.length === 0\n\t\t\t\t? \"Provide exactly one subagent mode: prompt (single task) or tasks (parallel tasks); none was provided.\"\n\t\t\t\t: `Provide exactly one subagent mode: received ${provided.join(\", \")}. Keep one and set the unused mode fields to null or omit them.`,\n\t\t);\n\t}\n\tif (params.tasks != null) {\n\t\treturn { mode: \"parallel\", tasks: params.tasks };\n\t}\n\tif (!params.description) throw new Error(\"description is required for single mode.\");\n\treturn {\n\t\tmode: \"single\",\n\t\ttasks: [\n\t\t\t{\n\t\t\t\tagent: params.agent,\n\t\t\t\tdescription: params.description,\n\t\t\t\tprompt: params.prompt!,\n\t\t\t\tcwd: params.cwd,\n\t\t\t},\n\t\t],\n\t};\n}\n\n// Cheap change detector: consecutive events that alter nothing user-visible\n// skip the bounded-details serialization in emitDetails entirely.\nfunction progressKey(runs: readonly SubagentRunDetails[]): string {\n\treturn runs\n\t\t.map((run) => {\n\t\t\tconst last = run.activities[run.activities.length - 1];\n\t\t\treturn [\n\t\t\t\trun.status,\n\t\t\t\trun.currentActivity ?? \"\",\n\t\t\t\trun.activities.length,\n\t\t\t\tlast?.status ?? \"\",\n\t\t\t\tlast?.resultSummary ?? \"\",\n\t\t\t\trun.liveText.length,\n\t\t\t\trun.liveText.slice(-24),\n\t\t\t\trun.finalOutput.length,\n\t\t\t\trun.usage.turns,\n\t\t\t\trun.usage.toolUses,\n\t\t\t\trun.usage.totalTokens,\n\t\t\t\trun.error ?? \"\",\n\t\t\t].join(\"|\");\n\t\t})\n\t\t.join(\"~\");\n}\n\nexport function isSubagentError(details: Pick<SubagentDetails, \"status\" | \"runs\">): boolean {\n\tif (details.status !== \"failed\" && details.status !== \"aborted\") return false;\n\t// A batch with any successful run is a partial result, not an error:\n\t// per-run status is already reported in the content sections.\n\treturn !details.runs.some((run) => run.status === \"completed\");\n}\n\nexport async function runSubagentInvocation(options: SubagentInvocationOptions): Promise<SubagentExecutionResult> {\n\tconst { mode, tasks } = invocationMode(options.params);\n\tconst resolved = await resolveTasks(tasks, options);\n\tconst runs = resolved.map((task, index) => createRun(task, index, options.parentCwd));\n\tconst startedAt = Date.now();\n\tlet latestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);\n\tlet lastProgressKey = progressKey(runs);\n\tconst progress = () => {\n\t\tconst key = progressKey(runs);\n\t\tif (key === lastProgressKey) return;\n\t\tlastProgressKey = key;\n\t\tlatestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);\n\t};\n\n\tif (mode === \"single\") {\n\t\tawait runWithGate(resolved[0]!, runs[0]!, options, progress);\n\t} else {\n\t\tawait Promise.all(resolved.map((task, index) => runWithGate(task, runs[index]!, options, progress)));\n\t}\n\tlatestDetails = emitDetails(mode, runs, startedAt, undefined);\n\tlatestDetails.endedAt = Date.now();\n\tconst isError = isSubagentError(latestDetails);\n\treturn {\n\t\tcontent: resultContent(latestDetails),\n\t\tdetails: boundSubagentDetails(latestDetails),\n\t\tusage: toNestedUsage(latestDetails.usage),\n\t\tisError,\n\t};\n}\n\nexport function statusSummary(details: SubagentDetails): string {\n\treturn statusText(details);\n}\n"]}
@@ -1,7 +1,11 @@
1
+ import { relative } from "node:path";
2
+ import { isRetryableAssistantError } from "@earendil-works/pi-ai/compat";
3
+ import { sleep } from "../../utils/sleep.js";
1
4
  import { boundText, emptyUsage, mergeUsage, toNestedUsage } from "./activity.js";
2
- import { CHAIN_HANDOFF_LIMIT, DETAILS_ACTIVITY_LIMIT, DETAILS_OUTPUT_LIMIT, ERROR_TEXT_LIMIT, MAX_CONCURRENCY, MAX_TASKS, PARALLEL_OUTPUT_LIMIT, PARALLEL_TASK_OUTPUT_LIMIT, SINGLE_OUTPUT_LIMIT, } from "./constants.js";
5
+ import { DETAILS_ACTIVITY_LIMIT, DETAILS_OUTPUT_LIMIT, ERROR_TEXT_LIMIT, MAX_CONCURRENCY, MAX_TASKS, PARALLEL_OUTPUT_LIMIT, PARALLEL_TASK_OUTPUT_LIMIT, SINGLE_OUTPUT_LIMIT, TASK_RETRY_BASE_DELAY_MS, TASK_RETRY_LIMIT, } from "./constants.js";
3
6
  import { resolveSubagentTask } from "./resolve.js";
4
7
  import { runSdkTask } from "./sdk-runner.js";
8
+ import { loadSubagentConfig } from "./settings.js";
5
9
  const EMPTY_OUTPUT = "(Subagent completed but returned no output.)";
6
10
  export class ConcurrencyGate {
7
11
  active = 0;
@@ -51,14 +55,16 @@ export class ConcurrencyGate {
51
55
  function runId(index) {
52
56
  return `subagent-${index + 1}`;
53
57
  }
54
- function createRun(task, index, step) {
58
+ function createRun(task, index, parentCwd) {
55
59
  return {
56
60
  id: runId(index),
57
61
  agent: task.agent.name,
58
62
  agentSource: task.agent.source,
59
63
  description: task.description,
60
64
  prompt: task.prompt,
61
- cwd: task.cwd,
65
+ // Display-only relative path; the worker session itself uses the
66
+ // resolved absolute task.cwd. Empty means "same as the parent".
67
+ cwd: relative(parentCwd, task.cwd),
62
68
  model: `${task.model.provider}/${task.model.id}`,
63
69
  thinking: task.thinking,
64
70
  status: "queued",
@@ -66,7 +72,6 @@ function createRun(task, index, step) {
66
72
  liveText: "",
67
73
  finalOutput: "",
68
74
  usage: emptyUsage(),
69
- step,
70
75
  };
71
76
  }
72
77
  function aggregateUsage(runs) {
@@ -181,13 +186,16 @@ function statusText(details) {
181
186
  }
182
187
  return run.status === "running" ? "Thinking…" : run.status;
183
188
  }
184
- if (details.mode === "chain") {
185
- const current = details.runs.find((run) => run.status === "running" || run.status === "queued");
186
- return current
187
- ? `Step ${current.step ?? 1}/${details.runs.length} · ${current.currentActivity ?? current.agent}`
188
- : `${completed}/${details.runs.length} steps complete`;
189
- }
190
- return `${completed}/${details.runs.length} complete · ${running} running · ${queued} queued${failed ? ` · ${failed} failed` : ""}${aborted ? ` · ${aborted} aborted` : ""}`;
189
+ const parts = [`${completed}/${details.runs.length} complete`];
190
+ if (running)
191
+ parts.push(`${running} running`);
192
+ if (queued)
193
+ parts.push(`${queued} queued`);
194
+ if (failed)
195
+ parts.push(`${failed} failed`);
196
+ if (aborted)
197
+ parts.push(`${aborted} aborted`);
198
+ return parts.join(" · ");
191
199
  }
192
200
  function emitDetails(mode, runs, startedAt, onUpdate) {
193
201
  const details = {
@@ -200,9 +208,6 @@ function emitDetails(mode, runs, startedAt, onUpdate) {
200
208
  onUpdate?.(boundSubagentDetails(details));
201
209
  return details;
202
210
  }
203
- function replacePrevious(prompt, previous) {
204
- return prompt.replace(/\{previous\}/gu, boundText(previous, CHAIN_HANDOFF_LIMIT));
205
- }
206
211
  function validateTaskCount(tasks) {
207
212
  if (tasks.length === 0)
208
213
  throw new Error("Subagent task list must not be empty.");
@@ -211,7 +216,34 @@ function validateTaskCount(tasks) {
211
216
  }
212
217
  async function resolveTasks(tasks, options) {
213
218
  validateTaskCount(tasks);
214
- return Promise.all(tasks.map((task) => resolveSubagentTask(task, options.parentCwd, options.agents, options.parent, options.configAgentDir)));
219
+ const config = await loadSubagentConfig(options.agentDir);
220
+ return Promise.all(tasks.map((task) => resolveSubagentTask(task, options.parentCwd, options.agents, options.parent, options.agentDir, config)));
221
+ }
222
+ // Reuses pi-ai's provider-error classification by wrapping the stored error
223
+ // text in the assistant-message shape it inspects.
224
+ function isRetryableRunError(error) {
225
+ if (!error)
226
+ return false;
227
+ const probe = { role: "assistant", content: [], stopReason: "error", errorMessage: error };
228
+ return isRetryableAssistantError(probe);
229
+ }
230
+ // Session-level auto-retry already covers errors that happen inside the agent
231
+ // loop; this catches the paths that bypass it (preflight throws such as auth
232
+ // checks). Only a run that produced nothing is retried, so partial work is
233
+ // never discarded.
234
+ function shouldRetryTask(run) {
235
+ return (run.status === "failed" && run.usage.turns === 0 && run.usage.toolUses === 0 && isRetryableRunError(run.error));
236
+ }
237
+ function resetRunForRetry(run, attempt, maxAttempts) {
238
+ run.status = "queued";
239
+ run.error = undefined;
240
+ run.startedAt = undefined;
241
+ run.endedAt = undefined;
242
+ run.finalOutput = "";
243
+ run.liveText = "";
244
+ run.currentActivity = `Retrying (attempt ${attempt}/${maxAttempts})…`;
245
+ run.activities.length = 0;
246
+ run.usage = emptyUsage();
215
247
  }
216
248
  async function runWithGate(task, run, options, onProgress) {
217
249
  onProgress();
@@ -223,16 +255,33 @@ async function runWithGate(task, run, options, onProgress) {
223
255
  run.error = "Subagent was aborted while queued.";
224
256
  return run;
225
257
  }
226
- return await runSdkTask({
227
- task,
228
- run,
229
- modelRuntime: options.modelRuntime,
230
- agentDir: options.agentDir,
231
- projectTrusted: options.projectTrusted,
232
- signal: options.signal,
233
- onProgress,
234
- registerAbort: options.registerAbort,
235
- });
258
+ const baseDelayMs = options.taskRetryBaseDelayMs ?? TASK_RETRY_BASE_DELAY_MS;
259
+ for (let attempt = 0;; attempt++) {
260
+ const result = await runSdkTask({
261
+ task,
262
+ run,
263
+ modelRuntime: options.modelRuntime,
264
+ agentDir: options.agentDir,
265
+ projectTrusted: options.projectTrusted,
266
+ signal: options.signal,
267
+ onProgress,
268
+ registerAbort: options.registerAbort,
269
+ });
270
+ if (attempt >= TASK_RETRY_LIMIT || options.signal?.aborted || !shouldRetryTask(result))
271
+ return result;
272
+ resetRunForRetry(run, attempt + 1, TASK_RETRY_LIMIT);
273
+ onProgress();
274
+ try {
275
+ await sleep(baseDelayMs * 2 ** attempt, options.signal);
276
+ }
277
+ catch {
278
+ run.status = "aborted";
279
+ run.error = "Subagent was aborted while waiting to retry.";
280
+ run.endedAt = Date.now();
281
+ onProgress();
282
+ return run;
283
+ }
284
+ }
236
285
  }
237
286
  catch (error) {
238
287
  run.status = options.signal?.aborted ? "aborted" : "failed";
@@ -255,18 +304,11 @@ function resultContent(details) {
255
304
  }
256
305
  return run.finalOutput || EMPTY_OUTPUT;
257
306
  }
258
- if (details.mode === "chain") {
259
- const failed = details.runs.find((run) => run.status === "failed" || run.status === "aborted");
260
- if (failed)
261
- return boundText(`Chain stopped at step ${failed.step ?? "?"} (${failed.agent}): ${failed.error ?? "unknown error"}`, ERROR_TEXT_LIMIT);
262
- const last = details.runs[details.runs.length - 1];
263
- return last?.finalOutput || EMPTY_OUTPUT;
264
- }
265
307
  const sections = details.runs.map((run) => {
266
308
  const output = run.finalOutput || run.error || EMPTY_OUTPUT;
267
- return `### ${run.agent} · ${run.status}\n\n${boundText(output, PARALLEL_TASK_OUTPUT_LIMIT)}`;
309
+ return `### ${run.description} (${run.agent}) — ${run.status}\n\n${boundText(output, PARALLEL_TASK_OUTPUT_LIMIT)}`;
268
310
  });
269
- return boundText(`Parallel results\n\n${sections.join("\n\n---\n\n")}`, PARALLEL_OUTPUT_LIMIT);
311
+ return boundText(sections.join("\n\n---\n\n"), PARALLEL_OUTPUT_LIMIT);
270
312
  }
271
313
  function invocationMode(params) {
272
314
  const provided = [];
@@ -274,22 +316,14 @@ function invocationMode(params) {
274
316
  provided.push("prompt");
275
317
  if (params.tasks != null)
276
318
  provided.push("tasks");
277
- if (params.chain != null)
278
- provided.push("chain");
279
319
  if (provided.length !== 1) {
280
320
  throw new Error(provided.length === 0
281
- ? "Provide exactly one subagent mode: prompt (single), tasks (parallel), or chain (sequential); none was provided."
321
+ ? "Provide exactly one subagent mode: prompt (single task) or tasks (parallel tasks); none was provided."
282
322
  : `Provide exactly one subagent mode: received ${provided.join(", ")}. Keep one and set the unused mode fields to null or omit them.`);
283
323
  }
284
324
  if (params.tasks != null) {
285
325
  return { mode: "parallel", tasks: params.tasks };
286
326
  }
287
- if (params.chain != null) {
288
- return { mode: "chain", tasks: params.chain };
289
- }
290
- if (params.prompt == null) {
291
- throw new Error("Provide exactly one subagent mode: prompt (single), tasks (parallel), or chain (sequential).");
292
- }
293
327
  if (!params.description)
294
328
  throw new Error("description is required for single mode.");
295
329
  return {
@@ -304,44 +338,59 @@ function invocationMode(params) {
304
338
  ],
305
339
  };
306
340
  }
341
+ // Cheap change detector: consecutive events that alter nothing user-visible
342
+ // skip the bounded-details serialization in emitDetails entirely.
343
+ function progressKey(runs) {
344
+ return runs
345
+ .map((run) => {
346
+ const last = run.activities[run.activities.length - 1];
347
+ return [
348
+ run.status,
349
+ run.currentActivity ?? "",
350
+ run.activities.length,
351
+ last?.status ?? "",
352
+ last?.resultSummary ?? "",
353
+ run.liveText.length,
354
+ run.liveText.slice(-24),
355
+ run.finalOutput.length,
356
+ run.usage.turns,
357
+ run.usage.toolUses,
358
+ run.usage.totalTokens,
359
+ run.error ?? "",
360
+ ].join("|");
361
+ })
362
+ .join("~");
363
+ }
364
+ export function isSubagentError(details) {
365
+ if (details.status !== "failed" && details.status !== "aborted")
366
+ return false;
367
+ // A batch with any successful run is a partial result, not an error:
368
+ // per-run status is already reported in the content sections.
369
+ return !details.runs.some((run) => run.status === "completed");
370
+ }
307
371
  export async function runSubagentInvocation(options) {
308
372
  const { mode, tasks } = invocationMode(options.params);
309
373
  const resolved = await resolveTasks(tasks, options);
310
- const runs = resolved.map((task, index) => createRun(task, index, mode === "chain" ? index + 1 : undefined));
374
+ const runs = resolved.map((task, index) => createRun(task, index, options.parentCwd));
311
375
  const startedAt = Date.now();
312
376
  let latestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);
377
+ let lastProgressKey = progressKey(runs);
313
378
  const progress = () => {
379
+ const key = progressKey(runs);
380
+ if (key === lastProgressKey)
381
+ return;
382
+ lastProgressKey = key;
314
383
  latestDetails = emitDetails(mode, runs, startedAt, options.onUpdate);
315
384
  };
316
385
  if (mode === "single") {
317
386
  await runWithGate(resolved[0], runs[0], options, progress);
318
387
  }
319
- else if (mode === "parallel") {
320
- await Promise.all(resolved.map((task, index) => runWithGate(task, runs[index], options, progress)));
321
- }
322
388
  else {
323
- let previous = "";
324
- for (let index = 0; index < resolved.length; index++) {
325
- if (options.signal?.aborted) {
326
- runs[index].status = "aborted";
327
- runs[index].error = "Skipped because the parent subagent call was aborted.";
328
- continue;
329
- }
330
- const task = { ...resolved[index], prompt: replacePrevious(resolved[index].prompt, previous) };
331
- await runWithGate(task, runs[index], options, progress);
332
- if (runs[index].status !== "completed") {
333
- for (let skipped = index + 1; skipped < runs.length; skipped++) {
334
- runs[skipped].status = options.signal?.aborted ? "aborted" : "failed";
335
- runs[skipped].error = `Skipped because chain step ${index + 1} did not complete.`;
336
- }
337
- break;
338
- }
339
- previous = runs[index].finalOutput;
340
- }
389
+ await Promise.all(resolved.map((task, index) => runWithGate(task, runs[index], options, progress)));
341
390
  }
342
391
  latestDetails = emitDetails(mode, runs, startedAt, undefined);
343
392
  latestDetails.endedAt = Date.now();
344
- const isError = latestDetails.status === "failed" || latestDetails.status === "aborted";
393
+ const isError = isSubagentError(latestDetails);
345
394
  return {
346
395
  content: resultContent(latestDetails),
347
396
  details: boundSubagentDetails(latestDetails),