@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,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>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getPackageDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const CORE_PACKAGE_NAME = "@earendil-works/pi-coding-agent";
|
|
6
|
+
const MAX_DISPLAY_PATH_LENGTH = 500;
|
|
7
|
+
|
|
8
|
+
export interface PiInvocation {
|
|
9
|
+
command: string;
|
|
10
|
+
args: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PiInvocationRuntime {
|
|
14
|
+
execPath: string;
|
|
15
|
+
packageDir: string;
|
|
16
|
+
runtimeKind: "node" | "bun" | "unsupported";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PiInvocationError extends Error {
|
|
20
|
+
constructor(message: string) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "PiInvocationError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function displayPath(value: string): string {
|
|
27
|
+
const suffix = value.length > MAX_DISPLAY_PATH_LENGTH ? "…" : "";
|
|
28
|
+
return JSON.stringify(`${value.slice(0, MAX_DISPLAY_PATH_LENGTH)}${suffix}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolutionError(packageDir: string, reason: string): PiInvocationError {
|
|
32
|
+
return new PiInvocationError(
|
|
33
|
+
`Unable to resolve the Pi CLI from the loaded ${CORE_PACKAGE_NAME} package at ${displayPath(packageDir)}: ${reason}. Reinstall the matching Pi core package before using the subprocess transport.`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function currentRuntime(): PiInvocationRuntime {
|
|
38
|
+
let packageDir: string;
|
|
39
|
+
try {
|
|
40
|
+
packageDir = getPackageDir();
|
|
41
|
+
} catch {
|
|
42
|
+
throw resolutionError("<unavailable>", "Pi core did not provide its package directory");
|
|
43
|
+
}
|
|
44
|
+
const runtimeKind = process.versions.bun
|
|
45
|
+
? "bun"
|
|
46
|
+
: process.release.name === "node" && !process.versions.electron
|
|
47
|
+
? "node"
|
|
48
|
+
: "unsupported";
|
|
49
|
+
return {
|
|
50
|
+
execPath: process.execPath,
|
|
51
|
+
packageDir,
|
|
52
|
+
runtimeKind,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
57
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isWithinDirectory(parent: string, candidate: string): boolean {
|
|
61
|
+
const relative = path.relative(parent, candidate);
|
|
62
|
+
return (
|
|
63
|
+
relative === "" ||
|
|
64
|
+
(!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function readCoreManifest(packageDir: string): Record<string, unknown> {
|
|
69
|
+
const manifestPath = path.join(packageDir, "package.json");
|
|
70
|
+
let source: string;
|
|
71
|
+
try {
|
|
72
|
+
source = fs.readFileSync(manifestPath, "utf8");
|
|
73
|
+
} catch {
|
|
74
|
+
throw resolutionError(packageDir, "the package manifest is unavailable");
|
|
75
|
+
}
|
|
76
|
+
let manifest: unknown;
|
|
77
|
+
try {
|
|
78
|
+
manifest = JSON.parse(source);
|
|
79
|
+
} catch {
|
|
80
|
+
throw resolutionError(packageDir, "the package manifest is invalid JSON");
|
|
81
|
+
}
|
|
82
|
+
if (!isRecord(manifest)) {
|
|
83
|
+
throw resolutionError(packageDir, "the package manifest is invalid");
|
|
84
|
+
}
|
|
85
|
+
if (manifest.name !== CORE_PACKAGE_NAME) {
|
|
86
|
+
throw resolutionError(packageDir, "the package manifest has an unexpected package name");
|
|
87
|
+
}
|
|
88
|
+
return manifest;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resolveDeclaredBin(packageDir: string, manifest: Record<string, unknown>): string {
|
|
92
|
+
const bin = manifest.bin;
|
|
93
|
+
const piBin = isRecord(bin) ? bin.pi : undefined;
|
|
94
|
+
if (typeof piBin !== "string" || !piBin.trim()) {
|
|
95
|
+
throw resolutionError(packageDir, "package.json bin.pi must be a non-empty string");
|
|
96
|
+
}
|
|
97
|
+
const candidate = path.resolve(packageDir, piBin);
|
|
98
|
+
if (path.isAbsolute(piBin) || !isWithinDirectory(packageDir, candidate)) {
|
|
99
|
+
throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
|
|
100
|
+
}
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveExistingFile(packageDir: string, candidate: string, reason: string): string {
|
|
105
|
+
let resolved: string;
|
|
106
|
+
try {
|
|
107
|
+
resolved = fs.realpathSync(candidate);
|
|
108
|
+
if (!fs.statSync(resolved).isFile()) throw new Error("not a file");
|
|
109
|
+
} catch {
|
|
110
|
+
throw resolutionError(packageDir, reason);
|
|
111
|
+
}
|
|
112
|
+
if (!isWithinDirectory(packageDir, resolved)) {
|
|
113
|
+
throw resolutionError(packageDir, "the declared bin.pi target escapes the package directory");
|
|
114
|
+
}
|
|
115
|
+
return resolved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function resolveStandaloneExecutable(
|
|
119
|
+
packageDir: string,
|
|
120
|
+
runtime: PiInvocationRuntime,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
if (runtime.runtimeKind !== "bun") return undefined;
|
|
123
|
+
const { execPath } = runtime;
|
|
124
|
+
if (!/^pi(?:\.exe)?$/i.test(path.basename(execPath))) return undefined;
|
|
125
|
+
let resolved: string;
|
|
126
|
+
let mode: number;
|
|
127
|
+
try {
|
|
128
|
+
resolved = fs.realpathSync(execPath);
|
|
129
|
+
const stat = fs.statSync(resolved);
|
|
130
|
+
if (!stat.isFile()) throw new Error("not a file");
|
|
131
|
+
mode = stat.mode;
|
|
132
|
+
} catch {
|
|
133
|
+
throw resolutionError(packageDir, "the standalone Pi executable is unavailable");
|
|
134
|
+
}
|
|
135
|
+
if (path.dirname(resolved) !== packageDir) return undefined;
|
|
136
|
+
if (process.platform !== "win32" && (mode & 0o111) === 0) {
|
|
137
|
+
throw resolutionError(packageDir, "the standalone Pi executable is not executable");
|
|
138
|
+
}
|
|
139
|
+
return resolved;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function resolvePiInvocation(
|
|
143
|
+
args: string[],
|
|
144
|
+
runtime: PiInvocationRuntime = currentRuntime(),
|
|
145
|
+
): PiInvocation {
|
|
146
|
+
let packageDir: string;
|
|
147
|
+
try {
|
|
148
|
+
packageDir = fs.realpathSync(runtime.packageDir);
|
|
149
|
+
if (!fs.statSync(packageDir).isDirectory()) throw new Error("not a directory");
|
|
150
|
+
} catch {
|
|
151
|
+
throw resolutionError(runtime.packageDir, "the package directory is unavailable");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const manifest = readCoreManifest(packageDir);
|
|
155
|
+
const declaredBin = resolveDeclaredBin(packageDir, manifest);
|
|
156
|
+
const standalone = resolveStandaloneExecutable(packageDir, runtime);
|
|
157
|
+
if (standalone) return { command: standalone, args: [...args] };
|
|
158
|
+
|
|
159
|
+
if (runtime.runtimeKind !== "node" && runtime.runtimeKind !== "bun") {
|
|
160
|
+
throw resolutionError(packageDir, "the host does not provide a supported Node or Bun runtime");
|
|
161
|
+
}
|
|
162
|
+
const cliPath = resolveExistingFile(
|
|
163
|
+
packageDir,
|
|
164
|
+
declaredBin,
|
|
165
|
+
"the declared bin.pi target is unavailable",
|
|
166
|
+
);
|
|
167
|
+
return { command: runtime.execPath, args: [cliPath, ...args] };
|
|
168
|
+
}
|
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],
|