@zihanw/pi-forge 0.4.0-beta.1 → 0.4.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/CHANGELOG.md +37 -1
- package/PUBLIC_API.md +3 -26
- package/README.md +90 -601
- package/README.zh-CN.md +86 -585
- package/SUBAGENT_ADAPTER_CONTRACT.md +3 -197
- package/dist/forge-config.d.ts +80 -0
- package/dist/forge-config.d.ts.map +1 -1
- package/dist/forge-config.js +268 -18
- package/dist/forge-config.js.map +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -4
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.js +1 -1
- package/dist/profile-service.d.ts +1 -1
- package/dist/profile-service.d.ts.map +1 -1
- package/dist/profile-service.js +10 -5
- package/dist/profile-service.js.map +1 -1
- package/dist/runtime/subagent-runtime.d.ts +23 -8
- package/dist/runtime/subagent-runtime.d.ts.map +1 -1
- package/dist/runtime/subagent-runtime.js +283 -62
- package/dist/runtime/subagent-runtime.js.map +1 -1
- package/dist/storage.d.ts +1 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +15 -1
- package/dist/storage.js.map +1 -1
- package/dist/subagent/canonical.d.ts +19 -7
- package/dist/subagent/canonical.d.ts.map +1 -1
- package/dist/subagent/canonical.js +19 -47
- package/dist/subagent/canonical.js.map +1 -1
- package/dist/subagent/contract.d.ts +1 -2
- package/dist/subagent/contract.d.ts.map +1 -1
- package/dist/subagent/contract.js +1 -2
- package/dist/subagent/contract.js.map +1 -1
- package/dist/subagent/index.d.ts +4 -3
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js +4 -3
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/plan.d.ts +5 -1
- package/dist/subagent/plan.d.ts.map +1 -1
- package/dist/subagent/plan.js +28 -31
- package/dist/subagent/plan.js.map +1 -1
- package/dist/subagent/types.d.ts +62 -178
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/types.js +1 -1
- package/dist/subagent/types.js.map +1 -1
- package/dist/subagent/validation.d.ts +14 -14
- package/dist/subagent/validation.d.ts.map +1 -1
- package/dist/subagent/validation.js +52 -238
- package/dist/subagent/validation.js.map +1 -1
- package/dist/subagent-command.d.ts.map +1 -1
- package/dist/subagent-command.js +109 -16
- package/dist/subagent-command.js.map +1 -1
- package/dist/subagent-host.d.ts.map +1 -1
- package/dist/subagent-host.js +1 -0
- package/dist/subagent-host.js.map +1 -1
- package/dist/subagent-profile-tool.d.ts +25 -2
- package/dist/subagent-profile-tool.d.ts.map +1 -1
- package/dist/subagent-profile-tool.js +39 -8
- package/dist/subagent-profile-tool.js.map +1 -1
- package/dist/subagent-tool.d.ts +6 -3
- package/dist/subagent-tool.d.ts.map +1 -1
- package/dist/subagent-tool.js +85 -14
- package/dist/subagent-tool.js.map +1 -1
- package/dist/web-editor/client-script.generated.d.ts +1 -1
- package/dist/web-editor/client-script.generated.d.ts.map +1 -1
- package/dist/web-editor/client-script.generated.js +1 -1
- package/dist/web-editor/client-script.generated.js.map +1 -1
- package/dist/web-editor/client-styles.d.ts +2 -0
- package/dist/web-editor/client-styles.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.d.ts +2 -0
- package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.js +3 -0
- package/dist/web-editor/client-styles.generated.js.map +1 -0
- package/dist/web-editor/client-styles.js +2 -0
- package/dist/web-editor/client-styles.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -1
- package/dist/web-editor/page.js +11 -73
- package/dist/web-editor/page.js.map +1 -1
- package/dist/web-editor/server.d.ts.map +1 -1
- package/dist/web-editor/server.js +148 -0
- package/dist/web-editor/server.js.map +1 -1
- package/dist/web-editor/styles.d.ts.map +1 -1
- package/dist/web-editor/styles.js +60 -3
- package/dist/web-editor/styles.js.map +1 -1
- package/dist/web-editor/types.d.ts +79 -0
- package/dist/web-editor/types.d.ts.map +1 -1
- package/dist/web-host.d.ts +13 -2
- package/dist/web-host.d.ts.map +1 -1
- package/dist/web-host.js +301 -0
- package/dist/web-host.js.map +1 -1
- package/docs/README.md +41 -0
- package/docs/concepts/agent-profiles.md +60 -0
- package/docs/concepts/prompt-stacks.md +90 -0
- package/docs/design/README.md +17 -0
- package/docs/design/roadmap-0.4-archive.md +216 -0
- package/docs/design/subagents/design-review.md +220 -0
- package/docs/design/subagents/interface-design.md +274 -0
- package/docs/design/subagents/sdk-spike-findings.md +117 -0
- package/docs/development/complexity-review.md +86 -0
- package/docs/development/release.md +31 -0
- package/docs/development/roadmap.md +42 -0
- package/docs/development/setup.md +75 -0
- package/docs/getting-started.md +93 -0
- package/docs/guides/custom-macros-and-slots.md +68 -0
- package/docs/guides/debugging.md +39 -0
- package/docs/guides/delegation.md +99 -0
- package/docs/guides/sillytavern-import.md +47 -0
- package/docs/guides/use-cases.md +65 -0
- package/docs/guides/web-editor.md +75 -0
- package/docs/reference/commands.md +60 -0
- package/docs/reference/configuration.md +64 -0
- package/docs/reference/features.md +279 -0
- package/docs/reference/macros-and-slots.md +82 -0
- package/docs/reference/public-api.md +28 -0
- package/docs/reference/stack-schema.md +167 -0
- package/docs/reference/subagent-adapter.md +204 -0
- package/docs/zh-CN/README.md +37 -0
- package/docs/zh-CN/concepts/agent-profiles.md +44 -0
- package/docs/zh-CN/concepts/prompt-stacks.md +40 -0
- package/docs/zh-CN/getting-started.md +79 -0
- package/docs/zh-CN/guides/delegation.md +66 -0
- package/docs/zh-CN/guides/web-editor.md +45 -0
- package/docs/zh-CN/reference/commands.md +58 -0
- package/package.json +28 -14
- package/dist/subagent/backend-registry.d.ts +0 -75
- package/dist/subagent/backend-registry.d.ts.map +0 -1
- package/dist/subagent/backend-registry.js +0 -463
- package/dist/subagent/backend-registry.js.map +0 -1
- package/dist/subagent/diagnostics.d.ts +0 -3
- package/dist/subagent/diagnostics.d.ts.map +0 -1
- package/dist/subagent/diagnostics.js +0 -5
- package/dist/subagent/diagnostics.js.map +0 -1
- package/dist/subagent/pi-model-runtime.d.ts +0 -8
- package/dist/subagent/pi-model-runtime.d.ts.map +0 -1
- package/dist/subagent/pi-model-runtime.js +0 -22
- package/dist/subagent/pi-model-runtime.js.map +0 -1
- package/dist/subagent/pi-sdk-backend.d.ts +0 -23
- package/dist/subagent/pi-sdk-backend.d.ts.map +0 -1
- package/dist/subagent/pi-sdk-backend.js +0 -383
- package/dist/subagent/pi-sdk-backend.js.map +0 -1
- package/dist/subagent/pi-subprocess-backend.d.ts +0 -72
- package/dist/subagent/pi-subprocess-backend.d.ts.map +0 -1
- package/dist/subagent/pi-subprocess-backend.js +0 -756
- package/dist/subagent/pi-subprocess-backend.js.map +0 -1
- package/dist/subagent/subprocess-bridge.d.ts +0 -21
- package/dist/subagent/subprocess-bridge.d.ts.map +0 -1
- package/dist/subagent/subprocess-bridge.js +0 -87
- package/dist/subagent/subprocess-bridge.js.map +0 -1
- package/dist/subagent/subprocess-report.d.ts +0 -4
- package/dist/subagent/subprocess-report.d.ts.map +0 -1
- package/dist/subagent/subprocess-report.js +0 -55
- package/dist/subagent/subprocess-report.js.map +0 -1
- package/dist/subagent-contract.d.ts +0 -8
- package/dist/subagent-contract.d.ts.map +0 -1
- package/dist/subagent-contract.js +0 -8
- package/dist/subagent-contract.js.map +0 -1
- package/dist/web-editor/client/api.d.ts +0 -9
- package/dist/web-editor/client/api.d.ts.map +0 -1
- package/dist/web-editor/client/api.js +0 -26
- package/dist/web-editor/client/api.js.map +0 -1
- package/dist/web-editor/client/dom.d.ts +0 -13
- package/dist/web-editor/client/dom.d.ts.map +0 -1
- package/dist/web-editor/client/dom.js +0 -30
- package/dist/web-editor/client/dom.js.map +0 -1
- package/dist/web-editor/client/inspector.d.ts +0 -22
- package/dist/web-editor/client/inspector.d.ts.map +0 -1
- package/dist/web-editor/client/inspector.js +0 -226
- package/dist/web-editor/client/inspector.js.map +0 -1
- package/dist/web-editor/client/main.d.ts +0 -2
- package/dist/web-editor/client/main.d.ts.map +0 -1
- package/dist/web-editor/client/main.js +0 -1468
- package/dist/web-editor/client/main.js.map +0 -1
- package/dist/web-editor/client/policy-editor.d.ts +0 -16
- package/dist/web-editor/client/policy-editor.d.ts.map +0 -1
- package/dist/web-editor/client/policy-editor.js +0 -330
- package/dist/web-editor/client/policy-editor.js.map +0 -1
- package/dist/web-editor/client/regex-editor.d.ts +0 -19
- package/dist/web-editor/client/regex-editor.d.ts.map +0 -1
- package/dist/web-editor/client/regex-editor.js +0 -281
- package/dist/web-editor/client/regex-editor.js.map +0 -1
- package/dist/web-editor/client/types.d.ts +0 -60
- package/dist/web-editor/client/types.d.ts.map +0 -1
- package/dist/web-editor/client/types.js +0 -2
- package/dist/web-editor/client/types.js.map +0 -1
|
@@ -1,756 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename, dirname, join } from "node:path";
|
|
6
|
-
import { createInterface } from "node:readline";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
9
|
-
import { DefaultResourceLoader, SessionManager, SettingsManager, createAgentSession, } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { SUBAGENT_CONTRACT_VERSION, canonicalSubagentJson, negotiateSubagentTools, subagentPromptRuntimeFingerprint, } from "./contract.js";
|
|
11
|
-
import { modelRuntimeFromRegistry } from "./pi-model-runtime.js";
|
|
12
|
-
import { PI_FORGE_SUBPROCESS_REPORT_FD_ENV, sanitizeSubprocessReportValue, } from "./subprocess-report.js";
|
|
13
|
-
export const PI_SUBPROCESS_READONLY_BACKEND_ID = "pi-subprocess-readonly";
|
|
14
|
-
export const PI_FORGE_SUBPROCESS_INPUT_ENV = "PI_FORGE_SUBAGENT_BRIDGE_INPUT";
|
|
15
|
-
const MAX_STDOUT_BYTES = 8 * 1024 * 1024;
|
|
16
|
-
const MAX_REPORT_STREAM_BYTES = 8 * 1024 * 1024;
|
|
17
|
-
const MAX_STDERR_BYTES = 64 * 1024;
|
|
18
|
-
export const MAX_RETAINED_SUBPROCESS_REPORT_BYTES = 512 * 1024;
|
|
19
|
-
const ACCESS_CAPABILITIES = {
|
|
20
|
-
readOnlyMountIsolation: false,
|
|
21
|
-
readWriteMountIsolation: false,
|
|
22
|
-
symlinkSafeContainment: false,
|
|
23
|
-
processIsolation: false,
|
|
24
|
-
agentNetworkIsolation: false,
|
|
25
|
-
};
|
|
26
|
-
const READ_ONLY_TOOL_CATALOG = [
|
|
27
|
-
{ id: "pi.read", name: "read", description: "Read a file.", effects: ["filesystem-read"], adapterMapping: "pi:read" },
|
|
28
|
-
{ id: "pi.grep", name: "grep", description: "Search file contents.", effects: ["filesystem-read"], adapterMapping: "pi:grep" },
|
|
29
|
-
{ id: "pi.find", name: "find", description: "Find files by pattern.", effects: ["filesystem-read"], adapterMapping: "pi:find" },
|
|
30
|
-
{ id: "pi.ls", name: "ls", description: "List directory contents.", effects: ["filesystem-read"], adapterMapping: "pi:ls" },
|
|
31
|
-
];
|
|
32
|
-
export const PI_SUBPROCESS_READONLY_BACKEND_DESCRIPTOR = {
|
|
33
|
-
id: PI_SUBPROCESS_READONLY_BACKEND_ID,
|
|
34
|
-
version: "0.1.0",
|
|
35
|
-
capabilities: {
|
|
36
|
-
access: { ...ACCESS_CAPABILITIES },
|
|
37
|
-
limits: {
|
|
38
|
-
timeoutMs: ["host-abort"],
|
|
39
|
-
maxTurns: ["unsupported"],
|
|
40
|
-
tokenBudget: ["unsupported"],
|
|
41
|
-
maxOutputBytes: ["unsupported"],
|
|
42
|
-
},
|
|
43
|
-
cancellation: true,
|
|
44
|
-
mediaMimeTypes: [],
|
|
45
|
-
traceInspection: false,
|
|
46
|
-
artifactRetention: false,
|
|
47
|
-
remoteTransport: true,
|
|
48
|
-
promptRuntimeFidelity: "backend-assisted",
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
export class PiSubprocessBackend {
|
|
52
|
-
descriptor = structuredClone(PI_SUBPROCESS_READONLY_BACKEND_DESCRIPTOR);
|
|
53
|
-
#modelRegistry;
|
|
54
|
-
#modelRuntime;
|
|
55
|
-
#cwd;
|
|
56
|
-
#now;
|
|
57
|
-
#idFactory;
|
|
58
|
-
#invocationFactory;
|
|
59
|
-
#bridgePath;
|
|
60
|
-
#primed = new Map();
|
|
61
|
-
#active = new Map();
|
|
62
|
-
#reports = new Map();
|
|
63
|
-
constructor(options) {
|
|
64
|
-
this.#modelRegistry = options.modelRegistry;
|
|
65
|
-
this.#modelRuntime = options.modelRuntime ?? modelRuntimeFromRegistry(options.modelRegistry);
|
|
66
|
-
this.#cwd = options.cwd;
|
|
67
|
-
this.#now = options.now ?? (() => new Date());
|
|
68
|
-
this.#idFactory = options.idFactory ?? (() => `pi-subprocess-preflight:${randomUUID()}`);
|
|
69
|
-
this.#invocationFactory = options.invocationFactory ?? defaultPiInvocation;
|
|
70
|
-
this.#bridgePath = options.bridgePath ?? defaultBridgePath();
|
|
71
|
-
}
|
|
72
|
-
preflight(input) {
|
|
73
|
-
const diagnostics = [];
|
|
74
|
-
const access = input.request.access;
|
|
75
|
-
if (access.level !== "read-only" || access.workspaces.length !== 1 || access.workspaces[0]?.mode !== "read-only") {
|
|
76
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.access", "The subprocess backend requires one read-only workspace.", "access"));
|
|
77
|
-
}
|
|
78
|
-
if (access.workingDirectory?.workspaceHandle !== access.workspaces[0]?.handle || access.workingDirectory?.path !== ".") {
|
|
79
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.cwd", "The subprocess backend requires the requested workspace root as its working directory.", "access.workingDirectory"));
|
|
80
|
-
}
|
|
81
|
-
if (access.network !== "allow")
|
|
82
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.network", "A shared-user subprocess cannot honestly enforce network deny.", "access.network"));
|
|
83
|
-
if (access.allowProcess === true)
|
|
84
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.process", "The read-only subprocess does not expose process tools.", "access.allowProcess"));
|
|
85
|
-
if ((input.request.input.media?.length ?? 0) > 0)
|
|
86
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.media", "The first subprocess backend supports text tasks only.", "input.media"));
|
|
87
|
-
for (const name of ["maxTurns", "tokenBudget", "maxOutputBytes"]) {
|
|
88
|
-
const requirement = input.request.limits[name];
|
|
89
|
-
if (requirement?.enforcement === "required")
|
|
90
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.limit", `${name} cannot be enforced by the subprocess backend.`, `limits.${name}`));
|
|
91
|
-
else if (requirement)
|
|
92
|
-
diagnostics.push(warningDiagnostic("pi-subprocess.limit-ignored", `${name} is unsupported and will not be accepted.`, `limits.${name}`));
|
|
93
|
-
}
|
|
94
|
-
const model = this.#modelRegistry.find(input.snapshot.profile.model.provider, input.snapshot.profile.model.id);
|
|
95
|
-
if (!model)
|
|
96
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.model", `Unknown model: ${input.snapshot.profile.model.provider}/${input.snapshot.profile.model.id}`, "profile.model"));
|
|
97
|
-
else {
|
|
98
|
-
if (!this.#modelRegistry.hasConfiguredAuth(model))
|
|
99
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.auth", `Model ${model.provider}/${model.id} has no configured authentication.`, "profile.model"));
|
|
100
|
-
const effectiveThinking = clampThinkingLevel(model, input.snapshot.profile.thinkingLevel);
|
|
101
|
-
if (effectiveThinking !== input.snapshot.profile.thinkingLevel)
|
|
102
|
-
diagnostics.push(errorDiagnostic("pi-subprocess.thinking", `Model ${model.provider}/${model.id} would clamp thinking level ${input.snapshot.profile.thinkingLevel} to ${effectiveThinking}.`, "profile.thinkingLevel"));
|
|
103
|
-
}
|
|
104
|
-
const preflightId = this.#idFactory();
|
|
105
|
-
if (diagnostics.some((diagnostic) => diagnostic.level === "error") || !model) {
|
|
106
|
-
return { status: "rejected", preflightId, backend: structuredClone(this.descriptor), diagnostics };
|
|
107
|
-
}
|
|
108
|
-
const workspace = access.workspaces[0];
|
|
109
|
-
const limits = {};
|
|
110
|
-
if (input.request.limits.timeoutMs)
|
|
111
|
-
limits.timeoutMs = { value: input.request.limits.timeoutMs.value, enforcement: "host-abort" };
|
|
112
|
-
diagnostics.push(warningDiagnostic("pi-subprocess.shared-user", "Read-only is enforced by the model-visible tool allowlist, not by OS isolation; the subprocess retains the invoking user's permissions.", "access"));
|
|
113
|
-
return {
|
|
114
|
-
status: "accepted",
|
|
115
|
-
preflightId,
|
|
116
|
-
backend: structuredClone(this.descriptor),
|
|
117
|
-
model: { provider: model.provider, id: model.id },
|
|
118
|
-
thinkingLevel: input.snapshot.profile.thinkingLevel,
|
|
119
|
-
toolCatalog: structuredClone(READ_ONLY_TOOL_CATALOG),
|
|
120
|
-
access: {
|
|
121
|
-
level: "read-only",
|
|
122
|
-
mounts: [{ workspaceHandle: workspace.handle, mountId: "host-workspace", mode: "read-only" }],
|
|
123
|
-
workingDirectory: { mountId: "host-workspace", path: "." },
|
|
124
|
-
network: "allow",
|
|
125
|
-
process: false,
|
|
126
|
-
executionBoundary: "shared-user",
|
|
127
|
-
enforcement: { ...ACCESS_CAPABILITIES },
|
|
128
|
-
},
|
|
129
|
-
limits,
|
|
130
|
-
diagnostics,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
async prepare(input, context) {
|
|
134
|
-
if (this.#primed.has(input.preflight.preflightId))
|
|
135
|
-
throw new Error(`Pi subprocess preflight is already prepared: ${input.preflight.preflightId}`);
|
|
136
|
-
const model = this.#requireModel(input.preflight.model.provider, input.preflight.model.id);
|
|
137
|
-
const negotiated = negotiateSubagentTools(input.preflight.toolCatalog, input.snapshot.promptStack?.tools, input.request.access);
|
|
138
|
-
const effectiveToolNames = negotiated.effectiveToolNames;
|
|
139
|
-
const tempDir = mkdtempSync(join(tmpdir(), "pi-forge-subprocess-"));
|
|
140
|
-
const providerGate = deferred();
|
|
141
|
-
const preparationReady = deferred();
|
|
142
|
-
let prepared;
|
|
143
|
-
let session;
|
|
144
|
-
try {
|
|
145
|
-
const settingsManager = SettingsManager.create(this.#cwd, tempDir, { projectTrusted: true });
|
|
146
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
147
|
-
cwd: this.#cwd,
|
|
148
|
-
agentDir: tempDir,
|
|
149
|
-
settingsManager,
|
|
150
|
-
extensionFactories: [{
|
|
151
|
-
name: "pi-forge-subprocess-preparation",
|
|
152
|
-
factory: this.#compilerBridge(input, context, providerGate, preparationReady, (result) => { prepared = result; }),
|
|
153
|
-
}],
|
|
154
|
-
noExtensions: true,
|
|
155
|
-
noSkills: true,
|
|
156
|
-
noPromptTemplates: true,
|
|
157
|
-
noThemes: true,
|
|
158
|
-
noContextFiles: true,
|
|
159
|
-
});
|
|
160
|
-
await resourceLoader.reload();
|
|
161
|
-
const created = await createAgentSession({
|
|
162
|
-
cwd: this.#cwd,
|
|
163
|
-
agentDir: tempDir,
|
|
164
|
-
modelRuntime: this.#modelRuntime,
|
|
165
|
-
model,
|
|
166
|
-
thinkingLevel: input.preflight.thinkingLevel,
|
|
167
|
-
resourceLoader,
|
|
168
|
-
settingsManager,
|
|
169
|
-
sessionManager: SessionManager.inMemory(this.#cwd),
|
|
170
|
-
noTools: "all",
|
|
171
|
-
tools: effectiveToolNames,
|
|
172
|
-
});
|
|
173
|
-
session = created.session;
|
|
174
|
-
session.setActiveToolsByName(effectiveToolNames);
|
|
175
|
-
const execution = this.#startPreparation(session, input.request.input.text, preparationReady);
|
|
176
|
-
const result = await abortable(preparationReady.promise, context.signal);
|
|
177
|
-
if (!prepared || canonicalSubagentJson(prepared) !== canonicalSubagentJson(result))
|
|
178
|
-
throw new Error("Pi subprocess preparation bridge returned inconsistent host preparation.");
|
|
179
|
-
this.#primed.set(input.preflight.preflightId, {
|
|
180
|
-
preflightId: input.preflight.preflightId,
|
|
181
|
-
requestId: input.request.requestId,
|
|
182
|
-
result,
|
|
183
|
-
session,
|
|
184
|
-
tempDir,
|
|
185
|
-
providerGate,
|
|
186
|
-
execution,
|
|
187
|
-
disposed: false,
|
|
188
|
-
});
|
|
189
|
-
return structuredClone(result);
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
providerGate.reject(new Error("Subprocess dry preparation stopped before provider transport."));
|
|
193
|
-
if (session) {
|
|
194
|
-
await session.abort().catch(() => undefined);
|
|
195
|
-
session.dispose();
|
|
196
|
-
}
|
|
197
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
198
|
-
throw error;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
async execute(plan, context) {
|
|
202
|
-
const primed = this.#primed.get(plan.preflightId);
|
|
203
|
-
if (!primed || primed.requestId !== plan.requestId)
|
|
204
|
-
throw new Error("Pi subprocess execution has no matching prepared plan.");
|
|
205
|
-
if (primed.result.runtime.promptRuntimeFingerprint !== plan.promptRuntimeFingerprint
|
|
206
|
-
|| primed.result.preparation.systemPrompt !== plan.systemPrompt
|
|
207
|
-
|| canonicalSubagentJson(primed.result.preparation.messages) !== canonicalSubagentJson(plan.messages)) {
|
|
208
|
-
await this.#stopPreparation(primed);
|
|
209
|
-
throw new Error("Pi subprocess execution plan does not match its prepared prompt.");
|
|
210
|
-
}
|
|
211
|
-
await this.#stopPreparation(primed);
|
|
212
|
-
const effectiveToolNames = toolNamesForPlan(plan);
|
|
213
|
-
const report = createReport(plan, this.#cwd, effectiveToolNames, this.#now());
|
|
214
|
-
this.#reports.set(plan.runId, report);
|
|
215
|
-
context.onUpdate?.({ phase: "starting", message: `Starting ${plan.profile.profile.id} with ${effectiveToolNames.join(", ") || "no tools"}.`, details: reportSummary(report) });
|
|
216
|
-
let runDir;
|
|
217
|
-
try {
|
|
218
|
-
runDir = mkdtempSync(join(tmpdir(), "pi-forge-subprocess-run-"));
|
|
219
|
-
const inputPath = join(runDir, "bridge-input.json");
|
|
220
|
-
const systemPromptPath = join(runDir, "system-prompt.md");
|
|
221
|
-
const marker = `PI_FORGE_SUBAGENT_MARKER_${randomUUID()}`;
|
|
222
|
-
writeFileSync(inputPath, JSON.stringify({
|
|
223
|
-
marker,
|
|
224
|
-
systemPrompt: plan.systemPrompt,
|
|
225
|
-
messages: plan.messages,
|
|
226
|
-
model: plan.model,
|
|
227
|
-
effectiveToolNames,
|
|
228
|
-
}), { encoding: "utf8", mode: 0o600 });
|
|
229
|
-
writeFileSync(systemPromptPath, plan.systemPrompt, { encoding: "utf8", mode: 0o600 });
|
|
230
|
-
const piArgs = subprocessArguments(plan, effectiveToolNames, this.#bridgePath, systemPromptPath, marker);
|
|
231
|
-
const invocation = this.#invocationFactory(piArgs);
|
|
232
|
-
const terminal = await this.#runChild(invocation, plan, report, inputPath, context);
|
|
233
|
-
return terminal;
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
if (report.status === "running") {
|
|
237
|
-
report.status = context.signal.aborted ? "cancelled" : "failed";
|
|
238
|
-
report.finishedAt = this.#now().toISOString();
|
|
239
|
-
if (!context.signal.aborted)
|
|
240
|
-
report.errorMessage = error instanceof Error ? error.message : String(error);
|
|
241
|
-
}
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
finally {
|
|
245
|
-
if (runDir)
|
|
246
|
-
rmSync(runDir, { recursive: true, force: true });
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
async cancel(input) {
|
|
250
|
-
await this.#terminateRun(input.runId, input.reason);
|
|
251
|
-
}
|
|
252
|
-
async discard(preflightId) {
|
|
253
|
-
const primed = this.#primed.get(preflightId);
|
|
254
|
-
if (!primed)
|
|
255
|
-
return false;
|
|
256
|
-
await this.#stopPreparation(primed);
|
|
257
|
-
return true;
|
|
258
|
-
}
|
|
259
|
-
takeReport(runId) {
|
|
260
|
-
const report = this.#reports.get(runId);
|
|
261
|
-
if (!report)
|
|
262
|
-
return undefined;
|
|
263
|
-
this.#reports.delete(runId);
|
|
264
|
-
return sanitizePiSubprocessRunReport(report);
|
|
265
|
-
}
|
|
266
|
-
async dispose() {
|
|
267
|
-
for (const primed of [...this.#primed.values()])
|
|
268
|
-
await this.#stopPreparation(primed);
|
|
269
|
-
await Promise.all([...this.#active.keys()].map((runId) => this.#terminateRun(runId, "Subprocess backend disposed.")));
|
|
270
|
-
this.#primed.clear();
|
|
271
|
-
this.#active.clear();
|
|
272
|
-
this.#reports.clear();
|
|
273
|
-
}
|
|
274
|
-
async #terminateRun(runId, reason) {
|
|
275
|
-
const active = this.#active.get(runId);
|
|
276
|
-
if (!active)
|
|
277
|
-
return;
|
|
278
|
-
active.terminationReason ??= reason;
|
|
279
|
-
active.termination ??= terminateChild(active.child);
|
|
280
|
-
await active.termination;
|
|
281
|
-
}
|
|
282
|
-
#compilerBridge(input, context, providerGate, preparationReady, setPrepared) {
|
|
283
|
-
return (pi) => {
|
|
284
|
-
let prepared;
|
|
285
|
-
pi.on("before_agent_start", async (event) => {
|
|
286
|
-
try {
|
|
287
|
-
const runtime = this.#runtimeSnapshot(input.preflight, event.systemPrompt, event.systemPromptOptions);
|
|
288
|
-
prepared = await context.prepare(runtime);
|
|
289
|
-
setPrepared(prepared);
|
|
290
|
-
preparationReady.resolve(prepared);
|
|
291
|
-
return { systemPrompt: prepared.preparation.systemPrompt };
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
preparationReady.reject(error);
|
|
295
|
-
providerGate.reject(error);
|
|
296
|
-
throw error;
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
pi.on("context", () => {
|
|
300
|
-
if (!prepared)
|
|
301
|
-
throw new Error("Pi subprocess context event arrived before host preparation.");
|
|
302
|
-
return { messages: prepared.preparation.messages.map((message, index) => preparedMessageToAgentMessage(message, input.preflight, index)) };
|
|
303
|
-
});
|
|
304
|
-
pi.on("before_provider_request", async () => {
|
|
305
|
-
await providerGate.promise;
|
|
306
|
-
});
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
#runtimeSnapshot(preflight, baseSystemPrompt, options) {
|
|
310
|
-
const runtime = {
|
|
311
|
-
baseSystemPrompt,
|
|
312
|
-
options: {
|
|
313
|
-
customPrompt: options.customPrompt,
|
|
314
|
-
selectedTools: [...options.selectedTools ?? []],
|
|
315
|
-
toolSnippets: { ...options.toolSnippets ?? {} },
|
|
316
|
-
promptGuidelines: [...options.promptGuidelines ?? []],
|
|
317
|
-
appendSystemPrompt: options.appendSystemPrompt,
|
|
318
|
-
cwd: options.cwd,
|
|
319
|
-
contextFiles: [],
|
|
320
|
-
skills: [],
|
|
321
|
-
},
|
|
322
|
-
model: structuredClone(preflight.model),
|
|
323
|
-
preparedAt: this.#now().toISOString(),
|
|
324
|
-
fidelity: "backend-assisted",
|
|
325
|
-
};
|
|
326
|
-
return { ...runtime, promptRuntimeFingerprint: subagentPromptRuntimeFingerprint(runtime) };
|
|
327
|
-
}
|
|
328
|
-
async #startPreparation(session, task, preparationReady) {
|
|
329
|
-
try {
|
|
330
|
-
await session.prompt(task, { source: "extension" });
|
|
331
|
-
await session.waitForIdle();
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
preparationReady.reject(error);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
async #stopPreparation(primed) {
|
|
338
|
-
if (primed.disposed)
|
|
339
|
-
return;
|
|
340
|
-
primed.disposed = true;
|
|
341
|
-
this.#primed.delete(primed.preflightId);
|
|
342
|
-
void primed.session.abort();
|
|
343
|
-
primed.providerGate.reject(new Error("Subprocess dry preparation completed without provider transport."));
|
|
344
|
-
await primed.execution.catch(() => undefined);
|
|
345
|
-
primed.session.dispose();
|
|
346
|
-
rmSync(primed.tempDir, { recursive: true, force: true });
|
|
347
|
-
}
|
|
348
|
-
async #runChild(invocation, plan, report, inputPath, context) {
|
|
349
|
-
let stdoutBytes = 0;
|
|
350
|
-
let reportBytes = 0;
|
|
351
|
-
let settled = false;
|
|
352
|
-
const child = spawn(invocation.command, invocation.args, {
|
|
353
|
-
cwd: this.#cwd,
|
|
354
|
-
shell: false,
|
|
355
|
-
stdio: ["ignore", "pipe", "pipe", "pipe"],
|
|
356
|
-
env: {
|
|
357
|
-
...process.env,
|
|
358
|
-
[PI_FORGE_SUBPROCESS_INPUT_ENV]: inputPath,
|
|
359
|
-
[PI_FORGE_SUBPROCESS_REPORT_FD_ENV]: "3",
|
|
360
|
-
},
|
|
361
|
-
});
|
|
362
|
-
const active = { child };
|
|
363
|
-
this.#active.set(plan.runId, active);
|
|
364
|
-
const abort = () => { void this.#terminateRun(plan.runId, abortReason(context.signal)); };
|
|
365
|
-
if (context.signal.aborted)
|
|
366
|
-
abort();
|
|
367
|
-
else
|
|
368
|
-
context.signal.addEventListener("abort", abort, { once: true });
|
|
369
|
-
const failStream = (message) => {
|
|
370
|
-
if (!report.errorMessage)
|
|
371
|
-
report.errorMessage = message;
|
|
372
|
-
void this.#terminateRun(plan.runId);
|
|
373
|
-
};
|
|
374
|
-
const processLine = (line) => {
|
|
375
|
-
if (!line.trim())
|
|
376
|
-
return;
|
|
377
|
-
let event;
|
|
378
|
-
try {
|
|
379
|
-
event = JSON.parse(line);
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
failStream("Subprocess bridge emitted malformed report JSON.");
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (event.type === "message_end" && event.message) {
|
|
386
|
-
const message = sanitizePiSubprocessMessage(event.message);
|
|
387
|
-
captureAssistantReceipt(report, message);
|
|
388
|
-
appendReportMessage(report, message);
|
|
389
|
-
if (isRecord(message) && message.role === "toolResult") {
|
|
390
|
-
context.onUpdate?.({ phase: "tool-result", message: toolResultSummary(message), details: reportSummary(report) });
|
|
391
|
-
}
|
|
392
|
-
else {
|
|
393
|
-
context.onUpdate?.({ phase: "message", message: latestAssistantText(report.messages) || "Subagent completed a model turn.", details: reportSummary(report) });
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
else if (event.type === "tool_result_end" && event.message) {
|
|
397
|
-
const message = sanitizePiSubprocessMessage(event.message);
|
|
398
|
-
appendReportMessage(report, message);
|
|
399
|
-
context.onUpdate?.({ phase: "tool-result", message: toolResultSummary(message), details: reportSummary(report) });
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
|
-
if (!child.stdout) {
|
|
403
|
-
failStream("Subprocess text output channel was unavailable.");
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
child.stdout.on("data", (chunk) => {
|
|
407
|
-
stdoutBytes += chunk.length;
|
|
408
|
-
if (stdoutBytes > MAX_STDOUT_BYTES)
|
|
409
|
-
failStream(`Subprocess text output exceeded ${MAX_STDOUT_BYTES} bytes.`);
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
const reportStream = child.stdio[3];
|
|
413
|
-
if (!reportStream) {
|
|
414
|
-
failStream("Subprocess bridge report channel was unavailable.");
|
|
415
|
-
}
|
|
416
|
-
else {
|
|
417
|
-
reportStream.on("data", (chunk) => {
|
|
418
|
-
reportBytes += chunk.length;
|
|
419
|
-
if (reportBytes > MAX_REPORT_STREAM_BYTES)
|
|
420
|
-
failStream(`Sanitized subprocess report stream exceeded ${MAX_REPORT_STREAM_BYTES} bytes.`);
|
|
421
|
-
});
|
|
422
|
-
createInterface({ input: reportStream, crlfDelay: Infinity }).on("line", processLine);
|
|
423
|
-
}
|
|
424
|
-
if (!child.stderr) {
|
|
425
|
-
failStream("Subprocess error output channel was unavailable.");
|
|
426
|
-
}
|
|
427
|
-
else {
|
|
428
|
-
child.stderr.on("data", (chunk) => {
|
|
429
|
-
if (Buffer.byteLength(report.stderr, "utf8") >= MAX_STDERR_BYTES)
|
|
430
|
-
return;
|
|
431
|
-
report.stderr = appendBounded(report.stderr, chunk.toString("utf8"), MAX_STDERR_BYTES);
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
const outcome = await new Promise((resolve) => {
|
|
435
|
-
child.once("error", (error) => resolve({ code: null, signal: null, spawnError: error }));
|
|
436
|
-
child.once("close", (code, signal) => resolve({ code, signal }));
|
|
437
|
-
});
|
|
438
|
-
if (settled)
|
|
439
|
-
throw new Error("Subprocess completion settled more than once.");
|
|
440
|
-
settled = true;
|
|
441
|
-
context.signal.removeEventListener("abort", abort);
|
|
442
|
-
this.#active.delete(plan.runId);
|
|
443
|
-
report.exitCode = outcome.code ?? undefined;
|
|
444
|
-
report.signal = outcome.signal ?? undefined;
|
|
445
|
-
report.finishedAt = this.#now().toISOString();
|
|
446
|
-
if (outcome.spawnError)
|
|
447
|
-
report.errorMessage = outcome.spawnError.message;
|
|
448
|
-
const output = latestAssistantText(report.messages);
|
|
449
|
-
const common = backendResponseCommon(plan);
|
|
450
|
-
if (context.signal.aborted || active.terminationReason) {
|
|
451
|
-
report.status = "cancelled";
|
|
452
|
-
context.onUpdate?.({ phase: "finishing", message: "Subagent cancelled.", details: reportSummary(report) });
|
|
453
|
-
return { ...common, status: "cancelled", reason: active.terminationReason ?? abortReason(context.signal) };
|
|
454
|
-
}
|
|
455
|
-
if (outcome.spawnError || outcome.code !== 0 || report.stopReason === "error" || report.stopReason === "aborted" || report.errorMessage) {
|
|
456
|
-
report.status = "failed";
|
|
457
|
-
const message = report.errorMessage || report.stderr.trim() || `Pi subprocess exited with code ${outcome.code ?? "unknown"}.`;
|
|
458
|
-
context.onUpdate?.({ phase: "finishing", message: `Subagent failed: ${message}`, details: reportSummary(report) });
|
|
459
|
-
return {
|
|
460
|
-
...common,
|
|
461
|
-
status: "failed",
|
|
462
|
-
error: { code: "subprocess", message, retryable: false },
|
|
463
|
-
output: output ? { text: output.slice(0, plan.resultProjection.maxChars), partial: true } : undefined,
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
if (!output) {
|
|
467
|
-
report.status = "failed";
|
|
468
|
-
report.errorMessage = "Pi subprocess produced no assistant report.";
|
|
469
|
-
return { ...common, status: "failed", error: { code: "subprocess-empty", message: report.errorMessage, retryable: false } };
|
|
470
|
-
}
|
|
471
|
-
report.status = "completed";
|
|
472
|
-
context.onUpdate?.({ phase: "finishing", message: "Subagent report ready.", details: reportSummary(report) });
|
|
473
|
-
return { ...common, status: "completed", output: { text: output.slice(0, plan.resultProjection.maxChars), partial: false } };
|
|
474
|
-
}
|
|
475
|
-
#requireModel(provider, id) {
|
|
476
|
-
const model = this.#modelRegistry.find(provider, id);
|
|
477
|
-
if (!model)
|
|
478
|
-
throw new Error(`Pi subprocess model disappeared after preflight: ${provider}/${id}`);
|
|
479
|
-
return model;
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
export function sanitizePiSubprocessRunReport(report) {
|
|
483
|
-
const sanitized = {
|
|
484
|
-
...report,
|
|
485
|
-
model: { ...report.model },
|
|
486
|
-
effectiveToolNames: [...report.effectiveToolNames],
|
|
487
|
-
messages: [],
|
|
488
|
-
retention: createRetention(report.retention?.omittedMessages ?? 0),
|
|
489
|
-
stderr: appendBounded("", String(sanitizeSubprocessReportValue(report.stderr)), MAX_STDERR_BYTES),
|
|
490
|
-
usage: { ...report.usage },
|
|
491
|
-
};
|
|
492
|
-
for (const message of report.messages)
|
|
493
|
-
appendReportMessage(sanitized, message);
|
|
494
|
-
return sanitized;
|
|
495
|
-
}
|
|
496
|
-
function sanitizePiSubprocessMessage(value) {
|
|
497
|
-
return sanitizeSubprocessReportValue(value);
|
|
498
|
-
}
|
|
499
|
-
function defaultPiInvocation(piArgs) {
|
|
500
|
-
const currentScript = process.argv[1];
|
|
501
|
-
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
502
|
-
if (currentScript && !isBunVirtualScript && existsSync(currentScript))
|
|
503
|
-
return { command: process.execPath, args: [currentScript, ...piArgs] };
|
|
504
|
-
const execName = basename(process.execPath).toLowerCase();
|
|
505
|
-
if (!/^(node|bun)(\.exe)?$/.test(execName))
|
|
506
|
-
return { command: process.execPath, args: piArgs };
|
|
507
|
-
return { command: "pi", args: piArgs };
|
|
508
|
-
}
|
|
509
|
-
function defaultBridgePath() {
|
|
510
|
-
const extension = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
511
|
-
return join(dirname(fileURLToPath(import.meta.url)), `subprocess-bridge${extension}`);
|
|
512
|
-
}
|
|
513
|
-
function subprocessArguments(plan, toolNames, bridgePath, systemPromptPath, marker) {
|
|
514
|
-
const args = [
|
|
515
|
-
"--mode", "text",
|
|
516
|
-
"--print",
|
|
517
|
-
"--no-session",
|
|
518
|
-
"--model", `${plan.model.provider}/${plan.model.id}`,
|
|
519
|
-
"--thinking", plan.thinkingLevel,
|
|
520
|
-
"--system-prompt", systemPromptPath,
|
|
521
|
-
"--extension", bridgePath,
|
|
522
|
-
"--no-extensions",
|
|
523
|
-
"--no-skills",
|
|
524
|
-
"--no-prompt-templates",
|
|
525
|
-
"--no-themes",
|
|
526
|
-
"--no-context-files",
|
|
527
|
-
"--approve",
|
|
528
|
-
];
|
|
529
|
-
if (toolNames.length > 0)
|
|
530
|
-
args.push("--tools", toolNames.join(","));
|
|
531
|
-
else
|
|
532
|
-
args.push("--no-tools");
|
|
533
|
-
args.push(marker);
|
|
534
|
-
return args;
|
|
535
|
-
}
|
|
536
|
-
function toolNamesForPlan(plan) {
|
|
537
|
-
const byId = new Map(plan.preflight.toolCatalog.map((tool) => [tool.id, tool.name]));
|
|
538
|
-
return plan.effectiveToolIds.map((id) => {
|
|
539
|
-
const name = byId.get(id);
|
|
540
|
-
if (!name)
|
|
541
|
-
throw new Error(`Prepared subprocess tool disappeared from its catalog: ${id}`);
|
|
542
|
-
return name;
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
function createReport(plan, cwd, toolNames, now) {
|
|
546
|
-
return {
|
|
547
|
-
runId: plan.runId,
|
|
548
|
-
executionFingerprint: plan.executionFingerprint,
|
|
549
|
-
status: "running",
|
|
550
|
-
startedAt: now.toISOString(),
|
|
551
|
-
model: structuredClone(plan.model),
|
|
552
|
-
thinkingLevel: plan.thinkingLevel,
|
|
553
|
-
effectiveToolNames: [...toolNames],
|
|
554
|
-
executionBoundary: "shared-user",
|
|
555
|
-
workingDirectory: cwd,
|
|
556
|
-
messages: [],
|
|
557
|
-
retention: createRetention(),
|
|
558
|
-
stderr: "",
|
|
559
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: 0, turns: 0 },
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
function reportSummary(report) {
|
|
563
|
-
const { messages, stderr, ...rest } = report;
|
|
564
|
-
return { ...rest, usage: { ...report.usage }, effectiveToolNames: [...report.effectiveToolNames], messageCount: messages.length, stderrBytes: Buffer.byteLength(stderr, "utf8") };
|
|
565
|
-
}
|
|
566
|
-
function createRetention(omittedMessages = 0) {
|
|
567
|
-
return {
|
|
568
|
-
maxBytes: MAX_RETAINED_SUBPROCESS_REPORT_BYTES,
|
|
569
|
-
retainedBytes: 0,
|
|
570
|
-
truncated: omittedMessages > 0,
|
|
571
|
-
omittedMessages,
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
function appendReportMessage(report, value) {
|
|
575
|
-
let message = sanitizeSubprocessReportValue(value);
|
|
576
|
-
let messageBytes = serializedBytes(message);
|
|
577
|
-
if (messageBytes > report.retention.maxBytes) {
|
|
578
|
-
message = summarizeOversizedMessage(message, messageBytes);
|
|
579
|
-
messageBytes = serializedBytes(message);
|
|
580
|
-
report.retention.truncated = true;
|
|
581
|
-
report.retention.omittedMessages += 1;
|
|
582
|
-
}
|
|
583
|
-
report.messages.push(message);
|
|
584
|
-
report.retention.retainedBytes += messageBytes;
|
|
585
|
-
while (report.retention.retainedBytes > report.retention.maxBytes && report.messages.length > 1) {
|
|
586
|
-
const removed = report.messages.shift();
|
|
587
|
-
report.retention.retainedBytes -= serializedBytes(removed);
|
|
588
|
-
report.retention.truncated = true;
|
|
589
|
-
report.retention.omittedMessages += 1;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
function summarizeOversizedMessage(value, originalBytes) {
|
|
593
|
-
if (!isRecord(value))
|
|
594
|
-
return `[Oversized subagent report message omitted: ${originalBytes} bytes]`;
|
|
595
|
-
const role = typeof value.role === "string" ? value.role : "custom";
|
|
596
|
-
const summary = {
|
|
597
|
-
role,
|
|
598
|
-
content: [{ type: "text", text: `[Oversized subagent report message compacted: ${originalBytes} bytes]` }],
|
|
599
|
-
reportDataOmitted: true,
|
|
600
|
-
originalBytes,
|
|
601
|
-
};
|
|
602
|
-
if (typeof value.toolName === "string")
|
|
603
|
-
summary.toolName = value.toolName;
|
|
604
|
-
if (typeof value.toolCallId === "string")
|
|
605
|
-
summary.toolCallId = value.toolCallId;
|
|
606
|
-
if (value.isError === true)
|
|
607
|
-
summary.isError = true;
|
|
608
|
-
if (role === "assistant") {
|
|
609
|
-
const text = assistantText(value);
|
|
610
|
-
if (text)
|
|
611
|
-
summary.content = [{ type: "text", text }];
|
|
612
|
-
}
|
|
613
|
-
return summary;
|
|
614
|
-
}
|
|
615
|
-
function assistantText(value) {
|
|
616
|
-
if (!Array.isArray(value.content))
|
|
617
|
-
return "";
|
|
618
|
-
return value.content
|
|
619
|
-
.filter(isRecord)
|
|
620
|
-
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
621
|
-
.map((part) => String(part.text))
|
|
622
|
-
.join("");
|
|
623
|
-
}
|
|
624
|
-
function serializedBytes(value) {
|
|
625
|
-
return Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8");
|
|
626
|
-
}
|
|
627
|
-
function captureAssistantReceipt(report, value) {
|
|
628
|
-
if (!isRecord(value) || value.role !== "assistant")
|
|
629
|
-
return;
|
|
630
|
-
report.usage.turns += 1;
|
|
631
|
-
if (isRecord(value.usage)) {
|
|
632
|
-
report.usage.input += numberOrZero(value.usage.input);
|
|
633
|
-
report.usage.output += numberOrZero(value.usage.output);
|
|
634
|
-
report.usage.cacheRead += numberOrZero(value.usage.cacheRead);
|
|
635
|
-
report.usage.cacheWrite += numberOrZero(value.usage.cacheWrite);
|
|
636
|
-
report.usage.totalTokens += numberOrZero(value.usage.totalTokens);
|
|
637
|
-
if (isRecord(value.usage.cost))
|
|
638
|
-
report.usage.cost += numberOrZero(value.usage.cost.total);
|
|
639
|
-
}
|
|
640
|
-
if (typeof value.stopReason === "string")
|
|
641
|
-
report.stopReason = value.stopReason;
|
|
642
|
-
if (typeof value.errorMessage === "string")
|
|
643
|
-
report.errorMessage = value.errorMessage;
|
|
644
|
-
}
|
|
645
|
-
function latestAssistantText(messages) {
|
|
646
|
-
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
647
|
-
const message = messages[index];
|
|
648
|
-
if (!isRecord(message) || message.role !== "assistant" || !Array.isArray(message.content))
|
|
649
|
-
continue;
|
|
650
|
-
const text = message.content.filter(isRecord).filter((part) => part.type === "text" && typeof part.text === "string").map((part) => String(part.text)).join("");
|
|
651
|
-
if (text.trim())
|
|
652
|
-
return text.trim();
|
|
653
|
-
}
|
|
654
|
-
return "";
|
|
655
|
-
}
|
|
656
|
-
function toolResultSummary(value) {
|
|
657
|
-
if (!isRecord(value))
|
|
658
|
-
return "Subagent tool result received.";
|
|
659
|
-
const name = typeof value.toolName === "string" ? value.toolName : "tool";
|
|
660
|
-
const error = value.isError === true ? " failed" : " completed";
|
|
661
|
-
return `${name}${error}.`;
|
|
662
|
-
}
|
|
663
|
-
function appendBounded(current, addition, maxBytes) {
|
|
664
|
-
const remaining = maxBytes - Buffer.byteLength(current, "utf8");
|
|
665
|
-
if (remaining <= 0)
|
|
666
|
-
return current;
|
|
667
|
-
const bytes = Buffer.from(addition, "utf8");
|
|
668
|
-
return current + bytes.subarray(0, remaining).toString("utf8");
|
|
669
|
-
}
|
|
670
|
-
async function terminateChild(child) {
|
|
671
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
672
|
-
return;
|
|
673
|
-
const closed = new Promise((resolve) => child.once("close", () => resolve()));
|
|
674
|
-
child.kill("SIGTERM");
|
|
675
|
-
const force = setTimeout(() => {
|
|
676
|
-
if (child.exitCode === null && child.signalCode === null)
|
|
677
|
-
child.kill("SIGKILL");
|
|
678
|
-
}, 5_000);
|
|
679
|
-
try {
|
|
680
|
-
await closed;
|
|
681
|
-
}
|
|
682
|
-
finally {
|
|
683
|
-
clearTimeout(force);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
function preparedMessageToAgentMessage(message, preflight, index) {
|
|
687
|
-
if (message.content.some((part) => part.type === "media"))
|
|
688
|
-
throw new Error("Subprocess media preparation is not implemented.");
|
|
689
|
-
const content = message.content.map((part) => ({ type: "text", text: part.type === "text" ? part.text : "" }));
|
|
690
|
-
if (message.role === "user")
|
|
691
|
-
return { role: "user", content: content.length === 1 ? content[0].text : content, timestamp: index };
|
|
692
|
-
if (message.role === "custom")
|
|
693
|
-
return { role: "custom", customType: "pi-forge-subagent", content, display: false, details: {}, timestamp: index };
|
|
694
|
-
return {
|
|
695
|
-
role: "assistant",
|
|
696
|
-
content,
|
|
697
|
-
api: "unknown",
|
|
698
|
-
provider: preflight.model.provider,
|
|
699
|
-
model: preflight.model.id,
|
|
700
|
-
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
|
|
701
|
-
stopReason: "stop",
|
|
702
|
-
timestamp: index,
|
|
703
|
-
};
|
|
704
|
-
}
|
|
705
|
-
function backendResponseCommon(plan) {
|
|
706
|
-
return {
|
|
707
|
-
schemaVersion: SUBAGENT_CONTRACT_VERSION,
|
|
708
|
-
requestId: plan.requestId,
|
|
709
|
-
runId: plan.runId,
|
|
710
|
-
backendId: plan.backendId,
|
|
711
|
-
profileFingerprint: plan.profile.profileFingerprint,
|
|
712
|
-
executionFingerprint: plan.executionFingerprint,
|
|
713
|
-
model: structuredClone(plan.model),
|
|
714
|
-
effectiveToolIds: [...plan.effectiveToolIds],
|
|
715
|
-
enforcement: { access: structuredClone(plan.access), limits: structuredClone(plan.limits) },
|
|
716
|
-
durationMs: 0,
|
|
717
|
-
artifacts: [],
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
function deferred() {
|
|
721
|
-
let resolve;
|
|
722
|
-
let reject;
|
|
723
|
-
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
724
|
-
resolve = resolvePromise;
|
|
725
|
-
reject = rejectPromise;
|
|
726
|
-
});
|
|
727
|
-
void promise.catch(() => undefined);
|
|
728
|
-
return { promise, resolve, reject };
|
|
729
|
-
}
|
|
730
|
-
async function abortable(promise, signal) {
|
|
731
|
-
if (!signal)
|
|
732
|
-
return promise;
|
|
733
|
-
if (signal.aborted)
|
|
734
|
-
throw new Error("Pi subprocess preparation was cancelled.");
|
|
735
|
-
return new Promise((resolve, reject) => {
|
|
736
|
-
const abort = () => reject(new Error("Pi subprocess preparation was cancelled."));
|
|
737
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
738
|
-
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
739
|
-
});
|
|
740
|
-
}
|
|
741
|
-
function abortReason(signal) {
|
|
742
|
-
return typeof signal.reason === "string" && signal.reason ? signal.reason : "Subprocess execution cancelled.";
|
|
743
|
-
}
|
|
744
|
-
function errorDiagnostic(code, message, path) {
|
|
745
|
-
return { level: "error", code, message, path };
|
|
746
|
-
}
|
|
747
|
-
function warningDiagnostic(code, message, path) {
|
|
748
|
-
return { level: "warning", code, message, path };
|
|
749
|
-
}
|
|
750
|
-
function isRecord(value) {
|
|
751
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
752
|
-
}
|
|
753
|
-
function numberOrZero(value) {
|
|
754
|
-
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
755
|
-
}
|
|
756
|
-
//# sourceMappingURL=pi-subprocess-backend.js.map
|