@narumitw/pi-subagents 0.41.0 → 0.43.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/README.md +96 -26
- package/package.json +2 -2
- package/src/agents.ts +33 -8
- package/src/config-ui.ts +85 -21
- package/src/consult-policy.ts +15 -0
- package/src/consult.ts +688 -0
- package/src/execution.ts +8 -2
- package/src/inspect.ts +405 -0
- package/src/limits.ts +1 -0
- package/src/params.ts +2 -0
- package/src/registry.ts +75 -0
- package/src/runner.ts +160 -22
- package/src/safe-text.ts +67 -0
- package/src/settings.ts +88 -1
- package/src/stateful.ts +24 -12
- package/src/subagents.ts +43 -4
package/src/execution.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type SingleResult,
|
|
20
20
|
type SubagentDetails,
|
|
21
21
|
} from "./runner.js";
|
|
22
|
+
import { safeTerminalLine } from "./safe-text.js";
|
|
22
23
|
import { readSubagentSettings, resolveSubagentThinkingLevel } from "./settings.js";
|
|
23
24
|
|
|
24
25
|
const MAX_PARALLEL_TASKS = 8;
|
|
@@ -107,6 +108,9 @@ export async function executeSubagent(
|
|
|
107
108
|
): Promise<AgentToolResult<SubagentDetails> & { isError?: boolean }> {
|
|
108
109
|
assertSubagentDepthAllowed();
|
|
109
110
|
const agentScope: AgentScope = params.agentScope ?? "user";
|
|
111
|
+
if ((agentScope === "project" || agentScope === "both") && !ctx.isProjectTrusted()) {
|
|
112
|
+
throw new Error("Project-local subagent definitions require a trusted project");
|
|
113
|
+
}
|
|
110
114
|
const aggregator = hasUsableAggregator(params.aggregator) ? params.aggregator : undefined;
|
|
111
115
|
const config = readSubagentSettings();
|
|
112
116
|
const discovery = discoverAgents(ctx.cwd, agentScope, config);
|
|
@@ -168,8 +172,10 @@ export async function executeSubagent(
|
|
|
168
172
|
throw new Error("Project-local subagent definitions require a trusted project");
|
|
169
173
|
}
|
|
170
174
|
if (confirmProjectAgents && ctx.hasUI) {
|
|
171
|
-
const names = projectAgentsRequested
|
|
172
|
-
|
|
175
|
+
const names = projectAgentsRequested
|
|
176
|
+
.map((agent) => safeTerminalLine(agent.name, 256))
|
|
177
|
+
.join(", ");
|
|
178
|
+
const dir = safeTerminalLine(discovery.projectAgentsDir ?? "(unknown)");
|
|
173
179
|
const ok = await ctx.ui.confirm(
|
|
174
180
|
"Run project-local agents?",
|
|
175
181
|
`Agents: ${names}\nSource: ${dir}\n\nProject agents are repo-controlled. Only continue for trusted repositories.`,
|
package/src/inspect.ts
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
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 { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
6
|
+
import { resolveConsultTools } from "./consult-policy.js";
|
|
7
|
+
import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
|
|
8
|
+
import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
|
|
9
|
+
import {
|
|
10
|
+
inspectConsultResourceSettings,
|
|
11
|
+
inspectDelegationWorkflowSettings,
|
|
12
|
+
inspectSubagentSettings,
|
|
13
|
+
resolveDelegationWorkflow,
|
|
14
|
+
} from "./settings.js";
|
|
15
|
+
import type { StatefulSubagentRuntimeStatus } from "./stateful.js";
|
|
16
|
+
|
|
17
|
+
const INSPECT_ACTIONS = [
|
|
18
|
+
"list_agents",
|
|
19
|
+
"get_agent",
|
|
20
|
+
"list_runs",
|
|
21
|
+
"get_run",
|
|
22
|
+
"list_models",
|
|
23
|
+
"status",
|
|
24
|
+
"diagnose",
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
28
|
+
default: "user",
|
|
29
|
+
description: "Agent definition scope. Project scopes require a trusted project.",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const LimitSchema = Type.Number({ minimum: 1, maximum: 100, multipleOf: 1 });
|
|
33
|
+
const MAX_DETAILS_LIST_BYTES = 40 * 1024;
|
|
34
|
+
|
|
35
|
+
export const SubagentInspectParams = Type.Object(
|
|
36
|
+
{
|
|
37
|
+
action: StringEnum(INSPECT_ACTIONS),
|
|
38
|
+
agent: Type.Optional(Type.String({ minLength: 1 })),
|
|
39
|
+
agentId: Type.Optional(Type.String({ minLength: 1 })),
|
|
40
|
+
agentScope: Type.Optional(AgentScopeSchema),
|
|
41
|
+
limit: Type.Optional(LimitSchema),
|
|
42
|
+
includeClosed: Type.Optional(Type.Boolean({ default: false })),
|
|
43
|
+
},
|
|
44
|
+
{ additionalProperties: false },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
|
|
48
|
+
|
|
49
|
+
export interface SubagentInspectRuntime {
|
|
50
|
+
getBlockingEnabled(): boolean;
|
|
51
|
+
getConsultResourcePolicy(): "project-context" | "none" | "all";
|
|
52
|
+
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
53
|
+
listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
|
|
54
|
+
getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface InspectToolResult {
|
|
58
|
+
content: Array<{ type: "text"; text: string }>;
|
|
59
|
+
details: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type ValidatedInspectOperation =
|
|
63
|
+
| { action: "list_agents"; agentScope: AgentScope; limit: number }
|
|
64
|
+
| { action: "get_agent"; agent: string; agentScope: AgentScope }
|
|
65
|
+
| { action: "list_runs"; includeClosed: boolean; limit: number }
|
|
66
|
+
| { action: "get_run"; agentId: string }
|
|
67
|
+
| { action: "list_models"; limit: number }
|
|
68
|
+
| { action: "status" }
|
|
69
|
+
| { action: "diagnose" };
|
|
70
|
+
|
|
71
|
+
export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspectRuntime): void {
|
|
72
|
+
pi.registerTool({
|
|
73
|
+
name: "subagent_inspect",
|
|
74
|
+
label: "Inspect Subagents",
|
|
75
|
+
description:
|
|
76
|
+
"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.",
|
|
77
|
+
promptSnippet: "Inspect subagent metadata and runtime state without changing it",
|
|
78
|
+
parameters: SubagentInspectParams,
|
|
79
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
|
|
80
|
+
return executeSubagentInspect(validateInspectParams(params), ctx, runtime);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function validateInspectParams(params: unknown): ValidatedInspectOperation {
|
|
86
|
+
const values = parameterRecord(params);
|
|
87
|
+
const rawAction = values.action;
|
|
88
|
+
if (
|
|
89
|
+
typeof rawAction !== "string" ||
|
|
90
|
+
!INSPECT_ACTIONS.includes(rawAction as (typeof INSPECT_ACTIONS)[number])
|
|
91
|
+
) {
|
|
92
|
+
throw new Error(`subagent_inspect action must be one of: ${INSPECT_ACTIONS.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
const action = rawAction as (typeof INSPECT_ACTIONS)[number];
|
|
95
|
+
const allowed: Record<(typeof INSPECT_ACTIONS)[number], readonly string[]> = {
|
|
96
|
+
list_agents: ["action", "agentScope", "limit"],
|
|
97
|
+
get_agent: ["action", "agent", "agentScope"],
|
|
98
|
+
list_runs: ["action", "includeClosed", "limit"],
|
|
99
|
+
get_run: ["action", "agentId"],
|
|
100
|
+
list_models: ["action", "limit"],
|
|
101
|
+
status: ["action"],
|
|
102
|
+
diagnose: ["action"],
|
|
103
|
+
};
|
|
104
|
+
const unexpected = Object.keys(values).find(
|
|
105
|
+
(key) => values[key] !== undefined && !allowed[action].includes(key),
|
|
106
|
+
);
|
|
107
|
+
if (unexpected)
|
|
108
|
+
throw new Error(`subagent_inspect action "${action}" does not accept ${unexpected}`);
|
|
109
|
+
|
|
110
|
+
if (action === "list_agents" || action === "get_agent") {
|
|
111
|
+
const agentScope = optionalAgentScope(values.agentScope);
|
|
112
|
+
if (action === "get_agent") {
|
|
113
|
+
return { action, agent: requiredString(values.agent, action, "agent"), agentScope };
|
|
114
|
+
}
|
|
115
|
+
return { action, agentScope, limit: optionalLimit(values.limit, 32) };
|
|
116
|
+
}
|
|
117
|
+
if (action === "list_runs") {
|
|
118
|
+
if (values.includeClosed !== undefined && typeof values.includeClosed !== "boolean") {
|
|
119
|
+
throw new Error('subagent_inspect action "list_runs" requires includeClosed to be boolean');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
action,
|
|
123
|
+
includeClosed: values.includeClosed === true,
|
|
124
|
+
limit: optionalLimit(values.limit, 50),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (action === "get_run") {
|
|
128
|
+
return { action, agentId: requiredString(values.agentId, action, "agentId") };
|
|
129
|
+
}
|
|
130
|
+
if (action === "list_models") {
|
|
131
|
+
return { action, limit: optionalLimit(values.limit, 50) };
|
|
132
|
+
}
|
|
133
|
+
return { action };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function executeSubagentInspect(
|
|
137
|
+
operation: ValidatedInspectOperation,
|
|
138
|
+
ctx: ExtensionContext,
|
|
139
|
+
runtime: SubagentInspectRuntime,
|
|
140
|
+
): Promise<InspectToolResult> {
|
|
141
|
+
if (operation.action === "list_agents" || operation.action === "get_agent") {
|
|
142
|
+
assertTrustedScope(operation.agentScope, ctx);
|
|
143
|
+
const settings = inspectSubagentSettings().settings;
|
|
144
|
+
const discovery = discoverAgents(ctx.cwd, operation.agentScope, settings);
|
|
145
|
+
const agents = [...discovery.agents].sort((left, right) =>
|
|
146
|
+
left.name === right.name
|
|
147
|
+
? left.source.localeCompare(right.source)
|
|
148
|
+
: left.name.localeCompare(right.name),
|
|
149
|
+
);
|
|
150
|
+
if (operation.action === "list_agents") {
|
|
151
|
+
const selected = boundedProjection(agents, operation.limit, (agent) =>
|
|
152
|
+
projectAgent(agent, ctx, false),
|
|
153
|
+
);
|
|
154
|
+
return inspectResult({
|
|
155
|
+
action: operation.action,
|
|
156
|
+
agents: selected.items,
|
|
157
|
+
returned: selected.items.length,
|
|
158
|
+
omitted: selected.omitted + (discovery.omittedAgentDefinitions ?? 0),
|
|
159
|
+
discoveryIncomplete: discovery.metadataDiscoveryIncomplete === true,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const agent = agents.find((candidate) => candidate.name === operation.agent);
|
|
163
|
+
if (!agent) {
|
|
164
|
+
throw new Error(`Unknown subagent definition: ${boundedPrivateText(operation.agent, 256)}`);
|
|
165
|
+
}
|
|
166
|
+
return inspectResult({ action: operation.action, agent: projectAgent(agent, ctx) });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (operation.action === "list_runs") {
|
|
170
|
+
const runs = runtime.listRunInspection(operation.includeClosed);
|
|
171
|
+
const selected = boundedProjection(runs, operation.limit, projectRunSummary);
|
|
172
|
+
return inspectResult({
|
|
173
|
+
action: operation.action,
|
|
174
|
+
runs: selected.items,
|
|
175
|
+
returned: selected.items.length,
|
|
176
|
+
omitted: selected.omitted,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (operation.action === "get_run") {
|
|
180
|
+
const run = runtime.getRunInspection(operation.agentId);
|
|
181
|
+
if (!run) {
|
|
182
|
+
throw new Error(`Unknown retained run: ${boundedPrivateText(operation.agentId, 256)}`);
|
|
183
|
+
}
|
|
184
|
+
return inspectResult({ action: operation.action, run: projectRun(run, ctx) });
|
|
185
|
+
}
|
|
186
|
+
if (operation.action === "list_models") {
|
|
187
|
+
return inspectResult({ action: operation.action, ...projectModels(ctx, operation.limit) });
|
|
188
|
+
}
|
|
189
|
+
if (operation.action === "status") {
|
|
190
|
+
return inspectResult({ action: operation.action, status: projectStatus(runtime) });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const settings = inspectSubagentSettings();
|
|
194
|
+
const userDiscovery = discoverAgents(ctx.cwd, "user", settings.settings);
|
|
195
|
+
const modelCount = availableModelCount(ctx);
|
|
196
|
+
const runtimeStatus = runtime.getRuntimeStatus();
|
|
197
|
+
const checks = [
|
|
198
|
+
{
|
|
199
|
+
name: "settings",
|
|
200
|
+
status: settings.error ? "fail" : "pass",
|
|
201
|
+
message: settings.error
|
|
202
|
+
? boundedPrivateText(settings.error, 2 * 1024)
|
|
203
|
+
: "Settings are valid or absent.",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "agent-discovery",
|
|
207
|
+
status:
|
|
208
|
+
userDiscovery.metadataDiscoveryIncomplete ||
|
|
209
|
+
(userDiscovery.omittedAgentDefinitions ?? 0) > 0
|
|
210
|
+
? "warning"
|
|
211
|
+
: "pass",
|
|
212
|
+
message: `${userDiscovery.agents.length} user-scope definitions available.`,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "models",
|
|
216
|
+
status: modelCount > 0 ? "pass" : "fail",
|
|
217
|
+
message: `${modelCount} session-usable models available.`,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "runtime",
|
|
221
|
+
status: runtimeStatus.enabled && !runtimeStatus.initialized ? "warning" : "pass",
|
|
222
|
+
message: runtimeStatus.initialized
|
|
223
|
+
? "Stateful runtime initialized."
|
|
224
|
+
: "Stateful runtime not initialized.",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "consultation",
|
|
228
|
+
status: runtime.getBlockingEnabled() && modelCount > 0 ? "pass" : "fail",
|
|
229
|
+
message: runtime.getBlockingEnabled()
|
|
230
|
+
? modelCount > 0
|
|
231
|
+
? "Read-only consultation is supported."
|
|
232
|
+
: "Consultation has no available model."
|
|
233
|
+
: "Blocking delegation is disabled, so consultation is not registered.",
|
|
234
|
+
},
|
|
235
|
+
] as const;
|
|
236
|
+
return inspectResult({
|
|
237
|
+
action: operation.action,
|
|
238
|
+
checks,
|
|
239
|
+
ok: checks.every((check) => check.status !== "fail"),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function projectAgent(
|
|
244
|
+
agent: AgentConfig,
|
|
245
|
+
ctx: ExtensionContext,
|
|
246
|
+
includeTools = true,
|
|
247
|
+
): Record<string, unknown> {
|
|
248
|
+
const tools = agent.tools === undefined ? undefined : projectToolNames(agent.tools);
|
|
249
|
+
return {
|
|
250
|
+
name: boundedPrivateText(agent.name, 256),
|
|
251
|
+
description: boundedPrivateText(agent.description, 256),
|
|
252
|
+
source: agent.source,
|
|
253
|
+
scope: agent.source === "project" ? "project" : "user",
|
|
254
|
+
path:
|
|
255
|
+
agent.source === "project"
|
|
256
|
+
? safeTerminalLine(path.posix.join(".pi", "agents", path.basename(agent.filePath)))
|
|
257
|
+
: safeDisplayPath(agent.filePath, ctx.cwd),
|
|
258
|
+
model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
|
|
259
|
+
thinkingLevel: agent.thinkingLevel,
|
|
260
|
+
...(includeTools
|
|
261
|
+
? { tools, toolCount: agent.tools?.length }
|
|
262
|
+
: { toolCount: agent.tools?.length }),
|
|
263
|
+
consultTools: resolveConsultTools(agent.tools),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function projectRunSummary(run: AgentRunInspectionSummary): Record<string, unknown> {
|
|
268
|
+
return {
|
|
269
|
+
id: boundedPrivateText(run.id, 256),
|
|
270
|
+
agent: boundedPrivateText(run.agent, 256),
|
|
271
|
+
state: run.state,
|
|
272
|
+
createdAt: run.createdAt,
|
|
273
|
+
updatedAt: run.updatedAt,
|
|
274
|
+
historyCount: run.historyCount,
|
|
275
|
+
unreadMessages: run.unreadMessages,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Record<string, unknown> {
|
|
280
|
+
return {
|
|
281
|
+
...projectRunSummary(run),
|
|
282
|
+
cwd: safeDisplayPath(run.cwd, ctx.cwd),
|
|
283
|
+
thinkingLevel: run.thinkingLevel,
|
|
284
|
+
currentTask: run.currentTask ? boundedPrivateText(run.currentTask, 2 * 1024) : undefined,
|
|
285
|
+
error: run.error ? boundedPrivateText(run.error, 2 * 1024) : undefined,
|
|
286
|
+
policy: run.policy
|
|
287
|
+
? {
|
|
288
|
+
inherited: projectToolNames(run.policy.inherited),
|
|
289
|
+
overridden: projectToolNames(run.policy.overridden),
|
|
290
|
+
unsupported: projectToolNames(run.policy.unsupported),
|
|
291
|
+
}
|
|
292
|
+
: undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function projectModels(ctx: ExtensionContext, limit: number): Record<string, unknown> {
|
|
297
|
+
const scoped = ctx.scopedModels ?? [];
|
|
298
|
+
const candidates =
|
|
299
|
+
scoped.length > 0
|
|
300
|
+
? scoped
|
|
301
|
+
: ctx.modelRegistry.getAvailable().map((model) => ({ model, thinkingLevel: undefined }));
|
|
302
|
+
const selected = boundedProjection(candidates, limit, ({ model, thinkingLevel }) => ({
|
|
303
|
+
provider: boundedPrivateText(model.provider, 256),
|
|
304
|
+
id: boundedPrivateText(model.id, 256),
|
|
305
|
+
name: boundedPrivateText(model.name, 256),
|
|
306
|
+
reasoning: model.reasoning,
|
|
307
|
+
input: [...model.input],
|
|
308
|
+
contextWindow: model.contextWindow,
|
|
309
|
+
maxTokens: model.maxTokens,
|
|
310
|
+
thinkingLevel,
|
|
311
|
+
current: ctx.model?.provider === model.provider && ctx.model?.id === model.id,
|
|
312
|
+
}));
|
|
313
|
+
return {
|
|
314
|
+
models: selected.items,
|
|
315
|
+
returned: selected.items.length,
|
|
316
|
+
omitted: selected.omitted,
|
|
317
|
+
source: scoped.length > 0 ? "session scope" : "available snapshot",
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown> {
|
|
322
|
+
const stateful = runtime.getRuntimeStatus();
|
|
323
|
+
const workflow = resolveDelegationWorkflow(runtime.getBlockingEnabled(), stateful.enabled);
|
|
324
|
+
const configured = inspectDelegationWorkflowSettings();
|
|
325
|
+
const resources = inspectConsultResourceSettings();
|
|
326
|
+
return {
|
|
327
|
+
workflow,
|
|
328
|
+
configuredWorkflow: configured.value,
|
|
329
|
+
stateful,
|
|
330
|
+
consultResources: runtime.getConsultResourcePolicy(),
|
|
331
|
+
configuredConsultResources: resources.value,
|
|
332
|
+
consultResourcesSource: resources.source,
|
|
333
|
+
settingsPath: safeDisplayPath(resources.path, process.cwd()),
|
|
334
|
+
settingsError:
|
|
335
|
+
configured.error || resources.error
|
|
336
|
+
? boundedPrivateText(configured.error ?? resources.error ?? "", 2 * 1024)
|
|
337
|
+
: undefined,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function availableModelCount(ctx: ExtensionContext): number {
|
|
342
|
+
return (ctx.scopedModels?.length ?? 0) > 0
|
|
343
|
+
? ctx.scopedModels.length
|
|
344
|
+
: ctx.modelRegistry.getAvailable().length;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function projectToolNames(tools: readonly string[]): string[] {
|
|
348
|
+
return tools.slice(0, 100).map((tool) => boundedPrivateText(tool, 256));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function boundedProjection<T, TProjected>(
|
|
352
|
+
values: readonly T[],
|
|
353
|
+
limit: number,
|
|
354
|
+
project: (value: T) => TProjected,
|
|
355
|
+
): { items: TProjected[]; omitted: number } {
|
|
356
|
+
const items: TProjected[] = [];
|
|
357
|
+
for (const value of values.slice(0, limit)) {
|
|
358
|
+
const next = project(value);
|
|
359
|
+
if (Buffer.byteLength(JSON.stringify([...items, next]), "utf8") > MAX_DETAILS_LIST_BYTES) break;
|
|
360
|
+
items.push(next);
|
|
361
|
+
}
|
|
362
|
+
return { items, omitted: Math.max(0, values.length - items.length) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function inspectResult(details: Record<string, unknown>): InspectToolResult {
|
|
366
|
+
const rendered = boundText(JSON.stringify(details, null, 2));
|
|
367
|
+
return {
|
|
368
|
+
content: [{ type: "text", text: rendered.text }],
|
|
369
|
+
details: { ...details, ...(rendered.truncated ? { truncated: true } : {}) },
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function assertTrustedScope(scope: AgentScope, ctx: ExtensionContext): void {
|
|
374
|
+
if ((scope === "project" || scope === "both") && !ctx.isProjectTrusted()) {
|
|
375
|
+
throw new Error("Project-local subagent definitions require a trusted project");
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function parameterRecord(params: unknown): Record<string, unknown> {
|
|
380
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
381
|
+
throw new Error("subagent_inspect parameters must be an object");
|
|
382
|
+
}
|
|
383
|
+
return params as Record<string, unknown>;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function optionalAgentScope(value: unknown): AgentScope {
|
|
387
|
+
if (value === undefined) return "user";
|
|
388
|
+
if (value === "user" || value === "project" || value === "both") return value;
|
|
389
|
+
throw new Error("subagent_inspect agentScope must be user, project, or both");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function requiredString(value: unknown, action: string, field: string): string {
|
|
393
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
394
|
+
throw new Error(`subagent_inspect action "${action}" requires ${field}`);
|
|
395
|
+
}
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function optionalLimit(value: unknown, defaultValue: number): number {
|
|
400
|
+
if (value === undefined) return defaultValue;
|
|
401
|
+
if (!Number.isSafeInteger(value) || (value as number) < 1 || (value as number) > 100) {
|
|
402
|
+
throw new Error("subagent_inspect limit must be an integer between 1 and 100");
|
|
403
|
+
}
|
|
404
|
+
return value as number;
|
|
405
|
+
}
|
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";
|
package/src/params.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
2
|
import { type Static, Type } from "typebox";
|
|
3
3
|
import { THINKING_LEVELS } from "./agents.js";
|
|
4
|
+
import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
|
|
4
5
|
|
|
5
6
|
const TimeoutMs = Type.Number({
|
|
6
7
|
description:
|
|
7
8
|
"Hard timeout in milliseconds for each subagent subprocess. Defaults to PI_SUBAGENT_TIMEOUT_MS or 600000.",
|
|
8
9
|
minimum: 1,
|
|
10
|
+
maximum: MAX_SUBAGENT_TIMEOUT_MS,
|
|
9
11
|
});
|
|
10
12
|
|
|
11
13
|
const ThinkingLevelSchema = StringEnum(THINKING_LEVELS, {
|
package/src/registry.ts
CHANGED
|
@@ -55,6 +55,29 @@ export interface ManagedAgent {
|
|
|
55
55
|
currentMailboxMessageIds?: string[];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export interface AgentRunInspectionSummary {
|
|
59
|
+
id: string;
|
|
60
|
+
agent: string;
|
|
61
|
+
state: AgentLifecycleState;
|
|
62
|
+
createdAt: number;
|
|
63
|
+
updatedAt: number;
|
|
64
|
+
historyCount: number;
|
|
65
|
+
unreadMessages: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface AgentRunInspectionDetail extends AgentRunInspectionSummary {
|
|
69
|
+
cwd: string;
|
|
70
|
+
thinkingLevel?: SubagentThinkingLevel;
|
|
71
|
+
currentTask?: string;
|
|
72
|
+
error?: string;
|
|
73
|
+
policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface AgentInspectionCounts {
|
|
77
|
+
activeAgents: number;
|
|
78
|
+
retainedAgents: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
58
81
|
export interface TurnOutcome {
|
|
59
82
|
output: string;
|
|
60
83
|
exitCode: number;
|
|
@@ -500,6 +523,42 @@ export class AgentRegistry {
|
|
|
500
523
|
if (shutdownError) throw shutdownError;
|
|
501
524
|
}
|
|
502
525
|
|
|
526
|
+
inspectionCounts(): AgentInspectionCounts {
|
|
527
|
+
let activeAgents = 0;
|
|
528
|
+
let retainedAgents = 0;
|
|
529
|
+
for (const agent of this.agents.values()) {
|
|
530
|
+
if (agent.state === "starting" || agent.state === "running") activeAgents++;
|
|
531
|
+
if (agent.state !== "closed") retainedAgents++;
|
|
532
|
+
}
|
|
533
|
+
return { activeAgents, retainedAgents };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
listInspection(includeClosed = false): AgentRunInspectionSummary[] {
|
|
537
|
+
return [...this.agents.values()]
|
|
538
|
+
.filter((agent) => includeClosed || agent.state !== "closed")
|
|
539
|
+
.sort((left, right) => left.createdAt - right.createdAt)
|
|
540
|
+
.map((agent) => this.inspectSummary(agent));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
getInspection(id: string): AgentRunInspectionDetail | undefined {
|
|
544
|
+
const agent = this.agents.get(id);
|
|
545
|
+
if (!agent) return undefined;
|
|
546
|
+
return {
|
|
547
|
+
...this.inspectSummary(agent),
|
|
548
|
+
cwd: agent.cwd,
|
|
549
|
+
thinkingLevel: agent.thinkingLevel,
|
|
550
|
+
currentTask: agent.currentTask,
|
|
551
|
+
error: agent.error,
|
|
552
|
+
policy: agent.policy
|
|
553
|
+
? {
|
|
554
|
+
inherited: [...agent.policy.inherited],
|
|
555
|
+
overridden: [...agent.policy.overridden],
|
|
556
|
+
unsupported: [...agent.policy.unsupported],
|
|
557
|
+
}
|
|
558
|
+
: undefined,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
503
562
|
list(includeClosed = false, rootId?: string): ManagedAgent[] {
|
|
504
563
|
return [...this.agents.values()]
|
|
505
564
|
.filter((agent) => !rootId || agent.rootId === rootId)
|
|
@@ -755,6 +814,22 @@ export class AgentRegistry {
|
|
|
755
814
|
return next;
|
|
756
815
|
}
|
|
757
816
|
|
|
817
|
+
private inspectSummary(agent: ManagedAgent): AgentRunInspectionSummary {
|
|
818
|
+
let unreadMessages = 0;
|
|
819
|
+
for (const message of agent.mailbox) {
|
|
820
|
+
if (message.readAt === undefined) unreadMessages++;
|
|
821
|
+
}
|
|
822
|
+
return {
|
|
823
|
+
id: agent.id,
|
|
824
|
+
agent: agent.agent,
|
|
825
|
+
state: agent.state,
|
|
826
|
+
createdAt: agent.createdAt,
|
|
827
|
+
updatedAt: agent.updatedAt,
|
|
828
|
+
historyCount: agent.history.length,
|
|
829
|
+
unreadMessages,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
758
833
|
private copy(agent: ManagedAgent): ManagedAgent {
|
|
759
834
|
return {
|
|
760
835
|
...agent,
|