pi-openai-toolkit 0.14.5 → 0.14.7

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 (35) hide show
  1. package/docs/internals.md +42 -21
  2. package/package.json +1 -1
  3. package/src/auto-mode/classifier.ts +165 -165
  4. package/src/auto-mode/policy.ts +146 -146
  5. package/src/auto-mode/prompt.ts +279 -279
  6. package/src/auto-mode/reviewer.ts +325 -325
  7. package/src/auto-mode/transcript.ts +133 -133
  8. package/src/auto-mode/types.ts +137 -137
  9. package/src/codex-astra/effort-planner.ts +175 -175
  10. package/src/codex-astra/extension.ts +122 -122
  11. package/src/context-management/codex-provider.ts +20 -20
  12. package/src/context-management/history-notes.ts +32 -0
  13. package/src/context-management/messages.ts +112 -106
  14. package/src/context-management/namespace-tools.ts +426 -426
  15. package/src/context-management/tool-contract.ts +23 -23
  16. package/src/context-management/tools.ts +279 -311
  17. package/src/context-management/types.ts +7 -0
  18. package/src/context-management/window-budget.ts +83 -83
  19. package/src/context-management/window-manager.ts +60 -51
  20. package/src/context-management/window-request.ts +125 -125
  21. package/src/debug.ts +1 -1
  22. package/src/extension-runtime.ts +34 -9
  23. package/src/image-generation/artifacts.ts +237 -237
  24. package/src/image-generation/client.ts +248 -248
  25. package/src/image-generation/eligibility.ts +18 -18
  26. package/src/image-generation/references.ts +194 -194
  27. package/src/image-generation/render.ts +252 -252
  28. package/src/model-scope.ts +17 -17
  29. package/src/provider-headers.ts +23 -23
  30. package/src/responses-headers.ts +76 -11
  31. package/src/runtime.ts +37 -1
  32. package/src/web-search/extension.ts +80 -80
  33. package/src/web-search/payload.ts +96 -96
  34. package/src/web-search/prompt.ts +20 -20
  35. package/src/web-search/types.ts +45 -45
@@ -1,7 +1,14 @@
1
1
  import type { Api, Model, ProviderHeaders } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { mergeProviderHeaders } from "../provider-headers";
4
- import { CODEX_CLIENT_VERSION } from "../responses-headers";
4
+ import {
5
+ CODEX_AFFINITY_SCOPE,
6
+ CODEX_CLIENT_VERSION,
7
+ CODEX_GATEWAY_ORIGINATOR,
8
+ buildCodexCliUserAgent,
9
+ filterCodexGatewayHeaders,
10
+ } from "../responses-headers";
11
+ export { CODEX_AFFINITY_SCOPE } from "../responses-headers";
5
12
  import { normalizeBaseUrl } from "../runtime";
6
13
  import { isExactModelAllowed } from "../model-scope";
