@narumitw/pi-subagents 0.43.0 → 0.46.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 +53 -13
- package/package.json +1 -1
- package/src/agents.ts +16 -0
- package/src/config-ui.ts +151 -20
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +164 -37
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +127 -64
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +51 -3
- package/src/persistence.ts +29 -0
- package/src/pi-invocation.ts +168 -0
- package/src/registry.ts +12 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +19 -22
- package/src/settings.ts +111 -11
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +235 -218
- package/src/subagents.ts +60 -18
- package/src/subprocess-transport.ts +19 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import {
|
|
3
|
+
getMarkdownTheme,
|
|
4
|
+
type Theme,
|
|
5
|
+
type ToolRenderResultOptions,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { ConsultDetails, SubagentConsultParams } from "./consult.js";
|
|
9
|
+
import { formatUsageStats } from "./render.js";
|
|
10
|
+
import {
|
|
11
|
+
COLLAPSED_ANSWER_LINES,
|
|
12
|
+
COLLAPSED_LIST_LIMIT,
|
|
13
|
+
expansionHint,
|
|
14
|
+
previewLines,
|
|
15
|
+
projectRenderActivity,
|
|
16
|
+
type RenderStatus,
|
|
17
|
+
recordValue,
|
|
18
|
+
renderActivityLines,
|
|
19
|
+
renderFallbackResult,
|
|
20
|
+
safeBlock,
|
|
21
|
+
safeLine,
|
|
22
|
+
statusBadge,
|
|
23
|
+
stringValue,
|
|
24
|
+
type ToolRendererContext,
|
|
25
|
+
textResult,
|
|
26
|
+
toolHeader,
|
|
27
|
+
} from "./render-common.js";
|
|
28
|
+
|
|
29
|
+
export function renderConsultCall(args: Partial<SubagentConsultParams>, theme: Theme) {
|
|
30
|
+
const scope = args.agentScope ?? "user";
|
|
31
|
+
const text = [
|
|
32
|
+
toolHeader(theme, "subagent_consult", args.agent, [`[${scope}]`, "read-only"]),
|
|
33
|
+
` ${theme.fg("dim", safeLine(args.task, "...", 2 * 1024))}`,
|
|
34
|
+
].join("\n");
|
|
35
|
+
return new Text(text, 0, 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderConsultResult(
|
|
39
|
+
result: AgentToolResult<ConsultDetails>,
|
|
40
|
+
options: ToolRenderResultOptions,
|
|
41
|
+
theme: Theme,
|
|
42
|
+
context: ToolRendererContext<SubagentConsultParams>,
|
|
43
|
+
) {
|
|
44
|
+
const details = recordValue(result.details);
|
|
45
|
+
if (!details || !recordValue(details.policy)) {
|
|
46
|
+
return renderFallbackResult(result, options, theme, context.isError);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const progress = recordValue(details.progress);
|
|
50
|
+
const child = recordValue(details.child);
|
|
51
|
+
const status = consultStatus(details, progress, options, context.isError);
|
|
52
|
+
const agent = safeLine(details.agent, safeLine(context.args?.agent, "subagent"), 256);
|
|
53
|
+
const source = safeLine(details.agentSource, "unknown", 128);
|
|
54
|
+
const usage = recordValue(progress?.usage ?? child?.usage);
|
|
55
|
+
const usageText = formatUnknownUsage(
|
|
56
|
+
usage,
|
|
57
|
+
stringValue(details.model) || undefined,
|
|
58
|
+
stringValue(details.thinkingLevel) || undefined,
|
|
59
|
+
stringValue(progress?.actualProvider ?? child?.actualProvider) || undefined,
|
|
60
|
+
stringValue(progress?.actualModel ?? child?.actualModel) || undefined,
|
|
61
|
+
);
|
|
62
|
+
const effectiveTools = stringList(recordValue(details.policy)?.effectiveTools);
|
|
63
|
+
const activity = projectRenderActivity(progress?.recentActivity);
|
|
64
|
+
const activityTotal = Math.max(
|
|
65
|
+
activity.length,
|
|
66
|
+
typeof progress?.recentActivityTotal === "number" ? progress.recentActivityTotal : 0,
|
|
67
|
+
);
|
|
68
|
+
const answer = safeBlock(textResult(result), "", 50 * 1024).trim();
|
|
69
|
+
|
|
70
|
+
if (options.expanded) {
|
|
71
|
+
const container = new Container();
|
|
72
|
+
container.addChild(
|
|
73
|
+
new Text(
|
|
74
|
+
`${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
|
|
75
|
+
0,
|
|
76
|
+
0,
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
if (usageText) container.addChild(new Text(theme.fg("dim", usageText), 0, 0));
|
|
80
|
+
container.addChild(new Spacer(1));
|
|
81
|
+
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
82
|
+
container.addChild(
|
|
83
|
+
new Text(theme.fg("dim", safeBlock(context.args?.task, "(unavailable)", 50 * 1024)), 0, 0),
|
|
84
|
+
);
|
|
85
|
+
container.addChild(new Spacer(1));
|
|
86
|
+
container.addChild(new Text(theme.fg("muted", "─── Policy ───"), 0, 0));
|
|
87
|
+
container.addChild(new Text(theme.fg("dim", policySummary(details.policy)), 0, 0));
|
|
88
|
+
if (activity.length > 0) {
|
|
89
|
+
container.addChild(new Spacer(1));
|
|
90
|
+
container.addChild(new Text(theme.fg("muted", "─── Activity ───"), 0, 0));
|
|
91
|
+
container.addChild(
|
|
92
|
+
new Text(renderActivityLines(activity, theme, undefined, activityTotal), 0, 0),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
container.addChild(new Spacer(1));
|
|
96
|
+
container.addChild(new Text(theme.fg("muted", "─── Answer ───"), 0, 0));
|
|
97
|
+
if (answer) container.addChild(new Markdown(answer, 0, 0, getMarkdownTheme()));
|
|
98
|
+
else
|
|
99
|
+
container.addChild(
|
|
100
|
+
new Text(
|
|
101
|
+
theme.fg("muted", options.isPartial ? "(waiting for output)" : "(no output)"),
|
|
102
|
+
0,
|
|
103
|
+
0,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
return container;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const lines = [
|
|
110
|
+
`${statusBadge(theme, status)} · ${theme.fg("toolTitle", theme.bold(agent))}${theme.fg("muted", ` (${source})`)}`,
|
|
111
|
+
];
|
|
112
|
+
if (usageText) lines.push(theme.fg("dim", usageText));
|
|
113
|
+
if (effectiveTools.length > 0 && options.isPartial) {
|
|
114
|
+
lines.push(theme.fg("dim", `tools: ${effectiveTools.join(", ")}`));
|
|
115
|
+
}
|
|
116
|
+
if (activity.length > 0) {
|
|
117
|
+
lines.push(renderActivityLines(activity, theme, COLLAPSED_LIST_LIMIT, activityTotal));
|
|
118
|
+
} else if (options.isPartial) {
|
|
119
|
+
lines.push(
|
|
120
|
+
theme.fg("muted", status === "starting" ? "(starting child)" : "(waiting for activity)"),
|
|
121
|
+
);
|
|
122
|
+
} else if (answer) {
|
|
123
|
+
lines.push(theme.fg("toolOutput", previewLines(answer, COLLAPSED_ANSWER_LINES)));
|
|
124
|
+
} else {
|
|
125
|
+
const error = safeBlock(child?.error, "", 2 * 1024).trim();
|
|
126
|
+
lines.push(theme.fg(status === "failed" ? "error" : "muted", error || "(no output)"));
|
|
127
|
+
}
|
|
128
|
+
if (!options.isPartial) lines.push(expansionHint());
|
|
129
|
+
return new Text(lines.filter(Boolean).join("\n"), 0, 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function consultStatus(
|
|
133
|
+
details: Record<string, unknown>,
|
|
134
|
+
progress: Record<string, unknown> | undefined,
|
|
135
|
+
options: ToolRenderResultOptions,
|
|
136
|
+
isError: boolean,
|
|
137
|
+
): RenderStatus {
|
|
138
|
+
if (details.cancelled === true) return "cancelled";
|
|
139
|
+
if (isError || details.isError === true) {
|
|
140
|
+
const child = recordValue(details.child);
|
|
141
|
+
return child?.aborted === true && child?.timedOut !== true ? "cancelled" : "failed";
|
|
142
|
+
}
|
|
143
|
+
if (options.isPartial) return progress?.phase === "starting" ? "starting" : "running";
|
|
144
|
+
return "completed";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stringList(value: unknown): string[] {
|
|
148
|
+
return Array.isArray(value)
|
|
149
|
+
? value.flatMap((item) => (typeof item === "string" ? [safeLine(item, "", 256)] : []))
|
|
150
|
+
: [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function policySummary(value: unknown): string {
|
|
154
|
+
const policy = recordValue(value);
|
|
155
|
+
if (!policy) return "(unavailable)";
|
|
156
|
+
const resources = recordValue(policy.effectiveResources);
|
|
157
|
+
const tools = stringList(policy.effectiveTools);
|
|
158
|
+
return [
|
|
159
|
+
`tools: ${tools.length > 0 ? tools.join(", ") : "none"}`,
|
|
160
|
+
`resources: ${safeLine(resources?.policy ?? policy.requestedResources, "unknown", 256)}`,
|
|
161
|
+
`extensions: ${safeLine(policy.extensions, "unknown", 128)}`,
|
|
162
|
+
`session persistence: ${safeLine(policy.sessionPersistence, "unknown", 128)}`,
|
|
163
|
+
`retained agent: ${policy.retainedAgent === false ? "no" : "unknown"}`,
|
|
164
|
+
].join("\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function formatUnknownUsage(
|
|
168
|
+
value: Record<string, unknown> | undefined,
|
|
169
|
+
model?: string,
|
|
170
|
+
thinkingLevel?: string,
|
|
171
|
+
actualProvider?: string,
|
|
172
|
+
actualModel?: string,
|
|
173
|
+
): string {
|
|
174
|
+
const usage = value ?? {};
|
|
175
|
+
return formatUsageStats(
|
|
176
|
+
{
|
|
177
|
+
input: safeNumber(usage.input),
|
|
178
|
+
output: safeNumber(usage.output),
|
|
179
|
+
cacheRead: safeNumber(usage.cacheRead),
|
|
180
|
+
cacheWrite: safeNumber(usage.cacheWrite),
|
|
181
|
+
cost: safeNumber(usage.cost),
|
|
182
|
+
contextTokens: safeNumber(usage.contextTokens),
|
|
183
|
+
turns: safeNumber(usage.turns),
|
|
184
|
+
},
|
|
185
|
+
model,
|
|
186
|
+
thinkingLevel as never,
|
|
187
|
+
actualProvider,
|
|
188
|
+
actualModel,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function safeNumber(value: unknown): number {
|
|
193
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
194
|
+
}
|
package/src/consult.ts
CHANGED
|
@@ -21,6 +21,12 @@ import {
|
|
|
21
21
|
THINKING_LEVELS,
|
|
22
22
|
} from "./agents.js";
|
|
23
23
|
import { resolveConsultTools } from "./consult-policy.js";
|
|
24
|
+
import { renderConsultCall, renderConsultResult } from "./consult-render.js";
|
|
25
|
+
import {
|
|
26
|
+
assertConsultationTargetAllowed,
|
|
27
|
+
type ResolvedSubagentTarget,
|
|
28
|
+
resolveSubagentTarget,
|
|
29
|
+
} from "./cwd-policy.js";
|
|
24
30
|
import { assertSubagentDepthAllowed, resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
25
31
|
import {
|
|
26
32
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
@@ -37,7 +43,11 @@ import {
|
|
|
37
43
|
type SubagentDetails,
|
|
38
44
|
} from "./runner.js";
|
|
39
45
|
import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
|
|
40
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
48
|
+
DEFAULT_CONSULTATION_CWD_POLICY,
|
|
49
|
+
resolveSubagentThinkingLevel,
|
|
50
|
+
} from "./settings.js";
|
|
41
51
|
|
|
42
52
|
const ConsultScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
43
53
|
default: "user",
|
|
@@ -80,6 +90,30 @@ export interface RegisterSubagentConsultOptions {
|
|
|
80
90
|
invocationOverride?: { command: string; argsPrefix?: string[] };
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
export interface ConsultProgressActivity {
|
|
94
|
+
type: "text" | "toolCall";
|
|
95
|
+
text?: string;
|
|
96
|
+
name?: "read" | "grep" | "find" | "ls";
|
|
97
|
+
args?: Record<string, string | number | boolean>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ConsultProgress {
|
|
101
|
+
phase: "starting" | "running";
|
|
102
|
+
recentActivity: ConsultProgressActivity[];
|
|
103
|
+
recentActivityTotal: number;
|
|
104
|
+
actualProvider?: string;
|
|
105
|
+
actualModel?: string;
|
|
106
|
+
usage: {
|
|
107
|
+
input: number;
|
|
108
|
+
output: number;
|
|
109
|
+
cacheRead: number;
|
|
110
|
+
cacheWrite: number;
|
|
111
|
+
cost: number;
|
|
112
|
+
contextTokens: number;
|
|
113
|
+
turns: number;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
83
117
|
export interface ConsultDetails {
|
|
84
118
|
agent: string;
|
|
85
119
|
agentSource: string;
|
|
@@ -91,6 +125,13 @@ export interface ConsultDetails {
|
|
|
91
125
|
policy: {
|
|
92
126
|
requestedTools: string[] | null;
|
|
93
127
|
effectiveTools: string[];
|
|
128
|
+
cwdBoundary: "current-workspace" | "external";
|
|
129
|
+
targetTrust: {
|
|
130
|
+
kind: string;
|
|
131
|
+
projectTrusted: boolean;
|
|
132
|
+
sourcePath?: string;
|
|
133
|
+
warning?: string;
|
|
134
|
+
};
|
|
94
135
|
requestedResources: ConsultResourcePolicy;
|
|
95
136
|
effectiveResources: {
|
|
96
137
|
policy: ConsultResourcePolicy;
|
|
@@ -99,11 +140,13 @@ export interface ConsultDetails {
|
|
|
99
140
|
skills: boolean;
|
|
100
141
|
promptTemplates: boolean;
|
|
101
142
|
};
|
|
143
|
+
resourceDowngradeReason?: string;
|
|
102
144
|
extensions: "disabled";
|
|
103
145
|
sessionPersistence: "disabled";
|
|
104
146
|
retainedAgent: false;
|
|
105
147
|
};
|
|
106
148
|
child?: Record<string, unknown>;
|
|
149
|
+
progress?: ConsultProgress;
|
|
107
150
|
cancelled?: boolean;
|
|
108
151
|
isError?: boolean;
|
|
109
152
|
truncated?: boolean;
|
|
@@ -143,12 +186,12 @@ export function registerSubagentConsult(
|
|
|
143
186
|
cancelAndWaitForChildren("Subagent consultation session shut down"),
|
|
144
187
|
);
|
|
145
188
|
|
|
146
|
-
const baseDescription =
|
|
147
|
-
|
|
189
|
+
const baseDescription = () =>
|
|
190
|
+
`Run one ephemeral subagent synchronously under enforced read-only tool and resource policies and return its answer. The child can use only the effective subset of Pi's built-in read, grep, find, and ls tools. Shell commands, file writes, extension tools, detached lifecycle operations, and persistent agent state are disabled. Working-directory target policy: ${options.getSettings()?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY}; configured trusted-target resources: ${options.getSettings()?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY}; allowed targets without effective trust inherit no target/project resources. This is not a filesystem sandbox.`;
|
|
148
191
|
const definition: ToolDefinition<typeof SubagentConsultParams, ConsultDetails> = {
|
|
149
192
|
name: "subagent_consult",
|
|
150
193
|
label: "Consult Read-only Subagent",
|
|
151
|
-
description: baseDescription,
|
|
194
|
+
description: baseDescription(),
|
|
152
195
|
promptSnippet: "Consult one constrained read-only subagent and wait for its answer",
|
|
153
196
|
promptGuidelines: [
|
|
154
197
|
"Use subagent_consult for bounded reconnaissance, planning, or review whose result is required in the current turn.",
|
|
@@ -187,6 +230,12 @@ export function registerSubagentConsult(
|
|
|
187
230
|
active.delete(ownedController);
|
|
188
231
|
}
|
|
189
232
|
},
|
|
233
|
+
renderCall(args, theme) {
|
|
234
|
+
return renderConsultCall(args, theme);
|
|
235
|
+
},
|
|
236
|
+
renderResult(result, renderOptions, theme, context) {
|
|
237
|
+
return renderConsultResult(result, renderOptions, theme, context);
|
|
238
|
+
},
|
|
190
239
|
};
|
|
191
240
|
pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
|
|
192
241
|
pi.on("tool_result", (event) => {
|
|
@@ -194,7 +243,7 @@ export function registerSubagentConsult(
|
|
|
194
243
|
if ((event.details as ConsultDetails | undefined)?.isError) return { isError: true };
|
|
195
244
|
});
|
|
196
245
|
return (catalog: string) => {
|
|
197
|
-
definition.description = catalog ? `${baseDescription}\n\n${catalog}` : baseDescription;
|
|
246
|
+
definition.description = catalog ? `${baseDescription()}\n\n${catalog}` : baseDescription();
|
|
198
247
|
pi.registerTool<typeof SubagentConsultParams, ConsultDetails>(definition);
|
|
199
248
|
};
|
|
200
249
|
}
|
|
@@ -294,6 +343,15 @@ async function executeConsult(
|
|
|
294
343
|
throw new Error("Project-local subagent definitions require a trusted project");
|
|
295
344
|
}
|
|
296
345
|
const settings = options.getSettings();
|
|
346
|
+
const target = resolveSubagentTarget({
|
|
347
|
+
workspace: ctx.cwd,
|
|
348
|
+
requestedCwd: operation.cwd,
|
|
349
|
+
currentProjectTrusted: ctx.isProjectTrusted(),
|
|
350
|
+
});
|
|
351
|
+
assertConsultationTargetAllowed(
|
|
352
|
+
target,
|
|
353
|
+
settings?.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY,
|
|
354
|
+
);
|
|
297
355
|
const discovery = discoverAgents(ctx.cwd, operation.agentScope, settings);
|
|
298
356
|
const agent = discovery.agents.find((candidate) => candidate.name === operation.agent);
|
|
299
357
|
if (!agent) {
|
|
@@ -302,7 +360,7 @@ async function executeConsult(
|
|
|
302
360
|
`Available agents for agentScope "${operation.agentScope}": ${formatAvailableConsultAgents(discovery)}`,
|
|
303
361
|
);
|
|
304
362
|
}
|
|
305
|
-
const setup = resolveConsultSetup(operation, agent, settings,
|
|
363
|
+
const setup = resolveConsultSetup(operation, agent, settings, target);
|
|
306
364
|
|
|
307
365
|
if (agent.source === "project" && operation.confirmProjectAgents) {
|
|
308
366
|
if (!ctx.hasUI) {
|
|
@@ -323,6 +381,8 @@ async function executeConsult(
|
|
|
323
381
|
}
|
|
324
382
|
}
|
|
325
383
|
assertCurrentRequest(signal, isCurrent);
|
|
384
|
+
emitUpdate(consultStartingUpdate(setup.details));
|
|
385
|
+
assertCurrentRequest(signal, isCurrent);
|
|
326
386
|
const runChild = options.runChild ?? ((request) => runConsultChild(request, options));
|
|
327
387
|
const child = runChild({
|
|
328
388
|
agent: setup.agent,
|
|
@@ -376,19 +436,13 @@ function resolveConsultSetup(
|
|
|
376
436
|
operation: ReturnType<typeof validateConsultParams>,
|
|
377
437
|
agent: AgentConfig,
|
|
378
438
|
settings: SubagentSettings | undefined,
|
|
379
|
-
|
|
439
|
+
target: ResolvedSubagentTarget,
|
|
380
440
|
) {
|
|
381
|
-
const
|
|
382
|
-
const
|
|
383
|
-
const resourcePolicy = settings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
|
|
384
|
-
if (!isWithin(cwd, workspace) && resourcePolicy !== "none") {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`Subagent consultation cwd is outside the current workspace; consult.resources must be "none": ${safeTerminalLine(cwd)}`,
|
|
387
|
-
);
|
|
388
|
-
}
|
|
441
|
+
const requestedResourcePolicy = settings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
|
|
442
|
+
const resourcePolicy = target.trust.projectTrusted ? requestedResourcePolicy : "none";
|
|
389
443
|
const effectiveTools = resolveConsultTools(agent.tools);
|
|
390
|
-
const projectTrusted =
|
|
391
|
-
const launchPolicy = resourceLaunchPolicy(resourcePolicy, projectTrusted,
|
|
444
|
+
const projectTrusted = target.trust.projectTrusted;
|
|
445
|
+
const launchPolicy = resourceLaunchPolicy(resourcePolicy, projectTrusted, target.cwd);
|
|
392
446
|
launchPolicy.tools = effectiveTools;
|
|
393
447
|
const thinkingLevel = resolveSubagentThinkingLevel([agent], agent.name, operation.thinkingLevel);
|
|
394
448
|
const timeoutMs = operation.timeoutMs ?? agent.timeoutMs ?? resolveDefaultSubagentTimeoutMs();
|
|
@@ -413,7 +467,7 @@ function resolveConsultSetup(
|
|
|
413
467
|
agent: boundedPrivateText(agent.name, 256),
|
|
414
468
|
agentSource: agent.source,
|
|
415
469
|
agentScope: operation.agentScope,
|
|
416
|
-
cwd: safeDisplayPath(cwd, workspace),
|
|
470
|
+
cwd: safeDisplayPath(target.cwd, target.workspace),
|
|
417
471
|
model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
|
|
418
472
|
thinkingLevel,
|
|
419
473
|
timeoutMs,
|
|
@@ -423,8 +477,20 @@ function resolveConsultSetup(
|
|
|
423
477
|
? null
|
|
424
478
|
: agent.tools.slice(0, 100).map((tool) => boundedPrivateText(tool, 256)),
|
|
425
479
|
effectiveTools,
|
|
426
|
-
|
|
480
|
+
cwdBoundary: target.boundary,
|
|
481
|
+
targetTrust: {
|
|
482
|
+
kind: target.trust.kind,
|
|
483
|
+
projectTrusted,
|
|
484
|
+
sourcePath: target.trust.sourcePath
|
|
485
|
+
? safeDisplayPath(target.trust.sourcePath, target.workspace)
|
|
486
|
+
: undefined,
|
|
487
|
+
warning: target.trust.warning,
|
|
488
|
+
},
|
|
489
|
+
requestedResources: requestedResourcePolicy,
|
|
427
490
|
effectiveResources,
|
|
491
|
+
...(resourcePolicy !== requestedResourcePolicy
|
|
492
|
+
? { resourceDowngradeReason: `Target trust is ${target.trust.kind}` }
|
|
493
|
+
: {}),
|
|
428
494
|
extensions: "disabled",
|
|
429
495
|
sessionPersistence: "disabled",
|
|
430
496
|
retainedAgent: false,
|
|
@@ -432,7 +498,7 @@ function resolveConsultSetup(
|
|
|
432
498
|
};
|
|
433
499
|
return {
|
|
434
500
|
agent: childAgent,
|
|
435
|
-
cwd,
|
|
501
|
+
cwd: target.cwd,
|
|
436
502
|
resourcePolicy,
|
|
437
503
|
effectiveTools,
|
|
438
504
|
thinkingLevel,
|
|
@@ -471,6 +537,21 @@ async function runConsultChild(
|
|
|
471
537
|
);
|
|
472
538
|
}
|
|
473
539
|
|
|
540
|
+
function consultStartingUpdate(details: ConsultDetails): AgentToolResult<ConsultDetails> {
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: "text", text: "Read-only subagent consultation starting." }],
|
|
543
|
+
details: {
|
|
544
|
+
...details,
|
|
545
|
+
progress: {
|
|
546
|
+
phase: "starting",
|
|
547
|
+
recentActivity: [],
|
|
548
|
+
recentActivityTotal: 0,
|
|
549
|
+
usage: emptyProgressUsage(),
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
474
555
|
function consultUpdate(
|
|
475
556
|
result: SingleResult,
|
|
476
557
|
details: ConsultDetails,
|
|
@@ -478,7 +559,69 @@ function consultUpdate(
|
|
|
478
559
|
const output = boundText(getResultFinalOutput(result) || "(running...)");
|
|
479
560
|
return {
|
|
480
561
|
content: [{ type: "text", text: output.text }],
|
|
481
|
-
details: {
|
|
562
|
+
details: {
|
|
563
|
+
...details,
|
|
564
|
+
child: projectChildResult(result),
|
|
565
|
+
progress: projectConsultProgress(result),
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const CONSULT_ACTIVITY_ARGUMENTS: Record<"read" | "grep" | "find" | "ls", readonly string[]> = {
|
|
571
|
+
read: ["path", "file_path", "offset", "limit"],
|
|
572
|
+
grep: ["pattern", "path", "glob", "limit"],
|
|
573
|
+
find: ["pattern", "path", "limit"],
|
|
574
|
+
ls: ["path", "limit"],
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
function projectConsultProgress(result: SingleResult): ConsultProgress {
|
|
578
|
+
const recentActivity: ConsultProgressActivity[] = [];
|
|
579
|
+
for (const item of result.recentActivity ?? []) {
|
|
580
|
+
if (item.type === "text") {
|
|
581
|
+
const text = boundedPrivateText(item.text, 1024).trim();
|
|
582
|
+
if (text) recentActivity.push({ type: "text", text });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (!Object.hasOwn(CONSULT_ACTIVITY_ARGUMENTS, item.name)) continue;
|
|
586
|
+
const name = item.name as keyof typeof CONSULT_ACTIVITY_ARGUMENTS;
|
|
587
|
+
const args: Record<string, string | number | boolean> = {};
|
|
588
|
+
for (const key of CONSULT_ACTIVITY_ARGUMENTS[name]) {
|
|
589
|
+
const value = item.args[key];
|
|
590
|
+
if (typeof value === "string") args[key] = safeTerminalLine(value, 512);
|
|
591
|
+
else if (typeof value === "number" && Number.isFinite(value)) args[key] = value;
|
|
592
|
+
else if (typeof value === "boolean") args[key] = value;
|
|
593
|
+
}
|
|
594
|
+
recentActivity.push({ type: "toolCall", name, args });
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
phase: "running",
|
|
598
|
+
recentActivity,
|
|
599
|
+
recentActivityTotal: Math.max(recentActivity.length, result.recentActivityTotal ?? 0),
|
|
600
|
+
actualProvider: result.actualProvider
|
|
601
|
+
? boundedPrivateText(result.actualProvider, 256)
|
|
602
|
+
: undefined,
|
|
603
|
+
actualModel: result.actualModel ? boundedPrivateText(result.actualModel, 256) : undefined,
|
|
604
|
+
usage: {
|
|
605
|
+
input: result.usage.input,
|
|
606
|
+
output: result.usage.output,
|
|
607
|
+
cacheRead: result.usage.cacheRead,
|
|
608
|
+
cacheWrite: result.usage.cacheWrite,
|
|
609
|
+
cost: result.usage.cost,
|
|
610
|
+
contextTokens: result.usage.contextTokens,
|
|
611
|
+
turns: result.usage.turns,
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function emptyProgressUsage(): ConsultProgress["usage"] {
|
|
617
|
+
return {
|
|
618
|
+
input: 0,
|
|
619
|
+
output: 0,
|
|
620
|
+
cacheRead: 0,
|
|
621
|
+
cacheWrite: 0,
|
|
622
|
+
cost: 0,
|
|
623
|
+
contextTokens: 0,
|
|
624
|
+
turns: 0,
|
|
482
625
|
};
|
|
483
626
|
}
|
|
484
627
|
|
|
@@ -623,22 +766,6 @@ function usageFromResult(result: SingleResult): Usage {
|
|
|
623
766
|
};
|
|
624
767
|
}
|
|
625
768
|
|
|
626
|
-
function canonicalDirectory(value: string, label: string): string {
|
|
627
|
-
try {
|
|
628
|
-
const resolved = fs.realpathSync(value);
|
|
629
|
-
if (!fs.statSync(resolved).isDirectory()) throw new Error("not a directory");
|
|
630
|
-
return resolved;
|
|
631
|
-
} catch (error) {
|
|
632
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
633
|
-
throw new Error(`Invalid ${label}: ${safeTerminalLine(value)} (${safeTerminalLine(reason)})`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function isWithin(candidate: string, root: string): boolean {
|
|
638
|
-
const relative = path.relative(root, candidate);
|
|
639
|
-
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
769
|
function assertCurrentRequest(signal: AbortSignal, isCurrent: () => boolean): void {
|
|
643
770
|
if (signal.aborted || !isCurrent()) throw abortError("Subagent consultation owner was replaced");
|
|
644
771
|
}
|