pi-subagents 0.31.1 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -4
- package/README.md +219 -40
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +71 -10
- package/src/agents/agent-management.ts +179 -2
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +193 -19
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/config.ts +27 -4
- package/src/extension/doctor.ts +1 -7
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +70 -41
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +54 -10
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +187 -38
- package/src/runs/background/async-job-tracker.ts +88 -2
- package/src/runs/background/async-status.ts +67 -10
- package/src/runs/background/chain-root-attachment.ts +34 -4
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +156 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +167 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/stale-run-reconciler.ts +28 -1
- package/src/runs/background/subagent-runner.ts +840 -127
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +123 -27
- package/src/runs/foreground/execution.ts +174 -27
- package/src/runs/foreground/subagent-executor.ts +569 -81
- package/src/runs/shared/acceptance.ts +45 -22
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/model-fallback.ts +171 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +89 -0
- package/src/runs/shared/parallel-utils.ts +50 -1
- package/src/runs/shared/pi-args.ts +35 -4
- package/src/runs/shared/pi-spawn.ts +52 -20
- package/src/runs/shared/single-output.ts +2 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +110 -4
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/runs/shared/worktree.ts +28 -5
- package/src/shared/artifacts.ts +16 -1
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/fork-context.ts +133 -22
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +197 -4
- package/src/shared/utils.ts +99 -14
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +133 -2
- package/src/tui/render.ts +32 -12
package/src/extension/schemas.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
|
-
import { SUBAGENT_ACTIONS } from "../shared/types.ts";
|
|
7
6
|
|
|
8
7
|
function keepTopLevelParameterDescriptions<T>(schema: T): T {
|
|
9
8
|
return pruneNestedDescriptions(schema, []) as T;
|
|
@@ -75,6 +74,24 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
75
74
|
description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
|
|
76
75
|
});
|
|
77
76
|
|
|
77
|
+
const TurnBudgetOverride = Type.Object({
|
|
78
|
+
maxTurns: Type.Integer({ minimum: 1 }),
|
|
79
|
+
graceTurns: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
80
|
+
}, { additionalProperties: false, description: "Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns additional assistant turns it is aborted and partial output is returned." });
|
|
81
|
+
|
|
82
|
+
const ToolBudgetBlock = Type.Unsafe({
|
|
83
|
+
anyOf: [
|
|
84
|
+
{ type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
85
|
+
{ type: "string", enum: ["*"] },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const ToolBudgetOverride = Type.Object({
|
|
90
|
+
soft: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
91
|
+
hard: Type.Integer({ minimum: 1 }),
|
|
92
|
+
block: Type.Optional(ToolBudgetBlock),
|
|
93
|
+
}, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." });
|
|
94
|
+
|
|
78
95
|
const TaskItem = Type.Object({
|
|
79
96
|
agent: Type.String(),
|
|
80
97
|
task: Type.String(),
|
|
@@ -86,6 +103,7 @@ const TaskItem = Type.Object({
|
|
|
86
103
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
87
104
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
88
105
|
skill: Type.Optional(SkillOverride),
|
|
106
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
89
107
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
90
108
|
});
|
|
91
109
|
|
|
@@ -105,6 +123,7 @@ const ParallelTaskSchema = Type.Object({
|
|
|
105
123
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
106
124
|
skill: Type.Optional(SkillOverride),
|
|
107
125
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
126
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
108
127
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
109
128
|
});
|
|
110
129
|
|
|
@@ -132,6 +151,7 @@ const DynamicParallelTemplateSchema = Type.Object({
|
|
|
132
151
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
133
152
|
skill: Type.Optional(SkillOverride),
|
|
134
153
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
154
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
135
155
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
136
156
|
}, { additionalProperties: false });
|
|
137
157
|
|
|
@@ -157,6 +177,7 @@ const ChainItem = Type.Object({
|
|
|
157
177
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
158
178
|
skill: Type.Optional(SkillOverride),
|
|
159
179
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
180
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
160
181
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
161
182
|
parallel: Type.Optional(Type.Unsafe({
|
|
162
183
|
anyOf: [
|
|
@@ -197,20 +218,26 @@ const SubagentParamsSchema = Type.Object({
|
|
|
197
218
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
198
219
|
// Management action (when present, tool operates in management mode)
|
|
199
220
|
action: Type.Optional(Type.String({
|
|
200
|
-
|
|
201
|
-
description: "Management/control action. Omit for execution mode."
|
|
221
|
+
description: "Management/control action only. Must be omitted for execution mode (single, parallel, or chain)."
|
|
202
222
|
})),
|
|
203
223
|
id: Type.Optional(Type.String({
|
|
204
|
-
description: "Run id or prefix for action='status', action='interrupt', action='resume', or action='append-step'."
|
|
224
|
+
description: "Run id or prefix for action='status', action='interrupt', action='resume', action='steer', or action='append-step'."
|
|
205
225
|
})),
|
|
206
226
|
runId: Type.Optional(Type.String({
|
|
207
|
-
description: "Target run ID for action='interrupt', action='resume', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
227
|
+
description: "Target run ID for action='interrupt', action='resume', action='steer', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
208
228
|
})),
|
|
209
229
|
dir: Type.Optional(Type.String({
|
|
210
|
-
description: "Async run directory for action='status' or action='
|
|
230
|
+
description: "Async run directory for action='status', action='resume', or action='steer'."
|
|
231
|
+
})),
|
|
232
|
+
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
233
|
+
view: Type.Optional(Type.String({
|
|
234
|
+
enum: ["fleet", "transcript"],
|
|
235
|
+
description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
|
|
211
236
|
})),
|
|
212
|
-
|
|
213
|
-
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'. Use index to choose a child from multi-child runs." })),
|
|
237
|
+
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
|
|
238
|
+
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume' or non-terminal guidance for action='steer'. Use index to choose a child from multi-child runs." })),
|
|
239
|
+
schedule: Type.Optional(Type.String({ description: "Explicit one-shot schedule for action='schedule'. Only honored when scheduledRuns.enabled is true. Use '+10m' or a future ISO timestamp with timezone; scheduled runs always launch async with fresh context." })),
|
|
240
|
+
scheduleName: Type.Optional(Type.String({ description: "Optional display name for action='schedule'." })),
|
|
214
241
|
// Chain identifier for management (can't reuse 'chain' — that's the execution array)
|
|
215
242
|
chainName: Type.Optional(Type.String({
|
|
216
243
|
description: "Chain name for get/update/delete management actions"
|
|
@@ -235,8 +262,10 @@ const SubagentParamsSchema = Type.Object({
|
|
|
235
262
|
})),
|
|
236
263
|
chainDir: Type.Optional(Type.String({ description: "Persistent chain artifact directory; defaults to user-scoped temp storage." })),
|
|
237
264
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
238
|
-
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "
|
|
239
|
-
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for foreground
|
|
265
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional run-level timeout in ms for foreground and async/background runs. Alias of maxRuntimeMs." })),
|
|
266
|
+
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for optional run-level timeout in foreground and async/background runs." })),
|
|
267
|
+
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
268
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
240
269
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
241
270
|
cwd: Type.Optional(Type.String()),
|
|
242
271
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
@@ -263,3 +292,18 @@ const SubagentParamsSchema = Type.Object({
|
|
|
263
292
|
});
|
|
264
293
|
|
|
265
294
|
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
295
|
+
|
|
296
|
+
const WaitParamsSchema = Type.Object({
|
|
297
|
+
id: Type.Optional(Type.String({
|
|
298
|
+
description: "Run id or prefix to wait for one specific run. Omit to wait across every active async run started in this session.",
|
|
299
|
+
})),
|
|
300
|
+
all: Type.Optional(Type.Boolean({
|
|
301
|
+
description: "Wait for ALL active runs to finish. Default false: return as soon as the first run finishes, so a fleet manager can spawn a replacement and wait again. Ignored when id targets a single run.",
|
|
302
|
+
})),
|
|
303
|
+
timeoutMs: Type.Optional(Type.Integer({
|
|
304
|
+
minimum: 1,
|
|
305
|
+
description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
|
|
306
|
+
})),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
export const WaitParams = keepTopLevelParameterDescriptions(WaitParamsSchema);
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import type { ExtensionConfig, ToolDescriptionMode } from "../shared/types.ts";
|
|
4
|
+
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
5
|
+
|
|
6
|
+
const CUSTOM_TOOL_DESCRIPTION_FILE = "subagent-tool-description.md";
|
|
7
|
+
const CUSTOM_TOOL_DESCRIPTION_MAX_BYTES = 50 * 1024;
|
|
8
|
+
|
|
9
|
+
export const SUBAGENT_SAFETY_GUIDANCE = `SAFETY-CRITICAL SUBAGENT GUIDANCE:
|
|
10
|
+
• Use { action: "list" } before execution and only run executable/non-disabled agents or chains.
|
|
11
|
+
• Keep execution and management separate: omit action for SINGLE/PARALLEL/CHAIN execution; use action only for list/get/models/create/update/delete/status/interrupt/resume/append-step/doctor.
|
|
12
|
+
• Async/background runs: launch with async:true only when work can proceed independently. Do not sleep or poll status just to wait; if this turn must block, use the wait tool. Otherwise continue useful work or respond and let completion notifications arrive.
|
|
13
|
+
• Child-safety boundary: ordinary child subagents are not orchestrators and must not run subagents. Only explicitly configured fanout children may use the child-safe subagent tool, still bounded by depth/session limits.
|
|
14
|
+
• Writing/review safety: keep one writer for the same cwd/worktree. Use fresh-context read-only reviewers/validators for independent review, then have the parent synthesize and apply fixes as the sole writer unless an isolated worktree was intentionally requested.
|
|
15
|
+
• Artifacts/status essentials: chain outputs live under {chain_dir}; async runs expose asyncId/asyncDir with status.json, events.jsonl, output logs, and status via { action: "status", id }. Include output paths and residual risks when reporting results.`;
|
|
16
|
+
|
|
17
|
+
export const FULL_SUBAGENT_TOOL_DESCRIPTION = `Delegate to subagents or manage agent definitions.
|
|
18
|
+
|
|
19
|
+
EXECUTION (use exactly ONE mode):
|
|
20
|
+
• Before executing, use { action: "list" } to inspect configured agents/chains. Only execute agents listed as executable/non-disabled.
|
|
21
|
+
• SINGLE: { agent, task? } - one task; omit task for self-contained agents
|
|
22
|
+
• CHAIN: { chain: [{agent:"agent-a"}, {parallel:[{agent:"agent-b",count:3}]}] } - sequential pipeline with optional parallel fan-out
|
|
23
|
+
• PARALLEL: { tasks: [{agent,task,count?,output?,reads?,progress?}, ...], concurrency?: number, worktree?: true } - concurrent execution (worktree: isolate each task in a git worktree)
|
|
24
|
+
• Optional context: { context: "fresh" | "fork" } (explicit value overrides every child; when omitted, each requested agent uses its own defaultContext, otherwise "fresh"; inspect agent defaults via { action: "list" })
|
|
25
|
+
• Optional timeout: { timeoutMs } or { maxRuntimeMs } sets a run-level max runtime for foreground and async/background runs
|
|
26
|
+
• If { action: "list" } shows proactive skill subagent suggestions, consider a small fresh-context fanout for broad tasks where one of those skills would materially help
|
|
27
|
+
|
|
28
|
+
CHAIN TEMPLATE VARIABLES (use in task strings):
|
|
29
|
+
• {task} - The original task/request from the user
|
|
30
|
+
• {previous} - Text response from the previous step (empty for first step)
|
|
31
|
+
• {chain_dir} - Shared directory for chain files (e.g., <tmpdir>/pi-subagents-<scope>/chain-runs/abc123/)
|
|
32
|
+
|
|
33
|
+
Example: { chain: [{agent:"agent-a", task:"Analyze {task}"}, {agent:"agent-b", task:"Plan based on {previous}"}] }
|
|
34
|
+
|
|
35
|
+
MANAGEMENT (use action field, omit agent/task/chain/tasks):
|
|
36
|
+
• { action: "list" } - discover executable agents/chains
|
|
37
|
+
• { action: "get", agent: "name" } - full detail; packaged agents use dotted runtime names like "package.agent"
|
|
38
|
+
• { action: "models", agent?: "name" } - show the runtime-loaded builtin subagent model mapping, optionally filtered to one builtin
|
|
39
|
+
• { action: "create", config: { name: "custom-agent", package: "code-analysis", systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext, ... } }
|
|
40
|
+
• { action: "update", agent: "code-analysis.custom-agent", config: { package: "analysis", ... } } - merge
|
|
41
|
+
• { action: "delete", agent: "code-analysis.custom-agent" }
|
|
42
|
+
• { action: "eject", agent: "reviewer", agentScope?: "user" | "project" } - copy a bundled/package agent to user/project scope as an editable custom file that shadows the original (default scope: user)
|
|
43
|
+
• { action: "disable", agent: "reviewer", agentScope?: "user" | "project" } - hide any agent from runtime discovery via a reversible settings override (default scope: user)
|
|
44
|
+
• { action: "enable", agent: "reviewer", agentScope?: "user" | "project" } - remove a disabled override and restore discovery
|
|
45
|
+
• { action: "reset", agent: "reviewer", agentScope?: "user" | "project" } - delete the scope's custom agent file and/or settings override, restoring the bundled default
|
|
46
|
+
• Use chainName for chain operations; packaged chains also use dotted runtime names
|
|
47
|
+
|
|
48
|
+
CONTROL:
|
|
49
|
+
• { action: "status", id: "..." } - inspect an async/background run by id or prefix
|
|
50
|
+
• { action: "status", view: "fleet" } - read-only active foreground/async fleet view with transcript commands
|
|
51
|
+
• { action: "status", id: "...", view: "transcript", index?: 0, lines?: 80 } - tail a run or child output/session transcript
|
|
52
|
+
• { action: "interrupt", id?: "..." } - soft-interrupt the current child turn and leave the run paused
|
|
53
|
+
• { action: "resume", id: "...", message: "...", index?: 0 } - interrupt then follow up with a live async child, or revive a completed async/foreground child from its session
|
|
54
|
+
• { action: "steer", id: "...", message: "...", index?: 0 } - queue non-terminal guidance for a live/queued async Pi child when supported
|
|
55
|
+
• { action: "append-step", id: "...", chain: [{agent:"agent-c", task:"Use {previous}"}] } - append one step to the tail of a running async chain
|
|
56
|
+
|
|
57
|
+
SCHEDULE (opt-in; requires { "scheduledRuns": { "enabled": true } } in config.json):
|
|
58
|
+
• { action: "schedule", agent, task?, schedule: "+10m" | "2030-01-01T09:00:00Z", scheduleName? } - defer a subagent launch until a future time. Also accepts tasks[] or chain[]. Scheduled runs always launch async with fresh context; they become normal tracked async runs once they fire. Only schedule explicit delayed runs the user asked for.
|
|
59
|
+
• { action: "schedule-list" } - list scheduled runs for this session
|
|
60
|
+
• { action: "schedule-status", id: "..." } - inspect one scheduled run
|
|
61
|
+
• { action: "schedule-cancel", id: "..." } - cancel a scheduled run before it fires
|
|
62
|
+
|
|
63
|
+
DIAGNOSTICS:
|
|
64
|
+
• { action: "doctor" } - read-only report for runtime paths, discovery, sessions, and intercom
|
|
65
|
+
|
|
66
|
+
${SUBAGENT_SAFETY_GUIDANCE}`;
|
|
67
|
+
|
|
68
|
+
export const COMPACT_SUBAGENT_TOOL_DESCRIPTION = `Delegate to subagents or manage definitions. Use exactly one mode per call.
|
|
69
|
+
|
|
70
|
+
EXECUTE:
|
|
71
|
+
• Before execution, call { action: "list" }; run only executable/non-disabled configured agents/chains.
|
|
72
|
+
• SINGLE {agent, task?}; PARALLEL {tasks:[{agent,task,count?,output?,reads?,progress?}], concurrency?, worktree?}; CHAIN {chain:[{agent,task?},{parallel:[...]}]}.
|
|
73
|
+
• context can be "fresh" or "fork"; omitted uses each agent defaultContext, otherwise fresh. timeoutMs/maxRuntimeMs apply to foreground and async/background runs.
|
|
74
|
+
• Chain templates may use {task}, {previous}, {chain_dir}, and named outputs. Parallel worktree isolation requires a clean git repo.
|
|
75
|
+
• If list shows proactive skill subagent suggestions, use a small fresh-context fanout only when the task is broad enough.
|
|
76
|
+
|
|
77
|
+
MANAGE / CONTROL:
|
|
78
|
+
• Use action without execution fields: list, get, models, create, update, delete, eject, disable, enable, reset, doctor.
|
|
79
|
+
• Async control actions: status, interrupt, resume, steer, append-step. Use status view:"fleet" for active-run overview, view:"transcript" to tail child output, and steer for non-terminal live guidance. Use id/runId prefixes carefully; use index for a specific child.
|
|
80
|
+
• Opt-in schedule actions: schedule, schedule-list, schedule-status, schedule-cancel. Schedule only explicit delayed runs the user asked for.
|
|
81
|
+
|
|
82
|
+
ASYNC / WAIT:
|
|
83
|
+
• async:true detaches background work. Do not sleep or poll just to wait; use the wait tool only when this turn must block. Otherwise continue useful work or respond and let completion notifications arrive.
|
|
84
|
+
• Status and artifacts live under asyncId/asyncDir with status.json, events.jsonl, output logs, session files, and { action:"status", id:"..." }.
|
|
85
|
+
|
|
86
|
+
SAFETY:
|
|
87
|
+
• Ordinary child subagents are not orchestrators and must not run subagents. Only explicit fanout children may use child-safe subagent, still bounded by depth/session limits.
|
|
88
|
+
• Keep one writer per cwd/worktree. Use fresh read-only review/validation fanout, then synthesize and apply fixes from the parent unless isolated worktrees were intentionally requested.`;
|
|
89
|
+
|
|
90
|
+
function isToolDescriptionMode(value: unknown): value is ToolDescriptionMode {
|
|
91
|
+
return value === "full" || value === "compact" || value === "custom";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function warn(options: ToolDescriptionOptions | undefined, message: string): void {
|
|
95
|
+
(options?.warn ?? console.warn)(`[pi-subagents] ${message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ToolDescriptionOptions {
|
|
99
|
+
cwd?: string;
|
|
100
|
+
agentDir?: string;
|
|
101
|
+
warn?: (message: string) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function resolveToolDescriptionMode(config: Pick<ExtensionConfig, "toolDescriptionMode">, options?: ToolDescriptionOptions): ToolDescriptionMode {
|
|
105
|
+
const mode = config.toolDescriptionMode;
|
|
106
|
+
if (mode === undefined) return "full";
|
|
107
|
+
if (isToolDescriptionMode(mode)) return mode;
|
|
108
|
+
warn(options, `Ignoring invalid toolDescriptionMode ${JSON.stringify(mode)}; expected "full", "compact", or "custom".`);
|
|
109
|
+
return "full";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function customDescriptionPaths(options?: ToolDescriptionOptions): string[] {
|
|
113
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
114
|
+
const agentDir = options?.agentDir ?? getAgentDir();
|
|
115
|
+
return [
|
|
116
|
+
path.join(getProjectConfigDir(cwd), CUSTOM_TOOL_DESCRIPTION_FILE),
|
|
117
|
+
path.join(agentDir, CUSTOM_TOOL_DESCRIPTION_FILE),
|
|
118
|
+
];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderCustomTemplate(template: string, options?: ToolDescriptionOptions): string {
|
|
122
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
123
|
+
const agentDir = options?.agentDir ?? getAgentDir();
|
|
124
|
+
const projectConfigDir = getProjectConfigDir(cwd);
|
|
125
|
+
const variables: Record<string, () => string> = {
|
|
126
|
+
fullDescription: () => FULL_SUBAGENT_TOOL_DESCRIPTION,
|
|
127
|
+
full: () => FULL_SUBAGENT_TOOL_DESCRIPTION,
|
|
128
|
+
compactDescription: () => COMPACT_SUBAGENT_TOOL_DESCRIPTION,
|
|
129
|
+
compact: () => COMPACT_SUBAGENT_TOOL_DESCRIPTION,
|
|
130
|
+
safetyGuidance: () => SUBAGENT_SAFETY_GUIDANCE,
|
|
131
|
+
safety: () => SUBAGENT_SAFETY_GUIDANCE,
|
|
132
|
+
agentDir: () => agentDir,
|
|
133
|
+
projectConfigDir: () => projectConfigDir,
|
|
134
|
+
};
|
|
135
|
+
return template.replace(/\{\{(\w+)\}\}/g, (raw, name: string) => {
|
|
136
|
+
const replacement = variables[name];
|
|
137
|
+
if (replacement) return replacement();
|
|
138
|
+
warn(options, `${CUSTOM_TOOL_DESCRIPTION_FILE}: unknown placeholder ${raw} left unchanged.`);
|
|
139
|
+
return raw;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function loadCustomToolDescription(options?: ToolDescriptionOptions): string | undefined {
|
|
144
|
+
for (const filePath of customDescriptionPaths(options)) {
|
|
145
|
+
let stat: fs.Stats;
|
|
146
|
+
try {
|
|
147
|
+
stat = fs.statSync(filePath);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (typeof error === "object" && error !== null && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
150
|
+
warn(options, `Failed to inspect custom tool description '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (!stat.isFile()) {
|
|
154
|
+
warn(options, `Ignoring custom tool description '${filePath}' because it is not a file.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (stat.size > CUSTOM_TOOL_DESCRIPTION_MAX_BYTES) {
|
|
158
|
+
warn(options, `Ignoring custom tool description '${filePath}' because it is larger than ${CUSTOM_TOOL_DESCRIPTION_MAX_BYTES} bytes.`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const template = fs.readFileSync(filePath, "utf-8").trim();
|
|
163
|
+
if (!template) {
|
|
164
|
+
warn(options, `Ignoring empty custom tool description '${filePath}'.`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const rendered = renderCustomTemplate(template, options).trim();
|
|
168
|
+
if (!rendered) {
|
|
169
|
+
warn(options, `Ignoring custom tool description '${filePath}' because it rendered empty.`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
return rendered;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
warn(options, `Failed to read custom tool description '${filePath}': ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function withMandatorySafetyGuidance(description: string): string {
|
|
181
|
+
const customDescription = description
|
|
182
|
+
.split(SUBAGENT_SAFETY_GUIDANCE)
|
|
183
|
+
.map((part) => part.trim())
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.join("\n\n");
|
|
186
|
+
return customDescription
|
|
187
|
+
? `${customDescription}\n\n${SUBAGENT_SAFETY_GUIDANCE}`
|
|
188
|
+
: SUBAGENT_SAFETY_GUIDANCE;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function buildSubagentToolDescription(config: Pick<ExtensionConfig, "toolDescriptionMode"> = {}, options?: ToolDescriptionOptions): string {
|
|
192
|
+
const mode = resolveToolDescriptionMode(config, options);
|
|
193
|
+
if (mode === "compact") return COMPACT_SUBAGENT_TOOL_DESCRIPTION;
|
|
194
|
+
if (mode === "custom") {
|
|
195
|
+
const custom = loadCustomToolDescription(options);
|
|
196
|
+
if (custom) return withMandatorySafetyGuidance(custom);
|
|
197
|
+
warn(options, `${CUSTOM_TOOL_DESCRIPTION_FILE} was not found or valid for toolDescriptionMode "custom"; using full description.`);
|
|
198
|
+
}
|
|
199
|
+
return FULL_SUBAGENT_TOOL_DESCRIPTION;
|
|
200
|
+
}
|