@ai-sdk/harness-pi 0.0.0 → 1.0.0-canary.1

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