@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.
- package/CHANGELOG.md +49 -0
- package/dist/cli/config-selector.d.ts.map +1 -1
- package/dist/cli/config-selector.js +3 -2
- package/dist/cli/config-selector.js.map +1 -1
- package/dist/cli/startup-ui.d.ts.map +1 -1
- package/dist/cli/startup-ui.js +3 -2
- package/dist/cli/startup-ui.js.map +1 -1
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +33 -14
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +2 -2
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +2 -3
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +18 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +11 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/index.js +2 -0
- package/dist/extensions/index.js.map +1 -1
- package/dist/extensions/plan/constants.d.ts +31 -0
- package/dist/extensions/plan/constants.d.ts.map +1 -0
- package/dist/extensions/plan/constants.js +57 -0
- package/dist/extensions/plan/constants.js.map +1 -0
- package/dist/extensions/plan/index.d.ts +21 -0
- package/dist/extensions/plan/index.d.ts.map +1 -0
- package/dist/extensions/plan/index.js +291 -0
- package/dist/extensions/plan/index.js.map +1 -0
- package/dist/extensions/plan/schema.d.ts +15 -0
- package/dist/extensions/plan/schema.d.ts.map +1 -0
- package/dist/extensions/plan/schema.js +13 -0
- package/dist/extensions/plan/schema.js.map +1 -0
- package/dist/extensions/plan/state.d.ts +29 -0
- package/dist/extensions/plan/state.d.ts.map +1 -0
- package/dist/extensions/plan/state.js +74 -0
- package/dist/extensions/plan/state.js.map +1 -0
- package/dist/extensions/plan/storage.d.ts +18 -0
- package/dist/extensions/plan/storage.d.ts.map +1 -0
- package/dist/extensions/plan/storage.js +58 -0
- package/dist/extensions/plan/storage.js.map +1 -0
- package/dist/extensions/question/dialog.d.ts +3 -1
- package/dist/extensions/question/dialog.d.ts.map +1 -1
- package/dist/extensions/question/dialog.js +106 -52
- package/dist/extensions/question/dialog.js.map +1 -1
- package/dist/extensions/question/index.d.ts.map +1 -1
- package/dist/extensions/question/index.js +2 -2
- package/dist/extensions/question/index.js.map +1 -1
- package/dist/extensions/question/render-cache.d.ts +9 -0
- package/dist/extensions/question/render-cache.d.ts.map +1 -1
- package/dist/extensions/question/render-cache.js +22 -0
- package/dist/extensions/question/render-cache.js.map +1 -1
- package/dist/extensions/question/results.d.ts.map +1 -1
- package/dist/extensions/question/results.js +6 -1
- package/dist/extensions/question/results.js.map +1 -1
- package/dist/extensions/question/schema.d.ts +4 -0
- package/dist/extensions/question/schema.d.ts.map +1 -1
- package/dist/extensions/question/schema.js +19 -28
- package/dist/extensions/question/schema.js.map +1 -1
- package/dist/extensions/question/types.d.ts +1 -1
- package/dist/extensions/question/types.d.ts.map +1 -1
- package/dist/extensions/question/types.js.map +1 -1
- package/dist/extensions/rewind/anchor.d.ts +29 -0
- package/dist/extensions/rewind/anchor.d.ts.map +1 -0
- package/dist/extensions/rewind/anchor.js +35 -0
- package/dist/extensions/rewind/anchor.js.map +1 -0
- package/dist/extensions/rewind/engine.d.ts +2 -6
- package/dist/extensions/rewind/engine.d.ts.map +1 -1
- package/dist/extensions/rewind/engine.js +41 -10
- package/dist/extensions/rewind/engine.js.map +1 -1
- package/dist/extensions/rewind/index.d.ts.map +1 -1
- package/dist/extensions/rewind/index.js +55 -21
- package/dist/extensions/rewind/index.js.map +1 -1
- package/dist/extensions/rewind/menu.d.ts.map +1 -1
- package/dist/extensions/rewind/menu.js +7 -2
- package/dist/extensions/rewind/menu.js.map +1 -1
- package/dist/extensions/rewind/restore.d.ts +26 -9
- package/dist/extensions/rewind/restore.d.ts.map +1 -1
- package/dist/extensions/rewind/restore.js +58 -16
- package/dist/extensions/rewind/restore.js.map +1 -1
- package/dist/extensions/subagent/activity.d.ts +1 -0
- package/dist/extensions/subagent/activity.d.ts.map +1 -1
- package/dist/extensions/subagent/activity.js +20 -4
- package/dist/extensions/subagent/activity.js.map +1 -1
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +13 -18
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/constants.d.ts +5 -4
- package/dist/extensions/subagent/constants.d.ts.map +1 -1
- package/dist/extensions/subagent/constants.js +12 -5
- package/dist/extensions/subagent/constants.js.map +1 -1
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +12 -5
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +166 -41
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/resolve.d.ts +2 -5
- package/dist/extensions/subagent/resolve.d.ts.map +1 -1
- package/dist/extensions/subagent/resolve.js +8 -13
- package/dist/extensions/subagent/resolve.js.map +1 -1
- package/dist/extensions/subagent/runner.d.ts +3 -1
- package/dist/extensions/subagent/runner.d.ts.map +1 -1
- package/dist/extensions/subagent/runner.js +115 -66
- package/dist/extensions/subagent/runner.js.map +1 -1
- package/dist/extensions/subagent/schema.d.ts +0 -6
- package/dist/extensions/subagent/schema.d.ts.map +1 -1
- package/dist/extensions/subagent/schema.js +3 -4
- package/dist/extensions/subagent/schema.js.map +1 -1
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
- package/dist/extensions/subagent/sdk-runner.js +37 -16
- package/dist/extensions/subagent/sdk-runner.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +10 -2
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/todo/constants.d.ts +1 -1
- package/dist/extensions/todo/constants.d.ts.map +1 -1
- package/dist/extensions/todo/constants.js +29 -4
- package/dist/extensions/todo/constants.js.map +1 -1
- package/dist/extensions/todo/index.d.ts +1 -1
- package/dist/extensions/todo/index.d.ts.map +1 -1
- package/dist/extensions/todo/index.js +22 -3
- package/dist/extensions/todo/index.js.map +1 -1
- package/dist/extensions/todo/schema.d.ts +6 -5
- package/dist/extensions/todo/schema.d.ts.map +1 -1
- package/dist/extensions/todo/schema.js +20 -9
- package/dist/extensions/todo/schema.js.map +1 -1
- package/dist/extensions/todo/state.d.ts +5 -1
- package/dist/extensions/todo/state.d.ts.map +1 -1
- package/dist/extensions/todo/state.js +184 -38
- package/dist/extensions/todo/state.js.map +1 -1
- package/dist/extensions/todo/view.d.ts.map +1 -1
- package/dist/extensions/todo/view.js +10 -6
- package/dist/extensions/todo/view.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +36 -2
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/coalescing-terminal.d.ts +116 -0
- package/dist/utils/coalescing-terminal.d.ts.map +1 -0
- package/dist/utils/coalescing-terminal.js +256 -0
- package/dist/utils/coalescing-terminal.js.map +1 -0
- package/docs/bundled/extensions/README.md +1 -0
- package/docs/bundled/extensions/plan.md +31 -0
- package/docs/bundled/extensions/question.md +9 -7
- package/docs/bundled/extensions/rewind.md +19 -8
- package/docs/bundled/extensions/subagent.md +16 -16
- package/docs/bundled/extensions/todo.md +5 -3
- package/docs/compaction.md +6 -2
- package/docs/docs.json +4 -0
- package/docs/extensions.md +4 -2
- package/npm-shrinkwrap.json +2 -2
- 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 {
|
|
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;
|
|
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
|
-
|
|
29
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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}
|
|
309
|
+
return `### ${run.description} (${run.agent}) — ${run.status}\n\n${boundText(output, PARALLEL_TASK_OUTPUT_LIMIT)}`;
|
|
268
310
|
});
|
|
269
|
-
return boundText(
|
|
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)
|
|
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,
|
|
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
|
-
|
|
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
|
|
393
|
+
const isError = isSubagentError(latestDetails);
|
|
345
394
|
return {
|
|
346
395
|
content: resultContent(latestDetails),
|
|
347
396
|
details: boundSubagentDetails(latestDetails),
|