@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/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