@ai-sdk/harness-pi 1.0.125 → 1.0.128

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @ai-sdk/harness-pi
2
2
 
3
+ ## 1.0.128
4
+
5
+ ### Patch Changes
6
+
7
+ - af9597b: Compile packages for ES2022 runtime target
8
+ - Updated dependencies [af9597b]
9
+ - Updated dependencies [31742b9]
10
+ - Updated dependencies [bc49f78]
11
+ - @ai-sdk/harness@1.0.126
12
+ - @ai-sdk/provider-utils@5.0.49
13
+
14
+ ## 1.0.127
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [be877ff]
19
+ - @ai-sdk/provider-utils@5.0.48
20
+ - @ai-sdk/harness@1.0.125
21
+
22
+ ## 1.0.126
23
+
24
+ ### Patch Changes
25
+
26
+ - e9c5e0f: fix(harness-pi): release suspended sessions when in-process reattachment is disabled
27
+ - 85f3ff6: fix(harness-pi): bound model-facing tool results and support paged file reads
28
+ - Updated dependencies [40231b6]
29
+ - @ai-sdk/harness@1.0.124
30
+
3
31
  ## 1.0.125
4
32
 
5
33
  ### Patch Changes
package/README.md CHANGED
@@ -13,14 +13,13 @@ npm i @ai-sdk/harness-pi @ai-sdk/harness @ai-sdk/sandbox-vercel
13
13
  ```ts
14
14
  import { HarnessAgent } from '@ai-sdk/harness/agent';
15
15
  import { createPi } from '@ai-sdk/harness-pi';
16
- import { createVercelSandbox } from '@ai-sdk/sandbox-vercel';
16
+ import { createVercelNetworkSandboxSession } from '@ai-sdk/sandbox-vercel';
17
17
  import { tool } from 'ai';
18
18
  import { z } from 'zod/v4';
19
19
 
20
20
  const agent = new HarnessAgent({
21
21
  harness: createPi({ thinkingLevel: 'medium' }),
22
22
  id: 'demo',
23
- sandbox: createVercelSandbox({ runtime: 'node24' }),
24
23
  skills: [
25
24
  {
26
25
  name: 'careful-refactors',
@@ -37,7 +36,10 @@ const agent = new HarnessAgent({
37
36
  },
38
37
  });
39
38
 
40
- const session = await agent.createSession();
39
+ const sandboxSession = await createVercelNetworkSandboxSession({
40
+ runtime: 'node24',
41
+ });
42
+ const session = await agent.createSession({ sandboxSession });
41
43
  try {
42
44
  const result = await agent.generate({
43
45
  session,
@@ -46,10 +48,12 @@ try {
46
48
  console.log(result.text);
47
49
  } finally {
48
50
  await session.destroy();
51
+ await sandboxSession.destroy();
49
52
  }
50
53
  ```
51
54
 
52
- The adapter requires a `HarnessV1SandboxProvider`. Pi has no in-sandbox bridge, so the sandbox doesn't need to expose any ports — `@ai-sdk/sandbox-vercel` or `@ai-sdk/sandbox-just-bash` both work.
55
+ Pi has no in-sandbox bridge, so the supplied sandbox session does not need
56
+ exposed ports. Vercel and just-bash sessions both work.
53
57
 
54
58
  ## Stateless session configuration
55
59
 
