pi-openai-toolkit 0.23.1 → 0.24.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.
Files changed (53) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +4 -11
  4. package/README.zh.md +4 -11
  5. package/config.schema.json +264 -278
  6. package/docs/configuration.md +304 -298
  7. package/docs/internals.md +98 -181
  8. package/package.json +123 -135
  9. package/src/compact-client.ts +412 -412
  10. package/src/compaction-projection.ts +35 -35
  11. package/src/config/legacy.ts +12 -62
  12. package/src/config/migration.ts +115 -103
  13. package/src/config/notifications.ts +10 -4
  14. package/src/config/policy.ts +9 -10
  15. package/src/config/v2.ts +12 -51
  16. package/src/config.ts +320 -310
  17. package/src/debug.ts +238 -238
  18. package/src/deferred-tool-carryover.ts +282 -282
  19. package/src/details-store.ts +151 -151
  20. package/src/extension-runtime.ts +1095 -1986
  21. package/src/image-generation/extension.ts +189 -189
  22. package/src/image-generation/protocol.ts +579 -579
  23. package/src/image-generation/types.ts +300 -300
  24. package/src/native-fallback.ts +14 -23
  25. package/src/payload-rewrite.ts +515 -515
  26. package/src/pi-context-hook.ts +117 -117
  27. package/src/remote-v2-client.ts +811 -811
  28. package/src/request-context-cache.ts +155 -155
  29. package/src/responses-headers.ts +180 -180
  30. package/src/retired-remote-windows.ts +35 -0
  31. package/src/runtime.ts +875 -865
  32. package/src/serializer.ts +882 -882
  33. package/src/supported-environment.ts +27 -27
  34. package/src/types.ts +541 -565
  35. package/src/web-search/alpha-client.ts +876 -876
  36. package/src/web-search/extension.ts +530 -530
  37. package/src/web-search/payload.ts +299 -299
  38. package/src/web-search/prompt.ts +60 -60
  39. package/src/web-search/types.ts +166 -166
  40. package/tsconfig.check.json +50 -50
  41. package/src/context-management/codex-provider.ts +0 -270
  42. package/src/context-management/history-notes.ts +0 -357
  43. package/src/context-management/manual-compact.ts +0 -599
  44. package/src/context-management/messages.ts +0 -238
  45. package/src/context-management/namespace-tools.ts +0 -426
  46. package/src/context-management/responses-tools.ts +0 -238
  47. package/src/context-management/tool-contract.ts +0 -62
  48. package/src/context-management/tools.ts +0 -371
  49. package/src/context-management/types.ts +0 -168
  50. package/src/context-management/window-budget.ts +0 -83
  51. package/src/context-management/window-bulk.ts +0 -155
  52. package/src/context-management/window-manager.ts +0 -905
  53. package/src/context-management/window-request.ts +0 -125
