pi-openai-toolkit 0.14.4 → 0.14.6

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 (43) hide show
  1. package/README.md +119 -147
  2. package/README.zh.md +119 -151
  3. package/docs/internals.md +42 -21
  4. package/package.json +127 -127
  5. package/src/auto-mode/classifier.ts +165 -165
  6. package/src/auto-mode/policy.ts +146 -146
  7. package/src/auto-mode/prompt.ts +279 -279
  8. package/src/auto-mode/reviewer.ts +325 -325
  9. package/src/auto-mode/transcript.ts +133 -133
  10. package/src/auto-mode/types.ts +137 -137
  11. package/src/codex-astra/effort-planner.ts +175 -175
  12. package/src/codex-astra/extension.ts +122 -122
  13. package/src/config.ts +621 -592
  14. package/src/context-management/codex-provider.ts +260 -260
  15. package/src/context-management/history-notes.ts +357 -325
  16. package/src/context-management/messages.ts +112 -106
  17. package/src/context-management/namespace-tools.ts +426 -426
  18. package/src/context-management/tool-contract.ts +23 -23
  19. package/src/context-management/tools.ts +306 -311
  20. package/src/context-management/types.ts +168 -161
  21. package/src/context-management/window-budget.ts +83 -83
  22. package/src/context-management/window-manager.ts +60 -51
  23. package/src/context-management/window-request.ts +125 -125
  24. package/src/debug.ts +1 -1
  25. package/src/extension-runtime.ts +34 -9
  26. package/src/image-generation/artifacts.ts +237 -237
  27. package/src/image-generation/client.ts +248 -248
  28. package/src/image-generation/eligibility.ts +18 -18
  29. package/src/image-generation/extension.ts +19 -2
  30. package/src/image-generation/protocol.ts +473 -405
  31. package/src/image-generation/references.ts +194 -194
  32. package/src/image-generation/render.ts +252 -252
  33. package/src/image-generation/service.ts +249 -243
  34. package/src/image-generation/types.ts +189 -184
  35. package/src/model-scope.ts +17 -17
  36. package/src/provider-headers.ts +23 -23
  37. package/src/responses-headers.ts +173 -108
  38. package/src/runtime.ts +513 -477
  39. package/src/types.ts +11 -0
  40. package/src/web-search/extension.ts +80 -80
  41. package/src/web-search/payload.ts +96 -96
  42. package/src/web-search/prompt.ts +20 -20
  43. package/src/web-search/types.ts +45 -45
