@narumitw/pi-subagents 0.43.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.
- 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/registry.ts +12 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +2 -0
- 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,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
CHANGED
|
@@ -2,12 +2,21 @@ import * as path from "node:path";
|
|
|
2
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
3
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { type Static, Type } from "typebox";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type AgentConfig,
|
|
7
|
+
type AgentScope,
|
|
8
|
+
type ConsultationCwdPolicy,
|
|
9
|
+
type DelegationCwdPolicy,
|
|
10
|
+
discoverAgents,
|
|
11
|
+
} from "./agents.js";
|
|
6
12
|
import { resolveConsultTools } from "./consult-policy.js";
|
|
13
|
+
import { renderInspectCall, renderInspectResult } from "./inspect-render.js";
|
|
7
14
|
import type { AgentRunInspectionDetail, AgentRunInspectionSummary } from "./registry.js";
|
|
8
15
|
import { boundedPrivateText, boundText, safeDisplayPath, safeTerminalLine } from "./safe-text.js";
|
|
9
16
|
import {
|
|
17
|
+
inspectCompletionDeliverySettings,
|
|
10
18
|
inspectConsultResourceSettings,
|
|
19
|
+
inspectCwdPolicySettings,
|
|
11
20
|
inspectDelegationWorkflowSettings,
|
|
12
21
|
inspectSubagentSettings,
|
|
13
22
|
resolveDelegationWorkflow,
|
|
@@ -49,6 +58,8 @@ export type SubagentInspectParams = Static<typeof SubagentInspectParams>;
|
|
|
49
58
|
export interface SubagentInspectRuntime {
|
|
50
59
|
getBlockingEnabled(): boolean;
|
|
51
60
|
getConsultResourcePolicy(): "project-context" | "none" | "all";
|
|
61
|
+
getConsultationCwdPolicy(): ConsultationCwdPolicy;
|
|
62
|
+
getDelegationCwdPolicy(): DelegationCwdPolicy;
|
|
52
63
|
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
53
64
|
listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
|
|
54
65
|
getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
|
|
@@ -79,6 +90,12 @@ export function registerSubagentInspect(pi: ExtensionAPI, runtime: SubagentInspe
|
|
|
79
90
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx): Promise<InspectToolResult> {
|
|
80
91
|
return executeSubagentInspect(validateInspectParams(params), ctx, runtime);
|
|
81
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
|
+
},
|
|
82
99
|
});
|
|
83
100
|
}
|
|
84
101
|
|
|
@@ -280,9 +297,26 @@ function projectRun(run: AgentRunInspectionDetail, ctx: ExtensionContext): Recor
|
|
|
280
297
|
return {
|
|
281
298
|
...projectRunSummary(run),
|
|
282
299
|
cwd: safeDisplayPath(run.cwd, ctx.cwd),
|
|
300
|
+
workspaceMode: run.workspaceMode ?? "shared",
|
|
283
301
|
thinkingLevel: run.thinkingLevel,
|
|
284
302
|
currentTask: run.currentTask ? boundedPrivateText(run.currentTask, 2 * 1024) : undefined,
|
|
285
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,
|
|
286
320
|
policy: run.policy
|
|
287
321
|
? {
|
|
288
322
|
inherited: projectToolNames(run.policy.inherited),
|
|
@@ -323,17 +357,31 @@ function projectStatus(runtime: SubagentInspectRuntime): Record<string, unknown>
|
|
|
323
357
|
const workflow = resolveDelegationWorkflow(runtime.getBlockingEnabled(), stateful.enabled);
|
|
324
358
|
const configured = inspectDelegationWorkflowSettings();
|
|
325
359
|
const resources = inspectConsultResourceSettings();
|
|
360
|
+
const cwdPolicy = inspectCwdPolicySettings();
|
|
361
|
+
const completion = inspectCompletionDeliverySettings();
|
|
326
362
|
return {
|
|
327
363
|
workflow,
|
|
328
364
|
configuredWorkflow: configured.value,
|
|
365
|
+
configuredWorkflowSource: configured.source,
|
|
329
366
|
stateful,
|
|
367
|
+
configuredCompletionDelivery: completion.value,
|
|
368
|
+
configuredCompletionDeliverySource: completion.source,
|
|
330
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,
|
|
331
376
|
configuredConsultResources: resources.value,
|
|
332
377
|
consultResourcesSource: resources.source,
|
|
333
378
|
settingsPath: safeDisplayPath(resources.path, process.cwd()),
|
|
334
379
|
settingsError:
|
|
335
|
-
configured.error || resources.error
|
|
336
|
-
? boundedPrivateText(
|
|
380
|
+
configured.error || resources.error || cwdPolicy.error || completion.error
|
|
381
|
+
? boundedPrivateText(
|
|
382
|
+
configured.error ?? resources.error ?? cwdPolicy.error ?? completion.error ?? "",
|
|
383
|
+
2 * 1024,
|
|
384
|
+
)
|
|
337
385
|
: undefined,
|
|
338
386
|
};
|
|
339
387
|
}
|
package/src/persistence.ts
CHANGED
|
@@ -167,6 +167,8 @@ function isStoredState(value: unknown): value is StoredState {
|
|
|
167
167
|
Number.isFinite(record.updatedAt) &&
|
|
168
168
|
(record.parentId === undefined || typeof record.parentId === "string") &&
|
|
169
169
|
(record.thinkingLevel === undefined || isThinkingLevel(record.thinkingLevel)) &&
|
|
170
|
+
(record.workspaceMode === undefined || record.workspaceMode === "worktree") &&
|
|
171
|
+
(record.target === undefined || isTargetPolicyAudit(record.target)) &&
|
|
170
172
|
(record.children === undefined ||
|
|
171
173
|
(Array.isArray(record.children) &&
|
|
172
174
|
record.children.every((id) => typeof id === "string"))) &&
|
|
@@ -178,6 +180,33 @@ function isStoredState(value: unknown): value is StoredState {
|
|
|
178
180
|
});
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
function isTargetPolicyAudit(value: unknown): boolean {
|
|
184
|
+
if (!value || typeof value !== "object") return false;
|
|
185
|
+
const target = value as Record<string, unknown>;
|
|
186
|
+
if (
|
|
187
|
+
typeof target.cwd !== "string" ||
|
|
188
|
+
(target.boundary !== "current-workspace" && target.boundary !== "external") ||
|
|
189
|
+
!target.trust ||
|
|
190
|
+
typeof target.trust !== "object"
|
|
191
|
+
) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
const trust = target.trust as Record<string, unknown>;
|
|
195
|
+
return (
|
|
196
|
+
[
|
|
197
|
+
"session-trusted",
|
|
198
|
+
"session-untrusted",
|
|
199
|
+
"saved-trusted",
|
|
200
|
+
"saved-denied",
|
|
201
|
+
"unsaved",
|
|
202
|
+
"trust-error",
|
|
203
|
+
].includes(String(trust.kind)) &&
|
|
204
|
+
typeof trust.projectTrusted === "boolean" &&
|
|
205
|
+
(trust.sourcePath === undefined || typeof trust.sourcePath === "string") &&
|
|
206
|
+
(trust.warning === undefined || typeof trust.warning === "string")
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
181
210
|
function isAgentTurn(value: unknown): boolean {
|
|
182
211
|
if (!value || typeof value !== "object") return false;
|
|
183
212
|
const turn = value as Record<string, unknown>;
|
package/src/registry.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import type { SubagentThinkingLevel } from "./agents.js";
|
|
3
|
+
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
3
4
|
import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
4
5
|
import { type AgentTurnRunner, normalizeTransport, type SubagentTransport } from "./transport.js";
|
|
5
6
|
|
|
@@ -50,6 +51,8 @@ export interface ManagedAgent {
|
|
|
50
51
|
context?: string;
|
|
51
52
|
contextSourceIds?: string[];
|
|
52
53
|
contextTruncated?: boolean;
|
|
54
|
+
workspaceMode?: "worktree";
|
|
55
|
+
target?: TargetPolicyAudit;
|
|
53
56
|
policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
|
|
54
57
|
mailbox: AgentMailboxMessage[];
|
|
55
58
|
currentMailboxMessageIds?: string[];
|
|
@@ -70,6 +73,8 @@ export interface AgentRunInspectionDetail extends AgentRunInspectionSummary {
|
|
|
70
73
|
thinkingLevel?: SubagentThinkingLevel;
|
|
71
74
|
currentTask?: string;
|
|
72
75
|
error?: string;
|
|
76
|
+
workspaceMode?: "worktree";
|
|
77
|
+
target?: TargetPolicyAudit;
|
|
73
78
|
policy?: { inherited: string[]; overridden: string[]; unsupported: string[] };
|
|
74
79
|
}
|
|
75
80
|
|
|
@@ -243,6 +248,8 @@ export class AgentRegistry {
|
|
|
243
248
|
context?: string;
|
|
244
249
|
contextSourceIds?: string[];
|
|
245
250
|
contextTruncated?: boolean;
|
|
251
|
+
workspaceMode?: "worktree";
|
|
252
|
+
target?: TargetPolicyAudit;
|
|
246
253
|
}): Promise<ManagedAgent> {
|
|
247
254
|
if (!input.task.trim()) throw new Error("Subagent tasks cannot be empty");
|
|
248
255
|
const task = truncateUtf8(input.task, this.maxTaskBytes).text;
|
|
@@ -286,6 +293,8 @@ export class AgentRegistry {
|
|
|
286
293
|
context: input.context,
|
|
287
294
|
contextSourceIds: input.contextSourceIds,
|
|
288
295
|
contextTruncated: input.contextTruncated,
|
|
296
|
+
workspaceMode: input.workspaceMode,
|
|
297
|
+
target: input.target,
|
|
289
298
|
};
|
|
290
299
|
this.agents.set(record.id, record);
|
|
291
300
|
if (parent) {
|
|
@@ -549,6 +558,8 @@ export class AgentRegistry {
|
|
|
549
558
|
thinkingLevel: agent.thinkingLevel,
|
|
550
559
|
currentTask: agent.currentTask,
|
|
551
560
|
error: agent.error,
|
|
561
|
+
workspaceMode: agent.workspaceMode,
|
|
562
|
+
target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
|
|
552
563
|
policy: agent.policy
|
|
553
564
|
? {
|
|
554
565
|
inherited: [...agent.policy.inherited],
|
|
@@ -840,6 +851,7 @@ export class AgentRegistry {
|
|
|
840
851
|
: undefined,
|
|
841
852
|
history: agent.history.map((turn) => ({ ...turn })),
|
|
842
853
|
mailbox: agent.mailbox.map((message) => ({ ...message })),
|
|
854
|
+
target: agent.target ? { ...agent.target, trust: { ...agent.target.trust } } : undefined,
|
|
843
855
|
policy: agent.policy
|
|
844
856
|
? {
|
|
845
857
|
inherited: [...agent.policy.inherited],
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import {
|
|
4
|
+
keyHint,
|
|
5
|
+
type Theme,
|
|
6
|
+
type ThemeColor,
|
|
7
|
+
type ToolRenderResultOptions,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { boundedPrivateText, safeTerminalLine } from "./safe-text.js";
|
|
11
|
+
|
|
12
|
+
export const COLLAPSED_LIST_LIMIT = 5;
|
|
13
|
+
export const COLLAPSED_ANSWER_LINES = 3;
|
|
14
|
+
|
|
15
|
+
export interface ToolRendererContext<TArgs> {
|
|
16
|
+
args: TArgs;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RenderStatus =
|
|
21
|
+
| "starting"
|
|
22
|
+
| "running"
|
|
23
|
+
| "completed"
|
|
24
|
+
| "failed"
|
|
25
|
+
| "cancelled"
|
|
26
|
+
| "interrupted"
|
|
27
|
+
| "idle"
|
|
28
|
+
| "closed"
|
|
29
|
+
| "warning";
|
|
30
|
+
|
|
31
|
+
const STATUS_PRESENTATION: Record<
|
|
32
|
+
RenderStatus,
|
|
33
|
+
{ icon: string; label: string; color: ThemeColor }
|
|
34
|
+
> = {
|
|
35
|
+
starting: { icon: "⏳", label: "Starting", color: "warning" },
|
|
36
|
+
running: { icon: "⏳", label: "Running", color: "warning" },
|
|
37
|
+
completed: { icon: "✓", label: "Completed", color: "success" },
|
|
38
|
+
failed: { icon: "✗", label: "Failed", color: "error" },
|
|
39
|
+
cancelled: { icon: "■", label: "Cancelled", color: "warning" },
|
|
40
|
+
interrupted: { icon: "■", label: "Interrupted", color: "warning" },
|
|
41
|
+
idle: { icon: "○", label: "Idle", color: "muted" },
|
|
42
|
+
closed: { icon: "✓", label: "Closed", color: "muted" },
|
|
43
|
+
warning: { icon: "◐", label: "Warning", color: "warning" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
48
|
+
? (value as Record<string, unknown>)
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordList(value: unknown): Record<string, unknown>[] {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.flatMap((item) => {
|
|
55
|
+
const record = recordValue(item);
|
|
56
|
+
return record ? [record] : [];
|
|
57
|
+
})
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stringValue(value: unknown, fallback = ""): string {
|
|
62
|
+
return typeof value === "string" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function numberValue(value: unknown, fallback = 0): number {
|
|
66
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function booleanValue(value: unknown): boolean {
|
|
70
|
+
return value === true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function safeLine(value: unknown, fallback = "...", maxBytes = 2 * 1024): string {
|
|
74
|
+
if (typeof value !== "string" || !value.trim()) return fallback;
|
|
75
|
+
return safeTerminalLine(value, maxBytes) || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function safeBlock(value: unknown, fallback = "", maxBytes = 50 * 1024): string {
|
|
79
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
80
|
+
return boundedPrivateText(value, maxBytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function previewLines(value: unknown, maxLines = COLLAPSED_ANSWER_LINES): string {
|
|
84
|
+
const text = safeBlock(value, "", 8 * 1024).trim();
|
|
85
|
+
return text.split("\n").slice(0, maxLines).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function textResult(result: AgentToolResult<unknown>): string {
|
|
89
|
+
return result.content
|
|
90
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
91
|
+
.join("\n")
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toolHeader(
|
|
96
|
+
theme: Theme,
|
|
97
|
+
toolName: string,
|
|
98
|
+
primary?: unknown,
|
|
99
|
+
metadata: readonly string[] = [],
|
|
100
|
+
): string {
|
|
101
|
+
let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
|
|
102
|
+
if (primary !== undefined) text += theme.fg("accent", safeLine(primary));
|
|
103
|
+
const safeMetadata = metadata.filter(Boolean).map((item) => safeLine(item, "", 512));
|
|
104
|
+
if (safeMetadata.length > 0) text += theme.fg("muted", ` · ${safeMetadata.join(" · ")}`);
|
|
105
|
+
return text.trimEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function statusBadge(theme: Theme, status: RenderStatus, suffix?: string): string {
|
|
109
|
+
const presentation = STATUS_PRESENTATION[status];
|
|
110
|
+
const label = suffix
|
|
111
|
+
? `${presentation.label} · ${safeLine(suffix, "", 2 * 1024)}`
|
|
112
|
+
: presentation.label;
|
|
113
|
+
return `${theme.fg(presentation.color, presentation.icon)} ${theme.fg(presentation.color, label)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderFallbackResult(
|
|
117
|
+
result: AgentToolResult<unknown>,
|
|
118
|
+
options: ToolRenderResultOptions,
|
|
119
|
+
theme: Theme,
|
|
120
|
+
isError = false,
|
|
121
|
+
) {
|
|
122
|
+
const status: RenderStatus = isError ? "failed" : options.isPartial ? "running" : "completed";
|
|
123
|
+
const content = safeBlock(textResult(result), "(no output)", 8 * 1024);
|
|
124
|
+
return new Text(`${statusBadge(theme, status)}\n${theme.fg("toolOutput", content)}`, 0, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function expansionHint(): string {
|
|
128
|
+
return keyHint("app.tools.expand", "to expand");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface RenderActivityItem {
|
|
132
|
+
type: "text" | "toolCall";
|
|
133
|
+
text?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
args?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function projectRenderActivity(value: unknown): RenderActivityItem[] {
|
|
139
|
+
if (!Array.isArray(value)) return [];
|
|
140
|
+
const items: RenderActivityItem[] = [];
|
|
141
|
+
for (const item of value) {
|
|
142
|
+
const record = recordValue(item);
|
|
143
|
+
if (!record) continue;
|
|
144
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
145
|
+
items.push({ type: "text", text: safeBlock(record.text, "", 1024) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
149
|
+
items.push({
|
|
150
|
+
type: "toolCall",
|
|
151
|
+
name: safeLine(record.name, "tool", 256),
|
|
152
|
+
args: recordValue(record.args) ?? {},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return items;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderActivityLines(
|
|
160
|
+
items: readonly RenderActivityItem[],
|
|
161
|
+
theme: Theme,
|
|
162
|
+
limit?: number,
|
|
163
|
+
total = items.length,
|
|
164
|
+
): string {
|
|
165
|
+
const selected = limit === undefined ? items : items.slice(-limit);
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
const skipped = Math.max(0, total - selected.length);
|
|
168
|
+
if (skipped > 0) lines.push(theme.fg("muted", `… ${skipped} earlier activities`));
|
|
169
|
+
for (const item of selected) {
|
|
170
|
+
if (item.type === "text") {
|
|
171
|
+
const text = safeBlock(item.text, "", 1024).trim();
|
|
172
|
+
if (text) lines.push(theme.fg("toolOutput", text));
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(
|
|
175
|
+
theme.fg("muted", "→ ") +
|
|
176
|
+
formatToolActivity(item.name ?? "tool", item.args ?? {}, theme.fg.bind(theme)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatToolActivity(
|
|
184
|
+
toolNameValue: unknown,
|
|
185
|
+
argsValue: unknown,
|
|
186
|
+
themeFg: (color: ThemeColor, text: string) => string,
|
|
187
|
+
): string {
|
|
188
|
+
const toolName = safeLine(toolNameValue, "tool", 256);
|
|
189
|
+
const args = recordValue(argsValue) ?? {};
|
|
190
|
+
const shortenPath = (value: unknown, fallback = ".") => {
|
|
191
|
+
const filePath = safeLine(value, fallback, 2 * 1024);
|
|
192
|
+
const home = os.homedir();
|
|
193
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
switch (toolName) {
|
|
197
|
+
case "bash": {
|
|
198
|
+
const command = safeLine(args.command, "...", 512);
|
|
199
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", command);
|
|
200
|
+
}
|
|
201
|
+
case "read": {
|
|
202
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
203
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
204
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
205
|
+
let text = themeFg("accent", filePath);
|
|
206
|
+
if (offset !== undefined || limit !== undefined) {
|
|
207
|
+
const startLine = offset ?? 1;
|
|
208
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
209
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
210
|
+
}
|
|
211
|
+
return themeFg("muted", "read ") + text;
|
|
212
|
+
}
|
|
213
|
+
case "write": {
|
|
214
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
215
|
+
const content = safeBlock(args.content, "", 2 * 1024);
|
|
216
|
+
const lines = content ? content.split("\n").length : 0;
|
|
217
|
+
return (
|
|
218
|
+
themeFg("muted", "write ") +
|
|
219
|
+
themeFg("accent", filePath) +
|
|
220
|
+
(lines > 1 ? themeFg("dim", ` (${lines} lines)`) : "")
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
case "edit":
|
|
224
|
+
return (
|
|
225
|
+
themeFg("muted", "edit ") +
|
|
226
|
+
themeFg("accent", shortenPath(args.file_path ?? args.path, "..."))
|
|
227
|
+
);
|
|
228
|
+
case "ls":
|
|
229
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(args.path));
|
|
230
|
+
case "find":
|
|
231
|
+
return (
|
|
232
|
+
themeFg("muted", "find ") +
|
|
233
|
+
themeFg("accent", safeLine(args.pattern, "*", 512)) +
|
|
234
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
235
|
+
);
|
|
236
|
+
case "grep":
|
|
237
|
+
return (
|
|
238
|
+
themeFg("muted", "grep ") +
|
|
239
|
+
themeFg("accent", `/${safeLine(args.pattern, "", 512)}/`) +
|
|
240
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
241
|
+
);
|
|
242
|
+
default: {
|
|
243
|
+
let serialized = "{}";
|
|
244
|
+
try {
|
|
245
|
+
serialized = JSON.stringify(args);
|
|
246
|
+
} catch {
|
|
247
|
+
serialized = "{…}";
|
|
248
|
+
}
|
|
249
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${safeLine(serialized, "{}", 512)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|