@ai-sdk/harness-pi 0.0.0 → 1.0.0-beta.10
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 +89 -0
- package/LICENSE +13 -0
- package/README.md +52 -0
- package/dist/index.d.ts +122 -0
- package/dist/index.js +2178 -0
- package/dist/index.js.map +1 -0
- package/package.json +71 -1
- package/src/index.ts +11 -0
- package/src/pi-auth.ts +168 -0
- package/src/pi-events.ts +137 -0
- package/src/pi-harness.ts +145 -0
- package/src/pi-model-resolver.ts +52 -0
- package/src/pi-paths.ts +136 -0
- package/src/pi-remote-ops.ts +293 -0
- package/src/pi-resume-state.ts +81 -0
- package/src/pi-session.ts +1179 -0
- package/src/pi-skills.ts +69 -0
- package/src/pi-translate.ts +331 -0
- package/src/pi-typebox-adapter.ts +11 -0
- package/src/pi-utils.ts +91 -0
- package/src/pi-workspace-mirror.ts +276 -0
- package/src/pi-workspace-vfs.ts +437 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2178 @@
|
|
|
1
|
+
// src/pi-harness.ts
|
|
2
|
+
import {
|
|
3
|
+
commonTool
|
|
4
|
+
} from "@ai-sdk/harness";
|
|
5
|
+
import { tool } from "@ai-sdk/provider-utils";
|
|
6
|
+
import { z as z3 } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/pi-resume-state.ts
|
|
9
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
var piResumeStateSchema = z.object({
|
|
13
|
+
sessionFileName: z.string().optional()
|
|
14
|
+
}).passthrough();
|
|
15
|
+
var PI_SESSIONS_DIR = ".pi-sessions";
|
|
16
|
+
async function persistSessionFileToSandbox(args) {
|
|
17
|
+
const hostPath = path.join(args.hostSessionDir, args.sessionFileName);
|
|
18
|
+
const content = await readFile(hostPath);
|
|
19
|
+
const remotePath = path.posix.join(
|
|
20
|
+
args.sessionWorkDir,
|
|
21
|
+
PI_SESSIONS_DIR,
|
|
22
|
+
args.sessionFileName
|
|
23
|
+
);
|
|
24
|
+
await args.sandbox.run({
|
|
25
|
+
command: `mkdir -p ${path.posix.dirname(remotePath)}`,
|
|
26
|
+
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
27
|
+
});
|
|
28
|
+
await args.sandbox.writeBinaryFile({
|
|
29
|
+
path: remotePath,
|
|
30
|
+
content,
|
|
31
|
+
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function pullSessionFileFromSandbox(args) {
|
|
35
|
+
const remotePath = path.posix.join(
|
|
36
|
+
args.sessionWorkDir,
|
|
37
|
+
PI_SESSIONS_DIR,
|
|
38
|
+
args.sessionFileName
|
|
39
|
+
);
|
|
40
|
+
const bytes = await args.sandbox.readBinaryFile({
|
|
41
|
+
path: remotePath,
|
|
42
|
+
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
43
|
+
});
|
|
44
|
+
if (!bytes) return void 0;
|
|
45
|
+
await mkdir(args.hostSessionDir, { recursive: true });
|
|
46
|
+
const hostPath = path.join(args.hostSessionDir, args.sessionFileName);
|
|
47
|
+
await writeFile(hostPath, bytes);
|
|
48
|
+
return hostPath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/pi-session.ts
|
|
52
|
+
import {
|
|
53
|
+
AuthStorage,
|
|
54
|
+
createAgentSession,
|
|
55
|
+
DefaultResourceLoader,
|
|
56
|
+
defineTool,
|
|
57
|
+
ModelRegistry,
|
|
58
|
+
SessionManager,
|
|
59
|
+
SettingsManager
|
|
60
|
+
} from "@earendil-works/pi-coding-agent";
|
|
61
|
+
import { mkdir as mkdir3, rm as rm2 } from "fs/promises";
|
|
62
|
+
import { tmpdir } from "os";
|
|
63
|
+
import path7 from "path";
|
|
64
|
+
import { Type as Type2 } from "typebox";
|
|
65
|
+
|
|
66
|
+
// src/pi-auth.ts
|
|
67
|
+
var DEFAULT_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
|
|
68
|
+
var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
69
|
+
var DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
70
|
+
function register(registries, provider, apiKey, config) {
|
|
71
|
+
registries.authStorage.setRuntimeApiKey(provider, apiKey);
|
|
72
|
+
registries.modelRegistry.registerProvider(provider, config);
|
|
73
|
+
}
|
|
74
|
+
function hasConfiguredValue(value) {
|
|
75
|
+
if (value == null) return false;
|
|
76
|
+
if (typeof value === "string") return value.length > 0;
|
|
77
|
+
if (typeof value !== "object") return true;
|
|
78
|
+
return Object.values(value).some(hasConfiguredValue);
|
|
79
|
+
}
|
|
80
|
+
function resolvePiAuth(options, env, registries) {
|
|
81
|
+
const customEnvConfigured = hasConfiguredValue(options?.customEnv);
|
|
82
|
+
const gatewayConfigured = hasConfiguredValue(options?.gateway);
|
|
83
|
+
if (customEnvConfigured) {
|
|
84
|
+
return applyCustomEnv(options.customEnv ?? {}, registries);
|
|
85
|
+
}
|
|
86
|
+
if (gatewayConfigured) {
|
|
87
|
+
const apiKey = options.gateway?.apiKey;
|
|
88
|
+
const baseUrl = options.gateway?.baseUrl ?? DEFAULT_GATEWAY_BASE_URL;
|
|
89
|
+
if (apiKey) {
|
|
90
|
+
register(registries, "vercel-ai-gateway", apiKey, {
|
|
91
|
+
apiKey,
|
|
92
|
+
baseUrl,
|
|
93
|
+
authHeader: true
|
|
94
|
+
});
|
|
95
|
+
return { AI_GATEWAY_API_KEY: apiKey, AI_GATEWAY_BASE_URL: baseUrl };
|
|
96
|
+
}
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
const ambientKey = env.AI_GATEWAY_API_KEY || env.VERCEL_OIDC_TOKEN;
|
|
100
|
+
if (ambientKey) {
|
|
101
|
+
const baseUrl = env.AI_GATEWAY_BASE_URL ?? DEFAULT_GATEWAY_BASE_URL;
|
|
102
|
+
register(registries, "vercel-ai-gateway", ambientKey, {
|
|
103
|
+
apiKey: ambientKey,
|
|
104
|
+
baseUrl,
|
|
105
|
+
authHeader: true
|
|
106
|
+
});
|
|
107
|
+
return { AI_GATEWAY_API_KEY: ambientKey, AI_GATEWAY_BASE_URL: baseUrl };
|
|
108
|
+
}
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
function applyCustomEnv(customEnv, registries) {
|
|
112
|
+
const out = {};
|
|
113
|
+
const gatewayKey = customEnv.AI_GATEWAY_API_KEY;
|
|
114
|
+
if (gatewayKey) {
|
|
115
|
+
const baseUrl = customEnv.AI_GATEWAY_BASE_URL ?? DEFAULT_GATEWAY_BASE_URL;
|
|
116
|
+
register(registries, "vercel-ai-gateway", gatewayKey, {
|
|
117
|
+
apiKey: gatewayKey,
|
|
118
|
+
baseUrl,
|
|
119
|
+
authHeader: true
|
|
120
|
+
});
|
|
121
|
+
out.AI_GATEWAY_API_KEY = gatewayKey;
|
|
122
|
+
out.AI_GATEWAY_BASE_URL = baseUrl;
|
|
123
|
+
}
|
|
124
|
+
if (customEnv.OPENAI_API_KEY) {
|
|
125
|
+
const baseUrl = customEnv.OPENAI_BASE_URL ?? DEFAULT_OPENAI_BASE_URL;
|
|
126
|
+
register(registries, "openai", customEnv.OPENAI_API_KEY, {
|
|
127
|
+
apiKey: customEnv.OPENAI_API_KEY,
|
|
128
|
+
baseUrl,
|
|
129
|
+
authHeader: true
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
if (customEnv.ANTHROPIC_API_KEY) {
|
|
133
|
+
const baseUrl = customEnv.ANTHROPIC_BASE_URL ?? DEFAULT_ANTHROPIC_BASE_URL;
|
|
134
|
+
register(registries, "anthropic", customEnv.ANTHROPIC_API_KEY, {
|
|
135
|
+
apiKey: customEnv.ANTHROPIC_API_KEY,
|
|
136
|
+
baseUrl,
|
|
137
|
+
...customEnv.ANTHROPIC_AUTH_TOKEN ? {
|
|
138
|
+
headers: {
|
|
139
|
+
authorization: `Bearer ${customEnv.ANTHROPIC_AUTH_TOKEN}`
|
|
140
|
+
}
|
|
141
|
+
} : {}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
for (const [name, apiKey] of Object.entries(customEnv)) {
|
|
145
|
+
if (!name.endsWith("_API_KEY") || !apiKey) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const prefix = name.slice(0, -"_API_KEY".length);
|
|
149
|
+
if (prefix === "AI_GATEWAY" || prefix === "OPENAI" || prefix === "ANTHROPIC") {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const provider = prefix.toLowerCase().replace(/_/g, "-");
|
|
153
|
+
const baseUrl = customEnv[`${prefix}_BASE_URL`];
|
|
154
|
+
if (!baseUrl) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
register(registries, provider, apiKey, {
|
|
158
|
+
apiKey,
|
|
159
|
+
baseUrl,
|
|
160
|
+
authHeader: true
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/pi-events.ts
|
|
167
|
+
import { z as z2 } from "zod";
|
|
168
|
+
var piSessionEventSchema = z2.object({
|
|
169
|
+
type: z2.string(),
|
|
170
|
+
assistantMessageEvent: z2.object({
|
|
171
|
+
type: z2.string().optional(),
|
|
172
|
+
delta: z2.string().optional()
|
|
173
|
+
}).passthrough().optional(),
|
|
174
|
+
toolCallId: z2.string().optional(),
|
|
175
|
+
toolName: z2.string().optional(),
|
|
176
|
+
args: z2.unknown().optional(),
|
|
177
|
+
input: z2.unknown().optional(),
|
|
178
|
+
result: z2.unknown().optional(),
|
|
179
|
+
content: z2.unknown().optional(),
|
|
180
|
+
isError: z2.boolean().optional(),
|
|
181
|
+
// Compaction events (`compaction_start` / `compaction_end`). `result` (a
|
|
182
|
+
// `CompactionResult`) rides the shared `result` field above; `reason`
|
|
183
|
+
// distinguishes manual vs automatic (threshold/overflow) compaction.
|
|
184
|
+
reason: z2.string().optional(),
|
|
185
|
+
aborted: z2.boolean().optional(),
|
|
186
|
+
error: z2.union([
|
|
187
|
+
z2.string(),
|
|
188
|
+
z2.object({
|
|
189
|
+
errorMessage: z2.string().optional(),
|
|
190
|
+
stopReason: z2.string().optional()
|
|
191
|
+
}).passthrough()
|
|
192
|
+
]).optional(),
|
|
193
|
+
message: z2.object({
|
|
194
|
+
role: z2.string().optional(),
|
|
195
|
+
content: z2.unknown().optional(),
|
|
196
|
+
stopReason: z2.string().optional(),
|
|
197
|
+
errorMessage: z2.string().optional()
|
|
198
|
+
}).passthrough().optional()
|
|
199
|
+
}).passthrough();
|
|
200
|
+
function parseNativeEvent(raw) {
|
|
201
|
+
const parsed = piSessionEventSchema.safeParse(raw);
|
|
202
|
+
return parsed.success ? parsed.data : void 0;
|
|
203
|
+
}
|
|
204
|
+
function getPiTerminalError(event) {
|
|
205
|
+
const isTerminalStopReason = (value) => value === "error" || value === "aborted";
|
|
206
|
+
if (typeof event.error === "string" && event.error.trim()) {
|
|
207
|
+
return event.error.trim();
|
|
208
|
+
}
|
|
209
|
+
if (event.error && typeof event.error === "object") {
|
|
210
|
+
const errorMessage = event.error.errorMessage?.trim();
|
|
211
|
+
if (errorMessage) {
|
|
212
|
+
return errorMessage;
|
|
213
|
+
}
|
|
214
|
+
const stopReason = event.error.stopReason?.trim();
|
|
215
|
+
if (isTerminalStopReason(stopReason)) {
|
|
216
|
+
return stopReason;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const messageError = event.message?.errorMessage?.trim();
|
|
220
|
+
if (messageError) {
|
|
221
|
+
return messageError;
|
|
222
|
+
}
|
|
223
|
+
const messageStopReason = event.message?.stopReason?.trim();
|
|
224
|
+
if (isTerminalStopReason(messageStopReason)) {
|
|
225
|
+
return messageStopReason;
|
|
226
|
+
}
|
|
227
|
+
if (event.isError && typeof event.content === "string" && event.content.trim()) {
|
|
228
|
+
return event.content.trim();
|
|
229
|
+
}
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
function extractAssistantText(message) {
|
|
233
|
+
if (!message || message.role !== "assistant") {
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
if (typeof message.content === "string") {
|
|
237
|
+
return message.content;
|
|
238
|
+
}
|
|
239
|
+
if (!Array.isArray(message.content)) {
|
|
240
|
+
return "";
|
|
241
|
+
}
|
|
242
|
+
return message.content.flatMap((part) => {
|
|
243
|
+
if (!part || typeof part !== "object") {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
const contentPart = part;
|
|
247
|
+
return contentPart.type === "text" && typeof contentPart.text === "string" ? [contentPart.text] : [];
|
|
248
|
+
}).join("");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/pi-model-resolver.ts
|
|
252
|
+
var DEFAULT_PI_GATEWAY_MODEL_ID = "anthropic/claude-sonnet-4.6";
|
|
253
|
+
function createPiModelResolver(modelRegistry, env = process.env) {
|
|
254
|
+
let cachedModels;
|
|
255
|
+
const loadModels = () => {
|
|
256
|
+
if (cachedModels) {
|
|
257
|
+
return cachedModels;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
cachedModels = modelRegistry.getAll();
|
|
261
|
+
} catch {
|
|
262
|
+
cachedModels = [];
|
|
263
|
+
}
|
|
264
|
+
return cachedModels;
|
|
265
|
+
};
|
|
266
|
+
return (modelId) => {
|
|
267
|
+
const useGateway = Boolean(env.AI_GATEWAY_API_KEY || env.VERCEL_OIDC_TOKEN);
|
|
268
|
+
const effectiveId = modelId ?? (useGateway ? DEFAULT_PI_GATEWAY_MODEL_ID : void 0);
|
|
269
|
+
if (!effectiveId) return void 0;
|
|
270
|
+
const models = loadModels();
|
|
271
|
+
const matches = (m) => m.id === effectiveId || m.name === effectiveId;
|
|
272
|
+
return useGateway && models.find((m) => m.provider === "vercel-ai-gateway" && matches(m)) || models.find(matches);
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/pi-paths.ts
|
|
277
|
+
import { realpathSync } from "fs";
|
|
278
|
+
import path2 from "path";
|
|
279
|
+
function isInsidePath(parent, candidate) {
|
|
280
|
+
const relative = path2.relative(parent, candidate);
|
|
281
|
+
return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
|
|
282
|
+
}
|
|
283
|
+
function isInsidePosixPath(parent, candidate) {
|
|
284
|
+
const relative = path2.posix.relative(parent, candidate);
|
|
285
|
+
return relative === "" || !relative.startsWith("..") && !path2.posix.isAbsolute(relative);
|
|
286
|
+
}
|
|
287
|
+
function canonicalizeForContainment(inputPath) {
|
|
288
|
+
try {
|
|
289
|
+
return realpathSync.native(inputPath);
|
|
290
|
+
} catch {
|
|
291
|
+
const parent = path2.dirname(inputPath);
|
|
292
|
+
if (parent === inputPath) {
|
|
293
|
+
return inputPath;
|
|
294
|
+
}
|
|
295
|
+
return path2.join(
|
|
296
|
+
canonicalizeForContainment(parent),
|
|
297
|
+
path2.basename(inputPath)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function createPiPathMapper(options) {
|
|
302
|
+
const normalizedHost = path2.resolve(options.hostWorkDir);
|
|
303
|
+
const normalizedSandbox = path2.posix.normalize(options.sandboxWorkDir);
|
|
304
|
+
const canonicalHost = canonicalizeForContainment(normalizedHost);
|
|
305
|
+
const readableRoots = options.readableRoots?.map((root) => ({
|
|
306
|
+
sandboxDir: path2.posix.normalize(root.sandboxDir)
|
|
307
|
+
})) ?? [];
|
|
308
|
+
const toWorkspaceSandboxPath = (inputPath) => {
|
|
309
|
+
if (path2.posix.isAbsolute(inputPath)) {
|
|
310
|
+
const normalizedInput = path2.posix.normalize(inputPath);
|
|
311
|
+
if (isInsidePosixPath(normalizedSandbox, normalizedInput)) {
|
|
312
|
+
return normalizedInput;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const resolvedHost = path2.isAbsolute(inputPath) ? path2.resolve(inputPath) : path2.resolve(normalizedHost, inputPath);
|
|
316
|
+
const canonicalResolvedHost = canonicalizeForContainment(resolvedHost);
|
|
317
|
+
if (!isInsidePath(normalizedHost, resolvedHost) || !isInsidePath(canonicalHost, canonicalResolvedHost)) {
|
|
318
|
+
throw new Error(`Pi path escapes the workspace: ${inputPath}`);
|
|
319
|
+
}
|
|
320
|
+
const relative = path2.relative(normalizedHost, resolvedHost).split(path2.sep).join("/");
|
|
321
|
+
return relative ? path2.posix.join(normalizedSandbox, relative) : normalizedSandbox;
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
hostWorkDir: normalizedHost,
|
|
325
|
+
sandboxWorkDir: normalizedSandbox,
|
|
326
|
+
toSandboxPath(inputPath) {
|
|
327
|
+
return toWorkspaceSandboxPath(inputPath);
|
|
328
|
+
},
|
|
329
|
+
toReadableSandboxPath(inputPath) {
|
|
330
|
+
if (path2.posix.isAbsolute(inputPath)) {
|
|
331
|
+
const normalizedInput = path2.posix.normalize(inputPath);
|
|
332
|
+
if (isInsidePosixPath(normalizedSandbox, normalizedInput) || readableRoots.some(
|
|
333
|
+
(root) => isInsidePosixPath(root.sandboxDir, normalizedInput)
|
|
334
|
+
)) {
|
|
335
|
+
return normalizedInput;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return toWorkspaceSandboxPath(inputPath);
|
|
339
|
+
},
|
|
340
|
+
toRelativePath(inputPath) {
|
|
341
|
+
const sandboxPath = path2.posix.isAbsolute(inputPath) ? path2.posix.normalize(inputPath) : path2.posix.join(
|
|
342
|
+
normalizedSandbox,
|
|
343
|
+
inputPath.split(path2.sep).join("/")
|
|
344
|
+
);
|
|
345
|
+
const relative = path2.posix.relative(normalizedSandbox, sandboxPath);
|
|
346
|
+
return relative || ".";
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/pi-remote-ops.ts
|
|
352
|
+
import path3 from "path";
|
|
353
|
+
|
|
354
|
+
// src/pi-utils.ts
|
|
355
|
+
import {
|
|
356
|
+
HarnessCapabilityUnsupportedError
|
|
357
|
+
} from "@ai-sdk/harness";
|
|
358
|
+
var HARNESS_ID = "pi";
|
|
359
|
+
function extractUserText(prompt) {
|
|
360
|
+
if (typeof prompt === "string") {
|
|
361
|
+
return prompt;
|
|
362
|
+
}
|
|
363
|
+
const { content } = prompt;
|
|
364
|
+
if (typeof content === "string") {
|
|
365
|
+
return content;
|
|
366
|
+
}
|
|
367
|
+
const parts = [];
|
|
368
|
+
for (const part of content) {
|
|
369
|
+
if (part.type !== "text") {
|
|
370
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
371
|
+
message: `pi: only text user-message parts are supported; got '${part.type}'.`,
|
|
372
|
+
harnessId: HARNESS_ID
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
parts.push(part.text);
|
|
376
|
+
}
|
|
377
|
+
return parts.join("\n\n");
|
|
378
|
+
}
|
|
379
|
+
function frameInstructions(instructions, userText) {
|
|
380
|
+
return `<session-instructions>
|
|
381
|
+
The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
|
|
382
|
+
|
|
383
|
+
${instructions}
|
|
384
|
+
</session-instructions>
|
|
385
|
+
|
|
386
|
+
<user-message>
|
|
387
|
+
${userText}
|
|
388
|
+
</user-message>`;
|
|
389
|
+
}
|
|
390
|
+
function shellQuote(value) {
|
|
391
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
392
|
+
}
|
|
393
|
+
function serializeToolOutput(output) {
|
|
394
|
+
if (typeof output === "string") {
|
|
395
|
+
return output;
|
|
396
|
+
}
|
|
397
|
+
const serialized = JSON.stringify(output);
|
|
398
|
+
return serialized ?? "null";
|
|
399
|
+
}
|
|
400
|
+
function safePiMetadataSegment(name, label) {
|
|
401
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
402
|
+
throw new Error(`Invalid Pi ${label} name: ${name}`);
|
|
403
|
+
}
|
|
404
|
+
return name;
|
|
405
|
+
}
|
|
406
|
+
function renderPiSkillFile(skill) {
|
|
407
|
+
return `---
|
|
408
|
+
name: ${skill.name}
|
|
409
|
+
description: ${skill.description}
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
${skill.content}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/pi-remote-ops.ts
|
|
416
|
+
function createPiRemoteOps(options) {
|
|
417
|
+
const runShell = async (command, input = {}) => {
|
|
418
|
+
const result = await options.sandbox.run({
|
|
419
|
+
command,
|
|
420
|
+
...input.cwd ? { workingDirectory: options.paths.toSandboxPath(input.cwd) } : {},
|
|
421
|
+
...options.env ? { env: options.env } : {},
|
|
422
|
+
...input.signal ? { abortSignal: input.signal } : {}
|
|
423
|
+
});
|
|
424
|
+
const combined = `${result.stdout}${result.stderr}`;
|
|
425
|
+
const output = Buffer.from(combined, "utf8");
|
|
426
|
+
if (output.length > 0) {
|
|
427
|
+
input.onData?.(output);
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
exitCode: result.exitCode,
|
|
431
|
+
output
|
|
432
|
+
};
|
|
433
|
+
};
|
|
434
|
+
const readBuffer = async (inputPath) => {
|
|
435
|
+
const bytes = await options.sandbox.readBinaryFile({
|
|
436
|
+
path: options.paths.toReadableSandboxPath(inputPath)
|
|
437
|
+
});
|
|
438
|
+
if (!bytes) {
|
|
439
|
+
throw new Error(`Path not found: ${inputPath}`);
|
|
440
|
+
}
|
|
441
|
+
return Buffer.from(bytes);
|
|
442
|
+
};
|
|
443
|
+
const writeFile3 = async (inputPath, content) => {
|
|
444
|
+
const remotePath = options.paths.toSandboxPath(inputPath);
|
|
445
|
+
const previous = await options.sandbox.readBinaryFile({ path: remotePath });
|
|
446
|
+
await runShell(`mkdir -p ${shellQuote(path3.posix.dirname(remotePath))}`);
|
|
447
|
+
await options.sandbox.writeTextFile({ path: remotePath, content });
|
|
448
|
+
options.onFileChange?.(
|
|
449
|
+
previous ? "modify" : "create",
|
|
450
|
+
options.paths.toRelativePath(remotePath),
|
|
451
|
+
Buffer.from(content, "utf8")
|
|
452
|
+
);
|
|
453
|
+
};
|
|
454
|
+
const editFile = async (inputPath, oldText, newText) => {
|
|
455
|
+
const current = (await readBuffer(inputPath)).toString("utf8");
|
|
456
|
+
const index = current.indexOf(oldText);
|
|
457
|
+
if (index === -1) {
|
|
458
|
+
throw new Error(`Text to replace was not found in ${inputPath}`);
|
|
459
|
+
}
|
|
460
|
+
const updated = `${current.slice(0, index)}${newText}${current.slice(
|
|
461
|
+
index + oldText.length
|
|
462
|
+
)}`;
|
|
463
|
+
await writeFile3(inputPath, updated);
|
|
464
|
+
return updated;
|
|
465
|
+
};
|
|
466
|
+
const listDirectory = async (inputPath = ".", limit = 500) => {
|
|
467
|
+
const remotePath = options.paths.toReadableSandboxPath(inputPath);
|
|
468
|
+
const result = await runShell(
|
|
469
|
+
[
|
|
470
|
+
`if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_LS_NOT_FOUND__"; exit 2; fi`,
|
|
471
|
+
`if [ ! -d ${shellQuote(remotePath)} ]; then echo "__PI_LS_NOT_DIR__"; exit 3; fi`,
|
|
472
|
+
`cd ${shellQuote(remotePath)}`,
|
|
473
|
+
"ls -1Ap"
|
|
474
|
+
].join("; ")
|
|
475
|
+
);
|
|
476
|
+
const output = result.output.toString("utf8").trim();
|
|
477
|
+
if (output.includes("__PI_LS_NOT_FOUND__")) {
|
|
478
|
+
throw new Error(`Path not found: ${inputPath}`);
|
|
479
|
+
}
|
|
480
|
+
if (output.includes("__PI_LS_NOT_DIR__")) {
|
|
481
|
+
throw new Error(`Not a directory: ${inputPath}`);
|
|
482
|
+
}
|
|
483
|
+
return output.split("\n").filter(Boolean).map((line) => line.replace(/[*=@|]$/, "")).sort(
|
|
484
|
+
(left, right) => left.toLowerCase().localeCompare(right.toLowerCase())
|
|
485
|
+
).slice(0, limit);
|
|
486
|
+
};
|
|
487
|
+
const findFiles = async (pattern, inputPath = ".", limit = 1e3) => {
|
|
488
|
+
const remotePath = options.paths.toReadableSandboxPath(inputPath);
|
|
489
|
+
const result = await runShell(
|
|
490
|
+
[
|
|
491
|
+
`if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_FIND_NOT_FOUND__"; exit 2; fi`,
|
|
492
|
+
`if [ -d ${shellQuote(remotePath)} ]; then find ${shellQuote(remotePath)} -type f -print; else printf '%s\\n' ${shellQuote(remotePath)}; fi`
|
|
493
|
+
].join("; ")
|
|
494
|
+
);
|
|
495
|
+
const output = result.output.toString("utf8").trim();
|
|
496
|
+
if (output.includes("__PI_FIND_NOT_FOUND__")) {
|
|
497
|
+
throw new Error(`Path not found: ${inputPath}`);
|
|
498
|
+
}
|
|
499
|
+
const searchRoot = remotePath;
|
|
500
|
+
return output.split("\n").filter(Boolean).map((absolutePath) => {
|
|
501
|
+
if (absolutePath === searchRoot) {
|
|
502
|
+
return path3.posix.basename(absolutePath);
|
|
503
|
+
}
|
|
504
|
+
return path3.posix.relative(searchRoot, absolutePath);
|
|
505
|
+
}).filter(
|
|
506
|
+
(candidate) => candidate.length > 0 && path3.matchesGlob(candidate, pattern)
|
|
507
|
+
).sort(
|
|
508
|
+
(left, right) => left.toLowerCase().localeCompare(right.toLowerCase())
|
|
509
|
+
).slice(0, limit);
|
|
510
|
+
};
|
|
511
|
+
const grepFiles = async (pattern, input) => {
|
|
512
|
+
const remotePath = options.paths.toReadableSandboxPath(input.path ?? ".");
|
|
513
|
+
const relativeTarget = options.paths.toRelativePath(remotePath);
|
|
514
|
+
const targetPath = relativeTarget.startsWith("../") || path3.posix.isAbsolute(relativeTarget) ? remotePath : relativeTarget;
|
|
515
|
+
const flags = [
|
|
516
|
+
"-R",
|
|
517
|
+
"-n",
|
|
518
|
+
"--binary-files=without-match",
|
|
519
|
+
...input.ignoreCase ? ["-i"] : [],
|
|
520
|
+
...input.literal ? ["-F"] : [],
|
|
521
|
+
...typeof input.context === "number" && input.context > 0 ? ["-C", String(input.context)] : [],
|
|
522
|
+
...input.glob ? ["--include", input.glob] : []
|
|
523
|
+
];
|
|
524
|
+
const limit = Math.max(1, input.limit ?? 100);
|
|
525
|
+
const result = await runShell(
|
|
526
|
+
[
|
|
527
|
+
`if [ ! -e ${shellQuote(remotePath)} ]; then echo "__PI_GREP_NOT_FOUND__"; exit 2; fi`,
|
|
528
|
+
`cd ${shellQuote(options.paths.sandboxWorkDir)}`,
|
|
529
|
+
`grep ${flags.map(shellQuote).join(" ")} -- ${shellQuote(pattern)} ${shellQuote(targetPath)} 2>/dev/null | head -n ${limit}`
|
|
530
|
+
].join("; ")
|
|
531
|
+
);
|
|
532
|
+
const output = result.output.toString("utf8").trim();
|
|
533
|
+
if (output.includes("__PI_GREP_NOT_FOUND__")) {
|
|
534
|
+
throw new Error(`Path not found: ${input.path ?? "."}`);
|
|
535
|
+
}
|
|
536
|
+
return output || "No matches found";
|
|
537
|
+
};
|
|
538
|
+
return {
|
|
539
|
+
paths: options.paths,
|
|
540
|
+
readBuffer,
|
|
541
|
+
writeFile: writeFile3,
|
|
542
|
+
editFile,
|
|
543
|
+
listDirectory,
|
|
544
|
+
findFiles,
|
|
545
|
+
grepFiles,
|
|
546
|
+
async access(inputPath) {
|
|
547
|
+
await readBuffer(inputPath);
|
|
548
|
+
},
|
|
549
|
+
async exec(command, cwd, input) {
|
|
550
|
+
const controller = new AbortController();
|
|
551
|
+
const timeoutId = typeof input.timeout === "number" && input.timeout > 0 ? setTimeout(() => controller.abort(), input.timeout * 1e3) : void 0;
|
|
552
|
+
const forwardedSignal = input.signal;
|
|
553
|
+
const onAbort = () => controller.abort();
|
|
554
|
+
forwardedSignal?.addEventListener("abort", onAbort, { once: true });
|
|
555
|
+
try {
|
|
556
|
+
const result = await runShell(command, {
|
|
557
|
+
cwd,
|
|
558
|
+
signal: controller.signal,
|
|
559
|
+
onData: input.onData
|
|
560
|
+
});
|
|
561
|
+
return { exitCode: result.exitCode };
|
|
562
|
+
} finally {
|
|
563
|
+
forwardedSignal?.removeEventListener("abort", onAbort);
|
|
564
|
+
if (timeoutId) {
|
|
565
|
+
clearTimeout(timeoutId);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/pi-skills.ts
|
|
573
|
+
import path4 from "path";
|
|
574
|
+
async function writePiSkills(args) {
|
|
575
|
+
for (const skill of args.skills) {
|
|
576
|
+
safePiMetadataSegment(skill.name, "skill");
|
|
577
|
+
for (const file of skill.files ?? []) {
|
|
578
|
+
safePiSkillFilePath({ skillName: skill.name, filePath: file.path });
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const skill of args.skills) {
|
|
582
|
+
const name = safePiMetadataSegment(skill.name, "skill");
|
|
583
|
+
const sandboxSkillDir = path4.posix.join(
|
|
584
|
+
args.sandboxHomeDir,
|
|
585
|
+
".agents",
|
|
586
|
+
"skills",
|
|
587
|
+
name
|
|
588
|
+
);
|
|
589
|
+
const content = renderPiSkillFile(skill);
|
|
590
|
+
await args.sandbox.writeTextFile({
|
|
591
|
+
path: path4.posix.join(sandboxSkillDir, "SKILL.md"),
|
|
592
|
+
content,
|
|
593
|
+
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
594
|
+
});
|
|
595
|
+
for (const file of skill.files ?? []) {
|
|
596
|
+
const filePath = safePiSkillFilePath({
|
|
597
|
+
skillName: skill.name,
|
|
598
|
+
filePath: file.path
|
|
599
|
+
});
|
|
600
|
+
await args.sandbox.writeTextFile({
|
|
601
|
+
path: path4.posix.join(sandboxSkillDir, filePath),
|
|
602
|
+
content: file.content,
|
|
603
|
+
...args.abortSignal ? { abortSignal: args.abortSignal } : {}
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function safePiSkillFilePath({
|
|
609
|
+
skillName,
|
|
610
|
+
filePath
|
|
611
|
+
}) {
|
|
612
|
+
const normalized = path4.posix.normalize(filePath);
|
|
613
|
+
if (normalized === "." || normalized.startsWith("../") || path4.posix.isAbsolute(normalized)) {
|
|
614
|
+
throw new Error(`Invalid Pi skill file path for ${skillName}: ${filePath}`);
|
|
615
|
+
}
|
|
616
|
+
return normalized;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/pi-translate.ts
|
|
620
|
+
import { randomBytes } from "crypto";
|
|
621
|
+
function createPiTranslatorState(options = {}) {
|
|
622
|
+
const map = options.nativeToCommon instanceof Map ? options.nativeToCommon : new Map(Object.entries(options.nativeToCommon ?? {}));
|
|
623
|
+
return {
|
|
624
|
+
promptStarted: false,
|
|
625
|
+
streamedAssistantText: "",
|
|
626
|
+
currentTextId: void 0,
|
|
627
|
+
currentReasoningId: void 0,
|
|
628
|
+
reasoningStarted: false,
|
|
629
|
+
observedToolNames: /* @__PURE__ */ new Map(),
|
|
630
|
+
hostToolResults: /* @__PURE__ */ new Map(),
|
|
631
|
+
builtinToolNames: new Set(options.builtinToolNames ?? []),
|
|
632
|
+
nativeToCommonNameMap: map
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function newId() {
|
|
636
|
+
return randomBytes(8).toString("hex");
|
|
637
|
+
}
|
|
638
|
+
function unwrapPiToolResult(event) {
|
|
639
|
+
const candidates = [];
|
|
640
|
+
const result = event.result;
|
|
641
|
+
if (result && typeof result === "object") {
|
|
642
|
+
const inner = result.content;
|
|
643
|
+
if (Array.isArray(inner)) candidates.push(inner);
|
|
644
|
+
}
|
|
645
|
+
if (Array.isArray(event.content)) candidates.push(event.content);
|
|
646
|
+
for (const content of candidates) {
|
|
647
|
+
if (!Array.isArray(content)) continue;
|
|
648
|
+
const text = content.filter(
|
|
649
|
+
(p) => !!p && typeof p === "object" && p.type === "text" && typeof p.text === "string"
|
|
650
|
+
).map((p) => p.text).join("");
|
|
651
|
+
if (text) return text;
|
|
652
|
+
}
|
|
653
|
+
if (typeof event.result === "string") return event.result;
|
|
654
|
+
if (typeof event.content === "string") return event.content;
|
|
655
|
+
return event.result ?? event.content ?? null;
|
|
656
|
+
}
|
|
657
|
+
function resolveToolName(state, nativeName) {
|
|
658
|
+
const common = state.nativeToCommonNameMap.get(nativeName);
|
|
659
|
+
return { wire: common ?? nativeName, native: nativeName };
|
|
660
|
+
}
|
|
661
|
+
function translatePiEvent(event, state) {
|
|
662
|
+
switch (event.type) {
|
|
663
|
+
case "turn_start":
|
|
664
|
+
case "message_start": {
|
|
665
|
+
if (event.type === "message_start" && event.message?.role !== "assistant") {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
state.promptStarted = true;
|
|
669
|
+
state.streamedAssistantText = "";
|
|
670
|
+
state.currentTextId = void 0;
|
|
671
|
+
state.currentReasoningId = void 0;
|
|
672
|
+
state.reasoningStarted = false;
|
|
673
|
+
return [];
|
|
674
|
+
}
|
|
675
|
+
case "message_update": {
|
|
676
|
+
if (!state.promptStarted) return [];
|
|
677
|
+
const update = event.assistantMessageEvent;
|
|
678
|
+
if (!update) return [];
|
|
679
|
+
if (update.type === "text_delta" && typeof update.delta === "string") {
|
|
680
|
+
const parts = [];
|
|
681
|
+
if (state.reasoningStarted && state.currentReasoningId) {
|
|
682
|
+
parts.push({
|
|
683
|
+
type: "reasoning-end",
|
|
684
|
+
id: state.currentReasoningId
|
|
685
|
+
});
|
|
686
|
+
state.reasoningStarted = false;
|
|
687
|
+
state.currentReasoningId = void 0;
|
|
688
|
+
}
|
|
689
|
+
if (!state.currentTextId) {
|
|
690
|
+
state.currentTextId = newId();
|
|
691
|
+
parts.push({ type: "text-start", id: state.currentTextId });
|
|
692
|
+
}
|
|
693
|
+
state.streamedAssistantText += update.delta;
|
|
694
|
+
parts.push({
|
|
695
|
+
type: "text-delta",
|
|
696
|
+
id: state.currentTextId,
|
|
697
|
+
delta: update.delta
|
|
698
|
+
});
|
|
699
|
+
return parts;
|
|
700
|
+
}
|
|
701
|
+
if (update.type === "thinking_delta" && typeof update.delta === "string") {
|
|
702
|
+
const parts = [];
|
|
703
|
+
if (state.currentTextId) {
|
|
704
|
+
parts.push({ type: "text-end", id: state.currentTextId });
|
|
705
|
+
state.currentTextId = void 0;
|
|
706
|
+
}
|
|
707
|
+
if (!state.currentReasoningId) {
|
|
708
|
+
state.currentReasoningId = newId();
|
|
709
|
+
}
|
|
710
|
+
if (!state.reasoningStarted) {
|
|
711
|
+
state.reasoningStarted = true;
|
|
712
|
+
parts.push({ type: "reasoning-start", id: state.currentReasoningId });
|
|
713
|
+
}
|
|
714
|
+
parts.push({
|
|
715
|
+
type: "reasoning-delta",
|
|
716
|
+
id: state.currentReasoningId,
|
|
717
|
+
delta: update.delta
|
|
718
|
+
});
|
|
719
|
+
return parts;
|
|
720
|
+
}
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
case "message_end":
|
|
724
|
+
case "turn_end": {
|
|
725
|
+
if (!state.promptStarted) return [];
|
|
726
|
+
const parts = [];
|
|
727
|
+
const fullText = extractAssistantText(event.message);
|
|
728
|
+
if (state.currentTextId && fullText.startsWith(state.streamedAssistantText) && fullText.length > state.streamedAssistantText.length) {
|
|
729
|
+
const missing = fullText.slice(state.streamedAssistantText.length);
|
|
730
|
+
state.streamedAssistantText = fullText;
|
|
731
|
+
parts.push({
|
|
732
|
+
type: "text-delta",
|
|
733
|
+
id: state.currentTextId,
|
|
734
|
+
delta: missing
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
if (state.currentTextId) {
|
|
738
|
+
parts.push({ type: "text-end", id: state.currentTextId });
|
|
739
|
+
state.currentTextId = void 0;
|
|
740
|
+
}
|
|
741
|
+
if (state.reasoningStarted && state.currentReasoningId) {
|
|
742
|
+
parts.push({ type: "reasoning-end", id: state.currentReasoningId });
|
|
743
|
+
state.reasoningStarted = false;
|
|
744
|
+
state.currentReasoningId = void 0;
|
|
745
|
+
}
|
|
746
|
+
return parts;
|
|
747
|
+
}
|
|
748
|
+
case "tool_execution_start": {
|
|
749
|
+
if (!event.toolCallId || !event.toolName) return [];
|
|
750
|
+
const { wire, native } = resolveToolName(state, event.toolName);
|
|
751
|
+
state.observedToolNames.set(event.toolCallId, wire);
|
|
752
|
+
const providerExecuted = state.builtinToolNames.has(native);
|
|
753
|
+
const input = serializeToolOutput(event.args ?? event.input ?? {});
|
|
754
|
+
return [
|
|
755
|
+
{
|
|
756
|
+
type: "tool-call",
|
|
757
|
+
toolCallId: event.toolCallId,
|
|
758
|
+
toolName: wire,
|
|
759
|
+
input,
|
|
760
|
+
...wire !== native ? { nativeName: native } : {},
|
|
761
|
+
...providerExecuted ? { providerExecuted: true } : {}
|
|
762
|
+
}
|
|
763
|
+
];
|
|
764
|
+
}
|
|
765
|
+
case "tool_execution_end":
|
|
766
|
+
case "tool_result": {
|
|
767
|
+
if (!event.toolCallId) return [];
|
|
768
|
+
const recordedName = state.observedToolNames.get(event.toolCallId);
|
|
769
|
+
const nativeName = event.toolName;
|
|
770
|
+
const wire = recordedName ?? (nativeName ? resolveToolName(state, nativeName).wire : void 0);
|
|
771
|
+
if (!wire) return [];
|
|
772
|
+
const result = state.hostToolResults.has(event.toolCallId) ? state.hostToolResults.get(event.toolCallId) ?? null : unwrapPiToolResult(event);
|
|
773
|
+
state.hostToolResults.delete(event.toolCallId);
|
|
774
|
+
return [
|
|
775
|
+
{
|
|
776
|
+
type: "tool-result",
|
|
777
|
+
toolCallId: event.toolCallId,
|
|
778
|
+
toolName: wire,
|
|
779
|
+
result,
|
|
780
|
+
...event.isError ? { isError: true } : {}
|
|
781
|
+
}
|
|
782
|
+
];
|
|
783
|
+
}
|
|
784
|
+
case "compaction_end": {
|
|
785
|
+
if (event.aborted) return [];
|
|
786
|
+
const result = event.result;
|
|
787
|
+
if (!result || typeof result !== "object") return [];
|
|
788
|
+
const rawSummary = result.summary;
|
|
789
|
+
const summary = typeof rawSummary === "string" ? rawSummary : "(no summary provided)";
|
|
790
|
+
const tokensBefore = result.tokensBefore;
|
|
791
|
+
return [
|
|
792
|
+
{
|
|
793
|
+
type: "compaction",
|
|
794
|
+
trigger: event.reason === "manual" ? "manual" : "auto",
|
|
795
|
+
summary,
|
|
796
|
+
...typeof tokensBefore === "number" ? { tokensBefore } : {}
|
|
797
|
+
}
|
|
798
|
+
];
|
|
799
|
+
}
|
|
800
|
+
default:
|
|
801
|
+
return [];
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/pi-typebox-adapter.ts
|
|
806
|
+
import { Type } from "typebox";
|
|
807
|
+
function toolSpecToTypeBoxParameters(jsonSchema) {
|
|
808
|
+
return Type.Unsafe(jsonSchema);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
// src/pi-workspace-vfs.ts
|
|
812
|
+
import fs from "fs";
|
|
813
|
+
import { syncBuiltinESMExports } from "module";
|
|
814
|
+
import path5 from "path";
|
|
815
|
+
var mountedRoots = /* @__PURE__ */ new Map();
|
|
816
|
+
var mutableFs = fs;
|
|
817
|
+
var mutableFsPromises = fs.promises;
|
|
818
|
+
var originalFsSyncMethods = {
|
|
819
|
+
existsSync: fs.existsSync,
|
|
820
|
+
mkdirSync: fs.mkdirSync,
|
|
821
|
+
openSync: fs.openSync,
|
|
822
|
+
readFileSync: fs.readFileSync,
|
|
823
|
+
readdirSync: fs.readdirSync,
|
|
824
|
+
realpathSync: fs.realpathSync,
|
|
825
|
+
renameSync: fs.renameSync,
|
|
826
|
+
rmSync: fs.rmSync,
|
|
827
|
+
statSync: fs.statSync,
|
|
828
|
+
writeFileSync: fs.writeFileSync
|
|
829
|
+
};
|
|
830
|
+
var originalFsCallbackMethods = {
|
|
831
|
+
mkdir: fs.mkdir,
|
|
832
|
+
realpath: fs.realpath,
|
|
833
|
+
rmdir: fs.rmdir,
|
|
834
|
+
stat: fs.stat,
|
|
835
|
+
utimes: fs.utimes,
|
|
836
|
+
writeFile: fs.writeFile
|
|
837
|
+
};
|
|
838
|
+
var originalFsPromises = {
|
|
839
|
+
mkdir: fs.promises.mkdir.bind(fs.promises),
|
|
840
|
+
readFile: fs.promises.readFile.bind(fs.promises),
|
|
841
|
+
writeFile: fs.promises.writeFile.bind(fs.promises)
|
|
842
|
+
};
|
|
843
|
+
function isInsidePath2(parent, candidate) {
|
|
844
|
+
const relative = path5.relative(parent, candidate);
|
|
845
|
+
return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
|
|
846
|
+
}
|
|
847
|
+
function resolveAbsolutePath(inputPath) {
|
|
848
|
+
return path5.isAbsolute(inputPath) ? path5.normalize(inputPath) : path5.resolve(inputPath);
|
|
849
|
+
}
|
|
850
|
+
function findMountedRoot(inputPath) {
|
|
851
|
+
const resolvedPath = resolveAbsolutePath(inputPath);
|
|
852
|
+
let bestMatch;
|
|
853
|
+
for (const mountedRoot of mountedRoots.values()) {
|
|
854
|
+
if (!isInsidePath2(mountedRoot.mountPoint, resolvedPath)) {
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (!bestMatch || mountedRoot.mountPoint.length > bestMatch.mountPoint.length) {
|
|
858
|
+
bestMatch = mountedRoot;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
return bestMatch;
|
|
862
|
+
}
|
|
863
|
+
function mapToBackingPath(inputPath) {
|
|
864
|
+
const mountedRoot = findMountedRoot(inputPath);
|
|
865
|
+
if (!mountedRoot) {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
const resolvedPath = resolveAbsolutePath(inputPath);
|
|
869
|
+
const relativePath = path5.relative(mountedRoot.mountPoint, resolvedPath);
|
|
870
|
+
return relativePath ? path5.join(mountedRoot.backingRoot, relativePath) : mountedRoot.backingRoot;
|
|
871
|
+
}
|
|
872
|
+
function mapRealpathResult(inputPath, result) {
|
|
873
|
+
const mountedRoot = findMountedRoot(inputPath);
|
|
874
|
+
if (!mountedRoot) {
|
|
875
|
+
return result;
|
|
876
|
+
}
|
|
877
|
+
let normalizedBackingRoot = path5.resolve(mountedRoot.backingRoot);
|
|
878
|
+
try {
|
|
879
|
+
const realpath = originalFsSyncMethods.realpathSync.native ?? originalFsSyncMethods.realpathSync;
|
|
880
|
+
normalizedBackingRoot = path5.resolve(realpath(mountedRoot.backingRoot));
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
const normalizedResult = path5.resolve(result);
|
|
884
|
+
if (!isInsidePath2(normalizedBackingRoot, normalizedResult)) {
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
const relativePath = path5.relative(normalizedBackingRoot, normalizedResult);
|
|
888
|
+
return relativePath ? path5.join(mountedRoot.mountPoint, relativePath) : mountedRoot.mountPoint;
|
|
889
|
+
}
|
|
890
|
+
function mapRenamePaths(sourcePath, destinationPath) {
|
|
891
|
+
const sourceRoot = findMountedRoot(sourcePath);
|
|
892
|
+
const destinationRoot = findMountedRoot(destinationPath);
|
|
893
|
+
if (!sourceRoot && !destinationRoot) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
if (!sourceRoot || !destinationRoot || sourceRoot.mountPoint !== destinationRoot.mountPoint) {
|
|
897
|
+
throw new Error(
|
|
898
|
+
"Pi logical VFS paths cannot rename across mount boundaries"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
return [
|
|
902
|
+
mapToBackingPath(sourcePath) ?? sourcePath,
|
|
903
|
+
mapToBackingPath(destinationPath) ?? destinationPath
|
|
904
|
+
];
|
|
905
|
+
}
|
|
906
|
+
function wrapSinglePathSync(original, options = {}) {
|
|
907
|
+
return ((...args) => {
|
|
908
|
+
const [inputPath] = args;
|
|
909
|
+
if (typeof inputPath === "string") {
|
|
910
|
+
const mappedPath = mapToBackingPath(inputPath);
|
|
911
|
+
if (mappedPath) {
|
|
912
|
+
const result = original(
|
|
913
|
+
...[mappedPath, ...args.slice(1)]
|
|
914
|
+
);
|
|
915
|
+
return options.mapResult ? options.mapResult(inputPath, result) : result;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return original(...args);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
function wrapRenameSync(original) {
|
|
922
|
+
return ((...args) => {
|
|
923
|
+
const [sourcePath, destinationPath] = args;
|
|
924
|
+
if (typeof sourcePath === "string" && typeof destinationPath === "string") {
|
|
925
|
+
const mappedPaths = mapRenamePaths(sourcePath, destinationPath);
|
|
926
|
+
if (mappedPaths) {
|
|
927
|
+
return original(
|
|
928
|
+
...[
|
|
929
|
+
mappedPaths[0],
|
|
930
|
+
mappedPaths[1],
|
|
931
|
+
...args.slice(2)
|
|
932
|
+
]
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return original(...args);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function wrapSinglePathCallback(original, options = {}) {
|
|
940
|
+
return ((...args) => {
|
|
941
|
+
const [inputPath] = args;
|
|
942
|
+
if (typeof inputPath === "string") {
|
|
943
|
+
const mappedPath = mapToBackingPath(inputPath);
|
|
944
|
+
if (mappedPath) {
|
|
945
|
+
const nextArgs = [mappedPath, ...args.slice(1)];
|
|
946
|
+
const maybeCallback = nextArgs.at(-1);
|
|
947
|
+
if (options.mapCallbackResult && typeof maybeCallback === "function") {
|
|
948
|
+
nextArgs[nextArgs.length - 1] = (error, result, ...rest) => {
|
|
949
|
+
const mappedResult = error ? result : options.mapCallbackResult?.(inputPath, result);
|
|
950
|
+
maybeCallback(
|
|
951
|
+
error,
|
|
952
|
+
mappedResult,
|
|
953
|
+
...rest
|
|
954
|
+
);
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
return original(...nextArgs);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return original(...args);
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
function wrapSinglePathPromise(original) {
|
|
964
|
+
return (async (...args) => {
|
|
965
|
+
const [inputPath] = args;
|
|
966
|
+
if (typeof inputPath === "string") {
|
|
967
|
+
const mappedPath = mapToBackingPath(inputPath);
|
|
968
|
+
if (mappedPath) {
|
|
969
|
+
return await original(
|
|
970
|
+
...[mappedPath, ...args.slice(1)]
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return await original(...args);
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
function createWrappedRealpathSync() {
|
|
978
|
+
const wrapped = wrapSinglePathSync(originalFsSyncMethods.realpathSync, {
|
|
979
|
+
mapResult: (inputPath, result) => typeof result === "string" ? mapRealpathResult(inputPath, result) : result
|
|
980
|
+
});
|
|
981
|
+
if (originalFsSyncMethods.realpathSync.native) {
|
|
982
|
+
wrapped.native = wrapSinglePathSync(
|
|
983
|
+
originalFsSyncMethods.realpathSync.native,
|
|
984
|
+
{
|
|
985
|
+
mapResult: (inputPath, result) => typeof result === "string" ? mapRealpathResult(inputPath, result) : result
|
|
986
|
+
}
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
return wrapped;
|
|
990
|
+
}
|
|
991
|
+
var wrappedFsSyncMethods = {
|
|
992
|
+
existsSync: wrapSinglePathSync(originalFsSyncMethods.existsSync),
|
|
993
|
+
mkdirSync: wrapSinglePathSync(originalFsSyncMethods.mkdirSync),
|
|
994
|
+
openSync: wrapSinglePathSync(originalFsSyncMethods.openSync),
|
|
995
|
+
readFileSync: wrapSinglePathSync(originalFsSyncMethods.readFileSync),
|
|
996
|
+
readdirSync: wrapSinglePathSync(originalFsSyncMethods.readdirSync),
|
|
997
|
+
realpathSync: createWrappedRealpathSync(),
|
|
998
|
+
renameSync: wrapRenameSync(originalFsSyncMethods.renameSync),
|
|
999
|
+
rmSync: wrapSinglePathSync(originalFsSyncMethods.rmSync),
|
|
1000
|
+
statSync: wrapSinglePathSync(originalFsSyncMethods.statSync),
|
|
1001
|
+
writeFileSync: wrapSinglePathSync(originalFsSyncMethods.writeFileSync)
|
|
1002
|
+
};
|
|
1003
|
+
var wrappedFsCallbackMethods = {
|
|
1004
|
+
mkdir: wrapSinglePathCallback(originalFsCallbackMethods.mkdir),
|
|
1005
|
+
realpath: wrapSinglePathCallback(originalFsCallbackMethods.realpath, {
|
|
1006
|
+
mapCallbackResult: (inputPath, result) => typeof result === "string" ? mapRealpathResult(inputPath, result) : result
|
|
1007
|
+
}),
|
|
1008
|
+
rmdir: wrapSinglePathCallback(originalFsCallbackMethods.rmdir),
|
|
1009
|
+
stat: wrapSinglePathCallback(originalFsCallbackMethods.stat),
|
|
1010
|
+
utimes: wrapSinglePathCallback(originalFsCallbackMethods.utimes),
|
|
1011
|
+
writeFile: wrapSinglePathCallback(originalFsCallbackMethods.writeFile)
|
|
1012
|
+
};
|
|
1013
|
+
var wrappedFsPromises = {
|
|
1014
|
+
mkdir: wrapSinglePathPromise(originalFsPromises.mkdir),
|
|
1015
|
+
readFile: wrapSinglePathPromise(originalFsPromises.readFile),
|
|
1016
|
+
writeFile: wrapSinglePathPromise(originalFsPromises.writeFile)
|
|
1017
|
+
};
|
|
1018
|
+
function areExtraFsPatchesInstalled() {
|
|
1019
|
+
return mutableFs.existsSync === wrappedFsSyncMethods.existsSync;
|
|
1020
|
+
}
|
|
1021
|
+
function installExtraFsPatches() {
|
|
1022
|
+
if (areExtraFsPatchesInstalled()) return;
|
|
1023
|
+
mutableFs.existsSync = wrappedFsSyncMethods.existsSync;
|
|
1024
|
+
mutableFs.mkdirSync = wrappedFsSyncMethods.mkdirSync;
|
|
1025
|
+
mutableFs.openSync = wrappedFsSyncMethods.openSync;
|
|
1026
|
+
mutableFs.readFileSync = wrappedFsSyncMethods.readFileSync;
|
|
1027
|
+
mutableFs.readdirSync = wrappedFsSyncMethods.readdirSync;
|
|
1028
|
+
mutableFs.realpathSync = wrappedFsSyncMethods.realpathSync;
|
|
1029
|
+
mutableFs.renameSync = wrappedFsSyncMethods.renameSync;
|
|
1030
|
+
mutableFs.rmSync = wrappedFsSyncMethods.rmSync;
|
|
1031
|
+
mutableFs.statSync = wrappedFsSyncMethods.statSync;
|
|
1032
|
+
mutableFs.writeFileSync = wrappedFsSyncMethods.writeFileSync;
|
|
1033
|
+
mutableFs.mkdir = wrappedFsCallbackMethods.mkdir;
|
|
1034
|
+
mutableFs.realpath = wrappedFsCallbackMethods.realpath;
|
|
1035
|
+
mutableFs.rmdir = wrappedFsCallbackMethods.rmdir;
|
|
1036
|
+
mutableFs.stat = wrappedFsCallbackMethods.stat;
|
|
1037
|
+
mutableFs.utimes = wrappedFsCallbackMethods.utimes;
|
|
1038
|
+
mutableFs.writeFile = wrappedFsCallbackMethods.writeFile;
|
|
1039
|
+
mutableFsPromises.mkdir = wrappedFsPromises.mkdir;
|
|
1040
|
+
mutableFsPromises.readFile = wrappedFsPromises.readFile;
|
|
1041
|
+
mutableFsPromises.writeFile = wrappedFsPromises.writeFile;
|
|
1042
|
+
syncBuiltinESMExports();
|
|
1043
|
+
}
|
|
1044
|
+
function restoreExtraFsPatches() {
|
|
1045
|
+
if (!areExtraFsPatchesInstalled()) return;
|
|
1046
|
+
mutableFs.existsSync = originalFsSyncMethods.existsSync;
|
|
1047
|
+
mutableFs.mkdirSync = originalFsSyncMethods.mkdirSync;
|
|
1048
|
+
mutableFs.openSync = originalFsSyncMethods.openSync;
|
|
1049
|
+
mutableFs.readFileSync = originalFsSyncMethods.readFileSync;
|
|
1050
|
+
mutableFs.readdirSync = originalFsSyncMethods.readdirSync;
|
|
1051
|
+
mutableFs.realpathSync = originalFsSyncMethods.realpathSync;
|
|
1052
|
+
mutableFs.renameSync = originalFsSyncMethods.renameSync;
|
|
1053
|
+
mutableFs.rmSync = originalFsSyncMethods.rmSync;
|
|
1054
|
+
mutableFs.statSync = originalFsSyncMethods.statSync;
|
|
1055
|
+
mutableFs.writeFileSync = originalFsSyncMethods.writeFileSync;
|
|
1056
|
+
mutableFs.mkdir = originalFsCallbackMethods.mkdir;
|
|
1057
|
+
mutableFs.realpath = originalFsCallbackMethods.realpath;
|
|
1058
|
+
mutableFs.rmdir = originalFsCallbackMethods.rmdir;
|
|
1059
|
+
mutableFs.stat = originalFsCallbackMethods.stat;
|
|
1060
|
+
mutableFs.utimes = originalFsCallbackMethods.utimes;
|
|
1061
|
+
mutableFs.writeFile = originalFsCallbackMethods.writeFile;
|
|
1062
|
+
mutableFsPromises.mkdir = originalFsPromises.mkdir;
|
|
1063
|
+
mutableFsPromises.readFile = originalFsPromises.readFile;
|
|
1064
|
+
mutableFsPromises.writeFile = originalFsPromises.writeFile;
|
|
1065
|
+
syncBuiltinESMExports();
|
|
1066
|
+
}
|
|
1067
|
+
var PiWorkspaceVfs = class {
|
|
1068
|
+
constructor() {
|
|
1069
|
+
this.backingRoot = null;
|
|
1070
|
+
this.mountPoint = null;
|
|
1071
|
+
}
|
|
1072
|
+
disposeMount() {
|
|
1073
|
+
if (this.mountPoint) {
|
|
1074
|
+
mountedRoots.delete(this.mountPoint);
|
|
1075
|
+
}
|
|
1076
|
+
this.backingRoot = null;
|
|
1077
|
+
this.mountPoint = null;
|
|
1078
|
+
}
|
|
1079
|
+
mount(backingRoot, mountPoint) {
|
|
1080
|
+
if (this.backingRoot === backingRoot && this.mountPoint === mountPoint) {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const resolvedBackingRoot = path5.resolve(backingRoot);
|
|
1084
|
+
const resolvedMountPoint = path5.resolve(mountPoint);
|
|
1085
|
+
if (this.mountPoint) {
|
|
1086
|
+
this.disposeMount();
|
|
1087
|
+
}
|
|
1088
|
+
mountedRoots.set(resolvedMountPoint, {
|
|
1089
|
+
backingRoot: resolvedBackingRoot,
|
|
1090
|
+
mountPoint: resolvedMountPoint
|
|
1091
|
+
});
|
|
1092
|
+
installExtraFsPatches();
|
|
1093
|
+
this.backingRoot = resolvedBackingRoot;
|
|
1094
|
+
this.mountPoint = resolvedMountPoint;
|
|
1095
|
+
}
|
|
1096
|
+
unmount() {
|
|
1097
|
+
this.disposeMount();
|
|
1098
|
+
if (mountedRoots.size === 0) {
|
|
1099
|
+
restoreExtraFsPatches();
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// src/pi-workspace-mirror.ts
|
|
1105
|
+
import {
|
|
1106
|
+
mkdir as mkdir2,
|
|
1107
|
+
readFile as readFile2,
|
|
1108
|
+
readdir,
|
|
1109
|
+
rm,
|
|
1110
|
+
stat,
|
|
1111
|
+
writeFile as writeFile2
|
|
1112
|
+
} from "fs/promises";
|
|
1113
|
+
import path6 from "path";
|
|
1114
|
+
var PI_CONFIG_DIRS = [".pi", ".agents"];
|
|
1115
|
+
var PI_CONTEXT_FILENAMES = ["AGENTS.md", "AGENTS.MD"];
|
|
1116
|
+
function normalizeRelativePath(inputPath) {
|
|
1117
|
+
const normalized = inputPath.split(path6.posix.sep).join(path6.sep);
|
|
1118
|
+
const relative = path6.normalize(normalized);
|
|
1119
|
+
if (relative === "" || relative === "." || path6.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path6.sep}`)) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Sandbox workspace mirror received an invalid relative path: ${inputPath}`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
return relative;
|
|
1125
|
+
}
|
|
1126
|
+
async function readCommandOutput(sandbox, command) {
|
|
1127
|
+
const result = await sandbox.run({ command });
|
|
1128
|
+
const output = result.stdout || result.stderr;
|
|
1129
|
+
if (result.exitCode != null && result.exitCode !== 0) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
output || `Sandbox command failed with exit code ${result.exitCode}`
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
return output;
|
|
1135
|
+
}
|
|
1136
|
+
async function listRemoteWorkspaceEntries(sandbox, sandboxWorkDir) {
|
|
1137
|
+
const contextPredicate = PI_CONTEXT_FILENAMES.map(
|
|
1138
|
+
(name) => `-name ${shellQuote(name)}`
|
|
1139
|
+
).join(" -o ");
|
|
1140
|
+
const configFinds = PI_CONFIG_DIRS.map(
|
|
1141
|
+
(dir) => ` if [ -d ./${dir} ]; then find -L ./${dir} \\( -type d -o -type f \\) -print0; fi;`
|
|
1142
|
+
);
|
|
1143
|
+
const listCommand = [
|
|
1144
|
+
"{",
|
|
1145
|
+
...configFinds,
|
|
1146
|
+
` find . -maxdepth 1 -type f \\( ${contextPredicate} \\) -print0;`,
|
|
1147
|
+
"} |",
|
|
1148
|
+
"while IFS= read -r -d '' entry; do",
|
|
1149
|
+
" rel=${entry#./}",
|
|
1150
|
+
' if [ -d "$entry" ]; then',
|
|
1151
|
+
` printf 'd\\t%s\\n' "$rel"`,
|
|
1152
|
+
' elif [ -f "$entry" ]; then',
|
|
1153
|
+
` printf 'f\\t%s\\n' "$rel"`,
|
|
1154
|
+
" fi",
|
|
1155
|
+
"done | LC_ALL=C sort"
|
|
1156
|
+
].join("\n");
|
|
1157
|
+
const output = await readCommandOutput(
|
|
1158
|
+
sandbox,
|
|
1159
|
+
[`cd ${shellQuote(sandboxWorkDir)}`, listCommand].join(" && ")
|
|
1160
|
+
);
|
|
1161
|
+
const directories = [];
|
|
1162
|
+
const files = [];
|
|
1163
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
1164
|
+
const [kind, rawPath] = line.split(" ", 2);
|
|
1165
|
+
if (!rawPath) continue;
|
|
1166
|
+
const relativePath = normalizeRelativePath(rawPath);
|
|
1167
|
+
if (kind === "d") directories.push(relativePath);
|
|
1168
|
+
else if (kind === "f") files.push(relativePath);
|
|
1169
|
+
}
|
|
1170
|
+
return { directories, files };
|
|
1171
|
+
}
|
|
1172
|
+
async function pathKind(target) {
|
|
1173
|
+
try {
|
|
1174
|
+
const stats = await stat(target);
|
|
1175
|
+
if (stats.isDirectory()) return "directory";
|
|
1176
|
+
if (stats.isFile()) return "file";
|
|
1177
|
+
return void 0;
|
|
1178
|
+
} catch {
|
|
1179
|
+
return void 0;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
async function collectHostSubtree(rootDir, currentDir, directories, files) {
|
|
1183
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
1184
|
+
for (const entry of entries) {
|
|
1185
|
+
if (entry.isSymbolicLink()) continue;
|
|
1186
|
+
const absolutePath = path6.join(currentDir, entry.name);
|
|
1187
|
+
const relativePath = path6.relative(rootDir, absolutePath);
|
|
1188
|
+
if (entry.isDirectory()) {
|
|
1189
|
+
directories.push(relativePath);
|
|
1190
|
+
await collectHostSubtree(rootDir, absolutePath, directories, files);
|
|
1191
|
+
} else if (entry.isFile()) {
|
|
1192
|
+
files.push(relativePath);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
async function collectHostScopedEntries(rootDir) {
|
|
1197
|
+
const directories = [];
|
|
1198
|
+
const files = [];
|
|
1199
|
+
for (const dir of PI_CONFIG_DIRS) {
|
|
1200
|
+
const configDir = path6.join(rootDir, dir);
|
|
1201
|
+
if (await pathKind(configDir) === "directory") {
|
|
1202
|
+
directories.push(dir);
|
|
1203
|
+
await collectHostSubtree(rootDir, configDir, directories, files);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
for (const name of PI_CONTEXT_FILENAMES) {
|
|
1207
|
+
if (await pathKind(path6.join(rootDir, name)) === "file") {
|
|
1208
|
+
files.push(name);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return { directories, files };
|
|
1212
|
+
}
|
|
1213
|
+
function buildRequiredDirectories(remoteDirectories, remoteFiles) {
|
|
1214
|
+
const directories = /* @__PURE__ */ new Set();
|
|
1215
|
+
for (const directory of remoteDirectories) {
|
|
1216
|
+
directories.add(normalizeRelativePath(directory));
|
|
1217
|
+
}
|
|
1218
|
+
for (const file of remoteFiles) {
|
|
1219
|
+
let current = path6.dirname(normalizeRelativePath(file));
|
|
1220
|
+
while (current !== "." && current !== path6.sep && current.length > 0) {
|
|
1221
|
+
directories.add(current);
|
|
1222
|
+
current = path6.dirname(current);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return directories;
|
|
1226
|
+
}
|
|
1227
|
+
async function syncHostWorkspaceFromSandbox(args) {
|
|
1228
|
+
const { sandbox, sandboxWorkDir, hostWorkDir } = args;
|
|
1229
|
+
const remoteEntries = await listRemoteWorkspaceEntries(
|
|
1230
|
+
sandbox,
|
|
1231
|
+
sandboxWorkDir
|
|
1232
|
+
);
|
|
1233
|
+
const hostEntries = await collectHostScopedEntries(hostWorkDir);
|
|
1234
|
+
const remoteFiles = new Set(remoteEntries.files);
|
|
1235
|
+
const requiredDirectories = buildRequiredDirectories(
|
|
1236
|
+
remoteEntries.directories,
|
|
1237
|
+
remoteEntries.files
|
|
1238
|
+
);
|
|
1239
|
+
for (const relativePath of hostEntries.files) {
|
|
1240
|
+
if (!remoteFiles.has(relativePath)) {
|
|
1241
|
+
await rm(path6.join(hostWorkDir, relativePath), { force: true });
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const removableDirectories = [...hostEntries.directories].filter((p) => !requiredDirectories.has(p)).sort((a, b) => b.length - a.length);
|
|
1245
|
+
for (const relativePath of removableDirectories) {
|
|
1246
|
+
await rm(path6.join(hostWorkDir, relativePath), {
|
|
1247
|
+
recursive: true,
|
|
1248
|
+
force: true
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
for (const relativePath of [...requiredDirectories].sort(
|
|
1252
|
+
(a, b) => a.length - b.length
|
|
1253
|
+
)) {
|
|
1254
|
+
await mkdir2(path6.join(hostWorkDir, relativePath), { recursive: true });
|
|
1255
|
+
}
|
|
1256
|
+
for (const relativePath of remoteEntries.files) {
|
|
1257
|
+
const remotePath = path6.posix.join(
|
|
1258
|
+
sandboxWorkDir,
|
|
1259
|
+
relativePath.split(path6.sep).join("/")
|
|
1260
|
+
);
|
|
1261
|
+
const bytes = await sandbox.readBinaryFile({ path: remotePath });
|
|
1262
|
+
if (!bytes) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
`Sandbox workspace file disappeared during mirror sync: ${remotePath}`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const content = Buffer.from(bytes);
|
|
1268
|
+
const hostPath = path6.join(hostWorkDir, relativePath);
|
|
1269
|
+
let shouldWrite = true;
|
|
1270
|
+
try {
|
|
1271
|
+
const existing = await readFile2(hostPath);
|
|
1272
|
+
shouldWrite = !existing.equals(content);
|
|
1273
|
+
} catch {
|
|
1274
|
+
shouldWrite = true;
|
|
1275
|
+
}
|
|
1276
|
+
if (shouldWrite) {
|
|
1277
|
+
await mkdir2(path6.dirname(hostPath), { recursive: true });
|
|
1278
|
+
await writeFile2(hostPath, content);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// src/pi-session.ts
|
|
1284
|
+
var HARNESS_ID2 = "pi";
|
|
1285
|
+
var parkedPiSessions = /* @__PURE__ */ new Map();
|
|
1286
|
+
function isWithinDirectory(parent, child) {
|
|
1287
|
+
const rel = path7.relative(parent, child);
|
|
1288
|
+
return rel === "" || !rel.startsWith("..") && !path7.isAbsolute(rel);
|
|
1289
|
+
}
|
|
1290
|
+
function isWithinWorkspace(candidate, sessionWorkDir, hostWorkDir) {
|
|
1291
|
+
return isWithinDirectory(sessionWorkDir, candidate) || isWithinDirectory(hostWorkDir, candidate);
|
|
1292
|
+
}
|
|
1293
|
+
function createHarnessPiSkills({
|
|
1294
|
+
skills,
|
|
1295
|
+
sandboxSkillRootDir
|
|
1296
|
+
}) {
|
|
1297
|
+
return skills.map((skill) => {
|
|
1298
|
+
const name = safePiMetadataSegment(skill.name, "skill");
|
|
1299
|
+
const baseDir = path7.posix.join(sandboxSkillRootDir, name);
|
|
1300
|
+
const filePath = path7.posix.join(baseDir, "SKILL.md");
|
|
1301
|
+
return {
|
|
1302
|
+
name: skill.name,
|
|
1303
|
+
description: skill.description,
|
|
1304
|
+
filePath,
|
|
1305
|
+
baseDir,
|
|
1306
|
+
sourceInfo: {
|
|
1307
|
+
path: filePath,
|
|
1308
|
+
source: "harness",
|
|
1309
|
+
scope: "temporary",
|
|
1310
|
+
origin: "top-level",
|
|
1311
|
+
baseDir
|
|
1312
|
+
},
|
|
1313
|
+
disableModelInvocation: false
|
|
1314
|
+
};
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
async function resolveSandboxHomeDir({
|
|
1318
|
+
sandbox,
|
|
1319
|
+
abortSignal
|
|
1320
|
+
}) {
|
|
1321
|
+
const result = await sandbox.run({
|
|
1322
|
+
command: 'printf "%s" "$HOME"',
|
|
1323
|
+
...abortSignal ? { abortSignal } : {}
|
|
1324
|
+
});
|
|
1325
|
+
const homeDir = result.stdout.trim();
|
|
1326
|
+
if (result.exitCode !== 0 || !homeDir || !path7.posix.isAbsolute(homeDir)) {
|
|
1327
|
+
throw new Error(
|
|
1328
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
return homeDir;
|
|
1332
|
+
}
|
|
1333
|
+
var PI_NATIVE_BUILTIN_NAMES = [
|
|
1334
|
+
"read",
|
|
1335
|
+
"write",
|
|
1336
|
+
"edit",
|
|
1337
|
+
"bash",
|
|
1338
|
+
"grep",
|
|
1339
|
+
"find",
|
|
1340
|
+
"ls"
|
|
1341
|
+
];
|
|
1342
|
+
var NATIVE_TO_COMMON = {
|
|
1343
|
+
find: "glob"
|
|
1344
|
+
};
|
|
1345
|
+
var PI_NATIVE_TOOL_KINDS = {
|
|
1346
|
+
read: "readonly",
|
|
1347
|
+
write: "edit",
|
|
1348
|
+
edit: "edit",
|
|
1349
|
+
bash: "bash",
|
|
1350
|
+
grep: "readonly",
|
|
1351
|
+
find: "readonly",
|
|
1352
|
+
ls: "readonly"
|
|
1353
|
+
};
|
|
1354
|
+
async function createPiSession(input) {
|
|
1355
|
+
if (input.isResume) {
|
|
1356
|
+
const parkedSession = parkedPiSessions.get(input.sessionId);
|
|
1357
|
+
if (parkedSession) {
|
|
1358
|
+
parkedPiSessions.delete(input.sessionId);
|
|
1359
|
+
return {
|
|
1360
|
+
...parkedSession,
|
|
1361
|
+
isResume: true
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const safeSessionId = input.sessionId.replace(/[\\/: ]/g, "-");
|
|
1366
|
+
const hostRoot = path7.join(tmpdir(), "ai-sdk-harness", "pi", safeSessionId);
|
|
1367
|
+
const hostWorkDir = path7.join(hostRoot, "workspace");
|
|
1368
|
+
const hostAgentDir = path7.join(hostRoot, "agent");
|
|
1369
|
+
const hostSessionDir = path7.join(hostRoot, "sessions");
|
|
1370
|
+
const sessionWorkDir = input.sessionWorkDir;
|
|
1371
|
+
await mkdir3(hostWorkDir, { recursive: true });
|
|
1372
|
+
await mkdir3(hostAgentDir, { recursive: true });
|
|
1373
|
+
await mkdir3(hostSessionDir, { recursive: true });
|
|
1374
|
+
const sandbox = input.sandboxSession.restricted();
|
|
1375
|
+
const permissionMode = input.permissionMode ?? "allow-all";
|
|
1376
|
+
let sandboxSkillRootDir;
|
|
1377
|
+
let harnessSkills = [];
|
|
1378
|
+
if (input.skills.length > 0) {
|
|
1379
|
+
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
1380
|
+
sandbox,
|
|
1381
|
+
...input.abortSignal ? { abortSignal: input.abortSignal } : {}
|
|
1382
|
+
});
|
|
1383
|
+
sandboxSkillRootDir = path7.posix.join(sandboxHomeDir, ".agents", "skills");
|
|
1384
|
+
harnessSkills = createHarnessPiSkills({
|
|
1385
|
+
skills: input.skills,
|
|
1386
|
+
sandboxSkillRootDir
|
|
1387
|
+
});
|
|
1388
|
+
await writePiSkills({
|
|
1389
|
+
sandbox,
|
|
1390
|
+
sandboxHomeDir,
|
|
1391
|
+
skills: input.skills,
|
|
1392
|
+
...input.abortSignal ? { abortSignal: input.abortSignal } : {}
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
let resumeSessionFilePath;
|
|
1396
|
+
if (input.isResume && input.resumeSessionFileName) {
|
|
1397
|
+
resumeSessionFilePath = await pullSessionFileFromSandbox({
|
|
1398
|
+
sandbox,
|
|
1399
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1400
|
+
hostSessionDir,
|
|
1401
|
+
sessionFileName: input.resumeSessionFileName,
|
|
1402
|
+
...input.abortSignal ? { abortSignal: input.abortSignal } : {}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
await syncHostWorkspaceFromSandbox({
|
|
1406
|
+
sandbox,
|
|
1407
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
1408
|
+
hostWorkDir
|
|
1409
|
+
});
|
|
1410
|
+
const workspaceVfs = new PiWorkspaceVfs();
|
|
1411
|
+
workspaceVfs.mount(hostWorkDir, sessionWorkDir);
|
|
1412
|
+
const paths = createPiPathMapper({
|
|
1413
|
+
hostWorkDir,
|
|
1414
|
+
sandboxWorkDir: sessionWorkDir,
|
|
1415
|
+
readableRoots: sandboxSkillRootDir ? [{ sandboxDir: sandboxSkillRootDir }] : []
|
|
1416
|
+
});
|
|
1417
|
+
const authStorage = AuthStorage.create(path7.join(hostAgentDir, "auth.json"));
|
|
1418
|
+
const modelRegistry = ModelRegistry.create(
|
|
1419
|
+
authStorage,
|
|
1420
|
+
path7.join(hostAgentDir, "models.json")
|
|
1421
|
+
);
|
|
1422
|
+
const settingsManager = SettingsManager.inMemory();
|
|
1423
|
+
const resolverEnv = resolvePiAuth(input.settings.auth, process.env, {
|
|
1424
|
+
authStorage,
|
|
1425
|
+
modelRegistry
|
|
1426
|
+
});
|
|
1427
|
+
const resolveModel = createPiModelResolver(modelRegistry, resolverEnv);
|
|
1428
|
+
const resolvedModel = resolveModel(input.settings.model);
|
|
1429
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
1430
|
+
cwd: sessionWorkDir,
|
|
1431
|
+
agentDir: hostAgentDir,
|
|
1432
|
+
settingsManager,
|
|
1433
|
+
appendSystemPromptOverride: () => [],
|
|
1434
|
+
extensionFactories: [],
|
|
1435
|
+
// Pi runs in the host process, so its default resource discovery reaches
|
|
1436
|
+
// the host developer's personal config (`~/.pi/agent/*`, `~/.agents/*`).
|
|
1437
|
+
// The harness does not expose extensions, themes, or prompt templates, so
|
|
1438
|
+
// disable those entirely — this also avoids loading and executing a host
|
|
1439
|
+
// developer's personal Pi extensions inside the server process. Skills are
|
|
1440
|
+
// kept but filtered to workspace project skills plus harness-provided
|
|
1441
|
+
// skills whose files live in sandbox HOME.
|
|
1442
|
+
noExtensions: true,
|
|
1443
|
+
noThemes: true,
|
|
1444
|
+
noPromptTemplates: true,
|
|
1445
|
+
skillsOverride: (base) => ({
|
|
1446
|
+
...base,
|
|
1447
|
+
skills: [
|
|
1448
|
+
...base.skills.filter(
|
|
1449
|
+
(skill) => isWithinWorkspace(skill.filePath, sessionWorkDir, hostWorkDir)
|
|
1450
|
+
),
|
|
1451
|
+
...harnessSkills
|
|
1452
|
+
]
|
|
1453
|
+
})
|
|
1454
|
+
});
|
|
1455
|
+
await resourceLoader.reload();
|
|
1456
|
+
let piSession;
|
|
1457
|
+
let unsubscribe;
|
|
1458
|
+
let lastToolsSignature;
|
|
1459
|
+
let sessionFileName;
|
|
1460
|
+
let stopped = false;
|
|
1461
|
+
let suspending = false;
|
|
1462
|
+
let instructionsApplied = input.isResume;
|
|
1463
|
+
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
1464
|
+
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
1465
|
+
let currentEmit;
|
|
1466
|
+
let translatorState;
|
|
1467
|
+
let activeTurn;
|
|
1468
|
+
const pendingCompactionParts = [];
|
|
1469
|
+
const remoteOps = createPiRemoteOps({
|
|
1470
|
+
sandbox,
|
|
1471
|
+
paths,
|
|
1472
|
+
onFileChange: (event, relPath) => {
|
|
1473
|
+
currentEmit?.({ type: "file-change", event, path: relPath });
|
|
1474
|
+
}
|
|
1475
|
+
});
|
|
1476
|
+
function settlePendingToolResults(reason) {
|
|
1477
|
+
for (const pending of pendingToolResults.values()) {
|
|
1478
|
+
pending.resolve({ error: reason });
|
|
1479
|
+
}
|
|
1480
|
+
pendingToolResults.clear();
|
|
1481
|
+
}
|
|
1482
|
+
function settlePendingToolApprovals(reason) {
|
|
1483
|
+
for (const pending of pendingToolApprovals.values()) {
|
|
1484
|
+
pending.resolve({ approved: false, reason });
|
|
1485
|
+
}
|
|
1486
|
+
pendingToolApprovals.clear();
|
|
1487
|
+
}
|
|
1488
|
+
async function persistSessionFile() {
|
|
1489
|
+
if (!sessionFileName) return;
|
|
1490
|
+
await persistSessionFileToSandbox({
|
|
1491
|
+
sandbox,
|
|
1492
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
1493
|
+
hostSessionDir,
|
|
1494
|
+
sessionFileName
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
function createPromptControl(input2) {
|
|
1498
|
+
const abortHandler = () => {
|
|
1499
|
+
piSession?.abort().catch(() => {
|
|
1500
|
+
});
|
|
1501
|
+
};
|
|
1502
|
+
if (input2.abortSignal) {
|
|
1503
|
+
input2.abortSignal.addEventListener("abort", abortHandler, {
|
|
1504
|
+
once: true
|
|
1505
|
+
});
|
|
1506
|
+
void input2.done.then(
|
|
1507
|
+
() => {
|
|
1508
|
+
input2.abortSignal?.removeEventListener("abort", abortHandler);
|
|
1509
|
+
},
|
|
1510
|
+
() => {
|
|
1511
|
+
input2.abortSignal?.removeEventListener("abort", abortHandler);
|
|
1512
|
+
}
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
1515
|
+
return {
|
|
1516
|
+
async submitToolResult(args) {
|
|
1517
|
+
const pending = pendingToolResults.get(args.toolCallId);
|
|
1518
|
+
if (!pending) return;
|
|
1519
|
+
pendingToolResults.delete(args.toolCallId);
|
|
1520
|
+
translatorState?.hostToolResults.set(args.toolCallId, args.output);
|
|
1521
|
+
pending.resolve(args.output);
|
|
1522
|
+
},
|
|
1523
|
+
async submitToolApproval(args) {
|
|
1524
|
+
const pending = pendingToolApprovals.get(args.approvalId);
|
|
1525
|
+
if (!pending) return;
|
|
1526
|
+
pendingToolApprovals.delete(args.approvalId);
|
|
1527
|
+
pending.resolve({
|
|
1528
|
+
approved: args.approved,
|
|
1529
|
+
reason: args.reason
|
|
1530
|
+
});
|
|
1531
|
+
},
|
|
1532
|
+
async submitUserMessage(text) {
|
|
1533
|
+
await piSession?.steer(text);
|
|
1534
|
+
},
|
|
1535
|
+
done: input2.done
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
async function requestBuiltinToolApproval(args) {
|
|
1539
|
+
if (!piBuiltinToolRequiresApproval({
|
|
1540
|
+
permissionMode,
|
|
1541
|
+
kind: PI_NATIVE_TOOL_KINDS[args.nativeName]
|
|
1542
|
+
})) {
|
|
1543
|
+
return { approved: true };
|
|
1544
|
+
}
|
|
1545
|
+
currentEmit?.({
|
|
1546
|
+
type: "tool-approval-request",
|
|
1547
|
+
approvalId: args.toolCallId,
|
|
1548
|
+
toolCallId: args.toolCallId
|
|
1549
|
+
});
|
|
1550
|
+
return new Promise((resolve) => {
|
|
1551
|
+
pendingToolApprovals.set(args.toolCallId, { resolve });
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
function buildToolDefinitions(userTools) {
|
|
1555
|
+
const customTools = [
|
|
1556
|
+
...PI_NATIVE_BUILTIN_NAMES.map(
|
|
1557
|
+
(native) => buildBuiltinToolDefinition({
|
|
1558
|
+
native,
|
|
1559
|
+
remoteOps,
|
|
1560
|
+
requestApproval: requestBuiltinToolApproval
|
|
1561
|
+
})
|
|
1562
|
+
),
|
|
1563
|
+
...userTools.map(
|
|
1564
|
+
(spec) => buildUserToolDefinition(spec, pendingToolResults)
|
|
1565
|
+
)
|
|
1566
|
+
];
|
|
1567
|
+
return {
|
|
1568
|
+
customTools,
|
|
1569
|
+
builtinNames: [...PI_NATIVE_BUILTIN_NAMES]
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
async function rebuildPiSession(userTools, isFirstBuild) {
|
|
1573
|
+
if (piSession) {
|
|
1574
|
+
unsubscribe?.();
|
|
1575
|
+
unsubscribe = void 0;
|
|
1576
|
+
piSession.dispose();
|
|
1577
|
+
piSession = void 0;
|
|
1578
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1579
|
+
}
|
|
1580
|
+
const { customTools, builtinNames } = buildToolDefinitions(userTools);
|
|
1581
|
+
const toolNames = customTools.map((t) => t.name);
|
|
1582
|
+
const sessionManager = isFirstBuild && resumeSessionFilePath ? SessionManager.open(
|
|
1583
|
+
resumeSessionFilePath,
|
|
1584
|
+
hostSessionDir,
|
|
1585
|
+
sessionWorkDir
|
|
1586
|
+
) : SessionManager.create(sessionWorkDir, hostSessionDir);
|
|
1587
|
+
const { session } = await createAgentSession({
|
|
1588
|
+
cwd: sessionWorkDir,
|
|
1589
|
+
agentDir: hostAgentDir,
|
|
1590
|
+
authStorage,
|
|
1591
|
+
modelRegistry,
|
|
1592
|
+
sessionManager,
|
|
1593
|
+
settingsManager,
|
|
1594
|
+
resourceLoader,
|
|
1595
|
+
customTools,
|
|
1596
|
+
tools: toolNames,
|
|
1597
|
+
...input.settings.thinkingLevel ? { thinkingLevel: input.settings.thinkingLevel } : {},
|
|
1598
|
+
...resolvedModel ? { model: resolvedModel } : {}
|
|
1599
|
+
});
|
|
1600
|
+
piSession = session;
|
|
1601
|
+
const candidatePath = sessionManager.getSessionFile();
|
|
1602
|
+
if (candidatePath) {
|
|
1603
|
+
sessionFileName = path7.basename(candidatePath);
|
|
1604
|
+
}
|
|
1605
|
+
translatorState = createPiTranslatorState({
|
|
1606
|
+
builtinToolNames: builtinNames,
|
|
1607
|
+
nativeToCommon: NATIVE_TO_COMMON
|
|
1608
|
+
});
|
|
1609
|
+
unsubscribe = piSession.subscribe((rawEvent) => {
|
|
1610
|
+
if (!translatorState) return;
|
|
1611
|
+
const event = parseNativeEvent(rawEvent);
|
|
1612
|
+
if (!event) return;
|
|
1613
|
+
for (const part of translatePiEvent(event, translatorState)) {
|
|
1614
|
+
if (currentEmit) {
|
|
1615
|
+
currentEmit(part);
|
|
1616
|
+
} else if (part.type === "compaction") {
|
|
1617
|
+
pendingCompactionParts.push(part);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
});
|
|
1621
|
+
}
|
|
1622
|
+
async function runTurn(turnOpts) {
|
|
1623
|
+
if (stopped) {
|
|
1624
|
+
throw new Error("Pi session has been stopped.");
|
|
1625
|
+
}
|
|
1626
|
+
const userTools = turnOpts.tools;
|
|
1627
|
+
const signature = JSON.stringify(userTools.map((t) => t.name).sort());
|
|
1628
|
+
const needsRebuild = piSession == null || signature !== lastToolsSignature;
|
|
1629
|
+
if (needsRebuild) {
|
|
1630
|
+
await rebuildPiSession(userTools, piSession == null);
|
|
1631
|
+
lastToolsSignature = signature;
|
|
1632
|
+
}
|
|
1633
|
+
await resourceLoader.reload();
|
|
1634
|
+
await syncHostWorkspaceFromSandbox({
|
|
1635
|
+
sandbox,
|
|
1636
|
+
sandboxWorkDir: input.sessionWorkDir,
|
|
1637
|
+
hostWorkDir
|
|
1638
|
+
});
|
|
1639
|
+
currentEmit = turnOpts.emit;
|
|
1640
|
+
translatorState = createPiTranslatorState({
|
|
1641
|
+
builtinToolNames: [...PI_NATIVE_BUILTIN_NAMES],
|
|
1642
|
+
nativeToCommon: NATIVE_TO_COMMON
|
|
1643
|
+
});
|
|
1644
|
+
turnOpts.emit({ type: "stream-start" });
|
|
1645
|
+
const turnPromise = (async () => {
|
|
1646
|
+
let terminalError;
|
|
1647
|
+
const session = piSession;
|
|
1648
|
+
const unsubErr = session.subscribe((raw) => {
|
|
1649
|
+
const ev = parseNativeEvent(raw);
|
|
1650
|
+
if (!ev) return;
|
|
1651
|
+
const err = getPiTerminalError(ev);
|
|
1652
|
+
if (err && !terminalError) {
|
|
1653
|
+
terminalError = err;
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
try {
|
|
1657
|
+
await session.prompt(turnOpts.text);
|
|
1658
|
+
if (terminalError) {
|
|
1659
|
+
if (suspending && isAbortError(terminalError)) return;
|
|
1660
|
+
currentEmit?.({ type: "error", error: new Error(terminalError) });
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
const stats = session.getSessionStats();
|
|
1664
|
+
const finishReason = {
|
|
1665
|
+
unified: "stop",
|
|
1666
|
+
raw: void 0
|
|
1667
|
+
};
|
|
1668
|
+
const usage = {
|
|
1669
|
+
inputTokens: {
|
|
1670
|
+
total: stats.tokens.input,
|
|
1671
|
+
noCache: void 0,
|
|
1672
|
+
cacheRead: stats.tokens.cacheRead,
|
|
1673
|
+
cacheWrite: stats.tokens.cacheWrite
|
|
1674
|
+
},
|
|
1675
|
+
outputTokens: {
|
|
1676
|
+
total: stats.tokens.output,
|
|
1677
|
+
text: void 0,
|
|
1678
|
+
reasoning: void 0
|
|
1679
|
+
}
|
|
1680
|
+
};
|
|
1681
|
+
currentEmit?.({ type: "finish-step", finishReason, usage });
|
|
1682
|
+
currentEmit?.({
|
|
1683
|
+
type: "finish",
|
|
1684
|
+
finishReason,
|
|
1685
|
+
totalUsage: usage
|
|
1686
|
+
});
|
|
1687
|
+
} catch (err) {
|
|
1688
|
+
if (suspending && isAbortError(err)) return;
|
|
1689
|
+
currentEmit?.({ type: "error", error: err });
|
|
1690
|
+
} finally {
|
|
1691
|
+
unsubErr();
|
|
1692
|
+
}
|
|
1693
|
+
})();
|
|
1694
|
+
const activeTurnToken = {};
|
|
1695
|
+
const done = turnPromise.finally(() => {
|
|
1696
|
+
if (activeTurn?.token === activeTurnToken) {
|
|
1697
|
+
activeTurn = void 0;
|
|
1698
|
+
}
|
|
1699
|
+
currentEmit = void 0;
|
|
1700
|
+
});
|
|
1701
|
+
activeTurn = {
|
|
1702
|
+
token: activeTurnToken,
|
|
1703
|
+
done
|
|
1704
|
+
};
|
|
1705
|
+
return createPromptControl({
|
|
1706
|
+
done,
|
|
1707
|
+
abortSignal: turnOpts.abortSignal
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
const doStop = async () => {
|
|
1711
|
+
if (stopped) {
|
|
1712
|
+
throw new Error("Pi session has been stopped.");
|
|
1713
|
+
}
|
|
1714
|
+
stopped = true;
|
|
1715
|
+
parkedPiSessions.delete(input.sessionId);
|
|
1716
|
+
settlePendingToolResults("Pi session stopped");
|
|
1717
|
+
settlePendingToolApprovals("Pi session stopped");
|
|
1718
|
+
if (sessionFileName) {
|
|
1719
|
+
try {
|
|
1720
|
+
await persistSessionFile();
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
unsubscribe?.();
|
|
1725
|
+
unsubscribe = void 0;
|
|
1726
|
+
piSession?.dispose();
|
|
1727
|
+
piSession = void 0;
|
|
1728
|
+
workspaceVfs.unmount();
|
|
1729
|
+
await rm2(hostRoot, { recursive: true, force: true });
|
|
1730
|
+
return {
|
|
1731
|
+
type: "resume-session",
|
|
1732
|
+
harnessId: HARNESS_ID2,
|
|
1733
|
+
specificationVersion: "harness-v1",
|
|
1734
|
+
data: sessionFileName ? { sessionFileName } : {}
|
|
1735
|
+
};
|
|
1736
|
+
};
|
|
1737
|
+
const sessionImpl = {
|
|
1738
|
+
sessionId: input.sessionId,
|
|
1739
|
+
isResume: input.isResume,
|
|
1740
|
+
// The model Pi actually resolves to (the configured id, or its default when
|
|
1741
|
+
// unset) — `gen_ai.request.model`.
|
|
1742
|
+
...resolvedModel?.id ? { modelId: resolvedModel.id } : {},
|
|
1743
|
+
// Pi has no bridge to attach to and no on-disk event log to replay; its
|
|
1744
|
+
// only resume path is restoring the session file on a fresh/snapshotted
|
|
1745
|
+
// sandbox, i.e. `rerun`.
|
|
1746
|
+
doPromptTurn: async (promptOpts) => {
|
|
1747
|
+
let text = extractUserText(promptOpts.prompt);
|
|
1748
|
+
if (!instructionsApplied && promptOpts.instructions) {
|
|
1749
|
+
text = frameInstructions(promptOpts.instructions, text);
|
|
1750
|
+
}
|
|
1751
|
+
instructionsApplied = true;
|
|
1752
|
+
return runTurn({
|
|
1753
|
+
text,
|
|
1754
|
+
tools: promptOpts.tools ?? [],
|
|
1755
|
+
emit: promptOpts.emit,
|
|
1756
|
+
abortSignal: promptOpts.abortSignal
|
|
1757
|
+
});
|
|
1758
|
+
},
|
|
1759
|
+
doContinueTurn: async (continueOpts) => {
|
|
1760
|
+
if (activeTurn != null) {
|
|
1761
|
+
currentEmit = continueOpts.emit;
|
|
1762
|
+
return createPromptControl({
|
|
1763
|
+
done: activeTurn.done,
|
|
1764
|
+
abortSignal: continueOpts.abortSignal
|
|
1765
|
+
});
|
|
1766
|
+
}
|
|
1767
|
+
return runTurn({
|
|
1768
|
+
text: "",
|
|
1769
|
+
tools: continueOpts.tools ?? [],
|
|
1770
|
+
emit: continueOpts.emit,
|
|
1771
|
+
abortSignal: continueOpts.abortSignal
|
|
1772
|
+
});
|
|
1773
|
+
},
|
|
1774
|
+
doCompact: async (customInstructions) => {
|
|
1775
|
+
if (stopped) {
|
|
1776
|
+
throw new Error("Pi session has been stopped.");
|
|
1777
|
+
}
|
|
1778
|
+
await piSession?.compact(customInstructions);
|
|
1779
|
+
},
|
|
1780
|
+
doDestroy: async () => {
|
|
1781
|
+
if (stopped) return;
|
|
1782
|
+
stopped = true;
|
|
1783
|
+
parkedPiSessions.delete(input.sessionId);
|
|
1784
|
+
settlePendingToolResults("Pi session stopped");
|
|
1785
|
+
settlePendingToolApprovals("Pi session stopped");
|
|
1786
|
+
unsubscribe?.();
|
|
1787
|
+
unsubscribe = void 0;
|
|
1788
|
+
piSession?.dispose();
|
|
1789
|
+
piSession = void 0;
|
|
1790
|
+
workspaceVfs.unmount();
|
|
1791
|
+
await rm2(hostRoot, { recursive: true, force: true });
|
|
1792
|
+
},
|
|
1793
|
+
doStop,
|
|
1794
|
+
doDetach: async () => {
|
|
1795
|
+
if (activeTurn != null || pendingToolResults.size > 0) {
|
|
1796
|
+
parkedPiSessions.set(input.sessionId, sessionImpl);
|
|
1797
|
+
if (sessionFileName) {
|
|
1798
|
+
try {
|
|
1799
|
+
await persistSessionFile();
|
|
1800
|
+
} catch {
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return {
|
|
1804
|
+
type: "resume-session",
|
|
1805
|
+
harnessId: HARNESS_ID2,
|
|
1806
|
+
specificationVersion: "harness-v1",
|
|
1807
|
+
data: sessionFileName ? { sessionFileName } : {}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
return doStop();
|
|
1811
|
+
},
|
|
1812
|
+
doSuspendTurn: async () => {
|
|
1813
|
+
if (stopped) {
|
|
1814
|
+
throw new Error("Pi session has been stopped.");
|
|
1815
|
+
}
|
|
1816
|
+
suspending = true;
|
|
1817
|
+
await Promise.resolve(piSession?.abort()).catch(() => {
|
|
1818
|
+
});
|
|
1819
|
+
if (sessionFileName) {
|
|
1820
|
+
try {
|
|
1821
|
+
await persistSessionFile();
|
|
1822
|
+
} catch {
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
stopped = true;
|
|
1826
|
+
parkedPiSessions.delete(input.sessionId);
|
|
1827
|
+
settlePendingToolResults("Pi session suspended");
|
|
1828
|
+
settlePendingToolApprovals("Pi session suspended");
|
|
1829
|
+
unsubscribe?.();
|
|
1830
|
+
unsubscribe = void 0;
|
|
1831
|
+
piSession?.dispose();
|
|
1832
|
+
piSession = void 0;
|
|
1833
|
+
workspaceVfs.unmount();
|
|
1834
|
+
await rm2(hostRoot, { recursive: true, force: true });
|
|
1835
|
+
return {
|
|
1836
|
+
type: "continue-turn",
|
|
1837
|
+
harnessId: HARNESS_ID2,
|
|
1838
|
+
specificationVersion: "harness-v1",
|
|
1839
|
+
data: sessionFileName ? { sessionFileName } : {}
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
return sessionImpl;
|
|
1844
|
+
}
|
|
1845
|
+
function isAbortError(value) {
|
|
1846
|
+
if (value == null) return false;
|
|
1847
|
+
if (typeof value === "object" && value.name === "AbortError") {
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
const text = typeof value === "string" ? value : value instanceof Error ? value.message : String(value);
|
|
1851
|
+
return /\baborted\b|AbortError|operation was aborted/i.test(text);
|
|
1852
|
+
}
|
|
1853
|
+
function asPiToolResult(text) {
|
|
1854
|
+
return {
|
|
1855
|
+
content: [{ type: "text", text }],
|
|
1856
|
+
details: void 0
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
async function maybeDenyPiBuiltinTool(input) {
|
|
1860
|
+
const decision = await input.requestApproval({
|
|
1861
|
+
toolCallId: input.toolCallId,
|
|
1862
|
+
nativeName: input.nativeName
|
|
1863
|
+
});
|
|
1864
|
+
if (decision.approved) return void 0;
|
|
1865
|
+
return asPiToolResult(
|
|
1866
|
+
serializeToolOutput({
|
|
1867
|
+
type: "execution-denied",
|
|
1868
|
+
reason: decision.reason
|
|
1869
|
+
})
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
function piBuiltinToolRequiresApproval(input) {
|
|
1873
|
+
if (input.permissionMode === "allow-all") return false;
|
|
1874
|
+
if (input.permissionMode === "allow-edits") return input.kind === "bash";
|
|
1875
|
+
return input.kind === "edit" || input.kind === "bash";
|
|
1876
|
+
}
|
|
1877
|
+
function buildBuiltinToolDefinition(input) {
|
|
1878
|
+
switch (input.native) {
|
|
1879
|
+
case "read":
|
|
1880
|
+
return defineTool({
|
|
1881
|
+
name: "read",
|
|
1882
|
+
label: "read",
|
|
1883
|
+
description: "Read file contents.",
|
|
1884
|
+
parameters: Type2.Object({ file_path: Type2.String() }),
|
|
1885
|
+
async execute(toolCallId, params) {
|
|
1886
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
1887
|
+
toolCallId,
|
|
1888
|
+
nativeName: "read",
|
|
1889
|
+
requestApproval: input.requestApproval
|
|
1890
|
+
});
|
|
1891
|
+
if (denied) return denied;
|
|
1892
|
+
const buf = await input.remoteOps.readBuffer(params.file_path);
|
|
1893
|
+
return asPiToolResult(buf.toString("utf8"));
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
case "write":
|
|
1897
|
+
return defineTool({
|
|
1898
|
+
name: "write",
|
|
1899
|
+
label: "write",
|
|
1900
|
+
description: "Write content to a file.",
|
|
1901
|
+
parameters: Type2.Object({
|
|
1902
|
+
file_path: Type2.String(),
|
|
1903
|
+
content: Type2.String()
|
|
1904
|
+
}),
|
|
1905
|
+
async execute(toolCallId, params) {
|
|
1906
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
1907
|
+
toolCallId,
|
|
1908
|
+
nativeName: "write",
|
|
1909
|
+
requestApproval: input.requestApproval
|
|
1910
|
+
});
|
|
1911
|
+
if (denied) return denied;
|
|
1912
|
+
await input.remoteOps.writeFile(params.file_path, params.content);
|
|
1913
|
+
return asPiToolResult(`Wrote ${params.file_path}`);
|
|
1914
|
+
}
|
|
1915
|
+
});
|
|
1916
|
+
case "edit":
|
|
1917
|
+
return defineTool({
|
|
1918
|
+
name: "edit",
|
|
1919
|
+
label: "edit",
|
|
1920
|
+
description: "Edit a file by exact-string replacement.",
|
|
1921
|
+
parameters: Type2.Object({
|
|
1922
|
+
file_path: Type2.String(),
|
|
1923
|
+
old_string: Type2.String(),
|
|
1924
|
+
new_string: Type2.String()
|
|
1925
|
+
}),
|
|
1926
|
+
async execute(toolCallId, params) {
|
|
1927
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
1928
|
+
toolCallId,
|
|
1929
|
+
nativeName: "edit",
|
|
1930
|
+
requestApproval: input.requestApproval
|
|
1931
|
+
});
|
|
1932
|
+
if (denied) return denied;
|
|
1933
|
+
await input.remoteOps.editFile(
|
|
1934
|
+
params.file_path,
|
|
1935
|
+
params.old_string,
|
|
1936
|
+
params.new_string
|
|
1937
|
+
);
|
|
1938
|
+
return asPiToolResult(`Edited ${params.file_path}`);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
case "bash":
|
|
1942
|
+
return defineTool({
|
|
1943
|
+
name: "bash",
|
|
1944
|
+
label: "bash",
|
|
1945
|
+
description: "Execute a shell command.",
|
|
1946
|
+
parameters: Type2.Object({
|
|
1947
|
+
command: Type2.String(),
|
|
1948
|
+
timeout: Type2.Optional(
|
|
1949
|
+
Type2.Number({ description: "Timeout in seconds." })
|
|
1950
|
+
)
|
|
1951
|
+
}),
|
|
1952
|
+
async execute(toolCallId, params, signal) {
|
|
1953
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
1954
|
+
toolCallId,
|
|
1955
|
+
nativeName: "bash",
|
|
1956
|
+
requestApproval: input.requestApproval
|
|
1957
|
+
});
|
|
1958
|
+
if (denied) return denied;
|
|
1959
|
+
const chunks = [];
|
|
1960
|
+
const result = await input.remoteOps.exec(params.command, ".", {
|
|
1961
|
+
onData(data) {
|
|
1962
|
+
chunks.push(data);
|
|
1963
|
+
},
|
|
1964
|
+
...signal ? { signal } : {},
|
|
1965
|
+
...typeof params.timeout === "number" ? { timeout: params.timeout } : {}
|
|
1966
|
+
});
|
|
1967
|
+
const out = Buffer.concat(chunks).toString("utf8");
|
|
1968
|
+
const text = `${out}${result.exitCode != null ? `
|
|
1969
|
+
|
|
1970
|
+
(exit ${result.exitCode})` : ""}`.trim();
|
|
1971
|
+
return asPiToolResult(text);
|
|
1972
|
+
}
|
|
1973
|
+
});
|
|
1974
|
+
case "grep":
|
|
1975
|
+
return defineTool({
|
|
1976
|
+
name: "grep",
|
|
1977
|
+
label: "grep",
|
|
1978
|
+
description: "Search file contents with regex.",
|
|
1979
|
+
parameters: Type2.Object({
|
|
1980
|
+
pattern: Type2.String(),
|
|
1981
|
+
path: Type2.Optional(Type2.String()),
|
|
1982
|
+
glob: Type2.Optional(Type2.String()),
|
|
1983
|
+
ignoreCase: Type2.Optional(Type2.Boolean()),
|
|
1984
|
+
literal: Type2.Optional(Type2.Boolean()),
|
|
1985
|
+
context: Type2.Optional(Type2.Number()),
|
|
1986
|
+
limit: Type2.Optional(Type2.Number())
|
|
1987
|
+
}),
|
|
1988
|
+
async execute(toolCallId, params) {
|
|
1989
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
1990
|
+
toolCallId,
|
|
1991
|
+
nativeName: "grep",
|
|
1992
|
+
requestApproval: input.requestApproval
|
|
1993
|
+
});
|
|
1994
|
+
if (denied) return denied;
|
|
1995
|
+
const out = await input.remoteOps.grepFiles(params.pattern, params);
|
|
1996
|
+
return asPiToolResult(out);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
case "find":
|
|
2000
|
+
return defineTool({
|
|
2001
|
+
name: "find",
|
|
2002
|
+
label: "find",
|
|
2003
|
+
description: "Find files matching a glob pattern.",
|
|
2004
|
+
parameters: Type2.Object({
|
|
2005
|
+
pattern: Type2.String(),
|
|
2006
|
+
path: Type2.Optional(Type2.String()),
|
|
2007
|
+
limit: Type2.Optional(Type2.Number())
|
|
2008
|
+
}),
|
|
2009
|
+
async execute(toolCallId, params) {
|
|
2010
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
2011
|
+
toolCallId,
|
|
2012
|
+
nativeName: "find",
|
|
2013
|
+
requestApproval: input.requestApproval
|
|
2014
|
+
});
|
|
2015
|
+
if (denied) return denied;
|
|
2016
|
+
const matches = await input.remoteOps.findFiles(
|
|
2017
|
+
params.pattern,
|
|
2018
|
+
params.path ?? ".",
|
|
2019
|
+
params.limit ?? 1e3
|
|
2020
|
+
);
|
|
2021
|
+
return asPiToolResult(matches.join("\n"));
|
|
2022
|
+
}
|
|
2023
|
+
});
|
|
2024
|
+
case "ls":
|
|
2025
|
+
return defineTool({
|
|
2026
|
+
name: "ls",
|
|
2027
|
+
label: "ls",
|
|
2028
|
+
description: "List directory entries.",
|
|
2029
|
+
parameters: Type2.Object({
|
|
2030
|
+
path: Type2.Optional(Type2.String()),
|
|
2031
|
+
limit: Type2.Optional(Type2.Number())
|
|
2032
|
+
}),
|
|
2033
|
+
async execute(toolCallId, params) {
|
|
2034
|
+
const denied = await maybeDenyPiBuiltinTool({
|
|
2035
|
+
toolCallId,
|
|
2036
|
+
nativeName: "ls",
|
|
2037
|
+
requestApproval: input.requestApproval
|
|
2038
|
+
});
|
|
2039
|
+
if (denied) return denied;
|
|
2040
|
+
const entries = await input.remoteOps.listDirectory(
|
|
2041
|
+
params.path ?? ".",
|
|
2042
|
+
params.limit ?? 500
|
|
2043
|
+
);
|
|
2044
|
+
return asPiToolResult(entries.join("\n"));
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
function buildUserToolDefinition(spec, pending) {
|
|
2050
|
+
const schema = spec.inputSchema ?? {
|
|
2051
|
+
type: "object",
|
|
2052
|
+
properties: {},
|
|
2053
|
+
additionalProperties: true
|
|
2054
|
+
};
|
|
2055
|
+
return defineTool({
|
|
2056
|
+
name: spec.name,
|
|
2057
|
+
label: spec.name,
|
|
2058
|
+
description: spec.description ?? `User-registered tool ${spec.name}`,
|
|
2059
|
+
parameters: toolSpecToTypeBoxParameters(schema),
|
|
2060
|
+
async execute(toolCallId) {
|
|
2061
|
+
return new Promise((resolve) => {
|
|
2062
|
+
pending.set(toolCallId, { resolve });
|
|
2063
|
+
}).then((output) => asPiToolResult(serializeToolOutput(output)));
|
|
2064
|
+
}
|
|
2065
|
+
});
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
// src/pi-harness.ts
|
|
2069
|
+
var PI_BUILTIN_TOOLS = {
|
|
2070
|
+
read: commonTool("read", {
|
|
2071
|
+
nativeName: "read",
|
|
2072
|
+
toolUseKind: "readonly",
|
|
2073
|
+
description: "Read file contents.",
|
|
2074
|
+
inputSchema: z3.object({
|
|
2075
|
+
file_path: z3.string()
|
|
2076
|
+
})
|
|
2077
|
+
}),
|
|
2078
|
+
write: commonTool("write", {
|
|
2079
|
+
nativeName: "write",
|
|
2080
|
+
toolUseKind: "edit",
|
|
2081
|
+
description: "Overwrite or create a file.",
|
|
2082
|
+
inputSchema: z3.object({
|
|
2083
|
+
file_path: z3.string(),
|
|
2084
|
+
content: z3.string()
|
|
2085
|
+
})
|
|
2086
|
+
}),
|
|
2087
|
+
edit: commonTool("edit", {
|
|
2088
|
+
nativeName: "edit",
|
|
2089
|
+
toolUseKind: "edit",
|
|
2090
|
+
description: "Edit a file by exact string replacement.",
|
|
2091
|
+
inputSchema: z3.object({
|
|
2092
|
+
file_path: z3.string(),
|
|
2093
|
+
old_string: z3.string(),
|
|
2094
|
+
new_string: z3.string()
|
|
2095
|
+
})
|
|
2096
|
+
}),
|
|
2097
|
+
bash: commonTool("bash", {
|
|
2098
|
+
nativeName: "bash",
|
|
2099
|
+
toolUseKind: "bash",
|
|
2100
|
+
description: "Execute a shell command in the sandbox.",
|
|
2101
|
+
inputSchema: z3.object({
|
|
2102
|
+
command: z3.string(),
|
|
2103
|
+
timeout: z3.number().optional()
|
|
2104
|
+
})
|
|
2105
|
+
}),
|
|
2106
|
+
grep: commonTool("grep", {
|
|
2107
|
+
nativeName: "grep",
|
|
2108
|
+
toolUseKind: "readonly",
|
|
2109
|
+
description: "Search file contents with regex.",
|
|
2110
|
+
inputSchema: z3.object({
|
|
2111
|
+
pattern: z3.string(),
|
|
2112
|
+
path: z3.string().optional(),
|
|
2113
|
+
glob: z3.string().optional(),
|
|
2114
|
+
ignoreCase: z3.boolean().optional(),
|
|
2115
|
+
literal: z3.boolean().optional(),
|
|
2116
|
+
context: z3.number().optional(),
|
|
2117
|
+
limit: z3.number().optional()
|
|
2118
|
+
})
|
|
2119
|
+
}),
|
|
2120
|
+
glob: commonTool("glob", {
|
|
2121
|
+
nativeName: "find",
|
|
2122
|
+
toolUseKind: "readonly",
|
|
2123
|
+
description: "Find files matching a glob pattern.",
|
|
2124
|
+
inputSchema: z3.object({
|
|
2125
|
+
pattern: z3.string(),
|
|
2126
|
+
path: z3.string().optional(),
|
|
2127
|
+
limit: z3.number().optional()
|
|
2128
|
+
})
|
|
2129
|
+
}),
|
|
2130
|
+
ls: {
|
|
2131
|
+
...tool({
|
|
2132
|
+
description: "List directory entries.",
|
|
2133
|
+
inputSchema: z3.object({
|
|
2134
|
+
path: z3.string().optional(),
|
|
2135
|
+
limit: z3.number().optional()
|
|
2136
|
+
}),
|
|
2137
|
+
outputSchema: z3.unknown()
|
|
2138
|
+
}),
|
|
2139
|
+
nativeName: "ls",
|
|
2140
|
+
toolUseKind: "readonly"
|
|
2141
|
+
}
|
|
2142
|
+
};
|
|
2143
|
+
function createPi(settings = {}) {
|
|
2144
|
+
return {
|
|
2145
|
+
specificationVersion: "harness-v1",
|
|
2146
|
+
harnessId: "pi",
|
|
2147
|
+
builtinTools: PI_BUILTIN_TOOLS,
|
|
2148
|
+
supportsBuiltinToolApprovals: true,
|
|
2149
|
+
lifecycleStateSchema: piResumeStateSchema,
|
|
2150
|
+
doStart: async (startOpts) => {
|
|
2151
|
+
const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
|
|
2152
|
+
const resumeData = lifecycleState?.data;
|
|
2153
|
+
return createPiSession({
|
|
2154
|
+
sessionId: startOpts.sessionId,
|
|
2155
|
+
sandboxSession: startOpts.sandboxSession,
|
|
2156
|
+
sessionWorkDir: startOpts.sessionWorkDir,
|
|
2157
|
+
skills: startOpts.skills ?? [],
|
|
2158
|
+
settings: {
|
|
2159
|
+
...settings.auth ? { auth: settings.auth } : {},
|
|
2160
|
+
...settings.model ? { model: settings.model } : {},
|
|
2161
|
+
...settings.thinkingLevel ? { thinkingLevel: settings.thinkingLevel } : {}
|
|
2162
|
+
},
|
|
2163
|
+
isResume: lifecycleState != null,
|
|
2164
|
+
permissionMode: startOpts.permissionMode,
|
|
2165
|
+
...resumeData?.sessionFileName ? { resumeSessionFileName: resumeData.sessionFileName } : {},
|
|
2166
|
+
...startOpts.abortSignal ? { abortSignal: startOpts.abortSignal } : {}
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
// src/index.ts
|
|
2173
|
+
var pi = createPi();
|
|
2174
|
+
export {
|
|
2175
|
+
createPi,
|
|
2176
|
+
pi
|
|
2177
|
+
};
|
|
2178
|
+
//# sourceMappingURL=index.js.map
|