@@ -1,238 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import { getCurrentSystemMessage, getCurrentTools } from "@earendil-works/pi-ai";
4
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
- import {
6
- CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
7
- CONTEXT_MANAGEMENT_PROTOCOL,
8
- CONTEXT_WINDOW_COMPACTION_STRATEGY,
9
- CONTEXT_WINDOW_FALLBACK_BUFFER,
10
- CONTEXT_WINDOW_REMINDER_THRESHOLD,
11
- type CodexContextManagementMessageDetails,
12
- type ContextManagementMessageKind,
13
- type ContextWindowCompactionDetails,
14
- type ContextWindowIdentity,
15
- type NotesCheckpointReceipt,
16
- isCodexContextManagementMessageDetails,
17
- isContextWindowCompactionDetails,
18
- } from "./types";
19
-
20
- export {
21
- CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
22
- CONTEXT_WINDOW_COMPACTION_STRATEGY,
23
- CONTEXT_WINDOW_FALLBACK_BUFFER,
24
- CONTEXT_WINDOW_REMINDER_THRESHOLD,
25
- isCodexContextManagementMessageDetails,
26
- isContextWindowCompactionDetails,
27
- };
28
- export type {
29
- CodexContextManagementMessageDetails,
30
- ContextManagementMessageKind,
31
- ContextWindowCompactionDetails,
32
- ContextWindowIdentity,
33
- };
34
-
35
- export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
36
- "[Pi Codex context-window boundary; no conversation summary was generated.]";
37
-
38
- const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
39
- Before calling new_context, checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes, and wait for that result to be persisted; only a persisted successful result in the current window unlocks the rollover. If new_context reports that a rollover is already scheduled, do not call it again in the same window. When this message includes a completed context-switch handoff, follow that post-rollover section first instead of restarting the pre-rollover checkpoint steps. A thread hint is supplemental and must not replace the local checkpoint receipt. No conversation summary carries over, and history is only for missing details.
40
- </context_window_guidance>`;
41
-
42
- export function renderContextWindowMessage(
43
- identity: ContextWindowIdentity,
44
- threadHint?: string,
45
- checkpoint?: NotesCheckpointReceipt,
46
- ): string {
47
- const lines = [
48
- "<context_window>",
49
- "Agent name: /root",
50
- `First context window id: ${identity.firstWindowId}`,
51
- `Current context window id: ${identity.currentWindowId}`,
52
- ];
53
- if (identity.previousWindowId) lines.push(`Previous context window id: ${identity.previousWindowId}`);
54
- if (checkpoint) {
55
- lines.push("Context switch completed. This is the first turn in the new context window.");
56
- lines.push("Checkpoint successfully written:");
57
- lines.push(` Read this note before doing anything else with notes action "read_file": ${JSON.stringify(checkpoint.path)}`);
58
- lines.push("After reading the checkpoint receipt, resume the active user task.");
59
- lines.push("Do not immediately create another checkpoint or call new_context as part of this handoff. Only prepare a new checkpoint when a later context rollover is actually needed.");
60
- }
61
- if (threadHint) lines.push(threadHint);
62
- lines.push("</context_window>");
63
- return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
64
- }
65
-
66
- export function renderContextWindowReminder(remainingTokens: number): string {
67
- return `<context_window_reminder>
68
- Only ${Math.max(0, Math.floor(remainingTokens))} context tokens remain. Checkpoint the active request, state and known history IDs in notes, then call new_context; no conversation summary carries over.
69
- </context_window_reminder>`;
70
- }
71
-
72
- export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
73
- Context exhausted. Do not continue or answer. Make exactly one notes write or append call that checkpoints the active request, state and known history IDs, then call new_context. Use no other tools before rollover.
74
- </context_window_reminder>`;
75
-
76
- export function isContextWindowBoundary(
77
- message: AgentMessage,
78
- ): message is Extract<AgentMessage, { role: "custom" }> & {
79
- details: CodexContextManagementMessageDetails;
80
- } {
81
- return (
82
- message.role === "custom" &&
83
- message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
84
- isCodexContextManagementMessageDetails(message.details) &&
85
- message.details.contextManagement.kind === "window"
86
- );
87
- }
88
-
89
- export function sendContextWindowMessage(
90
- pi: ExtensionAPI,
91
- content: string,
92
- kind: ContextManagementMessageKind,
93
- identity: ContextWindowIdentity,
94
- options: { triggerTurn: boolean; sessionId?: string },
95
- trimPreviousWindow = false,
96
- ): void {
97
- const details: CodexContextManagementMessageDetails = {
98
- protocol: CONTEXT_MANAGEMENT_PROTOCOL,
99
- id: randomUUID(),
100
- ...(options.sessionId ? { sessionId: options.sessionId } : {}),
101
- contextManagement: {
102
- protocol: CONTEXT_MANAGEMENT_PROTOCOL,
103
- kind,
104
- ...identity,
105
- ...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
106
- },
107
- };
108
- pi.sendMessage({
109
- customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
110
- content,
111
- display: true,
112
- details,
113
- }, options.triggerTurn
114
- ? { deliverAs: "steer", triggerTurn: true }
115
- : { triggerTurn: false });
116
- }
117
-
118
- /** Whether a transcript message is a Pi system message (prompt sections and tool deltas). */
119
- export function isSystemTranscriptMessage(message: AgentMessage): boolean {
120
- return (message as unknown as { role?: string }).role === "system";
121
- }
122
-
123
- /** Tool names the provider would send for this message list, after replaying every delta. */
124
- export function declaredToolNames(messages: readonly AgentMessage[]): string[] {
125
- return getCurrentTools(messages).map((tool) => tool.name);
126
- }
127
-
128
- export type SystemHeadRepair<TMessage extends AgentMessage = AgentMessage> = {
129
- /** The window to send: trimmed, trimmed plus a rebuilt head, or untrimmed when the trim is unsafe. */
130
- readonly messages: readonly TMessage[];
131
- /** A head from before the boundary was re-anchored at the front of the window. */
132
- repaired: boolean;
133
- /** The trim was refused because the window would lose declared tools. */
134
- shrinkRejected: boolean;
135
- /** Tool names the trimmed window would have lost; empty on every safe path. */
136
- lostToolNames: string[];
137
- /** The system message this window anchors on, when one could be determined. */
138
- head: AgentMessage | undefined;
139
- };
140
-
141
- /**
142
- * Keep Pi 0.86's prompt and tool head alive across a context-window trim.
143
- *
144
- * Pi 0.86 no longer sends the system prompt and the tool loadout beside the
145
- * transcript: `Agent#createContextSnapshot()` hands over messages only, and the
146
- * Responses transport rebuilds `params.tools` from the `system` messages inside the
147
- * message list (`resolveTranscriptTools(getInitialSystemMessage(messages))`). Trimming
148
- * everything before a window boundary therefore deletes the one message that declares
149
- * every tool, and Pi does not re-declare it because the persisted transcript still looks
150
- * unchanged. The result is a window the model can only answer from memory.
151
- *
152
- * The merged state of the trimmed-away system messages is re-anchored as the leading
153
- * message of the window. When even that would lose declared tools, the trim is refused:
154
- * a window that still carries the previous turns beats a window with no tools at all.
155
- */
156
- export function preserveSystemHead<TMessage extends AgentMessage>(args: {
157
- readonly messages: readonly TMessage[];
158
- boundaryIndex: number;
159
- /**
160
- * Head observed in an earlier projection. A durable compaction can drop the original
161
- * declaration from the branch, leaving nothing to rebuild from, so the last known head
162
- * is re-anchored instead of sending a window with no tools.
163
- */
164
- fallbackHead?: AgentMessage;
165
- }): SystemHeadRepair<TMessage> {
166
- const { messages, boundaryIndex, fallbackHead } = args;
167
- const window = messages.slice(Math.max(boundaryIndex, 0));
168
- const prefix = messages.slice(0, Math.max(boundaryIndex, 0));
169
-
170
- const anchorFallback = (): SystemHeadRepair<TMessage> | undefined => {
171
- if (!fallbackHead || messages.some(isSystemTranscriptMessage)) return undefined;
172
- // A boundary compaction can retire the entry that declared the tools, leaving a
173
- // branch whose only system state is gone. Nothing in the list can be rebuilt, so
174
- // the head remembered from an earlier projection is re-anchored instead.
175
- const reused: readonly TMessage[] = [
176
- fallbackHead as unknown as TMessage,
177
- ...window.filter((message) => !isSystemTranscriptMessage(message)),
178
- ];
179
- return { messages: reused, repaired: true, shrinkRejected: false, lostToolNames: [], head: fallbackHead };
180
- };
181
-
182
- if (boundaryIndex <= 0 || prefix.length === 0) {
183
- return anchorFallback() ?? {
184
- messages: window,
185
- repaired: false,
186
- shrinkRejected: false,
187
- lostToolNames: [],
188
- head: getCurrentSystemMessage(messages) ?? fallbackHead,
189
- };
190
- }
191
-
192
- const declared = declaredToolNames(messages);
193
- const completeHead = getCurrentSystemMessage(messages);
194
- if (!completeHead) {
195
- const reused = anchorFallback();
196
- if (reused) return reused;
197
- if (!messages.some(isSystemTranscriptMessage) && declared.length === 0) {
198
- // Pre-0.86 transcripts carry no system messages: the trim stays exactly as before.
199
- return { messages: window, repaired: false, shrinkRejected: false, lostToolNames: [], head: undefined };
200
- }
201
- return { messages, repaired: false, shrinkRejected: true, lostToolNames: declared, head: undefined };
202
- }
203
-
204
- const windowFirst = window[0];
205
- if (windowFirst && isSystemTranscriptMessage(windowFirst)) {
206
- const kept = new Set(declaredToolNames(window));
207
- const lostFromWindow = declared.filter((name) => !kept.has(name));
208
- if (lostFromWindow.length === 0) {
209
- return {
210
- messages: window,
211
- repaired: false,
212
- shrinkRejected: false,
213
- lostToolNames: [],
214
- head: windowFirst as unknown as AgentMessage,
215
- };
216
- }
217
- return {
218
- messages,
219
- repaired: false,
220
- shrinkRejected: true,
221
- lostToolNames: lostFromWindow,
222
- head: windowFirst as unknown as AgentMessage,
223
- };
224
- }
225
-
226
- // Later system messages are folded into the rebuilt head, so a patch cannot be read
227
- // as a complete declaration by a transport that only anchors the first one.
228
- const rebuilt: readonly TMessage[] = [
229
- completeHead as unknown as TMessage,
230
- ...window.filter((message) => !isSystemTranscriptMessage(message)),
231
- ];
232
- const after = new Set(declaredToolNames(rebuilt));
233
- const lostToolNames = declared.filter((name) => !after.has(name));
234
- if (lostToolNames.length > 0) {
235
- return { messages, repaired: false, shrinkRejected: true, lostToolNames, head: completeHead };
236
- }
237
- return { messages: rebuilt, repaired: true, shrinkRejected: false, lostToolNames: [], head: completeHead };
238
- }
@@ -1,426 +0,0 @@
1
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
2
- import {
3
- createAssistantMessageEventStream,
4
- type AssistantMessage,
5
- type AssistantMessageEvent,
6
- type AssistantMessageEventStream,
7
- type Context,
8
- type ToolCall,
9
- } from "@earendil-works/pi-ai";
10
- import {
11
- HISTORY_ACTIONS,
12
- HISTORY_DESCRIPTION,
13
- NOTES_ACTIONS,
14
- NOTES_DESCRIPTION,
15
- } from "./tool-contract.ts";
16
-
17
- type JsonSchema = Record<string, unknown>;
18
- type ContextNamespace = "history" | "notes";
19
-
20
- const ACTIONS = {
21
- history: new Set<string>(HISTORY_ACTIONS),
22
- notes: new Set<string>(NOTES_ACTIONS),
23
- } satisfies Record<ContextNamespace, ReadonlySet<string>>;
24
-
25
- function nullable(type: string, description?: string): JsonSchema {
26
- return {
27
- ...(description ? { description } : {}),
28
- anyOf: [{ type }, { type: "null" }],
29
- };
30
- }
31
-
32
- function nullableRole(): JsonSchema {
33
- return {
34
- anyOf: [
35
- {
36
- type: "string",
37
- enum: ["user", "assistant", "tool", "system", "developer"],
38
- },
39
- { type: "null" },
40
- ],
41
- };
42
- }
43
-
44
- function string(description?: string, encrypted = false): JsonSchema {
45
- return {
46
- type: "string",
47
- ...(description ? { description } : {}),
48
- ...(encrypted ? { encrypted: true } : {}),
49
- };
50
- }
51
-
52
- function integer(minimum?: number): JsonSchema {
53
- return {
54
- type: "integer",
55
- ...(minimum === undefined ? {} : { minimum }),
56
- };
57
- }
58
-
59
- function object(
60
- properties: Record<string, JsonSchema>,
61
- required?: string[],
62
- ) {
63
- return {
64
- type: "object",
65
- properties,
66
- ...(required ? { required } : {}),
67
- additionalProperties: false,
68
- };
69
- }
70
-
71
- function operation(
72
- name: string,
73
- description: string,
74
- parameters: ReturnType<typeof object>,
75
- ) {
76
- return { type: "function", name, description, strict: false, parameters };
77
- }
78
-
79
- function historyNamespace(encrypted: boolean) {
80
- return {
81
- type: "namespace",
82
- name: "history",
83
- description: HISTORY_DESCRIPTION,
84
- tools: [
85
- operation(
86
- "list_windows",
87
- "List context windows",
88
- object({
89
- agent_name: nullable("string"),
90
- limit: integer(1),
91
- recent_first: { type: "boolean" },
92
- }),
93
- ),
94
- operation(
95
- "list_items",
96
- "List history items",
97
- object({
98
- agent_name: nullable("string"),
99
- limit: integer(1),
100
- max_chars_per_item: integer(1),
101
- recent_first: { type: "boolean" },
102
- role: nullableRole(),
103
- tool_name: nullable("string"),
104
- tool_namespace: nullable("string"),
105
- window_id: nullable("string"),
106
- }),
107
- ),
108
- operation(
109
- "read_item",
110
- "Read history item range",
111
- object(
112
- {
113
- agent_name: nullable("string"),
114
- item_id: {
115
- type: "string",
116
- description: "Suffix from the item's [id: …] marker.",
117
- },
118
- limit_chars: integer(1),
119
- offset_chars: integer(0),
120
- window_id: { type: "string" },
121
- },
122
- ["item_id", "window_id"],
123
- ),
124
- ),
125
- operation(
126
- "search_contents",
127
- "Search history",
128
- object(
129
- {
130
- agent_name: nullable("string"),
131
- limit: integer(1),
132
- query: string("Case-sensitive", encrypted),
133
- recent_first: { type: "boolean" },
134
- role: nullableRole(),
135
- tool_name: nullable("string"),
136
- tool_namespace: nullable("string"),
137
- window_id: nullable("string"),
138
- },
139
- ["query"],
140
- ),
141
- ),
142
- ],
143
- };
144
- }
145
-
146
- function notesNamespace(encrypted: boolean) {
147
- return {
148
- type: "namespace",
149
- name: "notes",
150
- description: NOTES_DESCRIPTION,
151
- tools: [
152
- operation(
153
- "list_files_by_prefix",
154
- "List note files",
155
- object({
156
- file_order: {
157
- type: "string",
158
- enum: ["ascending", "descending"],
159
- },
160
- file_order_by: {
161
- type: "string",
162
- enum: ["name", "created_at", "updated_at"],
163
- },
164
- max_results: integer(1),
165
- prefix: nullable("string"),
166
- }),
167
- ),
168
- operation(
169
- "read_file",
170
- "Read note file; line bounds inclusive, 1-based, negative from end",
171
- object(
172
- {
173
- path: { type: "string" },
174
- start_line: nullable("integer"),
175
- stop_line: nullable("integer"),
176
- },
177
- ["path"],
178
- ),
179
- ),
180
- operation(
181
- "search_contents",
182
- "Search note lines by literal substring",
183
- object(
184
- {
185
- max_files: integer(1),
186
- max_matches_per_file: integer(1),
187
- path_prefix: nullable("string"),
188
- query: string("Case-sensitive", encrypted),
189
- recent_file_first: { type: "boolean" },
190
- },
191
- ["query"],
192
- ),
193
- ),
194
- operation(
195
- "append_to_file",
196
- "Append text exactly",
197
- object(
198
- {
199
- path: { type: "string" },
200
- text: string(undefined, encrypted),
201
- },
202
- ["text", "path"],
203
- ),
204
- ),
205
- operation(
206
- "write_file",
207
- "Create or replace a note file",
208
- object(
209
- {
210
- path: { type: "string" },
211
- text: string(undefined, encrypted),
212
- },
213
- ["text", "path"],
214
- ),
215
- ),
216
- ],
217
- };
218
- }
219
-
220
- function contextNamespace(
221
- name: ContextNamespace,
222
- encrypted: boolean,
223
- ): Record<string, unknown> {
224
- const namespace = name === "history"
225
- ? historyNamespace(encrypted)
226
- : notesNamespace(encrypted);
227
- // Codex validates reserved schemas against its serialized contract: open
228
- // objects, with numeric bounds omitted by its JsonSchema serializer.
229
- if (!encrypted) return namespace;
230
- return {
231
- ...namespace,
232
- tools: namespace.tools.map((tool) => {
233
- const { additionalProperties: _additionalProperties, ...parameters } = tool.parameters;
234
- for (const property of Object.values(parameters.properties)) {
235
- delete property["minimum"];
236
- }
237
- return { ...tool, parameters };
238
- }),
239
- };
240
- }
241
-
242
- function isRecord(value: unknown): value is Record<string, unknown> {
243
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
244
- }
245
-
246
- function namespaceName(value: unknown): ContextNamespace | undefined {
247
- if (!isRecord(value)) return undefined;
248
- const name = value["name"];
249
- return (name === "history" || name === "notes") &&
250
- (value["type"] === "function" || value["type"] === "namespace")
251
- ? name
252
- : undefined;
253
- }
254
-
255
- function rewriteTools(
256
- tools: readonly unknown[],
257
- encrypted: boolean,
258
- ): { tools: unknown[]; changed: boolean } {
259
- let changed = false;
260
- const rewritten = tools.map((tool) => {
261
- const name = namespaceName(tool);
262
- if (!name) return tool;
263
- changed = true;
264
- return contextNamespace(name, encrypted);
265
- });
266
- return { tools: rewritten, changed };
267
- }
268
-
269
- export function rewriteContextNamespaceTools(
270
- payload: unknown,
271
- options: { encrypted?: boolean } = {},
272
- ): unknown {
273
- if (!isRecord(payload)) return payload;
274
- const encrypted = options.encrypted === true;
275
- let changed = false;
276
- let tools = payload["tools"];
277
- if (Array.isArray(tools)) {
278
- const result = rewriteTools(tools, encrypted);
279
- tools = result.tools;
280
- changed ||= result.changed;
281
- }
282
- let input = payload["input"];
283
- if (Array.isArray(input)) {
284
- input = input.map((item) => {
285
- if (!isRecord(item) || !Array.isArray(item["tools"])) return item;
286
- const result = rewriteTools(item["tools"], encrypted);
287
- if (!result.changed) return item;
288
- changed = true;
289
- return { ...item, tools: result.tools };
290
- });
291
- }
292
- return changed ? { ...payload, tools, input } : payload;
293
- }
294
-
295
- export function rewriteContextNamespaceCalls(payload: unknown): unknown {
296
- if (!isRecord(payload) || !Array.isArray(payload.input)) return payload;
297
- let changed = false;
298
- const input = payload.input.map((item) => {
299
- if (!isRecord(item) || item.type !== "function_call") return item;
300
- if ((item.namespace !== "history" && item.namespace !== "notes") || item.name !== item.namespace) return item;
301
- if (typeof item.arguments !== "string") return item;
302
- let arguments_: unknown;
303
- try {
304
- arguments_ = JSON.parse(item.arguments) as unknown;
305
- } catch {
306
- return item;
307
- }
308
- if (!isRecord(arguments_) || typeof arguments_.action !== "string" || !ACTIONS[item.namespace].has(arguments_.action)) return item;
309
- const { action: _action, ...rest } = arguments_;
310
- changed = true;
311
- return { ...item, name: arguments_.action, arguments: JSON.stringify(rest) };
312
- });
313
- return changed ? { ...payload, input } : payload;
314
- }
315
-
316
- export function hasContextNamespaceRouters(
317
- context: Pick<Context, "tools">,
318
- ): boolean {
319
- const names = new Set(context.tools?.map((tool) => tool.name));
320
- return names.has("history") && names.has("notes");
321
- }
322
-
323
- function routedAction(call: ToolCall): string | undefined {
324
- if (call.namespace !== "history" && call.namespace !== "notes")
325
- return undefined;
326
- return ACTIONS[call.namespace].has(call.name) ? call.name : undefined;
327
- }
328
-
329
- function routeContextNamespaceToolCall(call: ToolCall): ToolCall {
330
- const action = routedAction(call);
331
- if (!action || !call.namespace) return call;
332
- return {
333
- ...call,
334
- name: call.namespace,
335
- arguments: { action, ...call.arguments },
336
- };
337
- }
338
-
339
- export function unrouteContextNamespaceToolCall(call: ToolCall): ToolCall {
340
- if (
341
- (call.namespace !== "history" && call.namespace !== "notes") ||
342
- call.name !== call.namespace
343
- )
344
- return call;
345
- const action = call.arguments["action"];
346
- if (typeof action !== "string" || !ACTIONS[call.namespace].has(action))
347
- return call;
348
- const args = { ...call.arguments };
349
- delete args["action"];
350
- return { ...call, name: action, arguments: args };
351
- }
352
-
353
- function routeMessage(message: AssistantMessage): AssistantMessage {
354
- return {
355
- ...message,
356
- content: message.content.map((block) =>
357
- block.type === "toolCall"
358
- ? routeContextNamespaceToolCall(block)
359
- : block,
360
- ),
361
- };
362
- }
363
-
364
- /**
365
- * Pi's native provider parser preserves Responses namespace calls, but the
366
- * agent loop resolves tools by ToolCall.name. Route the parsed assistant
367
- * message at the public message_end boundary before tool execution begins.
368
- */
369
- export function routeContextNamespaceToolMessage(message: AgentMessage): AgentMessage {
370
- if (message.role !== "assistant") return message;
371
- let changed = false;
372
- const content = message.content.map((block) => {
373
- if (block.type !== "toolCall") return block;
374
- const routed = routeContextNamespaceToolCall(block);
375
- changed ||= routed !== block;
376
- return routed;
377
- });
378
- return changed ? { ...message, content } : message;
379
- }
380
-
381
- function routeEvent(event: AssistantMessageEvent): AssistantMessageEvent {
382
- if (event.type === "done")
383
- return { ...event, message: routeMessage(event.message) };
384
- if (event.type === "error")
385
- return { ...event, error: routeMessage(event.error) };
386
- const partial = routeMessage(event.partial);
387
- return event.type === "toolcall_end"
388
- ? {
389
- ...event,
390
- toolCall: routeContextNamespaceToolCall(event.toolCall),
391
- partial,
392
- }
393
- : { ...event, partial };
394
- }
395
-
396
- export function routeContextNamespaceToolStream(
397
- source: AssistantMessageEventStream,
398
- ): AssistantMessageEventStream {
399
- const output = createAssistantMessageEventStream();
400
- void (async () => {
401
- let latest: AssistantMessage | undefined;
402
- try {
403
- for await (const event of source) {
404
- const routed = routeEvent(event);
405
- latest = routed.type === "done"
406
- ? routed.message
407
- : routed.type === "error"
408
- ? routed.error
409
- : routed.partial;
410
- output.push(routed);
411
- if (routed.type === "done") output.end(routed.message);
412
- if (routed.type === "error") output.end(routed.error);
413
- }
414
- } catch (error) {
415
- if (!latest) throw error;
416
- const failed: AssistantMessage = {
417
- ...latest,
418
- stopReason: "error",
419
- errorMessage: error instanceof Error ? error.message : String(error),
420
- };
421
- output.push({ type: "error", reason: "error", error: failed });
422
- output.end(failed);
423
- }
424
- })();
425
- return output;
426
- }