@@ -1,325 +1,357 @@
1
- import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import {
3
- codexContextProviderHeaders,
4
- resolveCodexContextProvider,
5
- } from "./codex-provider";
6
- import {
7
- isRecord,
8
- type HistoryAction,
9
- type HistoryNotesResponse,
10
- type HistoryNotesResult,
11
- type NotesAction,
12
- } from "./types";
13
-
14
- export const BACKEND_TIMEOUT_MS = 35_000;
15
- export const THREAD_HINT_MAX_BYTES = 4_000;
16
- export const TOOL_OUTPUT_TOKEN_LIMIT = 10_000;
17
- export const CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER = "\u0000pi-openai-toolkit:encrypted-output:";
18
-
19
- export function encodeEncryptedOutputForContext(value: string): string {
20
- return `${CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER}${JSON.stringify(value)}`;
21
- }
22
-
23
- export const HISTORY_ENDPOINTS = {
24
- list_windows: "alpha/history/v2/list_windows",
25
- list_items: "alpha/history/v2/list_items",
26
- read_item: "alpha/history/v2/read_item",
27
- search_contents: "alpha/history/v2/search_contents",
28
- } as const satisfies Record<HistoryAction, string>;
29
-
30
- export const THREAD_HINT_ENDPOINT = "alpha/notes/v2/thread_hint";
31
-
32
- export const NOTES_ENDPOINTS = {
33
- list_files_by_prefix: "alpha/notes/v2/list_files_by_prefix",
34
- read_file: "alpha/notes/v2/read_file",
35
- search_contents: "alpha/notes/v2/search_contents",
36
- append_to_file: "alpha/notes/v2/append_to_file",
37
- write_file: "alpha/notes/v2/write_file",
38
- } as const satisfies Record<NotesAction, string>;
39
-
40
- export const ENCRYPTED_ARGUMENT_ENDPOINTS = new Set<string>([
41
- HISTORY_ENDPOINTS.search_contents,
42
- NOTES_ENDPOINTS.search_contents,
43
- NOTES_ENDPOINTS.append_to_file,
44
- NOTES_ENDPOINTS.write_file,
45
- ]);
46
-
47
- export const HISTORY_ACTION_FIELDS: Record<HistoryAction, readonly string[]> = {
48
- list_windows: ["agent_name", "limit", "recent_first"],
49
- list_items: [
50
- "agent_name", "limit", "max_chars_per_item", "recent_first", "role",
51
- "tool_name", "tool_namespace", "window_id",
52
- ],
53
- read_item: ["agent_name", "item_id", "limit_chars", "offset_chars", "window_id"],
54
- search_contents: [
55
- "agent_name", "limit", "query", "recent_first", "role", "tool_name",
56
- "tool_namespace", "window_id",
57
- ],
58
- };
59
-
60
- export const NOTES_ACTION_FIELDS: Record<NotesAction, readonly string[]> = {
61
- list_files_by_prefix: ["file_order", "file_order_by", "max_results", "prefix"],
62
- read_file: ["path", "start_line", "stop_line"],
63
- search_contents: ["max_files", "max_matches_per_file", "path_prefix", "query", "recent_file_first"],
64
- append_to_file: ["path", "text"],
65
- write_file: ["path", "text"],
66
- };
67
-
68
- export interface CodexHistoryNotesDetails {
69
- codexHistoryNotes: Record<string, unknown>;
70
- }
71
-
72
- export async function callHistoryNotesBackend(
73
- endpoint: string,
74
- arguments_: Record<string, unknown>,
75
- ctx: ExtensionContext,
76
- signal?: AbortSignal,
77
- truncationPolicy: { mode: "bytes" | "tokens"; limit: number } = {
78
- mode: "tokens",
79
- limit: TOOL_OUTPUT_TOKEN_LIMIT,
80
- },
81
- gatewayModels: readonly string[] = [],
82
- ): Promise<HistoryNotesResult> {
83
- const timeoutSignal = AbortSignal.timeout(BACKEND_TIMEOUT_MS);
84
- if (signal?.aborted) return { ok: false, reason: "aborted" };
85
- const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
86
- const providerResult = await resolveProviderWithDeadline(ctx, requestSignal, timeoutSignal, gatewayModels);
87
- if (providerResult === "aborted") return { ok: false, reason: "aborted" };
88
- if (providerResult === "timeout") return { ok: false, reason: "backend-timeout" };
89
- const provider = providerResult;
90
- if (!provider.ok) {
91
- const reason = provider.reason === "unsupported-model" || provider.reason === "unsupported-api" || provider.reason === "context-management-inactive"
92
- ? "unsupported-backend"
93
- : provider.reason;
94
- return { ok: false, reason };
95
- }
96
- const sessionId = ctx.sessionManager.getSessionId();
97
- const headers = codexContextProviderHeaders(provider.provider, {
98
- sessionId,
99
- clientRequestId: sessionId,
100
- });
101
- headers.set("x-openai-tool-output-truncation-policy", JSON.stringify(truncationPolicy));
102
- if (ENCRYPTED_ARGUMENT_ENDPOINTS.has(endpoint)) {
103
- headers.set("x-openai-encrypted-tool-arguments", "true");
104
- }
105
- try {
106
- const response = await fetch(`${provider.provider.baseUrl}/${endpoint}`, {
107
- method: "POST",
108
- headers,
109
- signal: requestSignal,
110
- body: JSON.stringify({
111
- ...arguments_,
112
- context: {
113
- session_id: ctx.sessionManager.getSessionId(),
114
- current_agent_name: "/root",
115
- },
116
- }),
117
- });
118
- if (!response.ok) return { ok: false, reason: "http-error", status: response.status };
119
- let parsed: unknown;
120
- try {
121
- parsed = JSON.parse(await response.text()) as unknown;
122
- } catch {
123
- return { ok: false, reason: "invalid-json", status: response.status };
124
- }
125
- if (!isHistoryNotesResponse(parsed, endpoint)) return { ok: false, reason: "invalid-response", status: response.status };
126
- return { ok: true, value: parsed };
127
- } catch {
128
- if (signal?.aborted) return { ok: false, reason: "aborted" };
129
- if (timeoutSignal.aborted) return { ok: false, reason: "backend-timeout" };
130
- return { ok: false, reason: "protocol-error" };
131
- }
132
- }
133
-
134
- async function resolveProviderWithDeadline(
135
- ctx: ExtensionContext,
136
- requestSignal: AbortSignal,
137
- timeoutSignal: AbortSignal,
138
- gatewayModels: readonly string[] = [],
139
- ): Promise<Awaited<ReturnType<typeof resolveCodexContextProvider>> | "aborted" | "timeout"> {
140
- if (requestSignal.aborted) return timeoutSignal.aborted ? "timeout" : "aborted";
141
- return new Promise((resolve) => {
142
- let settled = false;
143
- const finish = (value: Awaited<ReturnType<typeof resolveCodexContextProvider>> | "aborted" | "timeout") => {
144
- if (settled) return;
145
- settled = true;
146
- requestSignal.removeEventListener("abort", onAbort);
147
- resolve(value);
148
- };
149
- const onAbort = () => finish(timeoutSignal.aborted ? "timeout" : "aborted");
150
- requestSignal.addEventListener("abort", onAbort, { once: true });
151
- resolveCodexContextProvider(ctx, ctx.model, gatewayModels).then(
152
- (value) => finish(value),
153
- () => finish({ ok: false, reason: "auth-resolution-failed" }),
154
- );
155
- });
156
- }
157
-
158
- export async function loadHistoryNotesThreadHint(
159
- ctx: ExtensionContext,
160
- signal?: AbortSignal,
161
- gatewayModels: readonly string[] = [],
162
- ): Promise<string | undefined> {
163
- const result = await callHistoryNotesBackend(
164
- THREAD_HINT_ENDPOINT,
165
- {},
166
- ctx,
167
- signal,
168
- { mode: "bytes", limit: THREAD_HINT_MAX_BYTES },
169
- gatewayModels,
170
- );
171
- if (!result.ok) {
172
- if (result.reason === "aborted") throw new Error("Remote context hint request was aborted");
173
- return undefined;
174
- }
175
- const text = result.value.text;
176
- return typeof text === "string" && Buffer.byteLength(text, "utf8") <= THREAD_HINT_MAX_BYTES ? text : undefined;
177
- }
178
-
179
- export async function executeHistoryNotesTool(
180
- namespace: "history" | "notes",
181
- action: HistoryAction | NotesAction,
182
- params: Record<string, unknown>,
183
- ctx: ExtensionContext,
184
- signal?: AbortSignal,
185
- gatewayModels: readonly string[] = [],
186
- ): Promise<AgentToolResult<CodexHistoryNotesDetails>> {
187
- const endpoint = namespace === "history"
188
- ? HISTORY_ENDPOINTS[action as HistoryAction]
189
- : NOTES_ENDPOINTS[action as NotesAction];
190
- if (!endpoint) throw new Error(`Unsupported ${namespace} action`);
191
- validateAction(namespace, action, params);
192
- const result = await callHistoryNotesBackend(
193
- endpoint,
194
- stripAction(params),
195
- ctx,
196
- signal,
197
- undefined,
198
- gatewayModels,
199
- );
200
- if (!result.ok) throw new Error(formatHistoryNotesFailure(result.reason, result.status));
201
- const modelResult = { ...result.value };
202
- delete modelResult.images;
203
- const content: AgentToolResult<CodexHistoryNotesDetails>["content"] = [
204
- {
205
- type: "text",
206
- text: typeof modelResult.encrypted_output === "string"
207
- ? `${namespace} operation completed`
208
- : JSON.stringify(modelResult),
209
- },
210
- ];
211
- for (const image of parseBackendImages(result.value.images)) content.push(image);
212
- return { content, details: { codexHistoryNotes: modelResult } };
213
- }
214
-
215
- export function formatHistoryNotesFailure(
216
- reason: Exclude<HistoryNotesResult, { ok: true }>["reason"],
217
- status?: number,
218
- ): string {
219
- switch (reason) {
220
- case "aborted": return "Remote history/notes request was aborted";
221
- case "backend-timeout": return "Remote history/notes backend timed out";
222
- case "http-error": return `Remote history/notes backend returned HTTP ${status ?? "error"}`;
223
- case "invalid-json": return "Remote history/notes backend returned invalid JSON";
224
- case "invalid-response": return "Remote history/notes backend returned an invalid response";
225
- case "auth-resolution-failed": return "Remote history/notes authentication could not be resolved";
226
- case "missing-token": return "Remote history/notes authentication is missing a token";
227
- case "missing-api-key": return "Remote history/notes gateway authentication is missing an API key";
228
- case "missing-account-id": return "Remote history/notes authentication is missing an account id";
229
- case "missing-base-url": return "Remote history/notes backend is missing a base URL";
230
- case "unsupported-backend": return "Remote history/notes require the native Codex backend";
231
- case "protocol-error": return "Remote history/notes request failed";
232
- case "invalid-account-token": return "Remote history/notes authentication token is invalid";
233
- default: return "Remote history/notes request failed";
234
- }
235
- }
236
-
237
- const RESPONSE_KEYS_BY_ENDPOINT: Record<string, readonly string[]> = {
238
- [HISTORY_ENDPOINTS.list_windows]: ["windows", "items", "results", "data", "output", "encrypted_output"],
239
- [HISTORY_ENDPOINTS.list_items]: ["items", "results", "data", "output", "encrypted_output"],
240
- [HISTORY_ENDPOINTS.read_item]: ["item", "content", "text", "data", "output", "encrypted_output"],
241
- [HISTORY_ENDPOINTS.search_contents]: ["matches", "items", "results", "data", "output", "encrypted_output"],
242
- [NOTES_ENDPOINTS.list_files_by_prefix]: ["files", "items", "results", "data", "output", "encrypted_output"],
243
- [NOTES_ENDPOINTS.read_file]: ["file", "content", "text", "data", "output", "encrypted_output"],
244
- [NOTES_ENDPOINTS.search_contents]: ["matches", "items", "results", "data", "output", "encrypted_output"],
245
- [NOTES_ENDPOINTS.append_to_file]: ["ok", "success", "file", "content", "data", "output", "encrypted_output"],
246
- [NOTES_ENDPOINTS.write_file]: ["ok", "success", "file", "content", "data", "output", "encrypted_output"],
247
- [THREAD_HINT_ENDPOINT]: ["text", "data", "output"],
248
- };
249
-
250
- const ARRAY_RESPONSE_KEYS = new Set(["windows", "items", "results", "matches", "files"]);
251
-
252
- function isHistoryNotesResponse(value: unknown, endpoint: string): value is HistoryNotesResponse {
253
- if (!isRecord(value) || Object.keys(value).length === 0) return false;
254
- if (value.encrypted_output !== undefined &&
255
- (typeof value.encrypted_output !== "string" || value.encrypted_output.length === 0)) return false;
256
- if (value.images !== undefined && !isValidImages(value.images)) return false;
257
- if (value.ok !== undefined && typeof value.ok !== "boolean") return false;
258
- if (value.success !== undefined && typeof value.success !== "boolean") return false;
259
- if (value.text !== undefined && typeof value.text !== "string") return false;
260
- for (const key of ARRAY_RESPONSE_KEYS) {
261
- if (value[key] !== undefined && !Array.isArray(value[key])) return false;
262
- }
263
- const expectedKeys = RESPONSE_KEYS_BY_ENDPOINT[endpoint];
264
- return expectedKeys ? expectedKeys.some((key) => key in value) : false;
265
- }
266
-
267
- type BackendImage = {
268
- data: string;
269
- mime_type: string;
270
- detail?: "auto" | "high" | "original" | null;
271
- };
272
-
273
- function isValidImages(value: unknown): value is BackendImage[] {
274
- return Array.isArray(value) && value.every((item): item is BackendImage => {
275
- if (!isRecord(item) || typeof item.data !== "string" || typeof item.mime_type !== "string") return false;
276
- return item.detail === undefined || item.detail === null || item.detail === "auto" || item.detail === "high" || item.detail === "original";
277
- });
278
- }
279
-
280
- function parseBackendImages(value: unknown): Array<{
281
- type: "image";
282
- data: string;
283
- mimeType: string;
284
- detail?: "auto" | "high" | "original";
285
- }> {
286
- if (value === undefined) return [];
287
- if (!isValidImages(value)) throw new Error("Remote history backend returned invalid image content");
288
- return value.map((item) => ({
289
- type: "image" as const,
290
- data: item.data,
291
- mimeType: item.mime_type,
292
- ...(item.detail ? { detail: item.detail } : {}),
293
- }));
294
- }
295
-
296
- function stripAction(params: Record<string, unknown>): Record<string, unknown> {
297
- const result = { ...params };
298
- delete result.action;
299
- return result;
300
- }
301
-
302
- function validateAction(
303
- namespace: "history" | "notes",
304
- action: HistoryAction | NotesAction,
305
- params: Record<string, unknown>,
306
- ): void {
307
- const fields = namespace === "history"
308
- ? HISTORY_ACTION_FIELDS[action as HistoryAction]
309
- : NOTES_ACTION_FIELDS[action as NotesAction];
310
- if (!fields) throw new Error(`Unsupported ${namespace} action`);
311
- const unexpected = Object.keys(params).find((field) => field !== "action" && !fields.includes(field));
312
- if (unexpected) throw new Error(`${namespace} ${action} does not accept ${unexpected}`);
313
- if (action === "read_item" && (typeof params.item_id !== "string" || !params.item_id || typeof params.window_id !== "string" || !params.window_id)) {
314
- throw new Error("history read_item requires item_id and window_id");
315
- }
316
- if (action === "search_contents" && (typeof params.query !== "string" || !params.query)) {
317
- throw new Error(`${namespace} search_contents requires query`);
318
- }
319
- if ((action === "read_file" || action === "append_to_file" || action === "write_file") && (typeof params.path !== "string" || !params.path)) {
320
- throw new Error(`notes ${action} requires path`);
321
- }
322
- if ((action === "append_to_file" || action === "write_file") && typeof params.text !== "string") {
323
- throw new Error(`notes ${action} requires text`);
324
- }
325
- }
1
+ import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ codexContextProviderHeaders,
4
+ resolveCodexContextProvider,
5
+ } from "./codex-provider";
6
+ import {
7
+ isRecord,
8
+ type HistoryAction,
9
+ type HistoryNotesResponse,
10
+ type HistoryNotesResult,
11
+ type NotesAction,
12
+ } from "./types";
13
+
14
+ export const BACKEND_TIMEOUT_MS = 35_000;
15
+ export const THREAD_HINT_MAX_BYTES = 4_000;
16
+ export const TOOL_OUTPUT_TOKEN_LIMIT = 10_000;
17
+ export const CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER = "\u0000pi-openai-toolkit:encrypted-output:";
18
+
19
+ export function encodeEncryptedOutputForContext(value: string): string {
20
+ return `${CODEX_ENCRYPTED_OUTPUT_CONTEXT_MARKER}${JSON.stringify(value)}`;
21
+ }
22
+
23
+ export const HISTORY_ENDPOINTS = {
24
+ list_windows: "alpha/history/v2/list_windows",
25
+ list_items: "alpha/history/v2/list_items",
26
+ read_item: "alpha/history/v2/read_item",
27
+ search_contents: "alpha/history/v2/search_contents",
28
+ } as const satisfies Record<HistoryAction, string>;
29
+
30
+ export const THREAD_HINT_ENDPOINT = "alpha/notes/v2/thread_hint";
31
+
32
+ export const NOTES_ENDPOINTS = {
33
+ list_files_by_prefix: "alpha/notes/v2/list_files_by_prefix",
34
+ read_file: "alpha/notes/v2/read_file",
35
+ search_contents: "alpha/notes/v2/search_contents",
36
+ append_to_file: "alpha/notes/v2/append_to_file",
37
+ write_file: "alpha/notes/v2/write_file",
38
+ } as const satisfies Record<NotesAction, string>;
39
+
40
+ const NOTES_WRITE_ENDPOINTS = new Set<string>([
41
+ NOTES_ENDPOINTS.append_to_file,
42
+ NOTES_ENDPOINTS.write_file,
43
+ ]);
44
+
45
+ export const ENCRYPTED_ARGUMENT_ENDPOINTS = new Set<string>([
46
+ HISTORY_ENDPOINTS.search_contents,
47
+ NOTES_ENDPOINTS.search_contents,
48
+ NOTES_ENDPOINTS.append_to_file,
49
+ NOTES_ENDPOINTS.write_file,
50
+ ]);
51
+
52
+ export const HISTORY_ACTION_FIELDS: Record<HistoryAction, readonly string[]> = {
53
+ list_windows: ["agent_name", "limit", "recent_first"],
54
+ list_items: [
55
+ "agent_name", "limit", "max_chars_per_item", "recent_first", "role",
56
+ "tool_name", "tool_namespace", "window_id",
57
+ ],
58
+ read_item: ["agent_name", "item_id", "limit_chars", "offset_chars", "window_id"],
59
+ search_contents: [
60
+ "agent_name", "limit", "query", "recent_first", "role", "tool_name",
61
+ "tool_namespace", "window_id",
62
+ ],
63
+ };
64
+
65
+ export const NOTES_ACTION_FIELDS: Record<NotesAction, readonly string[]> = {
66
+ list_files_by_prefix: ["file_order", "file_order_by", "max_results", "prefix"],
67
+ read_file: ["path", "start_line", "stop_line"],
68
+ search_contents: ["max_files", "max_matches_per_file", "path_prefix", "query", "recent_file_first"],
69
+ append_to_file: ["path", "text"],
70
+ write_file: ["path", "text"],
71
+ };
72
+
73
+ export interface CodexHistoryNotesDetails {
74
+ codexHistoryNotes: Record<string, unknown>;
75
+ }
76
+
77
+ /**
78
+ * Optional backend status flags are accepted only when explicitly affirmative;
79
+ * an omitted flag is compatible with older response shapes.
80
+ */
81
+ export function isSuccessfulHistoryNotesResponse(value: unknown): value is HistoryNotesResponse {
82
+ return isRecord(value) && isAffirmativeOptionalBoolean(value.ok) && isAffirmativeOptionalBoolean(value.success);
83
+ }
84
+
85
+ /** Validate persisted tool-result details before using them as a checkpoint. */
86
+ export function isSuccessfulHistoryNotesToolResult(details: unknown): details is CodexHistoryNotesDetails {
87
+ if (!isRecord(details) || !isRecord(details.codexHistoryNotes)) return false;
88
+ return isSuccessfulHistoryNotesResponse(details.codexHistoryNotes);
89
+ }
90
+
91
+ function isAffirmativeOptionalBoolean(value: unknown): boolean {
92
+ return value === undefined || value === true;
93
+ }
94
+
95
+ function isRejectedNotesWriteResponse(value: HistoryNotesResponse): boolean {
96
+ return value.ok === false || value.success === false;
97
+ }
98
+
99
+ export async function callHistoryNotesBackend(
100
+ endpoint: string,
101
+ arguments_: Record<string, unknown>,
102
+ ctx: ExtensionContext,
103
+ signal?: AbortSignal,
104
+ truncationPolicy: { mode: "bytes" | "tokens"; limit: number } = {
105
+ mode: "tokens",
106
+ limit: TOOL_OUTPUT_TOKEN_LIMIT,
107
+ },
108
+ gatewayModels: readonly string[] = [],
109
+ ): Promise<HistoryNotesResult> {
110
+ const timeoutSignal = AbortSignal.timeout(BACKEND_TIMEOUT_MS);
111
+ if (signal?.aborted) return { ok: false, reason: "aborted" };
112
+ const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
113
+ const providerResult = await resolveProviderWithDeadline(ctx, requestSignal, timeoutSignal, gatewayModels);
114
+ if (providerResult === "aborted") return { ok: false, reason: "aborted" };
115
+ if (providerResult === "timeout") return { ok: false, reason: "backend-timeout" };
116
+ const provider = providerResult;
117
+ if (!provider.ok) {
118
+ const reason = provider.reason === "unsupported-model" || provider.reason === "unsupported-api" || provider.reason === "context-management-inactive"
119
+ ? "unsupported-backend"
120
+ : provider.reason;
121
+ return { ok: false, reason };
122
+ }
123
+ const sessionId = ctx.sessionManager.getSessionId();
124
+ const headers = codexContextProviderHeaders(provider.provider, {
125
+ sessionId,
126
+ clientRequestId: sessionId,
127
+ });
128
+ headers.set("x-openai-tool-output-truncation-policy", JSON.stringify(truncationPolicy));
129
+ if (ENCRYPTED_ARGUMENT_ENDPOINTS.has(endpoint)) {
130
+ headers.set("x-openai-encrypted-tool-arguments", "true");
131
+ }
132
+ try {
133
+ const response = await fetch(`${provider.provider.baseUrl}/${endpoint}`, {
134
+ method: "POST",
135
+ headers,
136
+ signal: requestSignal,
137
+ body: JSON.stringify({
138
+ ...arguments_,
139
+ context: {
140
+ session_id: ctx.sessionManager.getSessionId(),
141
+ current_agent_name: "/root",
142
+ },
143
+ }),
144
+ });
145
+ if (!response.ok) return { ok: false, reason: "http-error", status: response.status };
146
+ let parsed: unknown;
147
+ try {
148
+ parsed = JSON.parse(await response.text()) as unknown;
149
+ } catch {
150
+ return { ok: false, reason: "invalid-json", status: response.status };
151
+ }
152
+ if (!isHistoryNotesResponse(parsed, endpoint)) return { ok: false, reason: "invalid-response", status: response.status };
153
+ return { ok: true, value: parsed };
154
+ } catch {
155
+ if (signal?.aborted) return { ok: false, reason: "aborted" };
156
+ if (timeoutSignal.aborted) return { ok: false, reason: "backend-timeout" };
157
+ return { ok: false, reason: "protocol-error" };
158
+ }
159
+ }
160
+
161
+ async function resolveProviderWithDeadline(
162
+ ctx: ExtensionContext,
163
+ requestSignal: AbortSignal,
164
+ timeoutSignal: AbortSignal,
165
+ gatewayModels: readonly string[] = [],
166
+ ): Promise<Awaited<ReturnType<typeof resolveCodexContextProvider>> | "aborted" | "timeout"> {
167
+ if (requestSignal.aborted) return timeoutSignal.aborted ? "timeout" : "aborted";
168
+ return new Promise((resolve) => {
169
+ let settled = false;
170
+ const finish = (value: Awaited<ReturnType<typeof resolveCodexContextProvider>> | "aborted" | "timeout") => {
171
+ if (settled) return;
172
+ settled = true;
173
+ requestSignal.removeEventListener("abort", onAbort);
174
+ resolve(value);
175
+ };
176
+ const onAbort = () => finish(timeoutSignal.aborted ? "timeout" : "aborted");
177
+ requestSignal.addEventListener("abort", onAbort, { once: true });
178
+ resolveCodexContextProvider(ctx, ctx.model, gatewayModels).then(
179
+ (value) => finish(value),
180
+ () => finish({ ok: false, reason: "auth-resolution-failed" }),
181
+ );
182
+ });
183
+ }
184
+
185
+ export async function loadHistoryNotesThreadHint(
186
+ ctx: ExtensionContext,
187
+ signal?: AbortSignal,
188
+ gatewayModels: readonly string[] = [],
189
+ ): Promise<string | undefined> {
190
+ const result = await callHistoryNotesBackend(
191
+ THREAD_HINT_ENDPOINT,
192
+ {},
193
+ ctx,
194
+ signal,
195
+ { mode: "bytes", limit: THREAD_HINT_MAX_BYTES },
196
+ gatewayModels,
197
+ );
198
+ if (!result.ok) {
199
+ if (result.reason === "aborted") throw new Error("Remote context hint request was aborted");
200
+ return undefined;
201
+ }
202
+ if (!isSuccessfulHistoryNotesResponse(result.value)) return undefined;
203
+ const text = result.value.text;
204
+ return typeof text === "string" && Buffer.byteLength(text, "utf8") <= THREAD_HINT_MAX_BYTES ? text : undefined;
205
+ }
206
+
207
+ export async function executeHistoryNotesTool(
208
+ namespace: "history" | "notes",
209
+ action: HistoryAction | NotesAction,
210
+ params: Record<string, unknown>,
211
+ ctx: ExtensionContext,
212
+ signal?: AbortSignal,
213
+ gatewayModels: readonly string[] = [],
214
+ ): Promise<AgentToolResult<CodexHistoryNotesDetails>> {
215
+ const endpoint = namespace === "history"
216
+ ? HISTORY_ENDPOINTS[action as HistoryAction]
217
+ : NOTES_ENDPOINTS[action as NotesAction];
218
+ if (!endpoint) throw new Error(`Unsupported ${namespace} action`);
219
+ validateAction(namespace, action, params);
220
+ const result = await callHistoryNotesBackend(
221
+ endpoint,
222
+ stripAction(params),
223
+ ctx,
224
+ signal,
225
+ undefined,
226
+ gatewayModels,
227
+ );
228
+ if (!result.ok) throw new Error(formatHistoryNotesFailure(result.reason, result.status));
229
+ if (NOTES_WRITE_ENDPOINTS.has(endpoint) && isRejectedNotesWriteResponse(result.value)) {
230
+ throw new Error("Remote notes write was rejected");
231
+ }
232
+ const modelResult = { ...result.value };
233
+ delete modelResult.images;
234
+ const content: AgentToolResult<CodexHistoryNotesDetails>["content"] = [
235
+ {
236
+ type: "text",
237
+ text: typeof modelResult.encrypted_output === "string"
238
+ ? `${namespace} operation completed`
239
+ : JSON.stringify(modelResult),
240
+ },
241
+ ];
242
+ for (const image of parseBackendImages(result.value.images)) content.push(image);
243
+ return { content, details: { codexHistoryNotes: modelResult } };
244
+ }
245
+
246
+ export function formatHistoryNotesFailure(
247
+ reason: Exclude<HistoryNotesResult, { ok: true }>["reason"],
248
+ status?: number,
249
+ ): string {
250
+ switch (reason) {
251
+ case "aborted": return "Remote history/notes request was aborted";
252
+ case "backend-timeout": return "Remote history/notes backend timed out";
253
+ case "http-error": return `Remote history/notes backend returned HTTP ${status ?? "error"}`;
254
+ case "invalid-json": return "Remote history/notes backend returned invalid JSON";
255
+ case "invalid-response": return "Remote history/notes backend returned an invalid response";
256
+ case "auth-resolution-failed": return "Remote history/notes authentication could not be resolved";
257
+ case "missing-token": return "Remote history/notes authentication is missing a token";
258
+ case "missing-api-key": return "Remote history/notes gateway authentication is missing an API key";
259
+ case "missing-account-id": return "Remote history/notes authentication is missing an account id";
260
+ case "missing-base-url": return "Remote history/notes backend is missing a base URL";
261
+ case "missing-session-id": return "Remote history/notes request is missing a session id";
262
+ case "unsupported-backend": return "Remote history/notes require the native Codex backend";
263
+ case "protocol-error": return "Remote history/notes request failed";
264
+ case "invalid-account-token": return "Remote history/notes authentication token is invalid";
265
+ default: return "Remote history/notes request failed";
266
+ }
267
+ }
268
+
269
+ const RESPONSE_KEYS_BY_ENDPOINT: Record<string, readonly string[]> = {
270
+ [HISTORY_ENDPOINTS.list_windows]: ["windows", "items", "results", "data", "output", "encrypted_output"],
271
+ [HISTORY_ENDPOINTS.list_items]: ["items", "results", "data", "output", "encrypted_output"],
272
+ [HISTORY_ENDPOINTS.read_item]: ["item", "content", "text", "data", "output", "encrypted_output"],
273
+ [HISTORY_ENDPOINTS.search_contents]: ["matches", "items", "results", "data", "output", "encrypted_output"],
274
+ [NOTES_ENDPOINTS.list_files_by_prefix]: ["files", "items", "results", "data", "output", "encrypted_output"],
275
+ [NOTES_ENDPOINTS.read_file]: ["file", "content", "text", "data", "output", "encrypted_output"],
276
+ [NOTES_ENDPOINTS.search_contents]: ["matches", "items", "results", "data", "output", "encrypted_output"],
277
+ [NOTES_ENDPOINTS.append_to_file]: ["ok", "success", "file", "content", "data", "output", "encrypted_output"],
278
+ [NOTES_ENDPOINTS.write_file]: ["ok", "success", "file", "content", "data", "output", "encrypted_output"],
279
+ [THREAD_HINT_ENDPOINT]: ["text", "data", "output"],
280
+ };
281
+
282
+ const ARRAY_RESPONSE_KEYS = new Set(["windows", "items", "results", "matches", "files"]);
283
+
284
+ function isHistoryNotesResponse(value: unknown, endpoint: string): value is HistoryNotesResponse {
285
+ if (!isRecord(value) || Object.keys(value).length === 0) return false;
286
+ if (value.encrypted_output !== undefined &&
287
+ (typeof value.encrypted_output !== "string" || value.encrypted_output.length === 0)) return false;
288
+ if (value.images !== undefined && !isValidImages(value.images)) return false;
289
+ if (value.ok !== undefined && typeof value.ok !== "boolean") return false;
290
+ if (value.success !== undefined && typeof value.success !== "boolean") return false;
291
+ if (value.text !== undefined && typeof value.text !== "string") return false;
292
+ for (const key of ARRAY_RESPONSE_KEYS) {
293
+ if (value[key] !== undefined && !Array.isArray(value[key])) return false;
294
+ }
295
+ const expectedKeys = RESPONSE_KEYS_BY_ENDPOINT[endpoint];
296
+ return expectedKeys ? expectedKeys.some((key) => key in value) : false;
297
+ }
298
+
299
+ type BackendImage = {
300
+ data: string;
301
+ mime_type: string;
302
+ detail?: "auto" | "high" | "original" | null;
303
+ };
304
+
305
+ function isValidImages(value: unknown): value is BackendImage[] {
306
+ return Array.isArray(value) && value.every((item): item is BackendImage => {
307
+ if (!isRecord(item) || typeof item.data !== "string" || typeof item.mime_type !== "string") return false;
308
+ return item.detail === undefined || item.detail === null || item.detail === "auto" || item.detail === "high" || item.detail === "original";
309
+ });
310
+ }
311
+
312
+ function parseBackendImages(value: unknown): Array<{
313
+ type: "image";
314
+ data: string;
315
+ mimeType: string;
316
+ detail?: "auto" | "high" | "original";
317
+ }> {
318
+ if (value === undefined) return [];
319
+ if (!isValidImages(value)) throw new Error("Remote history backend returned invalid image content");
320
+ return value.map((item) => ({
321
+ type: "image" as const,
322
+ data: item.data,
323
+ mimeType: item.mime_type,
324
+ ...(item.detail ? { detail: item.detail } : {}),
325
+ }));
326
+ }
327
+
328
+ function stripAction(params: Record<string, unknown>): Record<string, unknown> {
329
+ const result = { ...params };
330
+ delete result.action;
331
+ return result;
332
+ }
333
+
334
+ function validateAction(
335
+ namespace: "history" | "notes",
336
+ action: HistoryAction | NotesAction,
337
+ params: Record<string, unknown>,
338
+ ): void {
339
+ const fields = namespace === "history"
340
+ ? HISTORY_ACTION_FIELDS[action as HistoryAction]
341
+ : NOTES_ACTION_FIELDS[action as NotesAction];
342
+ if (!fields) throw new Error(`Unsupported ${namespace} action`);
343
+ const unexpected = Object.keys(params).find((field) => field !== "action" && !fields.includes(field));
344
+ if (unexpected) throw new Error(`${namespace} ${action} does not accept ${unexpected}`);
345
+ if (action === "read_item" && (typeof params.item_id !== "string" || !params.item_id || typeof params.window_id !== "string" || !params.window_id)) {
346
+ throw new Error("history read_item requires item_id and window_id");
347
+ }
348
+ if (action === "search_contents" && (typeof params.query !== "string" || !params.query)) {
349
+ throw new Error(`${namespace} search_contents requires query`);
350
+ }
351
+ if ((action === "read_file" || action === "append_to_file" || action === "write_file") && (typeof params.path !== "string" || !params.path)) {
352
+ throw new Error(`notes ${action} requires path`);
353
+ }
354
+ if ((action === "append_to_file" || action === "write_file") && typeof params.text !== "string") {
355
+ throw new Error(`notes ${action} requires text`);
356
+ }
357
+ }