7
14
  import {
@@ -13,13 +20,10 @@ import {
13
20
  const CODEX_PROVIDER = "openai-codex";
14
21
  const CODEX_API = "openai-codex-responses";
15
22
  const GATEWAY_API = "openai-responses";
16
- const CODEX_TOOL_ORIGINATOR = "codex_cli_rs";
17
- const CODEX_GATEWAY_ORIGINATOR = "codex_cli_rs";
23
+ const CODEX_TOOL_ORIGINATOR = CODEX_GATEWAY_ORIGINATOR;
18
24
  const CODEX_TOOL_VERSION = "0.0.0";
19
25
  const CODEX_SESSION_ID_MAX_LENGTH = 64;
20
26
 
21
- export const CODEX_AFFINITY_SCOPE = "codex-session-v1";
22
-
23
27
  export const CODEX_CONTEXT_PROVIDER_ERROR =
24
28
  "Remote Context management requires a configured Codex-compatible backend";
25
29
 
@@ -103,9 +107,7 @@ function accountIdFromToken(token: string): string | undefined {
103
107
  }
104
108
 
105
109
  function userAgent(version: string): string {
106
- const platform = typeof process !== "undefined" ? process.platform : "unknown";
107
- const arch = typeof process !== "undefined" ? process.arch : "unknown";
108
- return `codex_cli_rs/${version} (${platform}; ${arch})`;
110
+ return buildCodexCliUserAgent(version);
109
111
  }
110
112
 
111
113
  function clampSessionId(value: string | undefined): string | undefined {
@@ -117,17 +119,12 @@ function clampSessionId(value: string | undefined): string | undefined {
117
119
  : chars.slice(0, CODEX_SESSION_ID_MAX_LENGTH).join("");
118
120
  }
119
121
 
120
- function removeSensitiveGatewayHeaders(headers: ProviderHeaders): Record<string, string> {
121
- const result: Record<string, string> = {};
122
- for (const [name, value] of Object.entries(headers)) {
123
- if (typeof value !== "string") continue;
124
- const lower = name.toLowerCase();
125
- if (lower === "authorization" || lower === "cookie" || lower === "chatgpt-account-id" || lower === "x-api-key") {
126
- continue;
127
- }
128
- result[name] = value;
122
+ function resolveContextSessionId(ctx: ExtensionContext): string | undefined {
123
+ try {
124
+ return clampSessionId(ctx.sessionManager.getSessionId());
125
+ } catch {
126
+ return undefined;
129
127
  }
130
- return result;
131
128
  }
132
129
 
133
130
  export type CodexContextProviderHeaderOptions = {
@@ -164,7 +161,7 @@ export function codexContextProviderHeaders(
164
161
  const sessionId = clampSessionId(options.sessionId);
165
162
  if (sessionId) {
166
163
  headers.set("Session-Id", sessionId);
167
- headers.set("X-Client-Request-Id", options.clientRequestId?.trim() || sessionId);
164
+ headers.set("X-Client-Request-Id", clampSessionId(options.clientRequestId) ?? sessionId);
168
165
  }
169
166
  headers.set("content-type", "application/json");
170
167
  return headers;
@@ -208,6 +205,9 @@ export async function resolveCodexContextProvider(
208
205
  if (!rawBaseUrl) return { ok: false, reason: "missing-base-url", ...descriptor };
209
206
 
210
207
  if (isGateway) {
208
+ if (!resolveContextSessionId(ctx)) {
209
+ return { ok: false, reason: "missing-session-id", ...descriptor, baseUrl: rawBaseUrl };
210
+ }
211
211
  const apiKey = bearerToken(auth.apiKey) ?? bearerToken(headerValue(auth.headers, "authorization"));
212
212
  if (!apiKey) return { ok: false, reason: "missing-api-key", ...descriptor, baseUrl: rawBaseUrl };
213
213
  return {
@@ -220,7 +220,7 @@ export async function resolveCodexContextProvider(
220
220
  model: model.id,
221
221
  baseUrl: rawBaseUrl,
222
222
  apiKey,
223
- headers: removeSensitiveGatewayHeaders(mergeProviderHeaders(model.headers, auth.headers)),
223
+ headers: filterCodexGatewayHeaders(mergeProviderHeaders(model.headers, auth.headers)),
224
224
  },
225
225
  };
226
226
  }
@@ -37,6 +37,11 @@ export const NOTES_ENDPOINTS = {
37
37
  write_file: "alpha/notes/v2/write_file",
38
38
  } as const satisfies Record<NotesAction, string>;
39
39
 
40
+ const NOTES_WRITE_ENDPOINTS = new Set<string>([
41
+ NOTES_ENDPOINTS.append_to_file,
42
+ NOTES_ENDPOINTS.write_file,
43
+ ]);
44
+
40
45
  export const ENCRYPTED_ARGUMENT_ENDPOINTS = new Set<string>([
41
46
  HISTORY_ENDPOINTS.search_contents,
42
47
  NOTES_ENDPOINTS.search_contents,
@@ -69,6 +74,28 @@ export interface CodexHistoryNotesDetails {
69
74
  codexHistoryNotes: Record<string, unknown>;
70
75
  }
71
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
+
72
99
  export async function callHistoryNotesBackend(
73
100
  endpoint: string,
74
101
  arguments_: Record<string, unknown>,
@@ -172,6 +199,7 @@ export async function loadHistoryNotesThreadHint(
172
199
  if (result.reason === "aborted") throw new Error("Remote context hint request was aborted");
173
200
  return undefined;
174
201
  }
202
+ if (!isSuccessfulHistoryNotesResponse(result.value)) return undefined;
175
203
  const text = result.value.text;
176
204
  return typeof text === "string" && Buffer.byteLength(text, "utf8") <= THREAD_HINT_MAX_BYTES ? text : undefined;
177
205
  }
@@ -198,6 +226,9 @@ export async function executeHistoryNotesTool(
198
226
  gatewayModels,
199
227
  );
200
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
+ }
201
232
  const modelResult = { ...result.value };
202
233
  delete modelResult.images;
203
234
  const content: AgentToolResult<CodexHistoryNotesDetails>["content"] = [
@@ -227,6 +258,7 @@ export function formatHistoryNotesFailure(
227
258
  case "missing-api-key": return "Remote history/notes gateway authentication is missing an API key";
228
259
  case "missing-account-id": return "Remote history/notes authentication is missing an account id";
229
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";
230
262
  case "unsupported-backend": return "Remote history/notes require the native Codex backend";
231
263
  case "protocol-error": return "Remote history/notes request failed";
232
264
  case "invalid-account-token": return "Remote history/notes authentication token is invalid";
@@ -1,106 +1,112 @@
1
- import { randomUUID } from "node:crypto";
2
- import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
- import {
5
- CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
6
- CONTEXT_MANAGEMENT_PROTOCOL,
7
- CONTEXT_WINDOW_COMPACTION_STRATEGY,
8
- CONTEXT_WINDOW_FALLBACK_BUFFER,
9
- CONTEXT_WINDOW_REMINDER_THRESHOLD,
10
- type CodexContextManagementMessageDetails,
11
- type ContextManagementMessageKind,
12
- type ContextWindowCompactionDetails,
13
- type ContextWindowIdentity,
14
- isCodexContextManagementMessageDetails,
15
- isContextWindowCompactionDetails,
16
- } from "./types";
17
-
18
- export {
19
- CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
20
- CONTEXT_WINDOW_COMPACTION_STRATEGY,
21
- CONTEXT_WINDOW_FALLBACK_BUFFER,
22
- CONTEXT_WINDOW_REMINDER_THRESHOLD,
23
- isCodexContextManagementMessageDetails,
24
- isContextWindowCompactionDetails,
25
- };
26
- export type {
27
- CodexContextManagementMessageDetails,
28
- ContextManagementMessageKind,
29
- ContextWindowCompactionDetails,
30
- ContextWindowIdentity,
31
- };
32
-
33
- export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
34
- "[Pi Codex context-window boundary; no conversation summary was generated.]";
35
-
36
- const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
37
- Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context; no summary carries over. After rollover, read hinted notes. Use history only for a missing detail.
38
- </context_window_guidance>`;
39
-
40
- export function renderContextWindowMessage(
41
- identity: ContextWindowIdentity,
42
- threadHint?: string,
43
- ): string {
44
- const lines = [
45
- "<context_window>",
46
- "Agent name: /root",
47
- `First context window id: ${identity.firstWindowId}`,
48
- `Current context window id: ${identity.currentWindowId}`,
49
- ];
50
- if (identity.previousWindowId) lines.push(`Previous context window id: ${identity.previousWindowId}`);
51
- if (threadHint) lines.push(threadHint);
52
- lines.push("</context_window>");
53
- return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
54
- }
55
-
56
- export function renderContextWindowReminder(remainingTokens: number): string {
57
- return `<context_window_reminder>
58
- 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.
59
- </context_window_reminder>`;
60
- }
61
-
62
- export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
63
- 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.
64
- </context_window_reminder>`;
65
-
66
- export function isContextWindowBoundary(
67
- message: AgentMessage,
68
- ): message is Extract<AgentMessage, { role: "custom" }> & {
69
- details: CodexContextManagementMessageDetails;
70
- } {
71
- return (
72
- message.role === "custom" &&
73
- message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
74
- isCodexContextManagementMessageDetails(message.details) &&
75
- message.details.contextManagement.kind === "window"
76
- );
77
- }
78
-
79
- export function sendContextWindowMessage(
80
- pi: ExtensionAPI,
81
- content: string,
82
- kind: ContextManagementMessageKind,
83
- identity: ContextWindowIdentity,
84
- options: { triggerTurn: boolean; sessionId?: string },
85
- trimPreviousWindow = false,
86
- ): void {
87
- const details: CodexContextManagementMessageDetails = {
88
- protocol: CONTEXT_MANAGEMENT_PROTOCOL,
89
- id: randomUUID(),
90
- ...(options.sessionId ? { sessionId: options.sessionId } : {}),
91
- contextManagement: {
92
- protocol: CONTEXT_MANAGEMENT_PROTOCOL,
93
- kind,
94
- ...identity,
95
- ...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
96
- },
97
- };
98
- pi.sendMessage({
99
- customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
100
- content,
101
- display: true,
102
- details,
103
- }, options.triggerTurn
104
- ? { deliverAs: "steer", triggerTurn: true }
105
- : { triggerTurn: false });
106
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
6
+ CONTEXT_MANAGEMENT_PROTOCOL,
7
+ CONTEXT_WINDOW_COMPACTION_STRATEGY,
8
+ CONTEXT_WINDOW_FALLBACK_BUFFER,
9
+ CONTEXT_WINDOW_REMINDER_THRESHOLD,
10
+ type CodexContextManagementMessageDetails,
11
+ type ContextManagementMessageKind,
12
+ type ContextWindowCompactionDetails,
13
+ type ContextWindowIdentity,
14
+ type NotesCheckpointReceipt,
15
+ isCodexContextManagementMessageDetails,
16
+ isContextWindowCompactionDetails,
17
+ } from "./types";
18
+
19
+ export {
20
+ CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
21
+ CONTEXT_WINDOW_COMPACTION_STRATEGY,
22
+ CONTEXT_WINDOW_FALLBACK_BUFFER,
23
+ CONTEXT_WINDOW_REMINDER_THRESHOLD,
24
+ isCodexContextManagementMessageDetails,
25
+ isContextWindowCompactionDetails,
26
+ };
27
+ export type {
28
+ CodexContextManagementMessageDetails,
29
+ ContextManagementMessageKind,
30
+ ContextWindowCompactionDetails,
31
+ ContextWindowIdentity,
32
+ };
33
+
34
+ export const CONTEXT_WINDOW_COMPACTION_SUMMARY =
35
+ "[Pi Codex context-window boundary; no conversation summary was generated.]";
36
+
37
+ const CONTEXT_WINDOW_GUIDANCE = `<context_window_guidance>
38
+ Checkpoint the active request, known history IDs, decisions, progress, learnings and next steps in notes before new_context; no summary carries over. After rollover, read the checkpoint receipt note first when present; use a thread hint only as supplemental guidance. Use history only for a missing detail.
39
+ </context_window_guidance>`;
40
+
41
+ export function renderContextWindowMessage(
42
+ identity: ContextWindowIdentity,
43
+ threadHint?: string,
44
+ checkpoint?: NotesCheckpointReceipt,
45
+ ): string {
46
+ const lines = [
47
+ "<context_window>",
48
+ "Agent name: /root",
49
+ `First context window id: ${identity.firstWindowId}`,
50
+ `Current context window id: ${identity.currentWindowId}`,
51
+ ];
52
+ if (identity.previousWindowId) lines.push(`Previous context window id: ${identity.previousWindowId}`);
53
+ if (checkpoint) {
54
+ lines.push("Checkpoint successfully written:");
55
+ lines.push(` Read this note before doing anything else with notes action "read_file": ${JSON.stringify(checkpoint.path)}`);
56
+ }
57
+ if (threadHint) lines.push(threadHint);
58
+ lines.push("</context_window>");
59
+ return `${CONTEXT_WINDOW_GUIDANCE}\n\n${lines.join("\n")}`;
60
+ }
61
+
62
+ export function renderContextWindowReminder(remainingTokens: number): string {
63
+ return `<context_window_reminder>
64
+ 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.
65
+ </context_window_reminder>`;
66
+ }
67
+
68
+ export const CONTEXT_WINDOW_FALLBACK_MESSAGE = `<context_window_reminder>
69
+ 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.
70
+ </context_window_reminder>`;
71
+
72
+ export function isContextWindowBoundary(
73
+ message: AgentMessage,
74
+ ): message is Extract<AgentMessage, { role: "custom" }> & {
75
+ details: CodexContextManagementMessageDetails;
76
+ } {
77
+ return (
78
+ message.role === "custom" &&
79
+ message.customType === CODEX_CONTEXT_WINDOW_MESSAGE_TYPE &&
80
+ isCodexContextManagementMessageDetails(message.details) &&
81
+ message.details.contextManagement.kind === "window"
82
+ );
83
+ }
84
+
85
+ export function sendContextWindowMessage(
86
+ pi: ExtensionAPI,
87
+ content: string,
88
+ kind: ContextManagementMessageKind,
89
+ identity: ContextWindowIdentity,
90
+ options: { triggerTurn: boolean; sessionId?: string },
91
+ trimPreviousWindow = false,
92
+ ): void {
93
+ const details: CodexContextManagementMessageDetails = {
94
+ protocol: CONTEXT_MANAGEMENT_PROTOCOL,
95
+ id: randomUUID(),
96
+ ...(options.sessionId ? { sessionId: options.sessionId } : {}),
97
+ contextManagement: {
98
+ protocol: CONTEXT_MANAGEMENT_PROTOCOL,
99
+ kind,
100
+ ...identity,
101
+ ...(trimPreviousWindow ? { trimPreviousWindow: true as const } : {}),
102
+ },
103
+ };
104
+ pi.sendMessage({
105
+ customType: CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
106
+ content,
107
+ display: true,
108
+ details,
109
+ }, options.triggerTurn
110
+ ? { deliverAs: "steer", triggerTurn: true }
111
+ : { triggerTurn: false });
112
+ }