@ai-sdk/harness-pi 1.0.97 → 1.0.99
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 +14 -0
- package/dist/index.js +65 -4
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/pi-events.ts +8 -0
- package/src/pi-translate.ts +93 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-pi",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.99",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.97",
|
|
30
30
|
"@ai-sdk/provider-utils": "5.0.34",
|
|
31
31
|
"@earendil-works/pi-ai": "0.74.2",
|
|
32
32
|
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"zod": "^3.25.76 || ^4.1.8"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@ai-sdk/sandbox-just-bash": "1.0.
|
|
40
|
+
"@ai-sdk/sandbox-just-bash": "1.0.97",
|
|
41
41
|
"@types/node": "22.19.19",
|
|
42
42
|
"@vercel/ai-tsconfig": "0.0.0",
|
|
43
43
|
"tsup": "^8.5.1",
|
package/src/pi-events.ts
CHANGED
|
@@ -12,6 +12,14 @@ export const piSessionEventSchema = z.looseObject({
|
|
|
12
12
|
.looseObject({
|
|
13
13
|
type: z.string().optional(),
|
|
14
14
|
delta: z.string().optional(),
|
|
15
|
+
// `toolcall_start` / `toolcall_delta` / `toolcall_end` address a content
|
|
16
|
+
// block by index rather than by tool call id. The id and name live in the
|
|
17
|
+
// partial assistant message at that index, which Pi fills in before the
|
|
18
|
+
// first delta arrives.
|
|
19
|
+
contentIndex: z.number().optional(),
|
|
20
|
+
partial: z
|
|
21
|
+
.looseObject({ content: z.array(z.unknown()).optional() })
|
|
22
|
+
.optional(),
|
|
15
23
|
})
|
|
16
24
|
.optional(),
|
|
17
25
|
toolCallId: z.string().optional(),
|
package/src/pi-translate.ts
CHANGED
|
@@ -28,6 +28,13 @@ export interface PiTranslatorState {
|
|
|
28
28
|
reasoningStarted: boolean;
|
|
29
29
|
/** Tool-call id → tool name (used to fill in `toolName` on results). */
|
|
30
30
|
observedToolNames: Map<string, string>;
|
|
31
|
+
/**
|
|
32
|
+
* Content-block index → tool-call id for tool inputs that are still
|
|
33
|
+
* streaming. Pi addresses `toolcall_*` events by `contentIndex`, while the
|
|
34
|
+
* harness stream parts are keyed by the tool call id, so the id is resolved
|
|
35
|
+
* once at `toolcall_start` and reused for the deltas that follow.
|
|
36
|
+
*/
|
|
37
|
+
streamingToolInputIds: Map<number, string>;
|
|
31
38
|
/** Tool ids requested by the current assistant message but not yet completed. */
|
|
32
39
|
pendingStepToolCallIds: Set<string>;
|
|
33
40
|
/** Total tool calls requested by the current assistant message. */
|
|
@@ -85,6 +92,7 @@ export function createPiTranslatorState(
|
|
|
85
92
|
currentReasoningId: undefined,
|
|
86
93
|
reasoningStarted: false,
|
|
87
94
|
observedToolNames: new Map(),
|
|
95
|
+
streamingToolInputIds: new Map(),
|
|
88
96
|
pendingStepToolCallIds: new Set(),
|
|
89
97
|
stepToolCallCount: undefined,
|
|
90
98
|
stepOpen: false,
|
|
@@ -154,6 +162,48 @@ function resolveToolName(
|
|
|
154
162
|
return { wire: common ?? nativeName, native: nativeName };
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
/**
|
|
166
|
+
* How a tool call is dispatched, from the native tool name. Pi runs its
|
|
167
|
+
* builtin tools and MCP tools itself; everything else is handed back to the
|
|
168
|
+
* harness host. `tool-input-start` reports the same flags as the `tool-call`
|
|
169
|
+
* that follows it so a consumer does not have to wait for the call to know
|
|
170
|
+
* who will execute it.
|
|
171
|
+
*/
|
|
172
|
+
function resolveToolDispatch(
|
|
173
|
+
state: PiTranslatorState,
|
|
174
|
+
nativeName: string,
|
|
175
|
+
): { isMcpTool: boolean; providerExecuted: boolean } {
|
|
176
|
+
const isMcpTool =
|
|
177
|
+
!state.hostToolNames.has(nativeName) &&
|
|
178
|
+
(nativeName === 'mcp' || nativeName.startsWith('mcp__'));
|
|
179
|
+
return {
|
|
180
|
+
isMcpTool,
|
|
181
|
+
providerExecuted: state.builtinToolNames.has(nativeName) || isMcpTool,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The `{ id, name }` of the tool call a `toolcall_*` event refers to, read out
|
|
187
|
+
* of the partial assistant message it carries. Returns undefined when the
|
|
188
|
+
* block is missing or not a tool call yet, in which case the input is left
|
|
189
|
+
* unstreamed — the complete `tool-call` still arrives at `tool_execution_start`.
|
|
190
|
+
*/
|
|
191
|
+
function readStreamingToolCall(
|
|
192
|
+
event: PiSessionEvent,
|
|
193
|
+
): { contentIndex: number; id: string; name: string } | undefined {
|
|
194
|
+
const update = event.assistantMessageEvent;
|
|
195
|
+
const contentIndex = update?.contentIndex;
|
|
196
|
+
if (typeof contentIndex !== 'number') return undefined;
|
|
197
|
+
const block = update?.partial?.content?.[contentIndex];
|
|
198
|
+
if (!block || typeof block !== 'object') return undefined;
|
|
199
|
+
const record = block as Record<string, unknown>;
|
|
200
|
+
if (record.type !== 'toolCall') return undefined;
|
|
201
|
+
const { id, name } = record;
|
|
202
|
+
if (typeof id !== 'string' || id.length === 0) return undefined;
|
|
203
|
+
if (typeof name !== 'string' || name.length === 0) return undefined;
|
|
204
|
+
return { contentIndex, id, name };
|
|
205
|
+
}
|
|
206
|
+
|
|
157
207
|
function finishStep(state: PiTranslatorState): HarnessV1StreamPart[] {
|
|
158
208
|
if (!state.stepOpen || state.pendingStepToolCallIds.size > 0) return [];
|
|
159
209
|
state.stepOpen = false;
|
|
@@ -225,6 +275,8 @@ export function translatePiEvent(
|
|
|
225
275
|
state.currentTextId = undefined;
|
|
226
276
|
state.currentReasoningId = undefined;
|
|
227
277
|
state.reasoningStarted = false;
|
|
278
|
+
// Content-block indices restart with every assistant message.
|
|
279
|
+
state.streamingToolInputIds.clear();
|
|
228
280
|
return [];
|
|
229
281
|
}
|
|
230
282
|
|
|
@@ -282,6 +334,43 @@ export function translatePiEvent(
|
|
|
282
334
|
});
|
|
283
335
|
return parts;
|
|
284
336
|
}
|
|
337
|
+
// Tool inputs stream as raw JSON text, the same way text and reasoning
|
|
338
|
+
// stream. Surfacing them lets a consumer show what the model is writing
|
|
339
|
+
// before the call is complete, instead of waiting for the whole input to
|
|
340
|
+
// land at `tool_execution_start`.
|
|
341
|
+
if (update.type === 'toolcall_start') {
|
|
342
|
+
const call = readStreamingToolCall(event);
|
|
343
|
+
if (!call) return [];
|
|
344
|
+
const { wire, native } = resolveToolName(state, call.name);
|
|
345
|
+
const { isMcpTool, providerExecuted } = resolveToolDispatch(
|
|
346
|
+
state,
|
|
347
|
+
native,
|
|
348
|
+
);
|
|
349
|
+
state.streamingToolInputIds.set(call.contentIndex, call.id);
|
|
350
|
+
return [
|
|
351
|
+
{
|
|
352
|
+
type: 'tool-input-start',
|
|
353
|
+
id: call.id,
|
|
354
|
+
toolName: wire,
|
|
355
|
+
...(providerExecuted ? { providerExecuted: true } : {}),
|
|
356
|
+
...(isMcpTool ? { dynamic: true } : {}),
|
|
357
|
+
},
|
|
358
|
+
];
|
|
359
|
+
}
|
|
360
|
+
if (update.type === 'toolcall_delta' || update.type === 'toolcall_end') {
|
|
361
|
+
const contentIndex = update.contentIndex;
|
|
362
|
+
if (typeof contentIndex !== 'number') return [];
|
|
363
|
+
const id = state.streamingToolInputIds.get(contentIndex);
|
|
364
|
+
// Without a start there is no id to attach the input to. Dropping it
|
|
365
|
+
// is safe: the complete input still arrives with the `tool-call`.
|
|
366
|
+
if (id === undefined) return [];
|
|
367
|
+
if (update.type === 'toolcall_end') {
|
|
368
|
+
state.streamingToolInputIds.delete(contentIndex);
|
|
369
|
+
return [{ type: 'tool-input-end', id }];
|
|
370
|
+
}
|
|
371
|
+
if (typeof update.delta !== 'string') return [];
|
|
372
|
+
return [{ type: 'tool-input-delta', id, delta: update.delta }];
|
|
373
|
+
}
|
|
285
374
|
return [];
|
|
286
375
|
}
|
|
287
376
|
|
|
@@ -331,10 +420,10 @@ export function translatePiEvent(
|
|
|
331
420
|
if (!event.toolCallId || !event.toolName) return [];
|
|
332
421
|
const { wire, native } = resolveToolName(state, event.toolName);
|
|
333
422
|
state.observedToolNames.set(event.toolCallId, wire);
|
|
334
|
-
const isMcpTool =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
423
|
+
const { isMcpTool, providerExecuted } = resolveToolDispatch(
|
|
424
|
+
state,
|
|
425
|
+
native,
|
|
426
|
+
);
|
|
338
427
|
if (isMcpTool) state.dynamicToolCallIds.add(event.toolCallId);
|
|
339
428
|
const input = serializeToolOutput(event.args ?? event.input ?? {});
|
|
340
429
|
return [
|