@narumitw/pi-subagents 0.46.0 → 0.49.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 +6 -6
- package/package.json +8 -7
- package/src/agents.ts +2 -2
- package/src/config-ui.ts +8 -3
- package/src/consult-resources.ts +74 -0
- package/src/consult.ts +26 -113
- package/src/in-process-transport.ts +100 -104
- package/src/inspect.ts +8 -2
- package/src/limits.ts +4 -2
- package/src/prompt-source-safety.ts +48 -0
- package/src/safe-text.ts +2 -2
- package/src/subagents.ts +6 -2
package/README.md
CHANGED
|
@@ -253,11 +253,11 @@ without launching or charging a child.
|
|
|
253
253
|
|
|
254
254
|
| Value | Behavior |
|
|
255
255
|
| --- | --- |
|
|
256
|
-
| `"project-context"` (default) | Keep ordinary user context/system files and trusted project `AGENTS.md`, `CLAUDE.md`, and `
|
|
256
|
+
| `"project-context"` (default) | Keep ordinary user context/system files and trusted project `AGENTS.md`, `CLAUDE.md`, `SYSTEM.md`, and `APPEND_SYSTEM.md`; disable skills and prompt templates |
|
|
257
257
|
| `"none"` | Use only the package consultation base, selected agent prompt, and enforced read-only instruction |
|
|
258
258
|
| `"all"` | Keep ordinarily discoverable trusted context/system/append-system files, skills, and prompt templates |
|
|
259
259
|
|
|
260
|
-
Extensions remain disabled for all three values. A current target uses the session's effective project trust, including session-only or CLI overrides. An external target uses the nearest saved trust decision. For an untrusted, explicitly denied, unsaved, or trust-error target, consultation remains available when `cwdPolicy.consultation` permits it but automatically downgrades to `resources: "none"`. This also disables context files because Pi does not protect `AGENTS.md` and `CLAUDE.md` with project trust alone. A saved-trusted external target uses the configured resource policy and discovers `SYSTEM.md`, `APPEND_SYSTEM.md`, and ordinary child context from that target rather than the parent workspace.
|
|
260
|
+
Extensions remain disabled for all three values. Pi core owns system-prompt source precedence: a trusted project prompt wins over the global prompt, with the global prompt used as fallback. A selected Pi prompt source must be a readable regular file; directories, FIFOs, devices, sockets, and unreadable sources fail before child launch. A current target uses the session's effective project trust, including session-only or CLI overrides. An external target uses the nearest saved trust decision. For an untrusted, explicitly denied, unsaved, or trust-error target, consultation remains available when `cwdPolicy.consultation` permits it but automatically downgrades to `resources: "none"`. This also disables context files because Pi does not protect `AGENTS.md` and `CLAUDE.md` with project trust alone. A saved-trusted external target uses the configured resource policy and discovers `SYSTEM.md`, `APPEND_SYSTEM.md`, and ordinary child context from that target rather than the parent workspace.
|
|
261
261
|
|
|
262
262
|
Both settings are user-owned in `~/.pi/agent/pi-subagents.json`; projects cannot override them. `cwdPolicy.consultation: "current-workspace"` rejects every canonical external target before agent discovery or launch even when that target is saved-trusted. This is not a path sandbox: read-only tools can still read an explicitly requested accessible absolute path.
|
|
263
263
|
|
|
@@ -457,12 +457,12 @@ Reasoning, tool results, custom transport messages, and non-text parts are exclu
|
|
|
457
457
|
Stateful execution uses a transport boundary:
|
|
458
458
|
|
|
459
459
|
- `subprocess` is the default compatibility and rollback path.
|
|
460
|
-
- `in-process` uses only public Pi SDK APIs: `
|
|
460
|
+
- `in-process` uses only public Pi SDK APIs: `createAgentSessionServices()`, `createAgentSessionFromServices()`, `SessionManager.inMemory()`, and normal session lifecycle methods. It isolates conversation/tool selection, not memory or crashes; child failures share the parent Node.js process.
|
|
461
461
|
- Child resource loading sets `noExtensions: true`, preventing recursive `pi-subagents` loading and duplicate extension side effects while retaining trust-eligible context/skill resources and the selected agent prompt. Both transports receive the same resolved target-trust boolean: subprocess children get explicit `--approve`/`--no-approve`, and in-process children set the same `SettingsManager.projectTrusted` value.
|
|
462
|
-
- Agent model, thinking level
|
|
462
|
+
- Agent model strings use Pi core's CLI resolver, including provider/model patterns, fuzzy matching, custom provider model IDs, and `:<thinking>` suffixes. Thinking level and built-in tool allow-list overrides are applied when the child is created. Parent model/thinking changes are snapshotted for subsequently created children; an existing child keeps its own session configuration.
|
|
463
463
|
- Extension/custom tool names are rejected in-process with an actionable recommendation to use `subprocess`; permissions are never silently widened.
|
|
464
464
|
- Timeout, parent abort, close, expiry, and session shutdown abort/dispose owned child sessions. A child that does not settle after abort grace is discarded rather than reused.
|
|
465
|
-
- In-process startup failures do not silently retry through subprocesses, preventing duplicate side effects.
|
|
465
|
+
- In-process startup failures do not silently retry through subprocesses, preventing duplicate side effects. If the loaded Pi core lacks public `createAgentSessionServices()`, `createAgentSessionFromServices()`, or `resolveCliModel()` support, startup fails with an actionable instruction to select `stateful.transport: "subprocess"`.
|
|
466
466
|
|
|
467
467
|
No private Pi imports, runtime casts, or `ExtensionAPI` monkey-patching are used. Approval policy, sandbox profile, provider-header hooks, extension state, global scheduling, and parent/child transcript switching are not inherited or provided by the in-process transport.
|
|
468
468
|
|
|
@@ -610,7 +610,7 @@ For `subagent_spawn`, the root agent should choose the lowest sufficient level:
|
|
|
610
610
|
|
|
611
611
|
Blocking thinking precedence is: task/chain step/aggregator `thinkingLevel` → top-level `thinkingLevel` → agent default from config or frontmatter → Pi subprocess default.
|
|
612
612
|
|
|
613
|
-
Stateful spawn precedence is: `subagent_spawn.thinkingLevel` → agent default from config or frontmatter → transport fallback. The subprocess transport then uses spawned Pi model/default resolution. The in-process transport
|
|
613
|
+
Stateful spawn precedence is: `subagent_spawn.thinkingLevel` → agent default from config or frontmatter → transport fallback. The subprocess transport then uses spawned Pi model/default resolution. The in-process transport delegates configured model parsing to the loaded Pi core and uses its model thinking suffix before the parent thinking snapshot captured when the child is created. An explicit spawn value is retained for the agent lifecycle and wins over all of those fallbacks.
|
|
614
614
|
|
|
615
615
|
Omit `thinkingLevel` to preserve existing behavior. Reported stateful details show the requested level, not a guarantee of the provider's effective value. Pi still owns model capability clamping; `pi-subagents` does not duplicate capability detection.
|
|
616
616
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@narumitw/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"description": "Pi extension for delegating work to specialized isolated subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,16 +28,12 @@
|
|
|
28
28
|
"format": "biome check --write .",
|
|
29
29
|
"typecheck": "tsc --noEmit"
|
|
30
30
|
},
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@narumitw/pi-tui-kit": "^0.40.0",
|
|
33
|
-
"proper-lockfile": "^4.1.2",
|
|
34
|
-
"typebox": "^1.3.9"
|
|
35
|
-
},
|
|
36
31
|
"peerDependencies": {
|
|
37
32
|
"@earendil-works/pi-agent-core": "*",
|
|
38
33
|
"@earendil-works/pi-ai": "*",
|
|
39
34
|
"@earendil-works/pi-coding-agent": "*",
|
|
40
|
-
"@earendil-works/pi-tui": "*"
|
|
35
|
+
"@earendil-works/pi-tui": "*",
|
|
36
|
+
"typebox": "*"
|
|
41
37
|
},
|
|
42
38
|
"devDependencies": {
|
|
43
39
|
"@biomejs/biome": "2.5.6",
|
|
@@ -45,8 +41,13 @@
|
|
|
45
41
|
"@earendil-works/pi-ai": "0.83.0",
|
|
46
42
|
"@earendil-works/pi-coding-agent": "0.83.0",
|
|
47
43
|
"@earendil-works/pi-tui": "0.83.0",
|
|
44
|
+
"typebox": "1.3.9",
|
|
48
45
|
"typescript": "7.0.2"
|
|
49
46
|
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@narumitw/pi-tui-kit": "^0.46.0",
|
|
49
|
+
"proper-lockfile": "^4.1.2"
|
|
50
|
+
},
|
|
50
51
|
"repository": {
|
|
51
52
|
"type": "git",
|
|
52
53
|
"url": "https://github.com/narumiruna/pi-extensions",
|
package/src/agents.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
9
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
10
10
|
|
|
@@ -316,7 +316,7 @@ function isDirectory(p: string): boolean {
|
|
|
316
316
|
function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
317
317
|
let currentDir = cwd;
|
|
318
318
|
while (true) {
|
|
319
|
-
const candidate = path.join(currentDir,
|
|
319
|
+
const candidate = path.join(currentDir, CONFIG_DIR_NAME, "agents");
|
|
320
320
|
if (isDirectory(candidate)) return candidate;
|
|
321
321
|
|
|
322
322
|
const parentDir = path.dirname(currentDir);
|
package/src/config-ui.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { defineMenu, runMenu } from "@narumitw/pi-tui-kit";
|
|
3
2
|
import {
|
|
4
3
|
type CompletionDelivery,
|
|
5
4
|
type ConsultationCwdPolicy,
|
|
@@ -124,6 +123,9 @@ async function showSubagentManager(
|
|
|
124
123
|
return;
|
|
125
124
|
}
|
|
126
125
|
const generation = owner.generation;
|
|
126
|
+
const isCurrent = () => generation === owner.generation && !owner.controller.signal.aborted;
|
|
127
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
128
|
+
if (!isCurrent()) return;
|
|
127
129
|
let availableAgents = discoverAgents(ctx.cwd, "user", readSubagentSettings() ?? {}).agents;
|
|
128
130
|
let toolDraft: ToolDraft | undefined;
|
|
129
131
|
type Screen =
|
|
@@ -482,7 +484,7 @@ async function showSubagentManager(
|
|
|
482
484
|
await runMenu(ctx, menu, {
|
|
483
485
|
getState: () => undefined,
|
|
484
486
|
signal: owner.controller.signal,
|
|
485
|
-
isCurrent
|
|
487
|
+
isCurrent,
|
|
486
488
|
});
|
|
487
489
|
}
|
|
488
490
|
|
|
@@ -502,6 +504,9 @@ async function showSubagentSettings(
|
|
|
502
504
|
return;
|
|
503
505
|
}
|
|
504
506
|
const generation = owner.generation;
|
|
507
|
+
const isCurrent = () => generation === owner.generation && !owner.controller.signal.aborted;
|
|
508
|
+
const { defineMenu, runMenu } = await import("@narumitw/pi-tui-kit");
|
|
509
|
+
if (!isCurrent()) return;
|
|
505
510
|
type SettingsAction =
|
|
506
511
|
| "set-completion"
|
|
507
512
|
| "set-consult-resources"
|
|
@@ -521,7 +526,7 @@ async function showSubagentSettings(
|
|
|
521
526
|
await runMenu(ctx, menu, {
|
|
522
527
|
getState: () => undefined,
|
|
523
528
|
signal: owner.controller.signal,
|
|
524
|
-
isCurrent
|
|
529
|
+
isCurrent,
|
|
525
530
|
});
|
|
526
531
|
}
|
|
527
532
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultResourceLoader,
|
|
3
|
+
getAgentDir,
|
|
4
|
+
SettingsManager,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ConsultResourcePolicy } from "./agents.js";
|
|
7
|
+
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
8
|
+
import { assertPiPromptSourcesAreReadableFiles } from "./prompt-source-safety.js";
|
|
9
|
+
import type { ChildLaunchPolicy } from "./runner.js";
|
|
10
|
+
|
|
11
|
+
const MINIMAL_CONSULT_SYSTEM_PROMPT =
|
|
12
|
+
"You are a read-only consultation assistant. Analyze the delegated task using only executor-provided capabilities and return a grounded answer.";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve Pi-owned prompt files without loading target settings, packages, or extensions.
|
|
16
|
+
* The child still owns context-file, skill, and prompt-template loading according to this policy.
|
|
17
|
+
*/
|
|
18
|
+
export async function resolveConsultResourceLaunchPolicy(
|
|
19
|
+
policy: ConsultResourcePolicy,
|
|
20
|
+
projectTrusted: boolean,
|
|
21
|
+
cwd: string,
|
|
22
|
+
): Promise<ChildLaunchPolicy> {
|
|
23
|
+
if (policy === "none") {
|
|
24
|
+
return {
|
|
25
|
+
disableExtensions: true,
|
|
26
|
+
disableSkills: true,
|
|
27
|
+
disablePromptTemplates: true,
|
|
28
|
+
disableContextFiles: true,
|
|
29
|
+
projectTrust: false,
|
|
30
|
+
baseSystemPrompt: MINIMAL_CONSULT_SYSTEM_PROMPT,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const agentDir = getAgentDir();
|
|
35
|
+
assertPiPromptSourcesAreReadableFiles(cwd, agentDir, projectTrusted, [
|
|
36
|
+
"SYSTEM.md",
|
|
37
|
+
"APPEND_SYSTEM.md",
|
|
38
|
+
]);
|
|
39
|
+
const loader = new DefaultResourceLoader({
|
|
40
|
+
cwd,
|
|
41
|
+
agentDir,
|
|
42
|
+
settingsManager: SettingsManager.inMemory({}, { projectTrusted }),
|
|
43
|
+
noExtensions: true,
|
|
44
|
+
noSkills: true,
|
|
45
|
+
noPromptTemplates: true,
|
|
46
|
+
noThemes: true,
|
|
47
|
+
noContextFiles: true,
|
|
48
|
+
});
|
|
49
|
+
await loader.reload();
|
|
50
|
+
|
|
51
|
+
const discoveredSystemPrompt = loader.getSystemPrompt();
|
|
52
|
+
const baseSystemPrompt = discoveredSystemPrompt
|
|
53
|
+
? truncateUtf8(discoveredSystemPrompt, DEFAULT_MAX_CONTEXT_BYTES).text
|
|
54
|
+
: undefined;
|
|
55
|
+
const appendSystemPromptPaths = loader
|
|
56
|
+
.getAppendSystemPromptSources()
|
|
57
|
+
.map((source) => source.path);
|
|
58
|
+
const shared = {
|
|
59
|
+
disableExtensions: true,
|
|
60
|
+
disableContextFiles: !projectTrusted,
|
|
61
|
+
projectTrust: projectTrusted,
|
|
62
|
+
baseSystemPrompt,
|
|
63
|
+
appendSystemPromptPaths:
|
|
64
|
+
appendSystemPromptPaths.length > 0 ? appendSystemPromptPaths : undefined,
|
|
65
|
+
};
|
|
66
|
+
if (policy === "project-context") {
|
|
67
|
+
return {
|
|
68
|
+
...shared,
|
|
69
|
+
disableSkills: true,
|
|
70
|
+
disablePromptTemplates: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return shared;
|
|
74
|
+
}
|
package/src/consult.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
4
3
|
import { StringEnum, type Usage } from "@earendil-works/pi-ai";
|
|
5
4
|
import {
|
|
5
|
+
CONFIG_DIR_NAME,
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionContext,
|
|
8
|
-
getAgentDir,
|
|
9
8
|
type ToolDefinition,
|
|
10
9
|
} from "@earendil-works/pi-coding-agent";
|
|
11
10
|
import { type Static, Type } from "typebox";
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
} from "./agents.js";
|
|
23
22
|
import { resolveConsultTools } from "./consult-policy.js";
|
|
24
23
|
import { renderConsultCall, renderConsultResult } from "./consult-render.js";
|
|
24
|
+
import { resolveConsultResourceLaunchPolicy } from "./consult-resources.js";
|
|
25
25
|
import {
|
|
26
26
|
assertConsultationTargetAllowed,
|
|
27
27
|
type ResolvedSubagentTarget,
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
33
33
|
DEFAULT_MAX_STDERR_BYTES,
|
|
34
34
|
MAX_SUBAGENT_TIMEOUT_MS,
|
|
35
|
-
truncateUtf8,
|
|
36
35
|
} from "./limits.js";
|
|
37
36
|
import {
|
|
38
37
|
type ChildLaunchPolicy,
|
|
@@ -88,6 +87,7 @@ export interface RegisterSubagentConsultOptions {
|
|
|
88
87
|
getSettings(): SubagentSettings | undefined;
|
|
89
88
|
runChild?: (request: ConsultChildRequest) => Promise<SingleResult>;
|
|
90
89
|
invocationOverride?: { command: string; argsPrefix?: string[] };
|
|
90
|
+
resolveResourceLaunchPolicy?: typeof resolveConsultResourceLaunchPolicy;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export interface ConsultProgressActivity {
|
|
@@ -159,8 +159,6 @@ const READ_ONLY_INSTRUCTION = [
|
|
|
159
159
|
"If the task asks for implementation, return analysis or instructions instead of claiming changes.",
|
|
160
160
|
].join("\n");
|
|
161
161
|
|
|
162
|
-
const MINIMAL_CONSULT_SYSTEM_PROMPT =
|
|
163
|
-
"You are a read-only consultation assistant. Analyze the delegated task using only executor-provided capabilities and return a grounded answer.";
|
|
164
162
|
const MAX_UNKNOWN_AGENT_NAME_BYTES = 128;
|
|
165
163
|
|
|
166
164
|
export function registerSubagentConsult(
|
|
@@ -169,7 +167,7 @@ export function registerSubagentConsult(
|
|
|
169
167
|
): (catalog: string) => void {
|
|
170
168
|
let generation = 0;
|
|
171
169
|
const active = new Set<AbortController>();
|
|
172
|
-
const
|
|
170
|
+
const activeWork = new Set<Promise<unknown>>();
|
|
173
171
|
const cancelActive = (reason: string) => {
|
|
174
172
|
generation++;
|
|
175
173
|
for (const controller of active) {
|
|
@@ -177,14 +175,12 @@ export function registerSubagentConsult(
|
|
|
177
175
|
}
|
|
178
176
|
active.clear();
|
|
179
177
|
};
|
|
180
|
-
const
|
|
178
|
+
const cancelAndWaitForWork = async (reason: string) => {
|
|
181
179
|
cancelActive(reason);
|
|
182
|
-
await Promise.allSettled([...
|
|
180
|
+
await Promise.allSettled([...activeWork]);
|
|
183
181
|
};
|
|
184
|
-
pi.on("session_start", () =>
|
|
185
|
-
pi.on("session_shutdown", () =>
|
|
186
|
-
cancelAndWaitForChildren("Subagent consultation session shut down"),
|
|
187
|
-
);
|
|
182
|
+
pi.on("session_start", () => cancelAndWaitForWork("Subagent consultation session replaced"));
|
|
183
|
+
pi.on("session_shutdown", () => cancelAndWaitForWork("Subagent consultation session shut down"));
|
|
188
184
|
|
|
189
185
|
const baseDescription = () =>
|
|
190
186
|
`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.`;
|
|
@@ -217,11 +213,11 @@ export function registerSubagentConsult(
|
|
|
217
213
|
onUpdate?.(partial);
|
|
218
214
|
},
|
|
219
215
|
() => ownerGeneration === generation,
|
|
220
|
-
(
|
|
221
|
-
|
|
222
|
-
void
|
|
223
|
-
() =>
|
|
224
|
-
() =>
|
|
216
|
+
(work) => {
|
|
217
|
+
activeWork.add(work);
|
|
218
|
+
void work.then(
|
|
219
|
+
() => activeWork.delete(work),
|
|
220
|
+
() => activeWork.delete(work),
|
|
225
221
|
);
|
|
226
222
|
},
|
|
227
223
|
);
|
|
@@ -334,7 +330,7 @@ async function executeConsult(
|
|
|
334
330
|
options: RegisterSubagentConsultOptions,
|
|
335
331
|
emitUpdate: (partial: AgentToolResult<ConsultDetails>) => void,
|
|
336
332
|
isCurrent: () => boolean,
|
|
337
|
-
|
|
333
|
+
trackWork: (work: Promise<unknown>) => void,
|
|
338
334
|
): Promise<AgentToolResult<ConsultDetails>> {
|
|
339
335
|
if (
|
|
340
336
|
(operation.agentScope === "project" || operation.agentScope === "both") &&
|
|
@@ -360,7 +356,10 @@ async function executeConsult(
|
|
|
360
356
|
`Available agents for agentScope "${operation.agentScope}": ${formatAvailableConsultAgents(discovery)}`,
|
|
361
357
|
);
|
|
362
358
|
}
|
|
363
|
-
const
|
|
359
|
+
const setupWork = resolveConsultSetup(operation, agent, settings, target, options);
|
|
360
|
+
trackWork(setupWork);
|
|
361
|
+
const setup = await setupWork;
|
|
362
|
+
assertCurrentRequest(signal, isCurrent);
|
|
364
363
|
|
|
365
364
|
if (agent.source === "project" && operation.confirmProjectAgents) {
|
|
366
365
|
if (!ctx.hasUI) {
|
|
@@ -370,7 +369,7 @@ async function executeConsult(
|
|
|
370
369
|
}
|
|
371
370
|
const approved = await ctx.ui.confirm(
|
|
372
371
|
"Run project-local read-only agent?",
|
|
373
|
-
`Agent: ${safeTerminalLine(agent.name, 256)}\nSource: ${safeTerminalLine(path.posix.join(
|
|
372
|
+
`Agent: ${safeTerminalLine(agent.name, 256)}\nSource: ${safeTerminalLine(path.posix.join(CONFIG_DIR_NAME, "agents", path.basename(agent.filePath)))}`,
|
|
374
373
|
);
|
|
375
374
|
assertCurrentRequest(signal, isCurrent);
|
|
376
375
|
if (!approved) {
|
|
@@ -400,7 +399,7 @@ async function executeConsult(
|
|
|
400
399
|
emitUpdate(consultUpdate(result, setup.details));
|
|
401
400
|
},
|
|
402
401
|
});
|
|
403
|
-
|
|
402
|
+
trackWork(child);
|
|
404
403
|
const result = await child;
|
|
405
404
|
if (!isCurrent()) throw abortError("Subagent consultation owner was replaced");
|
|
406
405
|
if (result.aborted && !result.processStarted) {
|
|
@@ -432,18 +431,17 @@ async function executeConsult(
|
|
|
432
431
|
};
|
|
433
432
|
}
|
|
434
433
|
|
|
435
|
-
function resolveConsultSetup(
|
|
434
|
+
async function resolveConsultSetup(
|
|
436
435
|
operation: ReturnType<typeof validateConsultParams>,
|
|
437
436
|
agent: AgentConfig,
|
|
438
437
|
settings: SubagentSettings | undefined,
|
|
439
438
|
target: ResolvedSubagentTarget,
|
|
439
|
+
options: RegisterSubagentConsultOptions,
|
|
440
440
|
) {
|
|
441
441
|
const requestedResourcePolicy = settings?.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY;
|
|
442
442
|
const resourcePolicy = target.trust.projectTrusted ? requestedResourcePolicy : "none";
|
|
443
443
|
const effectiveTools = resolveConsultTools(agent.tools);
|
|
444
444
|
const projectTrusted = target.trust.projectTrusted;
|
|
445
|
-
const launchPolicy = resourceLaunchPolicy(resourcePolicy, projectTrusted, target.cwd);
|
|
446
|
-
launchPolicy.tools = effectiveTools;
|
|
447
445
|
const thinkingLevel = resolveSubagentThinkingLevel([agent], agent.name, operation.thinkingLevel);
|
|
448
446
|
const timeoutMs = operation.timeoutMs ?? agent.timeoutMs ?? resolveDefaultSubagentTimeoutMs();
|
|
449
447
|
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_SUBAGENT_TIMEOUT_MS) {
|
|
@@ -451,6 +449,10 @@ function resolveConsultSetup(
|
|
|
451
449
|
`Subagent consultation timeout must be between 1 and ${MAX_SUBAGENT_TIMEOUT_MS}ms`,
|
|
452
450
|
);
|
|
453
451
|
}
|
|
452
|
+
const resolveResources =
|
|
453
|
+
options.resolveResourceLaunchPolicy ?? resolveConsultResourceLaunchPolicy;
|
|
454
|
+
const launchPolicy = await resolveResources(resourcePolicy, projectTrusted, target.cwd);
|
|
455
|
+
launchPolicy.tools = effectiveTools;
|
|
454
456
|
const childAgent: AgentConfig = {
|
|
455
457
|
...agent,
|
|
456
458
|
tools: effectiveTools,
|
|
@@ -625,95 +627,6 @@ function emptyProgressUsage(): ConsultProgress["usage"] {
|
|
|
625
627
|
};
|
|
626
628
|
}
|
|
627
629
|
|
|
628
|
-
function resourceLaunchPolicy(
|
|
629
|
-
policy: ConsultResourcePolicy,
|
|
630
|
-
projectTrusted: boolean,
|
|
631
|
-
workspace: string,
|
|
632
|
-
): ChildLaunchPolicy {
|
|
633
|
-
if (policy === "none") {
|
|
634
|
-
return {
|
|
635
|
-
disableExtensions: true,
|
|
636
|
-
disableSkills: true,
|
|
637
|
-
disablePromptTemplates: true,
|
|
638
|
-
disableContextFiles: true,
|
|
639
|
-
projectTrust: false,
|
|
640
|
-
baseSystemPrompt: MINIMAL_CONSULT_SYSTEM_PROMPT,
|
|
641
|
-
};
|
|
642
|
-
}
|
|
643
|
-
const baseSystemPrompt = discoverSystemPrompt(workspace, projectTrusted);
|
|
644
|
-
if (policy === "project-context") {
|
|
645
|
-
return {
|
|
646
|
-
disableExtensions: true,
|
|
647
|
-
disableSkills: true,
|
|
648
|
-
disablePromptTemplates: true,
|
|
649
|
-
disableContextFiles: !projectTrusted,
|
|
650
|
-
projectTrust: projectTrusted,
|
|
651
|
-
baseSystemPrompt,
|
|
652
|
-
};
|
|
653
|
-
}
|
|
654
|
-
return {
|
|
655
|
-
disableExtensions: true,
|
|
656
|
-
disableContextFiles: !projectTrusted,
|
|
657
|
-
projectTrust: projectTrusted,
|
|
658
|
-
baseSystemPrompt,
|
|
659
|
-
appendSystemPromptPaths: discoverAppendSystemPrompts(workspace, projectTrusted),
|
|
660
|
-
};
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
function discoverSystemPrompt(workspace: string, projectTrusted: boolean): string | undefined {
|
|
664
|
-
const candidates = [
|
|
665
|
-
...(projectTrusted ? [path.join(workspace, ".pi", "SYSTEM.md")] : []),
|
|
666
|
-
path.join(getAgentDir(), "SYSTEM.md"),
|
|
667
|
-
];
|
|
668
|
-
for (const candidate of candidates) {
|
|
669
|
-
const prompt = readBoundedOptionalPrompt(candidate);
|
|
670
|
-
if (prompt !== undefined) return prompt;
|
|
671
|
-
}
|
|
672
|
-
return undefined;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function readBoundedOptionalPrompt(filePath: string): string | undefined {
|
|
676
|
-
let descriptor: number | undefined;
|
|
677
|
-
try {
|
|
678
|
-
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
679
|
-
if (!fs.fstatSync(descriptor).isFile()) return undefined;
|
|
680
|
-
const buffer = Buffer.alloc(DEFAULT_MAX_CONTEXT_BYTES + 4);
|
|
681
|
-
let bytesRead = 0;
|
|
682
|
-
while (bytesRead < buffer.length) {
|
|
683
|
-
const next = fs.readSync(descriptor, buffer, bytesRead, buffer.length - bytesRead, bytesRead);
|
|
684
|
-
if (next === 0) break;
|
|
685
|
-
bytesRead += next;
|
|
686
|
-
}
|
|
687
|
-
return truncateUtf8(buffer.subarray(0, bytesRead).toString("utf8"), DEFAULT_MAX_CONTEXT_BYTES)
|
|
688
|
-
.text;
|
|
689
|
-
} catch {
|
|
690
|
-
// Match Pi resource discovery: an unreadable optional prompt is skipped.
|
|
691
|
-
return undefined;
|
|
692
|
-
} finally {
|
|
693
|
-
if (descriptor !== undefined) {
|
|
694
|
-
try {
|
|
695
|
-
fs.closeSync(descriptor);
|
|
696
|
-
} catch {
|
|
697
|
-
// Optional prompt cleanup must not make consultation discovery fail.
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function discoverAppendSystemPrompts(cwd: string, projectTrusted: boolean): string[] {
|
|
704
|
-
const candidates = [
|
|
705
|
-
path.join(getAgentDir(), "APPEND_SYSTEM.md"),
|
|
706
|
-
...(projectTrusted ? [path.join(cwd, ".pi", "APPEND_SYSTEM.md")] : []),
|
|
707
|
-
];
|
|
708
|
-
return candidates.filter((candidate) => {
|
|
709
|
-
try {
|
|
710
|
-
return fs.statSync(candidate).isFile();
|
|
711
|
-
} catch {
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
|
|
717
630
|
function projectChildResult(result: SingleResult): Record<string, unknown> {
|
|
718
631
|
return {
|
|
719
632
|
exitCode: result.exitCode,
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
DefaultResourceLoader,
|
|
3
|
+
type AgentSessionServices,
|
|
6
4
|
getAgentDir,
|
|
7
5
|
type ModelRegistry,
|
|
6
|
+
type ModelRuntime,
|
|
8
7
|
SessionManager,
|
|
9
8
|
SettingsManager,
|
|
10
9
|
} from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
12
|
-
type AgentConfig,
|
|
13
|
-
discoverAgents,
|
|
14
|
-
isThinkingLevel,
|
|
15
|
-
type SubagentThinkingLevel,
|
|
16
|
-
} from "./agents.js";
|
|
10
|
+
import { type AgentConfig, discoverAgents, type SubagentThinkingLevel } from "./agents.js";
|
|
17
11
|
import { redactPrivateText } from "./context.js";
|
|
18
12
|
import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
19
13
|
import { DEFAULT_MAX_CONTEXT_BYTES, DEFAULT_MAX_OUTPUT_BYTES, truncateUtf8 } from "./limits.js";
|
|
14
|
+
import { assertPiPromptSourcesAreReadableFiles } from "./prompt-source-safety.js";
|
|
20
15
|
import type { AgentTurn, ManagedAgent, TurnOutcome } from "./registry.js";
|
|
21
16
|
import { readSubagentSettings } from "./settings.js";
|
|
22
17
|
import type { SubagentTransport } from "./transport.js";
|
|
@@ -32,9 +27,20 @@ interface ChildModelRuntime {
|
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
interface CodingAgentRuntimeModule {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
createAgentSessionFromServices?: typeof import("@earendil-works/pi-coding-agent").createAgentSessionFromServices;
|
|
31
|
+
createAgentSessionServices?: typeof import("@earendil-works/pi-coding-agent").createAgentSessionServices;
|
|
32
|
+
resolveCliModel?: typeof import("@earendil-works/pi-coding-agent").resolveCliModel;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CoreModelSupport {
|
|
36
|
+
modelRuntime: ModelRuntime;
|
|
37
|
+
resolveCliModel: typeof import("@earendil-works/pi-coding-agent").resolveCliModel;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface CoreSessionSupport {
|
|
41
|
+
createAgentSessionFromServices: typeof import("@earendil-works/pi-coding-agent").createAgentSessionFromServices;
|
|
42
|
+
createAgentSessionServices: typeof import("@earendil-works/pi-coding-agent").createAgentSessionServices;
|
|
43
|
+
resolveCliModel: typeof import("@earendil-works/pi-coding-agent").resolveCliModel;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
interface RegisteredProviderRegistry {
|
|
@@ -335,39 +341,36 @@ export async function createSdkChildSession(
|
|
|
335
341
|
options: ChildSessionCreateOptions,
|
|
336
342
|
): Promise<ChildSession> {
|
|
337
343
|
const agentDir = getAgentDir();
|
|
338
|
-
const
|
|
344
|
+
const projectTrusted =
|
|
345
|
+
options.agent.target?.trust.projectTrusted ??
|
|
346
|
+
(options.agent.agentScope === "project" || options.agent.agentScope === "both");
|
|
347
|
+
const { services, support: coreSupport } = await prepareInProcessServices(
|
|
339
348
|
options.agent.cwd,
|
|
340
349
|
agentDir,
|
|
341
350
|
options.agentConfig.systemPrompt,
|
|
342
|
-
|
|
343
|
-
(options.agent.agentScope === "project" || options.agent.agentScope === "both"),
|
|
351
|
+
projectTrusted,
|
|
344
352
|
);
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
resolved.model,
|
|
349
|
-
agentDir,
|
|
353
|
+
copyRegisteredProviders(
|
|
354
|
+
options.modelRegistry as unknown as RegisteredProviderRegistry,
|
|
355
|
+
services.modelRuntime as unknown as ChildModelRuntime,
|
|
350
356
|
);
|
|
351
|
-
const
|
|
352
|
-
modelRuntime
|
|
357
|
+
const modelSupport: CoreModelSupport = {
|
|
358
|
+
modelRuntime: services.modelRuntime,
|
|
359
|
+
resolveCliModel: coreSupport.resolveCliModel,
|
|
360
|
+
};
|
|
361
|
+
const resolved = await resolveChildModel(options, modelSupport);
|
|
362
|
+
await transferChildModelAuth(options.modelRegistry, resolved.model, services.modelRuntime);
|
|
363
|
+
const model = resolved.model;
|
|
353
364
|
const sessionManager = SessionManager.inMemory(options.agent.cwd);
|
|
354
365
|
seedChildSessionManager(sessionManager, options, model);
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
366
|
+
const created = await coreSupport.createAgentSessionFromServices({
|
|
367
|
+
services,
|
|
368
|
+
sessionManager,
|
|
358
369
|
model,
|
|
359
370
|
thinkingLevel: resolved.thinkingLevel,
|
|
360
|
-
resourceLoader,
|
|
361
|
-
settingsManager,
|
|
362
|
-
sessionManager,
|
|
363
371
|
tools: options.tools,
|
|
364
372
|
noTools: options.tools?.length === 0 ? "all" : undefined,
|
|
365
|
-
};
|
|
366
|
-
if (modelRuntime) sessionOptions.modelRuntime = modelRuntime;
|
|
367
|
-
else sessionOptions.modelRegistry = options.modelRegistry;
|
|
368
|
-
const created = await createAgentSession(
|
|
369
|
-
sessionOptions as NonNullable<Parameters<typeof createAgentSession>[0]>,
|
|
370
|
-
);
|
|
373
|
+
});
|
|
371
374
|
const session = created.session;
|
|
372
375
|
if (options.tools !== undefined) {
|
|
373
376
|
const active = session.getActiveToolNames();
|
|
@@ -397,25 +400,31 @@ export async function createSdkChildSession(
|
|
|
397
400
|
};
|
|
398
401
|
}
|
|
399
402
|
|
|
400
|
-
async function
|
|
401
|
-
modelRegistry: ModelRegistry,
|
|
402
|
-
model: Model<Api>,
|
|
403
|
-
agentDir: string,
|
|
404
|
-
): Promise<ChildModelRuntime | undefined> {
|
|
403
|
+
async function loadCoreSessionSupport(): Promise<CoreSessionSupport | undefined> {
|
|
405
404
|
const codingAgentModule = (await import(
|
|
406
405
|
"@earendil-works/pi-coding-agent"
|
|
407
406
|
)) as unknown as CodingAgentRuntimeModule;
|
|
408
|
-
if (
|
|
407
|
+
if (
|
|
408
|
+
!codingAgentModule.createAgentSessionFromServices ||
|
|
409
|
+
!codingAgentModule.createAgentSessionServices ||
|
|
410
|
+
!codingAgentModule.resolveCliModel
|
|
411
|
+
) {
|
|
412
|
+
return undefined;
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
createAgentSessionFromServices: codingAgentModule.createAgentSessionFromServices,
|
|
416
|
+
createAgentSessionServices: codingAgentModule.createAgentSessionServices,
|
|
417
|
+
resolveCliModel: codingAgentModule.resolveCliModel,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
409
420
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
copyRegisteredProviders(registeredProviders, modelRuntime);
|
|
421
|
+
async function transferChildModelAuth(
|
|
422
|
+
modelRegistry: ModelRegistry,
|
|
423
|
+
model: Model<Api>,
|
|
424
|
+
modelRuntime: ModelRuntime,
|
|
425
|
+
): Promise<void> {
|
|
416
426
|
const auth = await modelRegistry.getApiKeyAndHeaders(model);
|
|
417
427
|
if (auth.ok && auth.apiKey) await modelRuntime.setRuntimeApiKey(model.provider, auth.apiKey);
|
|
418
|
-
return modelRuntime;
|
|
419
428
|
}
|
|
420
429
|
|
|
421
430
|
export function copyRegisteredProviders(
|
|
@@ -433,20 +442,33 @@ export function copyRegisteredProviders(
|
|
|
433
442
|
}
|
|
434
443
|
}
|
|
435
444
|
|
|
436
|
-
export async function resolveChildModel(
|
|
445
|
+
export async function resolveChildModel(
|
|
446
|
+
options: ChildSessionCreateOptions,
|
|
447
|
+
support?: CoreModelSupport,
|
|
448
|
+
): Promise<{
|
|
437
449
|
model: Model<Api>;
|
|
438
450
|
thinkingLevel: SubagentThinkingLevel;
|
|
439
451
|
}> {
|
|
452
|
+
if (!support) throw unsupportedInProcessCoreError();
|
|
440
453
|
let model = options.parentRuntime.model;
|
|
441
454
|
let modelThinkingLevel: SubagentThinkingLevel | undefined;
|
|
442
|
-
if (options.agentConfig.model) {
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
455
|
+
if (options.agentConfig.model !== undefined) {
|
|
456
|
+
const requested = options.agentConfig.model.trim();
|
|
457
|
+
if (!requested) throw new Error("In-process subagent model cannot be empty");
|
|
458
|
+
const resolved = support.resolveCliModel({
|
|
459
|
+
cliModel: requested,
|
|
460
|
+
modelRuntime: support.modelRuntime,
|
|
461
|
+
});
|
|
462
|
+
if (resolved.error) throw new Error(resolved.error);
|
|
463
|
+
if (!resolved.model)
|
|
464
|
+
throw new Error(`Unable to resolve in-process subagent model ${requested}`);
|
|
465
|
+
model = resolved.model;
|
|
466
|
+
modelThinkingLevel = resolved.thinkingLevel;
|
|
446
467
|
}
|
|
447
468
|
if (!model) model = options.modelRegistry.getAvailable()[0];
|
|
448
469
|
if (!model)
|
|
449
470
|
throw new Error("No model with configured authentication is available for in-process subagent");
|
|
471
|
+
model = support.modelRuntime.getModel(model.provider, model.id) ?? model;
|
|
450
472
|
return {
|
|
451
473
|
model,
|
|
452
474
|
thinkingLevel:
|
|
@@ -457,68 +479,42 @@ export async function resolveChildModel(options: ChildSessionCreateOptions): Pro
|
|
|
457
479
|
};
|
|
458
480
|
}
|
|
459
481
|
|
|
460
|
-
function
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const requested = value.trim();
|
|
465
|
-
const separator = requested.lastIndexOf(":");
|
|
466
|
-
if (separator > 0) {
|
|
467
|
-
const suffix = requested.slice(separator + 1);
|
|
468
|
-
if (isThinkingLevel(suffix)) {
|
|
469
|
-
return { model: requested.slice(0, separator), thinkingLevel: suffix };
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return { model: requested };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function resolveConfiguredModel(value: string, modelRegistry: ModelRegistry): Model<Api> {
|
|
476
|
-
const requested = value.trim();
|
|
477
|
-
if (!requested) throw new Error("In-process subagent model cannot be empty");
|
|
478
|
-
const slash = requested.indexOf("/");
|
|
479
|
-
if (slash > 0) {
|
|
480
|
-
const exact = modelRegistry.find(requested.slice(0, slash), requested.slice(slash + 1));
|
|
481
|
-
if (exact) return exact;
|
|
482
|
-
}
|
|
483
|
-
const lowered = requested.toLowerCase();
|
|
484
|
-
const exactMatches = modelRegistry
|
|
485
|
-
.getAll()
|
|
486
|
-
.filter((model) => model.id.toLowerCase() === lowered || model.name.toLowerCase() === lowered);
|
|
487
|
-
if (exactMatches.length === 1) return exactMatches[0];
|
|
488
|
-
const partialMatches = modelRegistry
|
|
489
|
-
.getAll()
|
|
490
|
-
.filter(
|
|
491
|
-
(model) =>
|
|
492
|
-
model.id.toLowerCase().includes(lowered) || model.name.toLowerCase().includes(lowered),
|
|
493
|
-
);
|
|
494
|
-
if (partialMatches.length === 1) return partialMatches[0];
|
|
495
|
-
const displayedMatches = partialMatches
|
|
496
|
-
.slice(0, 8)
|
|
497
|
-
.map((model) => `${model.provider}/${model.id}`);
|
|
498
|
-
const remaining = partialMatches.length - displayedMatches.length;
|
|
499
|
-
const suffix =
|
|
500
|
-
displayedMatches.length > 0
|
|
501
|
-
? `; matches: ${displayedMatches.join(", ")}${remaining > 0 ? `, and ${remaining} more` : ""}`
|
|
502
|
-
: "";
|
|
503
|
-
throw new Error(`Unable to resolve in-process subagent model ${requested}${suffix}`);
|
|
482
|
+
function unsupportedInProcessCoreError(): Error {
|
|
483
|
+
return new Error(
|
|
484
|
+
'In-process subagents require Pi core createAgentSessionServices, createAgentSessionFromServices, and resolveCliModel support; set stateful.transport to "subprocess".',
|
|
485
|
+
);
|
|
504
486
|
}
|
|
505
487
|
|
|
506
|
-
|
|
488
|
+
async function prepareInProcessServices(
|
|
507
489
|
cwd: string,
|
|
508
490
|
agentDir: string,
|
|
509
491
|
agentSystemPrompt: string,
|
|
510
|
-
projectTrusted
|
|
511
|
-
): Promise<{
|
|
492
|
+
projectTrusted: boolean,
|
|
493
|
+
): Promise<{ services: AgentSessionServices; support: CoreSessionSupport }> {
|
|
494
|
+
assertPiPromptSourcesAreReadableFiles(cwd, agentDir, projectTrusted, ["SYSTEM.md"]);
|
|
512
495
|
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted });
|
|
513
|
-
const
|
|
496
|
+
const support = await loadCoreSessionSupport();
|
|
497
|
+
if (!support) throw unsupportedInProcessCoreError();
|
|
498
|
+
const services = await support.createAgentSessionServices({
|
|
514
499
|
cwd,
|
|
515
500
|
agentDir,
|
|
516
501
|
settingsManager,
|
|
517
|
-
|
|
518
|
-
|
|
502
|
+
resourceLoaderOptions: {
|
|
503
|
+
noExtensions: true,
|
|
504
|
+
appendSystemPrompt: agentSystemPrompt.trim() ? [agentSystemPrompt] : [],
|
|
505
|
+
},
|
|
519
506
|
});
|
|
520
|
-
|
|
521
|
-
|
|
507
|
+
return { services, support };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function createInProcessServices(
|
|
511
|
+
cwd: string,
|
|
512
|
+
agentDir: string,
|
|
513
|
+
agentSystemPrompt: string,
|
|
514
|
+
projectTrusted = false,
|
|
515
|
+
): Promise<AgentSessionServices> {
|
|
516
|
+
return (await prepareInProcessServices(cwd, agentDir, agentSystemPrompt, projectTrusted))
|
|
517
|
+
.services;
|
|
522
518
|
}
|
|
523
519
|
|
|
524
520
|
export function seedChildSessionManager(
|
package/src/inspect.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
CONFIG_DIR_NAME,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
8
|
import { type Static, Type } from "typebox";
|
|
5
9
|
import {
|
|
6
10
|
type AgentConfig,
|
|
@@ -270,7 +274,9 @@ function projectAgent(
|
|
|
270
274
|
scope: agent.source === "project" ? "project" : "user",
|
|
271
275
|
path:
|
|
272
276
|
agent.source === "project"
|
|
273
|
-
? safeTerminalLine(
|
|
277
|
+
? safeTerminalLine(
|
|
278
|
+
path.posix.join(CONFIG_DIR_NAME, "agents", path.basename(agent.filePath)),
|
|
279
|
+
)
|
|
274
280
|
: safeDisplayPath(agent.filePath, ctx.cwd),
|
|
275
281
|
model: agent.model ? boundedPrivateText(agent.model, 256) : undefined,
|
|
276
282
|
thinkingLevel: agent.thinkingLevel,
|
package/src/limits.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { DEFAULT_MAX_BYTES } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = DEFAULT_MAX_BYTES;
|
|
2
4
|
export const DEFAULT_MAX_STDERR_BYTES = 16 * 1024;
|
|
3
|
-
export const DEFAULT_MAX_CONTEXT_BYTES =
|
|
5
|
+
export const DEFAULT_MAX_CONTEXT_BYTES = DEFAULT_MAX_BYTES;
|
|
4
6
|
export const MAX_SUBAGENT_TIMEOUT_MS = 2_147_483_647;
|
|
5
7
|
export const DEFAULT_MAX_MESSAGES = 200;
|
|
6
8
|
export const TRUNCATION_MARKER = "\n… [truncated by pi-subagents]";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export type PiPromptFileName = "SYSTEM.md" | "APPEND_SYSTEM.md";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Prevent Pi's synchronous prompt loader from opening a selected directory, FIFO, socket, device,
|
|
9
|
+
* or unreadable source. Candidate order mirrors core only as a preflight guard; core still resolves
|
|
10
|
+
* and loads the prompt after this check.
|
|
11
|
+
*/
|
|
12
|
+
export function assertPiPromptSourcesAreReadableFiles(
|
|
13
|
+
cwd: string,
|
|
14
|
+
agentDir: string,
|
|
15
|
+
projectTrusted: boolean,
|
|
16
|
+
fileNames: readonly PiPromptFileName[],
|
|
17
|
+
): void {
|
|
18
|
+
for (const fileName of fileNames) {
|
|
19
|
+
const candidates = [
|
|
20
|
+
...(projectTrusted ? [path.join(cwd, CONFIG_DIR_NAME, fileName)] : []),
|
|
21
|
+
path.join(agentDir, fileName),
|
|
22
|
+
];
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
const status = promptSourceStatus(candidate);
|
|
25
|
+
if (status === "missing") continue;
|
|
26
|
+
if (status === "invalid") {
|
|
27
|
+
throw new Error(`Pi ${fileName} prompt source must be a readable regular file`);
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function promptSourceStatus(filePath: string): "missing" | "regular" | "invalid" {
|
|
35
|
+
let descriptor: number | undefined;
|
|
36
|
+
try {
|
|
37
|
+
descriptor = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
38
|
+
return fs.fstatSync(descriptor).isFile() ? "regular" : "invalid";
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const code =
|
|
41
|
+
error && typeof error === "object" && "code" in error
|
|
42
|
+
? (error as { code?: unknown }).code
|
|
43
|
+
: undefined;
|
|
44
|
+
return code === "ENOENT" || code === "ENOTDIR" ? "missing" : "invalid";
|
|
45
|
+
} finally {
|
|
46
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/safe-text.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { DEFAULT_MAX_LINES, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { redactPrivateText } from "./context.js";
|
|
5
5
|
import { DEFAULT_MAX_OUTPUT_BYTES, TRUNCATION_MARKER, truncateUtf8 } from "./limits.js";
|
|
6
6
|
|
|
7
|
-
export const DEFAULT_MAX_OUTPUT_LINES =
|
|
7
|
+
export const DEFAULT_MAX_OUTPUT_LINES = DEFAULT_MAX_LINES;
|
|
8
8
|
|
|
9
9
|
export function safeTerminalText(value: string): string {
|
|
10
10
|
return (
|
package/src/subagents.ts
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* Uses JSON mode to capture structured output from subagents.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
CONFIG_DIR_NAME,
|
|
17
|
+
type ExtensionAPI,
|
|
18
|
+
type ToolDefinition,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
20
|
import {
|
|
17
21
|
type ConsultationCwdPolicy,
|
|
18
22
|
type ConsultResourcePolicy,
|
|
@@ -137,7 +141,7 @@ function registerBlockingSubagent(
|
|
|
137
141
|
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
138
142
|
"Parallel mode may include an aggregator fan-in step that receives all task outputs. Use subagent_consult instead for one synchronous child that must be executor-constrained to read-only tools.",
|
|
139
143
|
'Default agent scope is "user" (from ~/.pi/agent/agents).',
|
|
140
|
-
|
|
144
|
+
`To enable project-local agents in ${CONFIG_DIR_NAME}/agents, pass agentScope: "both" (or "project") as a top-level argument for that call.`,
|
|
141
145
|
`Working-directory target policy: ${getSettings()?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY}. This controls launch targets and protected project resources, not filesystem access or sandboxing.`,
|
|
142
146
|
].join(" ");
|
|
143
147
|
const definition: ToolDefinition<typeof SubagentParams, SubagentDetails> = {
|