@ian-pascoe/pi-codemode 0.1.0
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/LICENSE +21 -0
- package/README.md +241 -0
- package/package.json +59 -0
- package/src/codemode-cell-transform.ts +612 -0
- package/src/codemode-deno-launch.ts +59 -0
- package/src/codemode-deno-process.ts +208 -0
- package/src/codemode-observer-ui.ts +517 -0
- package/src/codemode-presentation-output.ts +16 -0
- package/src/codemode-runtime.ts +24 -0
- package/src/codemode-session-coordinator.ts +1297 -0
- package/src/codemode-session-files.ts +80 -0
- package/src/codemode-tool-catalog.ts +350 -0
- package/src/codemode-tool-contract.ts +480 -0
- package/src/codemode-tool-exposure.ts +159 -0
- package/src/codemode-tool-rendering.ts +487 -0
- package/src/codemode-worker-protocol.ts +480 -0
- package/src/codemode-worker.ts +1092 -0
- package/src/index.ts +1 -0
- package/src/pi-agent-session-capture.ts +157 -0
- package/src/pi-codemode-extension.ts +469 -0
- package/src/pi-codemode-settings.ts +168 -0
- package/src/pi-tool-bridge.ts +744 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentContext,
|
|
3
|
+
AgentTool,
|
|
4
|
+
AgentToolCall,
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
} from "@earendil-works/pi-agent-core";
|
|
7
|
+
import { validateToolArguments, type AssistantMessage, type Usage } from "@earendil-works/pi-ai";
|
|
8
|
+
import { Type } from "typebox";
|
|
9
|
+
import { Value } from "typebox/value";
|
|
10
|
+
import {
|
|
11
|
+
isCodeModeJsonObject,
|
|
12
|
+
parseCodeModeJsonValue,
|
|
13
|
+
type CodeModeJsonValue,
|
|
14
|
+
} from "./codemode-tool-contract.js";
|
|
15
|
+
import type { CapturedPiAgentSession } from "./pi-agent-session-capture.js";
|
|
16
|
+
|
|
17
|
+
const PI_TOOL_BRIDGE_RESULT_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
|
|
19
|
+
const PiToolBridgeTextContentSchema = Type.Object(
|
|
20
|
+
{ type: Type.Literal("text"), text: Type.String() },
|
|
21
|
+
{ additionalProperties: true },
|
|
22
|
+
);
|
|
23
|
+
const PiToolBridgeImageContentSchema = Type.Object(
|
|
24
|
+
{ type: Type.Literal("image"), data: Type.String(), mimeType: Type.String() },
|
|
25
|
+
{ additionalProperties: true },
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/** Stable nested Pi tool failure categories exposed to the CodeMode guest bridge. */
|
|
29
|
+
export type CodeModeToolErrorCode =
|
|
30
|
+
| "blocked"
|
|
31
|
+
| "cancellation"
|
|
32
|
+
| "execution"
|
|
33
|
+
| "serialization"
|
|
34
|
+
| "termination"
|
|
35
|
+
| "unknown-tool"
|
|
36
|
+
| "validation";
|
|
37
|
+
|
|
38
|
+
/** Catchable nested Pi tool failure, except when terminate is true and the session owner tears down the guest. */
|
|
39
|
+
export class CodeModeToolError extends Error {
|
|
40
|
+
/** Stable guest-facing failure category. */
|
|
41
|
+
readonly code: CodeModeToolErrorCode;
|
|
42
|
+
/** Whether this failure must terminate the complete CodeMode Session. */
|
|
43
|
+
readonly terminate: boolean;
|
|
44
|
+
|
|
45
|
+
/** Creates one nested Pi tool failure without exposing the original host error object. */
|
|
46
|
+
constructor(
|
|
47
|
+
code: CodeModeToolErrorCode,
|
|
48
|
+
message: string,
|
|
49
|
+
options: { readonly terminate?: boolean } = {},
|
|
50
|
+
) {
|
|
51
|
+
super(message);
|
|
52
|
+
this.name = "CodeModeToolError";
|
|
53
|
+
this.code = code;
|
|
54
|
+
this.terminate = options.terminate ?? false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One guest-originated exact Pi tool call in a fixed-point bridge batch. */
|
|
59
|
+
export interface PiToolBridgeCall {
|
|
60
|
+
readonly callId: string;
|
|
61
|
+
readonly input: Record<string, CodeModeJsonValue>;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** JSON-safe text or image content returned to a Deno Cell. */
|
|
66
|
+
export type PiToolBridgeContent =
|
|
67
|
+
| { readonly type: "text"; readonly text: string }
|
|
68
|
+
| {
|
|
69
|
+
readonly type: "image";
|
|
70
|
+
readonly data: string;
|
|
71
|
+
readonly mimeType: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** JSON-safe nested Pi tool value resolved inside the Deno Cell. */
|
|
75
|
+
export interface PiToolBridgeValue {
|
|
76
|
+
readonly content: readonly PiToolBridgeContent[];
|
|
77
|
+
readonly details?: CodeModeJsonValue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One successfully translated nested Pi tool call. */
|
|
81
|
+
export interface PiToolBridgeCallSuccess {
|
|
82
|
+
readonly callId: string;
|
|
83
|
+
readonly ok: true;
|
|
84
|
+
readonly value: PiToolBridgeValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One catchable or terminating nested Pi tool call failure. */
|
|
88
|
+
export interface PiToolBridgeCallFailure {
|
|
89
|
+
readonly callId: string;
|
|
90
|
+
readonly error: CodeModeToolError;
|
|
91
|
+
readonly ok: false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Ordered outcome for one supplied nested Pi tool call. */
|
|
95
|
+
export type PiToolBridgeCallOutcome = PiToolBridgeCallSuccess | PiToolBridgeCallFailure;
|
|
96
|
+
|
|
97
|
+
/** Parent-only bounded presentation facts for one nested Pi tool call. */
|
|
98
|
+
export interface PiToolBridgeCallPresentation {
|
|
99
|
+
readonly callId: string;
|
|
100
|
+
/** Parent wall-clock elapsed time from preparation through final hooks, in milliseconds. */
|
|
101
|
+
readonly elapsedMs: number;
|
|
102
|
+
/** Exact registered Pi tool name; arguments and output are deliberately absent. */
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly outcome: "success" | "failed" | "cancelled";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Complete nested batch output plus metadata forwarded by the outer CodeMode tool. */
|
|
108
|
+
export interface PiToolBridgeBatchResult {
|
|
109
|
+
readonly addedToolNames: readonly string[];
|
|
110
|
+
readonly calls: readonly PiToolBridgeCallOutcome[];
|
|
111
|
+
readonly presentation: readonly PiToolBridgeCallPresentation[];
|
|
112
|
+
readonly terminate: boolean;
|
|
113
|
+
readonly usage?: Usage;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Inputs that keep one synthetic hook message and one Cell signal across a nested batch. */
|
|
117
|
+
export interface ExecutePiToolBridgeBatchOptions {
|
|
118
|
+
readonly calls: readonly PiToolBridgeCall[];
|
|
119
|
+
/** Injected parent wall clock used only for clamped presentation durations. */
|
|
120
|
+
readonly now: () => number;
|
|
121
|
+
readonly onTerminate: () => void;
|
|
122
|
+
readonly onUpdate?: (callId: string, result: AgentToolResult<unknown>) => void;
|
|
123
|
+
readonly outerAssistantMessage?: AssistantMessage;
|
|
124
|
+
readonly signal: AbortSignal;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type PreparedPiToolCall = {
|
|
128
|
+
readonly args: unknown;
|
|
129
|
+
readonly kind: "prepared";
|
|
130
|
+
readonly tool: AgentTool;
|
|
131
|
+
readonly toolCall: AgentToolCall;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
type FinalizedPiToolCall = {
|
|
135
|
+
readonly addedToolNames: readonly string[];
|
|
136
|
+
readonly outcome: PiToolBridgeCallOutcome;
|
|
137
|
+
readonly terminate: boolean;
|
|
138
|
+
readonly usage?: Usage;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
type PreparedOrFinalizedPiToolCall =
|
|
142
|
+
| PreparedPiToolCall
|
|
143
|
+
| { readonly kind: "finalized"; readonly value: FinalizedPiToolCall };
|
|
144
|
+
|
|
145
|
+
type FinalizedPiToolMetadata = {
|
|
146
|
+
readonly addedToolNames: readonly string[];
|
|
147
|
+
readonly usage?: Usage;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
type TimedFinalizedPiToolCall = {
|
|
151
|
+
readonly finalized: FinalizedPiToolCall;
|
|
152
|
+
readonly presentation: PiToolBridgeCallPresentation;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function normalizeThrownMessage(cause: unknown): string {
|
|
156
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function createBridgeFailure(
|
|
160
|
+
callId: string,
|
|
161
|
+
code: CodeModeToolErrorCode,
|
|
162
|
+
message: string,
|
|
163
|
+
options: { readonly terminate?: boolean } = {},
|
|
164
|
+
): FinalizedPiToolCall {
|
|
165
|
+
const terminate = options.terminate ?? false;
|
|
166
|
+
return {
|
|
167
|
+
addedToolNames: [],
|
|
168
|
+
outcome: {
|
|
169
|
+
callId,
|
|
170
|
+
error: new CodeModeToolError(code, message, options),
|
|
171
|
+
ok: false,
|
|
172
|
+
},
|
|
173
|
+
terminate,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createErrorToolResult(message: string): AgentToolResult<unknown> {
|
|
178
|
+
return {
|
|
179
|
+
content: [{ type: "text", text: message }],
|
|
180
|
+
details: {},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function bridgeToolCall(call: PiToolBridgeCall): AgentToolCall {
|
|
185
|
+
return {
|
|
186
|
+
type: "toolCall",
|
|
187
|
+
id: call.callId,
|
|
188
|
+
name: call.name,
|
|
189
|
+
arguments: call.input,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function zeroUsage(): Usage {
|
|
194
|
+
return {
|
|
195
|
+
input: 0,
|
|
196
|
+
output: 0,
|
|
197
|
+
cacheRead: 0,
|
|
198
|
+
cacheWrite: 0,
|
|
199
|
+
totalTokens: 0,
|
|
200
|
+
cost: {
|
|
201
|
+
input: 0,
|
|
202
|
+
output: 0,
|
|
203
|
+
cacheRead: 0,
|
|
204
|
+
cacheWrite: 0,
|
|
205
|
+
total: 0,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function createSyntheticAssistantMessage(
|
|
211
|
+
captured: CapturedPiAgentSession,
|
|
212
|
+
calls: readonly PiToolBridgeCall[],
|
|
213
|
+
outerAssistantMessage: AssistantMessage | undefined,
|
|
214
|
+
): AssistantMessage {
|
|
215
|
+
const model = captured.agent.state.model;
|
|
216
|
+
return {
|
|
217
|
+
role: "assistant",
|
|
218
|
+
content: calls.map(bridgeToolCall),
|
|
219
|
+
api: outerAssistantMessage?.api ?? model.api,
|
|
220
|
+
provider: outerAssistantMessage?.provider ?? model.provider,
|
|
221
|
+
model: outerAssistantMessage?.model ?? model.id,
|
|
222
|
+
usage: outerAssistantMessage?.usage ?? zeroUsage(),
|
|
223
|
+
stopReason: "toolUse",
|
|
224
|
+
timestamp: outerAssistantMessage?.timestamp ?? 0,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function currentAgentContext(captured: CapturedPiAgentSession): AgentContext {
|
|
229
|
+
const state = captured.agent.state;
|
|
230
|
+
return {
|
|
231
|
+
systemPrompt: state.systemPrompt,
|
|
232
|
+
messages: state.messages,
|
|
233
|
+
tools: state.tools,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function preparePiToolCall(
|
|
238
|
+
captured: CapturedPiAgentSession,
|
|
239
|
+
call: PiToolBridgeCall,
|
|
240
|
+
assistantMessage: AssistantMessage,
|
|
241
|
+
context: AgentContext,
|
|
242
|
+
signal: AbortSignal,
|
|
243
|
+
): Promise<PreparedOrFinalizedPiToolCall> {
|
|
244
|
+
const toolCall = bridgeToolCall(call);
|
|
245
|
+
const tool = captured.getToolRegistry().get(call.name);
|
|
246
|
+
if (tool === undefined) {
|
|
247
|
+
return Promise.resolve({
|
|
248
|
+
kind: "finalized",
|
|
249
|
+
value: createBridgeFailure(
|
|
250
|
+
call.callId,
|
|
251
|
+
"unknown-tool",
|
|
252
|
+
`Pi CodeMode tool not found: ${call.name}`,
|
|
253
|
+
),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (async () => {
|
|
258
|
+
try {
|
|
259
|
+
const preparedArguments =
|
|
260
|
+
tool.prepareArguments === undefined ? call.input : tool.prepareArguments(call.input);
|
|
261
|
+
// SAFETY: Pi defines prepareArguments as producing the tool-call argument representation; its exact schema validator remains authoritative below.
|
|
262
|
+
const preparedToolArguments = preparedArguments as AgentToolCall["arguments"];
|
|
263
|
+
const preparedToolCall =
|
|
264
|
+
preparedArguments === call.input
|
|
265
|
+
? toolCall
|
|
266
|
+
: { ...toolCall, arguments: preparedToolArguments };
|
|
267
|
+
const validatedArgs: unknown = validateToolArguments(tool, preparedToolCall);
|
|
268
|
+
const beforeResult = await captured.agent.beforeToolCall?.(
|
|
269
|
+
{
|
|
270
|
+
assistantMessage,
|
|
271
|
+
toolCall,
|
|
272
|
+
args: validatedArgs,
|
|
273
|
+
context,
|
|
274
|
+
},
|
|
275
|
+
signal,
|
|
276
|
+
);
|
|
277
|
+
if (signal.aborted) {
|
|
278
|
+
return {
|
|
279
|
+
kind: "finalized",
|
|
280
|
+
value: createBridgeFailure(call.callId, "cancellation", "Operation aborted"),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
if (beforeResult?.block === true) {
|
|
284
|
+
return {
|
|
285
|
+
kind: "finalized",
|
|
286
|
+
value: createBridgeFailure(
|
|
287
|
+
call.callId,
|
|
288
|
+
beforeResult.terminate === true ? "termination" : "blocked",
|
|
289
|
+
beforeResult.reason ?? "Tool execution was blocked",
|
|
290
|
+
{ terminate: beforeResult.terminate === true },
|
|
291
|
+
),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
kind: "prepared",
|
|
296
|
+
toolCall,
|
|
297
|
+
tool,
|
|
298
|
+
args: validatedArgs,
|
|
299
|
+
};
|
|
300
|
+
} catch (cause) {
|
|
301
|
+
return {
|
|
302
|
+
kind: "finalized",
|
|
303
|
+
value: createBridgeFailure(call.callId, "validation", normalizeThrownMessage(cause)),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
})();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function executePreparedPiToolCall(
|
|
310
|
+
prepared: PreparedPiToolCall,
|
|
311
|
+
signal: AbortSignal,
|
|
312
|
+
onUpdate: ((callId: string, result: AgentToolResult<unknown>) => void) | undefined,
|
|
313
|
+
): Promise<{ readonly isError: boolean; readonly result: AgentToolResult<unknown> }> {
|
|
314
|
+
let acceptingUpdates = true;
|
|
315
|
+
try {
|
|
316
|
+
// SAFETY: Pi's own validator above parsed this value against this exact wrapper's parameter schema.
|
|
317
|
+
const validatedArgs = prepared.args as never;
|
|
318
|
+
const result = await prepared.tool.execute(
|
|
319
|
+
prepared.toolCall.id,
|
|
320
|
+
validatedArgs,
|
|
321
|
+
signal,
|
|
322
|
+
(partialResult) => {
|
|
323
|
+
if (!acceptingUpdates) return;
|
|
324
|
+
onUpdate?.(prepared.toolCall.id, partialResult);
|
|
325
|
+
},
|
|
326
|
+
);
|
|
327
|
+
acceptingUpdates = false;
|
|
328
|
+
return { result, isError: false };
|
|
329
|
+
} catch (cause) {
|
|
330
|
+
acceptingUpdates = false;
|
|
331
|
+
return {
|
|
332
|
+
result: createErrorToolResult(normalizeThrownMessage(cause)),
|
|
333
|
+
isError: true,
|
|
334
|
+
};
|
|
335
|
+
} finally {
|
|
336
|
+
acceptingUpdates = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function finalizeExecutedPiToolCall(
|
|
341
|
+
captured: CapturedPiAgentSession,
|
|
342
|
+
assistantMessage: AssistantMessage,
|
|
343
|
+
context: AgentContext,
|
|
344
|
+
prepared: PreparedPiToolCall,
|
|
345
|
+
executed: {
|
|
346
|
+
readonly isError: boolean;
|
|
347
|
+
readonly result: AgentToolResult<unknown>;
|
|
348
|
+
},
|
|
349
|
+
signal: AbortSignal,
|
|
350
|
+
): Promise<FinalizedPiToolCall> {
|
|
351
|
+
let result = executed.result;
|
|
352
|
+
let isError = executed.isError;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const afterResult = await captured.agent.afterToolCall?.(
|
|
356
|
+
{
|
|
357
|
+
assistantMessage,
|
|
358
|
+
toolCall: prepared.toolCall,
|
|
359
|
+
args: prepared.args,
|
|
360
|
+
result,
|
|
361
|
+
isError,
|
|
362
|
+
context,
|
|
363
|
+
},
|
|
364
|
+
signal,
|
|
365
|
+
);
|
|
366
|
+
if (afterResult !== undefined) {
|
|
367
|
+
const mergedResult: AgentToolResult<unknown> = {
|
|
368
|
+
content: afterResult.content ?? result.content,
|
|
369
|
+
details: afterResult.details ?? result.details,
|
|
370
|
+
};
|
|
371
|
+
const usage = afterResult.usage ?? result.usage;
|
|
372
|
+
if (usage !== undefined) mergedResult.usage = usage;
|
|
373
|
+
if (result.addedToolNames !== undefined) {
|
|
374
|
+
mergedResult.addedToolNames = result.addedToolNames;
|
|
375
|
+
}
|
|
376
|
+
const terminate = afterResult.terminate ?? result.terminate;
|
|
377
|
+
if (terminate !== undefined) mergedResult.terminate = terminate;
|
|
378
|
+
result = mergedResult;
|
|
379
|
+
isError = afterResult.isError ?? isError;
|
|
380
|
+
}
|
|
381
|
+
} catch (cause) {
|
|
382
|
+
result = createErrorToolResult(normalizeThrownMessage(cause));
|
|
383
|
+
isError = true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const terminate = result.terminate === true;
|
|
387
|
+
const metadata: FinalizedPiToolMetadata =
|
|
388
|
+
result.usage === undefined
|
|
389
|
+
? { addedToolNames: result.addedToolNames ?? [] }
|
|
390
|
+
: {
|
|
391
|
+
addedToolNames: result.addedToolNames ?? [],
|
|
392
|
+
usage: result.usage,
|
|
393
|
+
};
|
|
394
|
+
if (terminate) {
|
|
395
|
+
return {
|
|
396
|
+
...metadata,
|
|
397
|
+
outcome: {
|
|
398
|
+
callId: prepared.toolCall.id,
|
|
399
|
+
error: new CodeModeToolError(
|
|
400
|
+
"termination",
|
|
401
|
+
resultErrorMessage(result, prepared.toolCall.name),
|
|
402
|
+
{ terminate: true },
|
|
403
|
+
),
|
|
404
|
+
ok: false,
|
|
405
|
+
},
|
|
406
|
+
terminate: true,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (isError) {
|
|
410
|
+
return {
|
|
411
|
+
...metadata,
|
|
412
|
+
outcome: {
|
|
413
|
+
callId: prepared.toolCall.id,
|
|
414
|
+
error: new CodeModeToolError(
|
|
415
|
+
"execution",
|
|
416
|
+
resultErrorMessage(result, prepared.toolCall.name),
|
|
417
|
+
),
|
|
418
|
+
ok: false,
|
|
419
|
+
},
|
|
420
|
+
terminate: false,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const translated = translatePiToolResult(result);
|
|
425
|
+
if (!translated.ok) {
|
|
426
|
+
return {
|
|
427
|
+
...metadata,
|
|
428
|
+
outcome: {
|
|
429
|
+
callId: prepared.toolCall.id,
|
|
430
|
+
error: new CodeModeToolError("serialization", translated.message),
|
|
431
|
+
ok: false,
|
|
432
|
+
},
|
|
433
|
+
terminate: false,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
...metadata,
|
|
438
|
+
outcome: {
|
|
439
|
+
callId: prepared.toolCall.id,
|
|
440
|
+
ok: true,
|
|
441
|
+
value: translated.value,
|
|
442
|
+
},
|
|
443
|
+
terminate: false,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function resultErrorMessage(result: AgentToolResult<unknown>, toolName: string): string {
|
|
448
|
+
for (const content of result.content ?? []) {
|
|
449
|
+
if (content.type === "text" && content.text.length > 0) return content.text;
|
|
450
|
+
}
|
|
451
|
+
return `Pi CodeMode tool failed: ${toolName}`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function parseBridgeContent(value: CodeModeJsonValue): PiToolBridgeContent | undefined {
|
|
455
|
+
if (Value.Check(PiToolBridgeTextContentSchema, value)) {
|
|
456
|
+
return { type: "text", text: value.text };
|
|
457
|
+
}
|
|
458
|
+
if (Value.Check(PiToolBridgeImageContentSchema, value)) {
|
|
459
|
+
return { type: "image", data: value.data, mimeType: value.mimeType };
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function translatePiToolResult(
|
|
465
|
+
result: AgentToolResult<unknown>,
|
|
466
|
+
):
|
|
467
|
+
| { readonly ok: true; readonly value: PiToolBridgeValue }
|
|
468
|
+
| { readonly ok: false; readonly message: string } {
|
|
469
|
+
const wireCandidate =
|
|
470
|
+
result.details === undefined
|
|
471
|
+
? { content: result.content ?? [] }
|
|
472
|
+
: { content: result.content ?? [], details: result.details };
|
|
473
|
+
const parsed = parseCodeModeJsonValue(wireCandidate, {
|
|
474
|
+
maxBytes: PI_TOOL_BRIDGE_RESULT_LIMIT_BYTES,
|
|
475
|
+
normalizeUndefinedForJsonTransport: true,
|
|
476
|
+
});
|
|
477
|
+
if (!parsed.ok) {
|
|
478
|
+
return {
|
|
479
|
+
ok: false,
|
|
480
|
+
message: `Pi CodeMode tool result is not JSON-safe: ${parsed.message}`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
if (parsed.value === undefined || !isCodeModeJsonObject(parsed.value)) {
|
|
484
|
+
return { ok: false, message: "Pi CodeMode tool result is not a JSON object" };
|
|
485
|
+
}
|
|
486
|
+
const contentValue = parsed.value.content;
|
|
487
|
+
if (!Array.isArray(contentValue)) {
|
|
488
|
+
return { ok: false, message: "Pi CodeMode tool result content is not an array" };
|
|
489
|
+
}
|
|
490
|
+
const content: PiToolBridgeContent[] = [];
|
|
491
|
+
for (const entry of contentValue) {
|
|
492
|
+
const parsedContent = parseBridgeContent(entry);
|
|
493
|
+
if (parsedContent === undefined) {
|
|
494
|
+
return {
|
|
495
|
+
ok: false,
|
|
496
|
+
message: "Pi CodeMode tool result contains unsupported content",
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
content.push(parsedContent);
|
|
500
|
+
}
|
|
501
|
+
const details = parsed.value.details;
|
|
502
|
+
return {
|
|
503
|
+
ok: true,
|
|
504
|
+
value: details === undefined ? { content } : { content, details },
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function addUsage(left: Usage | undefined, right: Usage | undefined): Usage | undefined {
|
|
509
|
+
if (right === undefined) return left;
|
|
510
|
+
if (left === undefined) return right;
|
|
511
|
+
const reasoning =
|
|
512
|
+
left.reasoning === undefined && right.reasoning === undefined
|
|
513
|
+
? undefined
|
|
514
|
+
: (left.reasoning ?? 0) + (right.reasoning ?? 0);
|
|
515
|
+
const cacheWrite1h =
|
|
516
|
+
left.cacheWrite1h === undefined && right.cacheWrite1h === undefined
|
|
517
|
+
? undefined
|
|
518
|
+
: (left.cacheWrite1h ?? 0) + (right.cacheWrite1h ?? 0);
|
|
519
|
+
const combined: Usage = {
|
|
520
|
+
input: left.input + right.input,
|
|
521
|
+
output: left.output + right.output,
|
|
522
|
+
cacheRead: left.cacheRead + right.cacheRead,
|
|
523
|
+
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
524
|
+
totalTokens: left.totalTokens + right.totalTokens,
|
|
525
|
+
cost: {
|
|
526
|
+
input: left.cost.input + right.cost.input,
|
|
527
|
+
output: left.cost.output + right.cost.output,
|
|
528
|
+
cacheRead: left.cost.cacheRead + right.cost.cacheRead,
|
|
529
|
+
cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
|
|
530
|
+
total: left.cost.total + right.cost.total,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
if (cacheWrite1h !== undefined) combined.cacheWrite1h = cacheWrite1h;
|
|
534
|
+
if (reasoning !== undefined) combined.reasoning = reasoning;
|
|
535
|
+
return combined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function terminatedPiToolCall(callId: string): FinalizedPiToolCall {
|
|
539
|
+
return createBridgeFailure(
|
|
540
|
+
callId,
|
|
541
|
+
"termination",
|
|
542
|
+
"CodeMode Session terminated by a sibling Pi tool call",
|
|
543
|
+
{ terminate: true },
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function presentationOutcome(
|
|
548
|
+
outcome: PiToolBridgeCallOutcome,
|
|
549
|
+
): PiToolBridgeCallPresentation["outcome"] {
|
|
550
|
+
if (outcome.ok) return "success";
|
|
551
|
+
return outcome.error.code === "cancellation" || outcome.error.code === "termination"
|
|
552
|
+
? "cancelled"
|
|
553
|
+
: "failed";
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function elapsedMilliseconds(startedAt: number, finishedAt: number): number {
|
|
557
|
+
const elapsed = Math.round(finishedAt - startedAt);
|
|
558
|
+
if (!Number.isFinite(elapsed)) return 0;
|
|
559
|
+
return Math.min(Number.MAX_SAFE_INTEGER, Math.max(0, elapsed));
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function timedFinalizedPiToolCall(
|
|
563
|
+
call: PiToolBridgeCall,
|
|
564
|
+
finalized: FinalizedPiToolCall,
|
|
565
|
+
startedAt: number,
|
|
566
|
+
now: () => number,
|
|
567
|
+
): TimedFinalizedPiToolCall {
|
|
568
|
+
return {
|
|
569
|
+
finalized,
|
|
570
|
+
presentation: {
|
|
571
|
+
callId: call.callId,
|
|
572
|
+
elapsedMs: elapsedMilliseconds(startedAt, now()),
|
|
573
|
+
name: call.name,
|
|
574
|
+
outcome: presentationOutcome(finalized.outcome),
|
|
575
|
+
},
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function cancelledPiToolCallPresentation(call: PiToolBridgeCall): TimedFinalizedPiToolCall {
|
|
580
|
+
return {
|
|
581
|
+
finalized: terminatedPiToolCall(call.callId),
|
|
582
|
+
presentation: { callId: call.callId, elapsedMs: 0, name: call.name, outcome: "cancelled" },
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function collectPiToolBridgeBatch(
|
|
587
|
+
timedCalls: readonly TimedFinalizedPiToolCall[],
|
|
588
|
+
): PiToolBridgeBatchResult {
|
|
589
|
+
let usage: Usage | undefined;
|
|
590
|
+
const addedToolNames: string[] = [];
|
|
591
|
+
const seenToolNames = new Set<string>();
|
|
592
|
+
for (const { finalized } of timedCalls) {
|
|
593
|
+
usage = addUsage(usage, finalized.usage);
|
|
594
|
+
for (const toolName of finalized.addedToolNames) {
|
|
595
|
+
if (seenToolNames.has(toolName)) continue;
|
|
596
|
+
seenToolNames.add(toolName);
|
|
597
|
+
addedToolNames.push(toolName);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const batch = {
|
|
601
|
+
addedToolNames,
|
|
602
|
+
calls: timedCalls.map(({ finalized }) => finalized.outcome),
|
|
603
|
+
presentation: timedCalls.map(({ presentation }) => presentation),
|
|
604
|
+
terminate: timedCalls.some(({ finalized }) => finalized.terminate),
|
|
605
|
+
};
|
|
606
|
+
return usage === undefined ? batch : { ...batch, usage };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Executes one nested Pi tool batch with Pi 0.84.2 preparation, hook, wrapper, and merge order. */
|
|
610
|
+
export async function executePiToolBridgeBatch(
|
|
611
|
+
captured: CapturedPiAgentSession,
|
|
612
|
+
options: ExecutePiToolBridgeBatchOptions,
|
|
613
|
+
): Promise<PiToolBridgeBatchResult> {
|
|
614
|
+
const assistantMessage = createSyntheticAssistantMessage(
|
|
615
|
+
captured,
|
|
616
|
+
options.calls,
|
|
617
|
+
options.outerAssistantMessage,
|
|
618
|
+
);
|
|
619
|
+
const context = currentAgentContext(captured);
|
|
620
|
+
const terminationCompletion = Promise.withResolvers<void>();
|
|
621
|
+
let acceptingBatchUpdates = true;
|
|
622
|
+
let terminationNotified = false;
|
|
623
|
+
const notifyTermination = (): void => {
|
|
624
|
+
if (terminationNotified) return;
|
|
625
|
+
terminationNotified = true;
|
|
626
|
+
acceptingBatchUpdates = false;
|
|
627
|
+
terminationCompletion.resolve();
|
|
628
|
+
options.onTerminate();
|
|
629
|
+
};
|
|
630
|
+
const forwardUpdate =
|
|
631
|
+
options.onUpdate === undefined
|
|
632
|
+
? undefined
|
|
633
|
+
: (callId: string, result: AgentToolResult<unknown>): void => {
|
|
634
|
+
if (acceptingBatchUpdates) options.onUpdate?.(callId, result);
|
|
635
|
+
};
|
|
636
|
+
const finalizePrepared = async (prepared: PreparedPiToolCall): Promise<FinalizedPiToolCall> => {
|
|
637
|
+
if (terminationNotified) return terminatedPiToolCall(prepared.toolCall.id);
|
|
638
|
+
const executed = await executePreparedPiToolCall(prepared, options.signal, forwardUpdate);
|
|
639
|
+
if (terminationNotified) return terminatedPiToolCall(prepared.toolCall.id);
|
|
640
|
+
const finalized = await finalizeExecutedPiToolCall(
|
|
641
|
+
captured,
|
|
642
|
+
assistantMessage,
|
|
643
|
+
context,
|
|
644
|
+
prepared,
|
|
645
|
+
executed,
|
|
646
|
+
options.signal,
|
|
647
|
+
);
|
|
648
|
+
return terminationNotified ? terminatedPiToolCall(prepared.toolCall.id) : finalized;
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const hasSequentialCall = options.calls.some(
|
|
652
|
+
(call) => captured.getToolRegistry().get(call.name)?.executionMode === "sequential",
|
|
653
|
+
);
|
|
654
|
+
if (hasSequentialCall) {
|
|
655
|
+
const finalizedCalls: TimedFinalizedPiToolCall[] = [];
|
|
656
|
+
for (const [index, call] of options.calls.entries()) {
|
|
657
|
+
if (terminationNotified) {
|
|
658
|
+
finalizedCalls.push(cancelledPiToolCallPresentation(call));
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const startedAt = options.now();
|
|
662
|
+
const preparation = await preparePiToolCall(
|
|
663
|
+
captured,
|
|
664
|
+
call,
|
|
665
|
+
assistantMessage,
|
|
666
|
+
context,
|
|
667
|
+
options.signal,
|
|
668
|
+
);
|
|
669
|
+
const finalized =
|
|
670
|
+
preparation.kind === "finalized" ? preparation.value : await finalizePrepared(preparation);
|
|
671
|
+
finalizedCalls.push(timedFinalizedPiToolCall(call, finalized, startedAt, options.now));
|
|
672
|
+
if (finalized.terminate) {
|
|
673
|
+
notifyTermination();
|
|
674
|
+
for (const sibling of options.calls.slice(index + 1)) {
|
|
675
|
+
finalizedCalls.push(cancelledPiToolCallPresentation(sibling));
|
|
676
|
+
}
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
acceptingBatchUpdates = false;
|
|
681
|
+
return collectPiToolBridgeBatch(finalizedCalls);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const preparations = await Promise.all(
|
|
685
|
+
options.calls.map(async (call) => {
|
|
686
|
+
const startedAt = options.now();
|
|
687
|
+
const preparation = await preparePiToolCall(
|
|
688
|
+
captured,
|
|
689
|
+
call,
|
|
690
|
+
assistantMessage,
|
|
691
|
+
context,
|
|
692
|
+
options.signal,
|
|
693
|
+
);
|
|
694
|
+
return { call, preparation, startedAt };
|
|
695
|
+
}),
|
|
696
|
+
);
|
|
697
|
+
const terminatingPreparation = preparations.find(
|
|
698
|
+
({ preparation }) => preparation.kind === "finalized" && preparation.value.terminate,
|
|
699
|
+
);
|
|
700
|
+
if (terminatingPreparation !== undefined) {
|
|
701
|
+
notifyTermination();
|
|
702
|
+
return collectPiToolBridgeBatch(
|
|
703
|
+
preparations.map(({ call, preparation, startedAt }) =>
|
|
704
|
+
preparation.kind === "finalized" && preparation.value.terminate
|
|
705
|
+
? timedFinalizedPiToolCall(call, preparation.value, startedAt, options.now)
|
|
706
|
+
: cancelledPiToolCallPresentation(call),
|
|
707
|
+
),
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const finalizedSlots: Array<TimedFinalizedPiToolCall | undefined> = Array.from({
|
|
712
|
+
length: preparations.length,
|
|
713
|
+
});
|
|
714
|
+
const trackedFinalizations = preparations.map(({ call, preparation, startedAt }, index) => {
|
|
715
|
+
const callId =
|
|
716
|
+
preparation.kind === "prepared" ? preparation.toolCall.id : preparation.value.outcome.callId;
|
|
717
|
+
const finalization =
|
|
718
|
+
preparation.kind === "finalized"
|
|
719
|
+
? Promise.resolve(preparation.value)
|
|
720
|
+
: finalizePrepared(preparation);
|
|
721
|
+
return finalization
|
|
722
|
+
.catch((cause) => createBridgeFailure(callId, "execution", normalizeThrownMessage(cause)))
|
|
723
|
+
.then((finalized) => {
|
|
724
|
+
const timed = timedFinalizedPiToolCall(call, finalized, startedAt, options.now);
|
|
725
|
+
finalizedSlots[index] = timed;
|
|
726
|
+
if (finalized.terminate) notifyTermination();
|
|
727
|
+
return timed;
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
const settled = await Promise.race([
|
|
731
|
+
Promise.all(trackedFinalizations).then(
|
|
732
|
+
(finalizedCalls) => ({ kind: "complete", finalizedCalls }) as const,
|
|
733
|
+
),
|
|
734
|
+
terminationCompletion.promise.then(() => ({ kind: "termination" }) as const),
|
|
735
|
+
]);
|
|
736
|
+
acceptingBatchUpdates = false;
|
|
737
|
+
return settled.kind === "complete"
|
|
738
|
+
? collectPiToolBridgeBatch(settled.finalizedCalls)
|
|
739
|
+
: collectPiToolBridgeBatch(
|
|
740
|
+
options.calls.map(
|
|
741
|
+
(call, index) => finalizedSlots[index] ?? cancelledPiToolCallPresentation(call),
|
|
742
|
+
),
|
|
743
|
+
);
|
|
744
|
+
}
|