pi-openai-toolkit 0.14.8 → 0.14.9
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/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/internals.md +18 -0
- package/extensions/auto-mode.ts +1 -1
- package/extensions/codex-astra.ts +1 -1
- package/extensions/compaction.ts +1 -1
- package/extensions/image-generation.ts +1 -1
- package/extensions/web-search.ts +1 -1
- package/package.json +2 -1
- package/src/auto-mode/classifier.ts +165 -165
- package/src/auto-mode/extension.ts +129 -20
- package/src/auto-mode/policy.ts +146 -146
- package/src/auto-mode/prompt.ts +279 -279
- package/src/auto-mode/reviewer.ts +325 -325
- package/src/auto-mode/tool-review-tui.ts +211 -0
- package/src/auto-mode/transcript.ts +133 -133
- package/src/auto-mode/types.ts +137 -137
- package/src/codex-astra/effort-planner.ts +175 -175
- package/src/codex-astra/extension.ts +122 -122
- package/src/compaction-projection.ts +31 -31
- package/src/context-management/messages.ts +112 -112
- package/src/context-management/namespace-tools.ts +426 -426
- package/src/context-management/tool-contract.ts +23 -23
- package/src/context-management/tools.ts +291 -291
- package/src/context-management/window-budget.ts +83 -83
- package/src/context-management/window-request.ts +125 -125
- package/src/image-generation/artifacts.ts +237 -237
- package/src/image-generation/client.ts +248 -248
- package/src/image-generation/eligibility.ts +18 -18
- package/src/image-generation/references.ts +194 -194
- package/src/image-generation/render.ts +252 -252
- package/src/model-scope.ts +17 -17
- package/src/pi-context-hook.ts +116 -116
- package/src/provider-headers.ts +23 -23
- package/src/web-search/alpha-client.ts +875 -875
- package/src/web-search/extension.ts +523 -523
- package/src/web-search/payload.ts +295 -295
- package/src/web-search/prompt.ts +59 -59
- package/src/web-search/types.ts +144 -144
|
@@ -1,426 +1,426 @@
|
|
|
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
|
-
}
|
|
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
|
+
}
|