@narumitw/pi-subagents 0.42.0 → 0.43.1

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.
@@ -0,0 +1,234 @@
1
+ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
2
+ import type { Theme, ToolRenderResultOptions } from "@earendil-works/pi-coding-agent";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import type { SubagentInspectParams } from "./inspect.js";
5
+ import {
6
+ booleanValue,
7
+ COLLAPSED_LIST_LIMIT,
8
+ expansionHint,
9
+ numberValue,
10
+ recordList,
11
+ recordValue,
12
+ renderFallbackResult,
13
+ safeBlock,
14
+ safeLine,
15
+ statusBadge,
16
+ stringValue,
17
+ type ToolRendererContext,
18
+ toolHeader,
19
+ } from "./render-common.js";
20
+
21
+ export function renderInspectCall(args: Partial<SubagentInspectParams>, theme: Theme) {
22
+ const action = safeLine(args.action, "...", 128);
23
+ const metadata: string[] = [];
24
+ if (args.agentScope) metadata.push(`[${args.agentScope}]`);
25
+ if (args.agent) metadata.push(`agent:${safeLine(args.agent, "", 256)}`);
26
+ if (args.agentId) metadata.push(`id:${safeLine(args.agentId, "", 256)}`);
27
+ if (args.includeClosed) metadata.push("include closed");
28
+ return new Text(toolHeader(theme, "subagent_inspect", action, metadata), 0, 0);
29
+ }
30
+
31
+ export function renderInspectResult(
32
+ result: AgentToolResult<Record<string, unknown>>,
33
+ options: ToolRenderResultOptions,
34
+ theme: Theme,
35
+ context: ToolRendererContext<SubagentInspectParams>,
36
+ ) {
37
+ const details = recordValue(result.details);
38
+ const action = stringValue(details?.action);
39
+ if (!details || !action) return renderFallbackResult(result, options, theme, context.isError);
40
+
41
+ const rendered = renderAction(action, details, options.expanded, theme);
42
+ if (!rendered) return renderFallbackResult(result, options, theme, context.isError);
43
+ return new Text(rendered, 0, 0);
44
+ }
45
+
46
+ function renderAction(
47
+ action: string,
48
+ details: Record<string, unknown>,
49
+ expanded: boolean,
50
+ theme: Theme,
51
+ ): string | undefined {
52
+ switch (action) {
53
+ case "list_agents":
54
+ return renderList("agent", recordList(details.agents), details, expanded, theme, formatAgent);
55
+ case "get_agent": {
56
+ const agent = recordValue(details.agent);
57
+ if (!agent) return undefined;
58
+ const lines = [
59
+ `${statusBadge(theme, "completed")} · ${theme.fg("toolTitle", theme.bold(safeLine(agent.name, "agent", 256)))}${theme.fg("muted", ` (${safeLine(agent.source, "unknown", 128)})`)}`,
60
+ formatAgent(agent, theme, true),
61
+ ];
62
+ if (!expanded) lines.push(expansionHint());
63
+ return lines.join("\n");
64
+ }
65
+ case "list_runs":
66
+ return renderList("run", recordList(details.runs), details, expanded, theme, formatRun);
67
+ case "get_run": {
68
+ const run = recordValue(details.run);
69
+ if (!run) return undefined;
70
+ const lines = [
71
+ `${statusBadge(theme, "completed")} · run ${theme.fg("accent", safeLine(run.id, "run", 256))}`,
72
+ formatRun(run, theme, true),
73
+ ];
74
+ if (!expanded) lines.push(expansionHint());
75
+ return lines.join("\n");
76
+ }
77
+ case "list_models":
78
+ return renderList(
79
+ "model",
80
+ recordList(details.models),
81
+ details,
82
+ expanded,
83
+ theme,
84
+ formatModel,
85
+ stringValue(details.source),
86
+ );
87
+ case "status":
88
+ return renderStatus(details, expanded, theme);
89
+ case "diagnose":
90
+ return renderDiagnose(details, expanded, theme);
91
+ default:
92
+ return undefined;
93
+ }
94
+ }
95
+
96
+ function renderList(
97
+ label: string,
98
+ items: Record<string, unknown>[],
99
+ details: Record<string, unknown>,
100
+ expanded: boolean,
101
+ theme: Theme,
102
+ format: (item: Record<string, unknown>, theme: Theme, expanded: boolean) => string,
103
+ source = "",
104
+ ): string {
105
+ const returned = numberValue(details.returned, items.length);
106
+ const omitted = numberValue(details.omitted);
107
+ const noun = `${label}${returned === 1 ? "" : "s"}`;
108
+ const lines = [
109
+ `${statusBadge(theme, "completed")} · ${returned} ${noun}${source ? theme.fg("muted", ` · ${safeLine(source, "", 256)}`) : ""}`,
110
+ ];
111
+ const selected = expanded ? items : items.slice(0, COLLAPSED_LIST_LIMIT);
112
+ for (const item of selected) lines.push(format(item, theme, expanded));
113
+ const hidden = Math.max(0, items.length - selected.length) + omitted;
114
+ if (hidden > 0) lines.push(theme.fg("muted", `… ${hidden} omitted`));
115
+ if (items.length === 0) lines.push(theme.fg("muted", `(no ${label}s)`));
116
+ if (!expanded && (items.length > 0 || omitted > 0)) lines.push(expansionHint());
117
+ return lines.join("\n");
118
+ }
119
+
120
+ function formatAgent(agent: Record<string, unknown>, theme: Theme, expanded: boolean): string {
121
+ const name = safeLine(agent.name, "agent", 256);
122
+ const source = safeLine(agent.source, "unknown", 128);
123
+ const model = stringValue(agent.model);
124
+ const toolCount = typeof agent.toolCount === "number" ? agent.toolCount : undefined;
125
+ const description = safeBlock(agent.description, "", 512).trim();
126
+ const lines = [
127
+ `${theme.fg("muted", "• ")}${theme.fg("accent", name)} ${theme.fg("muted", source)}${model ? theme.fg("dim", ` · ${safeLine(model, "", 256)}`) : ""}${toolCount !== undefined ? theme.fg("dim", ` · ${toolCount} tools`) : ""}`,
128
+ ];
129
+ if (description) lines.push(` ${theme.fg("toolOutput", description)}`);
130
+ if (expanded) {
131
+ const path = stringValue(agent.path);
132
+ const consultTools = stringList(agent.consultTools);
133
+ if (path) lines.push(` ${theme.fg("dim", `path: ${safeLine(path, "", 2 * 1024)}`)}`);
134
+ lines.push(` ${theme.fg("dim", `consult tools: ${consultTools.join(", ") || "none"}`)}`);
135
+ }
136
+ return lines.join("\n");
137
+ }
138
+
139
+ function formatRun(run: Record<string, unknown>, theme: Theme, expanded: boolean): string {
140
+ const id = safeLine(run.id, "run", 256);
141
+ const agent = safeLine(run.agent, "agent", 256);
142
+ const state = safeLine(run.state, "unknown", 128);
143
+ const unread = numberValue(run.unreadMessages);
144
+ const lines = [
145
+ `${theme.fg("muted", "• ")}${theme.fg("accent", id)} ${theme.fg("toolOutput", agent)} ${theme.fg("muted", state)}${unread > 0 ? theme.fg("warning", ` · unread:${unread}`) : ""}`,
146
+ ];
147
+ if (expanded) {
148
+ const thinking = stringValue(run.thinkingLevel);
149
+ const task = safeBlock(run.currentTask, "", 2 * 1024).trim();
150
+ const error = safeBlock(run.error, "", 2 * 1024).trim();
151
+ lines.push(
152
+ ` ${theme.fg("dim", `${numberValue(run.historyCount)} history · ${thinking ? `thinking:${safeLine(thinking, "", 128)} · ` : ""}${numberValue(run.children)} children`)}`,
153
+ );
154
+ if (task) lines.push(` ${theme.fg("dim", `task: ${task}`)}`);
155
+ if (error) lines.push(` ${theme.fg("error", `error: ${error}`)}`);
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+
160
+ function formatModel(model: Record<string, unknown>, theme: Theme, expanded: boolean): string {
161
+ const identity = `${safeLine(model.provider, "provider", 256)}/${safeLine(model.id, "model", 256)}`;
162
+ const current = booleanValue(model.current) ? theme.fg("success", " · current") : "";
163
+ const reasoning = booleanValue(model.reasoning) ? theme.fg("dim", " · reasoning") : "";
164
+ const lines = [`${theme.fg("muted", "• ")}${theme.fg("accent", identity)}${current}${reasoning}`];
165
+ if (expanded) {
166
+ const name = stringValue(model.name);
167
+ const thinking = stringValue(model.thinkingLevel);
168
+ if (name) lines.push(` ${theme.fg("toolOutput", safeLine(name, "", 512))}`);
169
+ lines.push(
170
+ ` ${theme.fg("dim", `context:${numberValue(model.contextWindow)} · max:${numberValue(model.maxTokens)}${thinking ? ` · thinking:${safeLine(thinking, "", 128)}` : ""}`)}`,
171
+ );
172
+ }
173
+ return lines.join("\n");
174
+ }
175
+
176
+ function renderStatus(
177
+ details: Record<string, unknown>,
178
+ expanded: boolean,
179
+ theme: Theme,
180
+ ): string | undefined {
181
+ const status = recordValue(details.status);
182
+ const stateful = recordValue(status?.stateful);
183
+ if (!status || !stateful) return undefined;
184
+ const lines = [
185
+ `${statusBadge(theme, "completed")} · runtime status`,
186
+ `${theme.fg("muted", "workflow: ")}${theme.fg("accent", safeLine(status.workflow, "unknown", 128))} · ${numberValue(stateful.activeAgents)} active · ${numberValue(stateful.retainedAgents)} retained`,
187
+ `${theme.fg("muted", "stateful: ")}${stateful.initialized === true ? "initialized" : "not initialized"} · resources: ${safeLine(status.consultResources, "unknown", 128)}`,
188
+ ];
189
+ if (expanded) {
190
+ const delivery = stringValue(stateful.completionDelivery);
191
+ const transport = stringValue(stateful.transport);
192
+ if (transport || delivery) {
193
+ lines.push(
194
+ theme.fg(
195
+ "dim",
196
+ [
197
+ transport && `transport:${safeLine(transport)}`,
198
+ delivery && `delivery:${safeLine(delivery)}`,
199
+ ]
200
+ .filter(Boolean)
201
+ .join(" · "),
202
+ ),
203
+ );
204
+ }
205
+ } else lines.push(expansionHint());
206
+ return lines.join("\n");
207
+ }
208
+
209
+ function renderDiagnose(details: Record<string, unknown>, expanded: boolean, theme: Theme): string {
210
+ const checks = recordList(details.checks);
211
+ const hasFail = checks.some((check) => check.status === "fail");
212
+ const hasWarning = checks.some((check) => check.status === "warning");
213
+ const overall = hasFail ? "failed" : hasWarning ? "warnings" : "passed";
214
+ const status = hasFail ? "failed" : hasWarning ? "warning" : "completed";
215
+ const lines = [`${statusBadge(theme, status)} · Diagnostics ${overall}`];
216
+ const selected = expanded ? checks : checks.slice(0, COLLAPSED_LIST_LIMIT);
217
+ for (const check of selected) {
218
+ const checkStatus = safeLine(check.status, "unknown", 64);
219
+ const color =
220
+ checkStatus === "fail" ? "error" : checkStatus === "warning" ? "warning" : "success";
221
+ lines.push(
222
+ `${theme.fg(color, checkStatus.toUpperCase())} ${theme.fg("accent", safeLine(check.name, "check", 256))} · ${theme.fg("toolOutput", safeBlock(check.message, "", 2 * 1024))}`,
223
+ );
224
+ }
225
+ if (checks.length === 0) lines.push(theme.fg("muted", "(no diagnostic checks)"));
226
+ if (!expanded && checks.length > 0) lines.push(expansionHint());
227
+ return lines.join("\n");
228
+ }
229
+
230
+ function stringList(value: unknown): string[] {
231
+ return Array.isArray(value)
232
+ ? value.flatMap((item) => (typeof item === "string" ? [safeLine(item, "", 256)] : []))
233
+ : [];
234
+ }
package/src/inspect.ts ADDED
@@ -0,0 +1,453 @@
1
+ import * as path from "node:path";
2
+ import { StringEnum } from "@earendil-works/pi-ai";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { type Static, Type } from "typebox";
5
+ import {
6
+ type AgentConfig,
7
+ type AgentScope,
8
+ type ConsultationCwdPolicy,
9
+ type DelegationCwdPolicy,
10
+ discoverAgents,
11
+ } from "./agents.js";
12
+ import { resolveConsultTools } from "./consult-policy.js";
13
+ import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
14
+ import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
15
+ import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
16
+ import {
17
+ inspectCompletionDeliverySettings,
18
+ inspectConsultResourceSettings,
19
+ inspectCwdPolicySettings,
20
+ inspectDelegationWorkflowSettings,
21
+ inspectSubagentSettings,
22
+ resolveDelegationWorkflow,
23
+ } from "./settings.js";
24
+ import type { StatefulSubagentRuntimeStatus } from "./stateful.js";
25
+
26
+ const INSPECT_ACTIONS = [
27
+ "list_agents",
28
+ "get_agent",
29
+ "list_runs",
30
+ "get_run",
31
+ "list_models",
32
+ "status",
33
+ "diagnose",
34
+ ] as const;
35
+
36
+ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
37
+ default: "user",
38
+ description: "Agent definition scope. Project scopes require a trusted project.",
39
+ });
40
+
41
+ const LimitSchema = Type.Number({ minimum: 1, maximum: 100, multipleOf: 1 });
42
+ const MAX_DETAILS_LIST_BYTES = 40 * 1024;
43
+
44
+ export const SubagentInspectParams = Type.Object(
45
+ {
46
+ action: StringEnum(INSPECT_ACTIONS),
47
+ agent: Type.Optional(Type.String({ minLength: 1 })),
48
+ agentId: Type.Optional(Type.String({ minLength: 1 })),
49
+ agentScope: Type.Optional(AgentScopeSchema),
50
+ limit: Type.Optional(LimitSchema),
51
+ includeClosed: Type.Optional(Type.Boolean({ default: false })),
52
+ },
53
+ { additionalProperties: false },
54
+ );
55
+
56
+ export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
57
+
58
+ export interface SubagentInspectRuntime {
59
+ getBlockingEnabled(): boolean;
60
+ getConsultResourcePolicy(): "project-context" | "none" | "all";
61
+ getConsultationCwdPolicy(): ConsultationCwdPolicy;
62
+ getDelegationCwdPolicy(): DelegationCwdPolicy;
63
+ getRuntimeStatus(): StatefulSubagentRuntimeStatus;
64
+ listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
65
+ getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
66
+ }
67
+
68
+ interface InspectToolResult {
69
+ content: Array<{ type: "text"; text: string }>;
70
+ details: Record<string, unknown>;
71
+ }
72
+
73
+ type ValidatedInspectOperation =
74
+ | { action: "list_agents"; agentScope: AgentScope; limit: number }
75
+ | { action: "get_agent"; agent: string; agentScope: AgentScope }
76
+ | { action: "list_runs"; includeClosed: boolean; limit: number }
77
+ | { action: "get_run"; agentId: string }
78
+ | { action: "list_models"; limit: number }
79
+ | { action: "status" }
80
+ | { action: "diagnose" };
81
+
82
+ export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspectRuntime): void {
83
+ pi.registerTool({
84
+ name: "subagent_inspect",
85
+ label: "Inspect Subagents",
86
+ description:
87
+ "Inspect available subagent definitions, models, retained runs, runtime status, and diagnostics without changing subagent or workspace state. This tool never starts a child, sends or acknowledges messages, interrupts or closes runs, changes settings, or modifies files.",
88
+ promptSnippet: "Inspect subagent metadata and runtime state without changing it",
89
+ parameters: SubagentInspectParams,
90
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
91
+ return executeSubagentInspect(validateInspectParams(params), ctx, runtime);
92
+ },
93
+ renderCall(args, theme) {
94
+ return renderInspectCall(args, theme);
95
+ },
96
+ renderResult(result, options, theme, context) {
97
+ return renderInspectResult(result, options, theme, context);
98
+ },
99
+ });
100
+ }
101
+
102
+ export function validateInspectParams(params: unknown): ValidatedInspectOperation {
103
+ const values = parameterRecord(params);
104
+ const rawAction = values.action;
105
+ if (
106
+ typeof rawAction !== "string" ||
107
+ !INSPECT_ACTIONS.includes(rawAction as (typeof INSPECT_ACTIONS)[number])
108
+ ) {
109
+ throw new Error(`subagent_inspect action must be one of: ${INSPECT_ACTIONS.join(", ")}`);
110
+ }
111
+ const action = rawAction as (typeof INSPECT_ACTIONS)[number];
112
+ const allowed: Record<(typeof INSPECT_ACTIONS)[number], readonly string[]> = {
113
+ list_agents: ["action", "agentScope", "limit"],
114
+ get_agent: ["action", "agent", "agentScope"],
115
+ list_runs: ["action", "includeClosed", "limit"],
116
+ get_run: ["action", "agentId"],
117
+ list_models: ["action", "limit"],
118
+ status: ["action"],
119
+ diagnose: ["action"],
120
+ };
121
+ const unexpected = Object.keys(values).find(
122
+ (key) => values[key] !== undefined && !allowed[action].includes(key),
123
+ );
124
+ if (unexpected)
125
+ throw new Error(`subagent_inspect action "${action}" does not accept ${unexpected}`);
126
+
127
+ if (action === "list_agents" || action === "get_agent") {
128
+ const agentScope = optionalAgentScope(values.agentScope);
129
+ if (action === "get_agent") {
130
+ return { action, agent: requiredString(values.agent, action, "agent"), agentScope };
131
+ }
132
+ return { action, agentScope, limit: optionalLimit(values.limit, 32) };
133
+ }
134
+ if (action === "list_runs") {
135
+ if (values.includeClosed !== undefined && typeof values.includeClosed !== "boolean") {
136
+ throw new Error('subagent_inspect action "list_runs" requires includeClosed to be boolean');
137
+ }
138
+ return {
139
+ action,
140
+ includeClosed: values.includeClosed === true,
141
+ limit: optionalLimit(values.limit, 50),
142
+ };
143
+ }
144
+ if (action === "get_run") {
145
+ return { action, agentId: requiredString(values.agentId, action, "agentId") };
146
+ }
147
+ if (action === "list_models") {
148
+ return { action, limit: optionalLimit(values.limit, 50) };
149
+ }
150
+ return { action };
151
+ }
152
+
153
+ async function executeSubagentInspect(
154
+ operation: ValidatedInspectOperation,
155
+ ctx: ExtensionContext,
156
+ runtime: SubagentInspectRuntime,
157
+ ): Promise<InspectToolResult> {
158
+ if (operation.action === "list_agents" || operation.action === "get_agent") {
159
+ assertTrustedScope(operation.agentScope, ctx);
160
+ const settings = inspectSubagentSettings().settings;
161
+ const discovery = discoverAgents(ctx.cwd, operation.agentScope, settings);
162
+ const agents = [...discovery.agents].sort((left, right) =>
163
+ left.name === right.name
164
+ ? left.source.localeCompare(right.source)
165
+ : left.name.localeCompare(right.name),
166
+ );
167
+ if (operation.action === "list_agents") {
168
+ const selected = boundedProjection(agents, operation.limit, (agent) =>
169
+ projectAgent(agent, ctx, false),
170
+ );
171
+ return inspectResult({
172
+ action: operation.action,
173
+ agents: selected.items,
174
+ returned: selected.items.length,
175
+ omitted: selected.omitted + (discovery.omittedAgentDefinitions ?? 0),
176
+ discoveryIncomplete: discovery.metadataDiscoveryIncomplete === true,
177
+ });
178
+ }
179
+ const agent = agents.find((candidate) => candidate.name === operation.agent);
180
+ if (!agent) {
181
+ throw new Error(`Unknown subagent definition: ${boundedPrivateText(operation.agent, 256)}`);
182
+ }
183
+ return inspectResult({ action: operation.action, agent: projectAgent(agent, ctx) });
184
+ }
185
+
186
+ if (operation.action === "list_runs") {
187
+ const runs = runtime.listRunInspection(operation.includeClosed);
188
+ const selected = boundedProjection(runs, operation.limit, projectRunSummary);
189
+ return inspectResult({
190
+ action: operation.action,
191
+ runs: selected.items,
192
+ returned: selected.items.length,
193
+ omitted: selected.omitted,
194
+ });
195
+ }
196
+ if (operation.action === "get_run") {
197
+ const run = runtime.getRunInspection(operation.agentId);
198
+ if (!run) {
199
+ throw new Error(`Unknown retained run: ${boundedPrivateText(operation.agentId, 256)}`);
200
+ }
201
+ return inspectResult({ action: operation.action, run: projectRun(run, ctx) });
202
+ }
203
+ if (operation.action === "list_models") {
204
+ return inspectResult({ action: operation.action, ...projectModels(ctx, operation.limit) });
205
+ }
206
+ if (operation.action === "status") {
207
+ return inspectResult({ action: operation.action, status: projectStatus(runtime) });
208
+ }
209
+
210
+ const settings = inspectSubagentSettings();
211
+ const userDiscovery = discoverAgents(ctx.cwd, "user", settings.settings);
212
+ const modelCount = availableModelCount(ctx);
213
+ const runtimeStatus = runtime.getRuntimeStatus();
214
+ const checks = [
215
+ {
216
+ name: "settings",
217
+ status: settings.error ? "fail" : "pass",
218
+ message: settings.error
219
+ ? boundedPrivateText(settings.error, 2 * 1024)
220
+ : "Settings are valid or absent.",
221
+ },
222
+ {
223
+ name: "agent-discovery",
224
+ status:
225
+ userDiscovery.metadataDiscoveryIncomplete ||
226
+ (userDiscovery.omittedAgentDefinitions ?? 0) > 0
227
+ ? "warning"
228
+ : "pass",
229
+ message: `${userDiscovery.agents.length} user-scope definitions available.`,
230
+ },
231
+ {
232
+ name: "models",
233
+ status: modelCount > 0 ? "pass" : "fail",
234
+ message: `${modelCount} session-usable models available.`,
235
+ },
236
+ {
237
+ name: "runtime",
238
+ status: runtimeStatus.enabled && !runtimeStatus.initialized ? "warning" : "pass",
239
+ message: runtimeStatus.initialized
240
+ ? "Stateful runtime initialized."
241
+ : "Stateful runtime not initialized.",
242
+ },
243
+ {
244
+ name: "consultation",
245
+ status: runtime.getBlockingEnabled() && modelCount > 0 ? "pass" : "fail",
246
+ message: runtime.getBlockingEnabled()
247
+ ? modelCount > 0
248
+ ? "Read-only consultation is supported."
249
+ : "Consultation has no available model."
250
+ : "Blocking delegation is disabled, so consultation is not registered.",
251
+ },
252
+ ] as const;
253
+ return inspectResult({
254
+ action: operation.action,
255
+ checks,
256
+ ok: checks.every((check) => check.status !== "fail"),
257
+ });
258
+ }
259
+
260
+ function projectAgent(
261
+ agent: AgentConfig,
262
+ ctx: ExtensionContext,
263
+ includeTools = true,
264
+ ): Record<string, unknown> {
265
+ const tools = agent.tools === undefined ? undefined : projectToolNames(agent.tools);
266
+ return {
267
+ name: boundedPrivateText(agent.name, 256),
268
+ description: boundedPrivateText(agent.description, 256),
269
+ source: agent.source,
270
+ scope: agent.source === "project" ? "project" : "user",
271
+ path:
272
+ agent.source === "project"
273
+ ? safeTerminalLine(path.posix.join(".pi", "agents", path.basename(agent.filePath)))
274
+ : safeDisplayPath(agent.filePath, ctx.cwd),
275
+ model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
276
+ thinkingLevel: agent.thinkingLevel,
277
+ ...(includeTools
278
+ ? { tools, toolCount: agent.tools?.length }
279
+ : { toolCount: agent.tools?.length }),
280
+ consultTools: resolveConsultTools(agent.tools),
281
+ };
282
+ }
283
+
284
+ function projectRunSummary(run: AgentRunInspectionSummary): Record<string, unknown> {
285
+ return {
286
+ id: boundedPrivateText(run.id, 256),
287
+ agent: boundedPrivateText(run.agent, 256),
288
+ state: run.state,
289
+ createdAt: run.createdAt,
290
+ updatedAt: run.updatedAt,
291
+ historyCount: run.historyCount,
292
+ unreadMessages: run.unreadMessages,
293
+ };
294
+ }
295
+
296
+ function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Record<string, unknown> {
297
+ return {
298
+ ...projectRunSummary(run),
299
+ cwd: safeDisplayPath(run.cwd, ctx.cwd),
300
+ workspaceMode: run.workspaceMode ?? "shared",
301
+ thinkingLevel: run.thinkingLevel,
302
+ currentTask: run.currentTask ? boundedPrivateText(run.currentTask, 2 * 1024) : undefined,
303
+ error: run.error ? boundedPrivateText(run.error, 2 * 1024) : undefined,
304
+ target: run.target
305
+ ? {
306
+ cwd: safeDisplayPath(run.target.cwd, ctx.cwd),
307
+ boundary: run.target.boundary,
308
+ trust: {
309
+ kind: run.target.trust.kind,
310
+ projectTrusted: run.target.trust.projectTrusted,
311
+ sourcePath: run.target.trust.sourcePath
312
+ ? safeDisplayPath(run.target.trust.sourcePath, ctx.cwd)
313
+ : undefined,
314
+ warning: run.target.trust.warning
315
+ ? boundedPrivateText(run.target.trust.warning, 512)
316
+ : undefined,
317
+ },
318
+ }
319
+ : undefined,
320
+ policy: run.policy
321
+ ? {
322
+ inherited: projectToolNames(run.policy.inherited),
323
+ overridden: projectToolNames(run.policy.overridden),
324
+ unsupported: projectToolNames(run.policy.unsupported),
325
+ }
326
+ : undefined,
327
+ };
328
+ }
329
+
330
+ function projectModels(ctx: ExtensionContext, limit: number): Record<string, unknown> {
331
+ const scoped = ctx.scopedModels ?? [];
332
+ const candidates =
333
+ scoped.length > 0
334
+ ? scoped
335
+ : ctx.modelRegistry.getAvailable().map((model) => ({ model, thinkingLevel: undefined }));
336
+ const selected = boundedProjection(candidates, limit, ({ model, thinkingLevel }) => ({
337
+ provider: boundedPrivateText(model.provider, 256),
338
+ id: boundedPrivateText(model.id, 256),
339
+ name: boundedPrivateText(model.name, 256),
340
+ reasoning: model.reasoning,
341
+ input: [...model.input],
342
+ contextWindow: model.contextWindow,
343
+ maxTokens: model.maxTokens,
344
+ thinkingLevel,
345
+ current: ctx.model?.provider === model.provider && ctx.model?.id === model.id,
346
+ }));
347
+ return {
348
+ models: selected.items,
349
+ returned: selected.items.length,
350
+ omitted: selected.omitted,
351
+ source: scoped.length > 0 ? "session scope" : "available snapshot",
352
+ };
353
+ }
354
+
355
+ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown> {
356
+ const stateful = runtime.getRuntimeStatus();
357
+ const workflow = resolveDelegationWorkflow(runtime.getBlockingEnabled(), stateful.enabled);
358
+ const configured = inspectDelegationWorkflowSettings();
359
+ const resources = inspectConsultResourceSettings();
360
+ const cwdPolicy = inspectCwdPolicySettings();
361
+ const completion = inspectCompletionDeliverySettings();
362
+ return {
363
+ workflow,
364
+ configuredWorkflow: configured.value,
365
+ configuredWorkflowSource: configured.source,
366
+ stateful,
367
+ configuredCompletionDelivery: completion.value,
368
+ configuredCompletionDeliverySource: completion.source,
369
+ consultResources: runtime.getConsultResourcePolicy(),
370
+ consultationCwdPolicy: runtime.getConsultationCwdPolicy(),
371
+ configuredConsultationCwdPolicy: cwdPolicy.consultation.value,
372
+ consultationCwdPolicySource: cwdPolicy.consultation.source,
373
+ delegationCwdPolicy: runtime.getDelegationCwdPolicy(),
374
+ configuredDelegationCwdPolicy: cwdPolicy.delegation.value,
375
+ delegationCwdPolicySource: cwdPolicy.delegation.source,
376
+ configuredConsultResources: resources.value,
377
+ consultResourcesSource: resources.source,
378
+ settingsPath: safeDisplayPath(resources.path, process.cwd()),
379
+ settingsError:
380
+ configured.error || resources.error || cwdPolicy.error || completion.error
381
+ ? boundedPrivateText(
382
+ configured.error ?? resources.error ?? cwdPolicy.error ?? completion.error ?? "",
383
+ 2 * 1024,
384
+ )
385
+ : undefined,
386
+ };
387
+ }
388
+
389
+ function availableModelCount(ctx: ExtensionContext): number {
390
+ return (ctx.scopedModels?.length ?? 0) > 0
391
+ ? ctx.scopedModels.length
392
+ : ctx.modelRegistry.getAvailable().length;
393
+ }
394
+
395
+ function projectToolNames(tools: readonly string[]): string[] {
396
+ return tools.slice(0, 100).map((tool) => boundedPrivateText(tool, 256));
397
+ }
398
+
399
+ function boundedProjection<T, TProjected>(
400
+ values: readonly T[],
401
+ limit: number,
402
+ project: (value: T) => TProjected,
403
+ ): { items: TProjected[]; omitted: number } {
404
+ const items: TProjected[] = [];
405
+ for (const value of values.slice(0, limit)) {
406
+ const next = project(value);
407
+ if (Buffer.byteLength(JSON.stringify([...items, next]), "utf8") > MAX_DETAILS_LIST_BYTES) break;
408
+ items.push(next);
409
+ }
410
+ return { items, omitted: Math.max(0, values.length - items.length) };
411
+ }
412
+
413
+ function inspectResult(details: Record<string, unknown>): InspectToolResult {
414
+ const rendered = boundText(JSON.stringify(details, null, 2));
415
+ return {
416
+ content: [{ type: "text", text: rendered.text }],
417
+ details: { ...details, ...(rendered.truncated ? { truncated: true } : {}) },
418
+ };
419
+ }
420
+
421
+ function assertTrustedScope(scope: AgentScope, ctx: ExtensionContext): void {
422
+ if ((scope === "project" || scope === "both") && !ctx.isProjectTrusted()) {
423
+ throw new Error("Project-local subagent definitions require a trusted project");
424
+ }
425
+ }
426
+
427
+ function parameterRecord(params: unknown): Record<string, unknown> {
428
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
429
+ throw new Error("subagent_inspect parameters must be an object");
430
+ }
431
+ return params as Record<string, unknown>;
432
+ }
433
+
434
+ function optionalAgentScope(value: unknown): AgentScope {
435
+ if (value === undefined) return "user";
436
+ if (value === "user" || value === "project" || value === "both") return value;
437
+ throw new Error("subagent_inspect agentScope must be user, project, or both");
438
+ }
439
+
440
+ function requiredString(value: unknown, action: string, field: string): string {
441
+ if (typeof value !== "string" || value.length === 0) {
442
+ throw new Error(`subagent_inspect action "${action}" requires ${field}`);
443
+ }
444
+ return value;
445
+ }
446
+
447
+ function optionalLimit(value: unknown, defaultValue: number): number {
448
+ if (value === undefined) return defaultValue;
449
+ if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 100) {
450
+ throw new Error("subagent_inspect limit must be an integer between 1 and 100");
451
+ }
452
+ return value as number;
453
+ }
package/src/limits.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export const DEFAULT_MAX_OUTPUT_BYTES = 50 * 1024;
2
2
  export const DEFAULT_MAX_STDERR_BYTES = 16 * 1024;
3
3
  export const DEFAULT_MAX_CONTEXT_BYTES = 50 * 1024;
4
+ export const MAX_SUBAGENT_TIMEOUT_MS = 2_147_483_647;
4
5
  export const DEFAULT_MAX_MESSAGES = 200;
5
6
  export const TRUNCATION_MARKER = "\n… [truncated by pi-subagents]";
6
7
  export const TAIL_TRUNCATION_MARKER = "… [truncated by pi-subagents]\n";