package/dist/index.d.ts CHANGED
@@ -65,6 +65,8 @@ type PiHarnessSettings = {
65
65
  declare const PI_BUILTIN_TOOLS: {
66
66
  readonly read: HarnessV1BuiltinTool<{
67
67
  file_path: string;
68
+ offset?: number | undefined;
69
+ limit?: number | undefined;
68
70
  }, unknown>;
69
71
  readonly write: HarnessV1BuiltinTool<{
70
72
  file_path: string;
@@ -106,6 +108,8 @@ declare const VERSION: string;
106
108
  declare const pi: _ai_sdk_harness.HarnessV1<{
107
109
  readonly read: _ai_sdk_harness.HarnessV1BuiltinTool<{
108
110
  file_path: string;
111
+ offset?: number | undefined;
112
+ limit?: number | undefined;
109
113
  }, unknown>;
110
114
  readonly write: _ai_sdk_harness.HarnessV1BuiltinTool<{
111
115
  file_path: string;
package/dist/index.js CHANGED
@@ -136,7 +136,7 @@ import {
136
136
  import { access } from "fs/promises";
137
137
 
138
138
  // src/version.ts
139
- var VERSION = true ? "1.0.125" : "0.0.0-test";
139
+ var VERSION = true ? "1.0.128" : "0.0.0-test";
140
140
 
141
141
  // src/pi-auth.ts
142
142
  var DEFAULT_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh";
@@ -1154,6 +1154,104 @@ ${stderr}`;
1154
1154
  };
1155
1155
  }
1156
1156
 
1157
+ // src/pi-tool-result.ts
1158
+ import {
1159
+ DEFAULT_MAX_BYTES,
1160
+ formatSize,
1161
+ truncateHead,
1162
+ truncateTail
1163
+ } from "@earendil-works/pi-coding-agent";
1164
+ function joinContentAndNotice(content, notice) {
1165
+ return content.length > 0 ? `${content}
1166
+
1167
+ [${notice}]` : `[${notice}]`;
1168
+ }
1169
+ function truncateUtf8Start(text, maxBytes) {
1170
+ const buffer = Buffer.from(text, "utf8");
1171
+ if (buffer.length <= maxBytes) return text;
1172
+ let end = maxBytes;
1173
+ while (end > 0 && (buffer[end] & 192) === 128) {
1174
+ end--;
1175
+ }
1176
+ return buffer.subarray(0, end).toString("utf8");
1177
+ }
1178
+ function formatHeadNotice(truncation, continuation) {
1179
+ const limit = truncation.truncatedBy === "bytes" ? ` (${formatSize(truncation.maxBytes)} limit)` : "";
1180
+ return `Output truncated: showing the first ${truncation.outputLines} of ${truncation.totalLines} lines${limit}. ${continuation}`;
1181
+ }
1182
+ function formatTailNotice(truncation, continuation) {
1183
+ const limit = truncation.truncatedBy === "bytes" ? ` (${formatSize(truncation.maxBytes)} limit)` : "";
1184
+ return `Output truncated: showing the last ${truncation.outputLines} of ${truncation.totalLines} lines${limit}. ${continuation}`;
1185
+ }
1186
+ function truncatePiToolOutputHead(text, continuation) {
1187
+ const truncation = truncateHead(text);
1188
+ if (!truncation.truncated) return text;
1189
+ if (truncation.firstLineExceedsLimit) {
1190
+ const content = truncateUtf8Start(text, DEFAULT_MAX_BYTES);
1191
+ return joinContentAndNotice(
1192
+ content,
1193
+ `Output truncated: showing the first ${formatSize(
1194
+ Buffer.byteLength(content, "utf8")
1195
+ )} of a ${formatSize(truncation.totalBytes)} line. ${continuation}`
1196
+ );
1197
+ }
1198
+ return joinContentAndNotice(
1199
+ truncation.content,
1200
+ formatHeadNotice(truncation, continuation)
1201
+ );
1202
+ }
1203
+ function truncatePiToolOutputTail(text, continuation) {
1204
+ const truncation = truncateTail(text);
1205
+ if (!truncation.truncated) return text;
1206
+ return joinContentAndNotice(
1207
+ truncation.content,
1208
+ formatTailNotice(truncation, continuation)
1209
+ );
1210
+ }
1211
+ function formatPiReadToolOutput({
1212
+ text,
1213
+ filePath,
1214
+ offset,
1215
+ limit
1216
+ }) {
1217
+ const allLines = text.split("\n");
1218
+ const startLine = offset == null ? 0 : Math.max(0, Math.floor(offset) - 1);
1219
+ const startLineDisplay = startLine + 1;
1220
+ if (startLine >= allLines.length) {
1221
+ throw new Error(
1222
+ `Offset ${offset} is beyond end of file (${allLines.length} lines total)`
1223
+ );
1224
+ }
1225
+ const normalizedLimit = limit == null ? void 0 : Math.max(1, Math.floor(limit));
1226
+ const endLine = normalizedLimit == null ? allLines.length : Math.min(startLine + normalizedLimit, allLines.length);
1227
+ const selectedContent = allLines.slice(startLine, endLine).join("\n");
1228
+ const truncation = truncateHead(selectedContent);
1229
+ if (truncation.firstLineExceedsLimit) {
1230
+ const lineSize = formatSize(
1231
+ Buffer.byteLength(allLines[startLine] ?? "", "utf8")
1232
+ );
1233
+ return `[Line ${startLineDisplay} is ${lineSize}, exceeds ${formatSize(
1234
+ DEFAULT_MAX_BYTES
1235
+ )} limit. Use bash: sed -n '${startLineDisplay}p' ${filePath} | head -c ${DEFAULT_MAX_BYTES}]`;
1236
+ }
1237
+ if (truncation.truncated) {
1238
+ const endLineDisplay = startLineDisplay + Math.max(0, truncation.outputLines - 1);
1239
+ const nextOffset = endLineDisplay + 1;
1240
+ const limitNotice = truncation.truncatedBy === "bytes" ? ` (${formatSize(DEFAULT_MAX_BYTES)} limit)` : "";
1241
+ return joinContentAndNotice(
1242
+ truncation.content,
1243
+ `Showing lines ${startLineDisplay}-${endLineDisplay} of ${allLines.length}${limitNotice}. Use offset=${nextOffset} to continue.`
1244
+ );
1245
+ }
1246
+ if (endLine < allLines.length) {
1247
+ return joinContentAndNotice(
1248
+ truncation.content,
1249
+ `${allLines.length - endLine} more lines in file. Use offset=${endLine + 1} to continue.`
1250
+ );
1251
+ }
1252
+ return truncation.content;
1253
+ }
1254
+
1157
1255
  // src/pi-translate.ts
1158
1256
  import { randomBytes } from "crypto";
1159
1257
  import { secureJsonParse } from "@ai-sdk/provider-utils";
@@ -2559,7 +2657,13 @@ async function createPiSession(input) {
2559
2657
  toolCallId,
2560
2658
  toolName: delivered.toolName,
2561
2659
  content: [
2562
- { type: "text", text: serializeToolOutput(delivered.output) }
2660
+ {
2661
+ type: "text",
2662
+ text: truncatePiToolOutputHead(
2663
+ serializeToolOutput(delivered.output),
2664
+ "Call the tool again with narrower parameters to inspect the omitted output."
2665
+ )
2666
+ }
2563
2667
  ],
2564
2668
  isError: delivered.isError,
2565
2669
  timestamp: Date.now()
@@ -3089,7 +3193,7 @@ async function createPiSession(input) {
3089
3193
  },
3090
3194
  doStop,
3091
3195
  doDetach: async () => {
3092
- if (activeTurn != null || pendingToolResults.size > 0) {
3196
+ if (input.settings.reattachInProcess !== false && (activeTurn != null || pendingToolResults.size > 0)) {
3093
3197
  parkedPiSessions.set(input.sessionId, {
3094
3198
  session: sessionImpl,
3095
3199
  input,
@@ -3114,7 +3218,7 @@ async function createPiSession(input) {
3114
3218
  if (stopped) {
3115
3219
  throw new Error("Pi session has been stopped.");
3116
3220
  }
3117
- if (activeTurn != null && (pendingToolResults.size > 0 || pendingToolApprovals.size > 0)) {
3221
+ if (input.settings.reattachInProcess !== false && activeTurn != null && (pendingToolResults.size > 0 || pendingToolApprovals.size > 0)) {
3118
3222
  parkedPiSessions.set(input.sessionId, {
3119
3223
  session: sessionImpl,
3120
3224
  input,
@@ -3135,8 +3239,11 @@ async function createPiSession(input) {
3135
3239
  }
3136
3240
  suspending = true;
3137
3241
  const turnToSuspend = activeTurn;
3138
- await turnToSuspend?.abort();
3242
+ const abortingTurn = turnToSuspend?.abort();
3139
3243
  deferredRerun?.cancel();
3244
+ settlePendingToolResults("Pi session suspended");
3245
+ settlePendingToolApprovals("Pi session suspended");
3246
+ await abortingTurn;
3140
3247
  await turnToSuspend?.done.catch(() => {
3141
3248
  });
3142
3249
  try {
@@ -3151,8 +3258,6 @@ async function createPiSession(input) {
3151
3258
  }
3152
3259
  stopped = true;
3153
3260
  parkedPiSessions.delete(input.sessionId);
3154
- settlePendingToolResults("Pi session suspended");
3155
- settlePendingToolApprovals("Pi session suspended");
3156
3261
  await disposePiSession();
3157
3262
  workspaceVfs.unmount();
3158
3263
  await rm2(hostRoot, { recursive: true, force: true });
@@ -3217,8 +3322,22 @@ function buildBuiltinToolDefinition(input) {
3217
3322
  return defineTool({
3218
3323
  name: "read",
3219
3324
  label: "read",
3220
- description: "Read file contents.",
3221
- parameters: Type2.Object({ file_path: Type2.String() }),
3325
+ description: "Read file contents. Output is limited to 2,000 lines or 50KB. Use offset and limit to read large files in pages.",
3326
+ parameters: Type2.Object({
3327
+ file_path: Type2.String(),
3328
+ offset: Type2.Optional(
3329
+ Type2.Integer({
3330
+ minimum: 1,
3331
+ description: "Line number to start reading from (1-indexed)."
3332
+ })
3333
+ ),
3334
+ limit: Type2.Optional(
3335
+ Type2.Integer({
3336
+ minimum: 1,
3337
+ description: "Maximum number of lines to read."
3338
+ })
3339
+ )
3340
+ }),
3222
3341
  async execute(toolCallId, params) {
3223
3342
  const denied = await maybeDenyPiBuiltinTool({
3224
3343
  toolCallId,
@@ -3227,7 +3346,14 @@ function buildBuiltinToolDefinition(input) {
3227
3346
  });
3228
3347
  if (denied) return denied;
3229
3348
  const buf = await input.remoteOps.readBuffer(params.file_path);
3230
- return asPiToolResult(buf.toString("utf8"));
3349
+ return asPiToolResult(
3350
+ formatPiReadToolOutput({
3351
+ text: buf.toString("utf8"),
3352
+ filePath: params.file_path,
3353
+ offset: params.offset,
3354
+ limit: params.limit
3355
+ })
3356
+ );
3231
3357
  }
3232
3358
  });
3233
3359
  case "write":
@@ -3305,7 +3431,12 @@ function buildBuiltinToolDefinition(input) {
3305
3431
  const text = `${out}${result.exitCode != null ? `
3306
3432
 
3307
3433
  (exit ${result.exitCode})` : ""}`.trim();
3308
- return asPiToolResult(text);
3434
+ return asPiToolResult(
3435
+ truncatePiToolOutputTail(
3436
+ text,
3437
+ "Re-run the command with narrower output, or redirect it to a file and use read with offset and limit."
3438
+ )
3439
+ );
3309
3440
  }
3310
3441
  });
3311
3442
  case "grep":
@@ -3330,7 +3461,12 @@ function buildBuiltinToolDefinition(input) {
3330
3461
  });
3331
3462
  if (denied) return denied;
3332
3463
  const out = await input.remoteOps.grepFiles(params.pattern, params);
3333
- return asPiToolResult(out);
3464
+ return asPiToolResult(
3465
+ truncatePiToolOutputHead(
3466
+ out,
3467
+ "Narrow the pattern or path, or read the matching file with offset and limit."
3468
+ )
3469
+ );
3334
3470
  }
3335
3471
  });
3336
3472
  case "find":
@@ -3355,7 +3491,12 @@ function buildBuiltinToolDefinition(input) {
3355
3491
  params.path ?? ".",
3356
3492
  params.limit ?? 1e3
3357
3493
  );
3358
- return asPiToolResult(matches.join("\n"));
3494
+ return asPiToolResult(
3495
+ truncatePiToolOutputHead(
3496
+ matches.join("\n"),
3497
+ "Narrow the pattern or search path to inspect the omitted matches."
3498
+ )
3499
+ );
3359
3500
  }
3360
3501
  });
3361
3502
  case "ls":
@@ -3378,7 +3519,12 @@ function buildBuiltinToolDefinition(input) {
3378
3519
  params.path ?? ".",
3379
3520
  params.limit ?? 500
3380
3521
  );
3381
- return asPiToolResult(entries.join("\n"));
3522
+ return asPiToolResult(
3523
+ truncatePiToolOutputHead(
3524
+ entries.join("\n"),
3525
+ "List a narrower directory path to inspect the omitted entries."
3526
+ )
3527
+ );
3382
3528
  }
3383
3529
  });
3384
3530
  }
@@ -3397,7 +3543,14 @@ function buildUserToolDefinition(spec, pending) {
3397
3543
  async execute(toolCallId) {
3398
3544
  return new Promise((resolve2) => {
3399
3545
  pending.set(toolCallId, { resolve: resolve2 });
3400
- }).then((output) => asPiToolResult(serializeToolOutput(output)));
3546
+ }).then(
3547
+ (output) => asPiToolResult(
3548
+ truncatePiToolOutputHead(
3549
+ serializeToolOutput(output),
3550
+ "Call the tool again with narrower parameters to inspect the omitted output."
3551
+ )
3552
+ )
3553
+ );
3401
3554
  }
3402
3555
  });
3403
3556
  }
@@ -3408,9 +3561,11 @@ var PI_BUILTIN_TOOLS = {
3408
3561
  read: commonTool("read", {
3409
3562
  nativeName: "read",
3410
3563
  toolUseKind: "readonly",
3411
- description: "Read file contents.",
3564
+ description: "Read file contents. Output is limited to 2,000 lines or 50KB. Use offset and limit to read large files in pages.",
3412
3565
  inputSchema: z3.object({
3413
- file_path: z3.string()
3566
+ file_path: z3.string(),
3567
+ offset: z3.number().int().positive().optional(),
3568
+ limit: z3.number().int().positive().optional()
3414
3569
  })
3415
3570
  }),
3416
3571
  write: commonTool("write", {