@oai404iao/pi-codex-runtime 0.1.0-alpha.1

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 (49) hide show
  1. package/LICENSE +28 -0
  2. package/LICENSES/Apache-2.0.txt +201 -0
  3. package/LICENSES/OpenAI-Codex-NOTICE.txt +6 -0
  4. package/README.md +26 -0
  5. package/THIRD_PARTY_NOTICES.md +19 -0
  6. package/config.schema.json +174 -0
  7. package/models.schema.json +217 -0
  8. package/package.json +68 -0
  9. package/provenance/openai-codex-eb9dceba-reserved-tools.json +140 -0
  10. package/src/activation.ts +56 -0
  11. package/src/broker.ts +133 -0
  12. package/src/capabilities.ts +146 -0
  13. package/src/codex-http.ts +133 -0
  14. package/src/codex-identity-extension.ts +341 -0
  15. package/src/codex-request-profile.ts +45 -0
  16. package/src/codex-reserved-tools.ts +33 -0
  17. package/src/codex-wire-identity.ts +596 -0
  18. package/src/extension/provider-presentation.ts +11 -0
  19. package/src/glyphs.ts +70 -0
  20. package/src/index.ts +4 -0
  21. package/src/model-catalog/catalog.ts +636 -0
  22. package/src/model-catalog/default-models.json +252 -0
  23. package/src/model-catalog/runtime.ts +113 -0
  24. package/src/model-catalog/types.ts +95 -0
  25. package/src/provider-headers.ts +54 -0
  26. package/src/providers/openai-codex/stream-effects.ts +21 -0
  27. package/src/providers/openai-codex/types.ts +155 -0
  28. package/src/providers/responses/citations.ts +105 -0
  29. package/src/providers/responses/history.ts +144 -0
  30. package/src/providers/responses/items.ts +87 -0
  31. package/src/providers/responses/markdown.ts +92 -0
  32. package/src/providers/responses/messages.ts +169 -0
  33. package/src/providers/responses/signatures.ts +102 -0
  34. package/src/providers/responses/stream-state.ts +99 -0
  35. package/src/providers/responses/stream.ts +386 -0
  36. package/src/providers/responses/text-renderer.ts +147 -0
  37. package/src/providers/responses/text.ts +32 -0
  38. package/src/providers/responses/tool-identity.ts +25 -0
  39. package/src/providers/responses/tools.ts +14 -0
  40. package/src/providers/responses/types.ts +113 -0
  41. package/src/providers/responses/usage.ts +53 -0
  42. package/src/reserved-tools/image-generation.ts +53 -0
  43. package/src/reserved-tools/types.ts +32 -0
  44. package/src/reserved-tools/web-search.ts +269 -0
  45. package/src/session-claims.ts +19 -0
  46. package/src/settings.ts +247 -0
  47. package/src/subagent-inline.ts +8 -0
  48. package/src/tool-activation.ts +89 -0
  49. package/src/utils/theme.ts +7 -0
@@ -0,0 +1,146 @@
1
+ import type { CodexMinimalToolsSettings } from "./settings.js";
2
+ import { loadModelSettings } from "./model-catalog/runtime.js";
3
+
4
+ export const PACKAGE_TOOL_NAMES = ["image_generation", "view_image", "apply_patch", "web_search"] as const;
5
+ export type PackageToolName = (typeof PACKAGE_TOOL_NAMES)[number];
6
+ export const NATIVE_MUTATION_TOOL_NAMES = ["edit", "write"] as const;
7
+ export type NativeMutationToolName = (typeof NATIVE_MUTATION_TOOL_NAMES)[number];
8
+
9
+ export interface ModelLike {
10
+ provider?: string;
11
+ id?: string;
12
+ name?: string;
13
+ api?: string;
14
+ input?: string[];
15
+ capabilities?: {
16
+ input?: string[];
17
+ inputModalities?: string[];
18
+ };
19
+ }
20
+
21
+ export interface ToolCapability {
22
+ enabled: boolean;
23
+ reason: string;
24
+ }
25
+
26
+ export type ToolCapabilityMap = Record<PackageToolName, ToolCapability>;
27
+
28
+ export function modelKey(model: ModelLike | undefined): string {
29
+ if (!model) return "no model";
30
+ return `${model.provider ?? "unknown"}/${model.id ?? model.name ?? "unknown"}`;
31
+ }
32
+
33
+ export function supportsImageInput(model: ModelLike | undefined): boolean {
34
+ const inputs = [
35
+ ...(model?.input ?? []),
36
+ ...(model?.capabilities?.input ?? []),
37
+ ...(model?.capabilities?.inputModalities ?? []),
38
+ ].map((value) => value.toLowerCase());
39
+ return inputs.includes("image") || inputs.includes("images") || inputs.includes("vision");
40
+ }
41
+
42
+ export function computeToolCapabilities(model: ModelLike | undefined, settings: CodexMinimalToolsSettings): ToolCapabilityMap {
43
+ if (!settings.enabled) {
44
+ return {
45
+ image_generation: { enabled: false, reason: "package disabled" },
46
+ view_image: { enabled: false, reason: "package disabled" },
47
+ apply_patch: { enabled: false, reason: "package disabled" },
48
+ web_search: { enabled: false, reason: "package disabled" },
49
+ };
50
+ }
51
+
52
+ const modelSettings = loadModelSettings(model, undefined, settings);
53
+ const profile = modelSettings.modelProfile?.effective;
54
+ if (!profile || !profile.enabled) {
55
+ return {
56
+ image_generation: { enabled: false, reason: "model has no enabled model catalog profile" },
57
+ view_image: { enabled: false, reason: "model has no enabled model catalog profile" },
58
+ apply_patch: { enabled: false, reason: "model has no enabled model catalog profile" },
59
+ web_search: { enabled: false, reason: "model has no enabled model catalog profile" },
60
+ };
61
+ }
62
+
63
+ const imageInput = supportsImageInput(model);
64
+ const imageGeneration = profile.tools.imageGeneration;
65
+ const webSearch = profile.tools.webSearch;
66
+ const providerShimActive = modelSettings.providerShimActive;
67
+
68
+ return {
69
+ image_generation: imageGeneration === "hosted" && providerShimActive && imageInput
70
+ ? { enabled: true, reason: "model profile enables hosted image_generation" }
71
+ : imageGeneration === "standalone" && imageInput
72
+ ? { enabled: true, reason: "model profile enables standalone image generation" }
73
+ : profile.tools.imageGeneration !== false && settings.directImageApiFallback
74
+ ? { enabled: true, reason: "direct Images API fallback enabled" }
75
+ : { enabled: false, reason: imageGeneration === false ? "image_generation disabled by model profile" : "model does not advertise image input" },
76
+ view_image: profile.tools.viewImage && imageInput
77
+ ? { enabled: true, reason: "model profile enables view_image and model accepts image input" }
78
+ : { enabled: false, reason: !profile.tools.viewImage ? "view_image disabled by model profile" : "model does not advertise image input" },
79
+ apply_patch: profile.tools.applyPatch === "custom" && !providerShimActive
80
+ ? {
81
+ enabled: false,
82
+ reason: "custom apply_patch requires an OpenAI Responses provider shim API",
83
+ }
84
+ : profile.tools.applyPatch
85
+ ? { enabled: true, reason: `model profile enables ${profile.tools.applyPatch} apply_patch` }
86
+ : {
87
+ enabled: false,
88
+ reason: "apply_patch disabled by model profile",
89
+ },
90
+ web_search: webSearch && (webSearch.implementation === "standalone" || providerShimActive)
91
+ ? { enabled: true, reason: `model profile enables ${webSearch.implementation} web search` }
92
+ : {
93
+ enabled: false,
94
+ reason: webSearch !== false && webSearch.implementation === "hosted"
95
+ ? "hosted web_search requires an OpenAI Responses provider shim API"
96
+ : "web_search disabled by model profile",
97
+ },
98
+ };
99
+ }
100
+
101
+ export function desiredPackageTools(model: ModelLike | undefined, settings: CodexMinimalToolsSettings): PackageToolName[] {
102
+ const capabilities = computeToolCapabilities(model, settings);
103
+ return PACKAGE_TOOL_NAMES.filter((name) => capabilities[name].enabled);
104
+ }
105
+
106
+ export interface ActiveToolSyncResult {
107
+ activeTools: string[];
108
+ added: string[];
109
+ removed: string[];
110
+ preserved: string[];
111
+ }
112
+
113
+ export function computeNextActiveTools(currentActive: readonly string[], model: ModelLike | undefined, settings: CodexMinimalToolsSettings): ActiveToolSyncResult {
114
+ const current = new Set(currentActive);
115
+ const desired = new Set(desiredPackageTools(model, settings));
116
+ const added: string[] = [];
117
+ const removed: string[] = [];
118
+
119
+ for (const tool of PACKAGE_TOOL_NAMES) {
120
+ if (!desired.has(tool) && current.delete(tool)) removed.push(tool);
121
+ }
122
+
123
+ if (settings.enabled && settings.autoEnable) {
124
+ for (const tool of desired) {
125
+ if (!current.has(tool)) {
126
+ current.add(tool);
127
+ added.push(tool);
128
+ }
129
+ }
130
+ }
131
+
132
+ if (current.has("apply_patch")) {
133
+ for (const nativeMutationTool of NATIVE_MUTATION_TOOL_NAMES) {
134
+ if (current.delete(nativeMutationTool)) removed.push(nativeMutationTool);
135
+ }
136
+ }
137
+
138
+ const activeTools = currentActive.filter((name) => current.has(name));
139
+ for (const name of current) if (!activeTools.includes(name)) activeTools.push(name);
140
+ return {
141
+ activeTools,
142
+ added,
143
+ removed,
144
+ preserved: currentActive.filter((name) => !PACKAGE_TOOL_NAMES.includes(name as PackageToolName) && activeTools.includes(name)),
145
+ };
146
+ }
@@ -0,0 +1,133 @@
1
+ import type { ProviderHeaders } from "@earendil-works/pi-ai";
2
+ import {
3
+ isProviderHeaderSuppressed,
4
+ mergeProviderHeaders,
5
+ providerHeaderDirective,
6
+ setProviderDefaultHeader,
7
+ setProviderGeneratedHeader,
8
+ } from "./provider-headers.js";
9
+
10
+ const DEFAULT_CODEX_BASE_URL = "https://chatgpt.com/backend-api";
11
+ const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
12
+ const JWT_CLAIM_PATH = "https://api.openai.com/auth";
13
+
14
+ export interface CodexRequestAuth {
15
+ apiKey?: string;
16
+ headers?: ProviderHeaders;
17
+ }
18
+
19
+ function bearerToken(headers: Headers): string | undefined {
20
+ const authorization = headers.get("authorization");
21
+ const match = authorization ? /^Bearer\s+(.+)$/i.exec(authorization.trim()) : null;
22
+ return match?.[1]?.trim() || undefined;
23
+ }
24
+
25
+ function authHeaders(options: {
26
+ modelHeaders?: ProviderHeaders;
27
+ auth: CodexRequestAuth;
28
+ }): Headers {
29
+ return mergeProviderHeaders(options.modelHeaders, options.auth.headers);
30
+ }
31
+
32
+ export function hasCodexRequestAuth(options: {
33
+ modelHeaders?: ProviderHeaders;
34
+ auth: CodexRequestAuth;
35
+ }): boolean {
36
+ const headers = authHeaders(options);
37
+ if (
38
+ options.auth.apiKey
39
+ && providerHeaderDirective(options.auth.headers, "authorization") === undefined
40
+ && !isProviderHeaderSuppressed(headers, "authorization")
41
+ ) {
42
+ return true;
43
+ }
44
+ return ["authorization", "api-key", "x-api-key", "x-openai-actor-authorization"]
45
+ .some((name) => Boolean(headers.get(name)?.trim()));
46
+ }
47
+
48
+ export function resolveCodexRequestAccountId(options: {
49
+ modelHeaders?: ProviderHeaders;
50
+ auth: CodexRequestAuth;
51
+ apiKeyMode: boolean;
52
+ }): string | undefined {
53
+ if (options.apiKeyMode) return undefined;
54
+ const headers = authHeaders(options);
55
+ if (
56
+ headers.get("chatgpt-account-id")?.trim()
57
+ || headers.get("x-openai-actor-authorization")?.trim()
58
+ || isProviderHeaderSuppressed(headers, "chatgpt-account-id")
59
+ ) {
60
+ return undefined;
61
+ }
62
+ const requestAuthorization = providerHeaderDirective(options.auth.headers, "authorization");
63
+ const token = requestAuthorization !== undefined
64
+ ? typeof requestAuthorization === "string" && requestAuthorization.trim()
65
+ ? bearerToken(new Headers({ authorization: requestAuthorization }))
66
+ : undefined
67
+ : isProviderHeaderSuppressed(headers, "authorization")
68
+ ? undefined
69
+ : options.auth.apiKey ?? bearerToken(headers);
70
+ return token ? extractCodexAccountId(token) : undefined;
71
+ }
72
+
73
+ function responseEndpoint(baseUrl: string | undefined, apiKeyMode: boolean): string {
74
+ const raw = baseUrl?.trim() || (apiKeyMode ? DEFAULT_OPENAI_BASE_URL : DEFAULT_CODEX_BASE_URL);
75
+ const normalized = raw.replace(/\/+$/, "");
76
+ if (apiKeyMode) {
77
+ if (normalized.endsWith("/responses")) return normalized;
78
+ return `${normalized}/responses`;
79
+ }
80
+ if (normalized.endsWith("/codex/responses")) return normalized;
81
+ if (normalized.endsWith("/codex")) return `${normalized}/responses`;
82
+ return `${normalized}/codex/responses`;
83
+ }
84
+
85
+ export function resolveCodexApiEndpoint(
86
+ baseUrl: string | undefined,
87
+ apiKeyMode: boolean,
88
+ path: string,
89
+ ): string {
90
+ const root = responseEndpoint(baseUrl, apiKeyMode).replace(/\/responses$/, "");
91
+ return `${root}/${path.replace(/^\/+/, "")}`;
92
+ }
93
+
94
+ export function extractCodexAccountId(token: string): string {
95
+ try {
96
+ const parts = token.split(".");
97
+ if (parts.length !== 3) throw new Error("Invalid token");
98
+ const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64").toString("utf8"));
99
+ const accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;
100
+ if (typeof accountId !== "string" || !accountId) throw new Error("No account ID in token");
101
+ return accountId;
102
+ } catch {
103
+ throw new Error("Failed to extract accountId from Codex OAuth token");
104
+ }
105
+ }
106
+
107
+ export function buildCodexJsonHeaders(options: {
108
+ modelHeaders?: ProviderHeaders;
109
+ auth: CodexRequestAuth;
110
+ apiKeyMode: boolean;
111
+ extraHeaders?: Record<string, string>;
112
+ }): Headers {
113
+ const headers = authHeaders(options);
114
+ for (const [name, value] of Object.entries(options.extraHeaders ?? {})) {
115
+ setProviderGeneratedHeader(headers, name, value);
116
+ }
117
+ const requestAuthorization = providerHeaderDirective(options.auth.headers, "authorization");
118
+ if (requestAuthorization === undefined && options.auth.apiKey) {
119
+ setProviderGeneratedHeader(headers, "Authorization", `Bearer ${options.auth.apiKey}`);
120
+ }
121
+ if (
122
+ !options.apiKeyMode
123
+ && !headers.has("chatgpt-account-id")
124
+ && !headers.has("x-openai-actor-authorization")
125
+ ) {
126
+ const accountId = resolveCodexRequestAccountId(options);
127
+ if (accountId) setProviderDefaultHeader(headers, "chatgpt-account-id", accountId);
128
+ }
129
+ setProviderDefaultHeader(headers, "originator", "pi");
130
+ setProviderDefaultHeader(headers, "accept", "application/json");
131
+ setProviderDefaultHeader(headers, "content-type", "application/json");
132
+ return headers;
133
+ }
@@ -0,0 +1,341 @@
1
+ import { claimSessionFeature } from "./session-claims.js";
2
+ import {
3
+ SessionManager,
4
+ type ExtensionAPI,
5
+ type InlineExtension,
6
+ type SessionEntry,
7
+ } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ advanceCodexWindow,
10
+ beginCodexTurn,
11
+ codexThreadIdentityFor,
12
+ createCodexChildIdentity,
13
+ createCodexRootIdentity,
14
+ endCodexTurn,
15
+ parseCodexThreadIdentity,
16
+ registerCodexThreadIdentity,
17
+ type CodexThreadIdentity,
18
+ } from "./codex-wire-identity.js";
19
+
20
+ export const CODEX_IDENTITY_CUSTOM_TYPE = "pi-codex/thread-identity";
21
+ const SUBAGENT_DESCRIPTOR_CUSTOM_TYPE = "pi-subagent/descriptor";
22
+ const SUBAGENT_LINEAGE_CUSTOM_TYPE = "pi-subagent/lineage";
23
+ const IDENTITY_LIFECYCLE_SYMBOL = Symbol.for(
24
+ "@oai404iao/pi-codex/identity-lifecycle/v1",
25
+ );
26
+
27
+ export interface CodexIdentitySessionView {
28
+ getSessionId(): string;
29
+ getSessionFile(): string | undefined;
30
+ getSessionDir(): string;
31
+ getCwd(): string;
32
+ getEntries(): SessionEntry[];
33
+ getBranch?(): SessionEntry[];
34
+ appendCustomEntry?(customType: string, data?: unknown): string;
35
+ }
36
+
37
+ interface SubagentLineage {
38
+ agentId: string;
39
+ parentAgentId: string;
40
+ parentPiSessionId: string;
41
+ parentSessionFile?: string;
42
+ relation: "spawn" | "fork";
43
+ agentName?: string;
44
+ }
45
+
46
+ function record(value: unknown): Record<string, unknown> | undefined {
47
+ return value && typeof value === "object" && !Array.isArray(value)
48
+ ? value as Record<string, unknown>
49
+ : undefined;
50
+ }
51
+
52
+ function readIdentity(
53
+ entries: readonly SessionEntry[],
54
+ piSessionId?: string,
55
+ ): CodexThreadIdentity | undefined {
56
+ for (let index = entries.length - 1; index >= 0; index--) {
57
+ const entry = entries[index];
58
+ if (
59
+ entry?.type !== "custom"
60
+ || entry.customType !== CODEX_IDENTITY_CUSTOM_TYPE
61
+ ) {
62
+ continue;
63
+ }
64
+ try {
65
+ const identity = parseCodexThreadIdentity(entry.data);
66
+ if (!piSessionId || identity.piSessionId === piSessionId) {
67
+ return identity;
68
+ }
69
+ } catch {
70
+ // A newer valid entry can repair a corrupt historical checkpoint.
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function readSubagentLineage(
77
+ entries: readonly SessionEntry[],
78
+ ): SubagentLineage | undefined {
79
+ for (let index = entries.length - 1; index >= 0; index--) {
80
+ const entry = entries[index];
81
+ if (
82
+ entry?.type === "custom"
83
+ && entry.customType === SUBAGENT_LINEAGE_CUSTOM_TYPE
84
+ ) {
85
+ const lineage = record(entry.data);
86
+ if (
87
+ lineage?.version !== 1
88
+ || lineage.openAIIdentity !== true
89
+ || (lineage.relation !== "spawn" && lineage.relation !== "fork")
90
+ || typeof lineage.agentId !== "string"
91
+ || typeof lineage.parentAgentId !== "string"
92
+ || typeof lineage.parentPiSessionId !== "string"
93
+ ) {
94
+ return undefined;
95
+ }
96
+ return {
97
+ agentId: lineage.agentId,
98
+ parentAgentId: lineage.parentAgentId,
99
+ parentPiSessionId: lineage.parentPiSessionId,
100
+ relation: lineage.relation,
101
+ ...(typeof lineage.parentSessionFile === "string"
102
+ ? { parentSessionFile: lineage.parentSessionFile }
103
+ : {}),
104
+ ...(typeof lineage.agentName === "string"
105
+ ? { agentName: lineage.agentName }
106
+ : {}),
107
+ };
108
+ }
109
+ if (
110
+ entry?.type !== "custom"
111
+ || entry.customType !== SUBAGENT_DESCRIPTOR_CUSTOM_TYPE
112
+ ) {
113
+ continue;
114
+ }
115
+ const descriptor = record(entry.data);
116
+ const runtime = record(descriptor?.runtime);
117
+ if (
118
+ descriptor?.version !== 2
119
+ || runtime?.openAIIdentity !== true
120
+ || (descriptor.provider !== "spawn" && descriptor.provider !== "fork")
121
+ || typeof descriptor.agentId !== "string"
122
+ || typeof descriptor.parentAgentId !== "string"
123
+ || typeof descriptor.parentPiSessionId !== "string"
124
+ ) {
125
+ return undefined;
126
+ }
127
+ const agent = record(descriptor.agent);
128
+ return {
129
+ agentId: descriptor.agentId,
130
+ parentAgentId: descriptor.parentAgentId,
131
+ parentPiSessionId: descriptor.parentPiSessionId,
132
+ relation: descriptor.provider,
133
+ ...(typeof descriptor.parentSessionFile === "string"
134
+ ? { parentSessionFile: descriptor.parentSessionFile }
135
+ : {}),
136
+ ...(typeof agent?.name === "string" ? { agentName: agent.name } : {}),
137
+ };
138
+ }
139
+ return undefined;
140
+ }
141
+
142
+ function appendIdentity(
143
+ session: CodexIdentitySessionView,
144
+ identity: CodexThreadIdentity,
145
+ appendCurrent?: (identity: CodexThreadIdentity) => void,
146
+ ): void {
147
+ if (appendCurrent) {
148
+ appendCurrent(identity);
149
+ return;
150
+ }
151
+ session.appendCustomEntry?.(
152
+ CODEX_IDENTITY_CUSTOM_TYPE,
153
+ structuredClone(identity),
154
+ );
155
+ }
156
+
157
+ function openParentSession(
158
+ session: CodexIdentitySessionView,
159
+ lineage: SubagentLineage,
160
+ ): CodexIdentitySessionView | undefined {
161
+ if (!lineage.parentSessionFile) return undefined;
162
+ try {
163
+ return SessionManager.open(
164
+ lineage.parentSessionFile,
165
+ session.getSessionDir(),
166
+ session.getCwd(),
167
+ );
168
+ } catch {
169
+ return undefined;
170
+ }
171
+ }
172
+
173
+ function resolveParentIdentity(
174
+ session: CodexIdentitySessionView,
175
+ lineage: SubagentLineage,
176
+ ): CodexThreadIdentity {
177
+ const active = codexThreadIdentityFor(lineage.parentPiSessionId);
178
+ if (active) return active;
179
+
180
+ const parent = openParentSession(session, lineage);
181
+ if (parent) {
182
+ const persisted = readIdentity(
183
+ parent.getEntries(),
184
+ lineage.parentPiSessionId,
185
+ );
186
+ if (persisted) return registerCodexThreadIdentity(persisted);
187
+
188
+ // pi-codex-minimal-tools owns the Codex identity even when the parent
189
+ // happened to use a non-Codex model before creating this OpenAI child.
190
+ const root = createCodexRootIdentity(lineage.parentPiSessionId);
191
+ appendIdentity(parent, root);
192
+ return registerCodexThreadIdentity(root);
193
+ }
194
+
195
+ // Ephemeral parents have no file to reopen. They are still represented by a
196
+ // process-local root identity for the lifetime of the child tree.
197
+ const root = createCodexRootIdentity(lineage.parentPiSessionId);
198
+ return registerCodexThreadIdentity(root);
199
+ }
200
+
201
+ export function ensureCodexSessionIdentity(
202
+ session: CodexIdentitySessionView,
203
+ options: {
204
+ sessionStartReason?: string;
205
+ appendCurrent?: (identity: CodexThreadIdentity) => void;
206
+ } = {},
207
+ ): CodexThreadIdentity {
208
+ const piSessionId = session.getSessionId();
209
+ const persisted = readIdentity(session.getEntries(), piSessionId);
210
+ if (persisted) return registerCodexThreadIdentity(persisted);
211
+
212
+ const lineage = readSubagentLineage(session.getEntries());
213
+ let identity: CodexThreadIdentity;
214
+ if (lineage) {
215
+ identity = createCodexChildIdentity(
216
+ piSessionId,
217
+ resolveParentIdentity(session, lineage),
218
+ {
219
+ relation: lineage.relation,
220
+ agentName: lineage.agentName,
221
+ },
222
+ );
223
+ } else {
224
+ const copied = readIdentity(session.getEntries());
225
+ identity = createCodexRootIdentity(piSessionId, {
226
+ ...(options.sessionStartReason === "fork" && copied
227
+ ? { forkedFromThreadId: copied.threadId }
228
+ : {}),
229
+ });
230
+ }
231
+
232
+ appendIdentity(session, identity, options.appendCurrent);
233
+ return registerCodexThreadIdentity(identity);
234
+ }
235
+
236
+ /**
237
+ * Install only the session/turn/window lifecycle needed by the Codex provider.
238
+ * It deliberately does not register providers, tools, commands, or renderers.
239
+ */
240
+ export function installCodexIdentityLifecycle(pi: ExtensionAPI): void {
241
+ const guard = pi as unknown as Record<PropertyKey, unknown>;
242
+ if (guard[IDENTITY_LIFECYCLE_SYMBOL]) return;
243
+ if (!claimSessionFeature(pi, "wire-identity")) return;
244
+ guard[IDENTITY_LIFECYCLE_SYMBOL] = true;
245
+
246
+ const ensure = (
247
+ ctx: {
248
+ sessionManager?: Partial<CodexIdentitySessionView>;
249
+ },
250
+ sessionStartReason?: string,
251
+ ): CodexThreadIdentity | undefined => {
252
+ const session = ctx.sessionManager;
253
+ if (!session || typeof session.getSessionId !== "function") {
254
+ return undefined;
255
+ }
256
+ const piSessionId = session.getSessionId();
257
+ if (typeof session.getEntries !== "function") {
258
+ const existing = codexThreadIdentityFor(piSessionId);
259
+ if (existing) return existing;
260
+ return registerCodexThreadIdentity(
261
+ createCodexRootIdentity(piSessionId),
262
+ );
263
+ }
264
+ return ensureCodexSessionIdentity(session as CodexIdentitySessionView, {
265
+ sessionStartReason,
266
+ appendCurrent: (identity) => {
267
+ pi.appendEntry(
268
+ CODEX_IDENTITY_CUSTOM_TYPE,
269
+ structuredClone(identity),
270
+ );
271
+ },
272
+ });
273
+ };
274
+
275
+ pi.on("session_start", async (event, ctx) => {
276
+ ensure(ctx as unknown as { sessionManager: CodexIdentitySessionView }, event.reason);
277
+ });
278
+
279
+ pi.on("before_agent_start", async (_event, ctx) => {
280
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
281
+ const identity = ensure(typed);
282
+ if (!identity) return;
283
+ const lineage =
284
+ typeof typed.sessionManager.getEntries === "function"
285
+ ? readSubagentLineage(typed.sessionManager.getEntries())
286
+ : undefined;
287
+ beginCodexTurn(identity.piSessionId, {
288
+ ...(lineage?.parentPiSessionId
289
+ ? { parentPiSessionId: lineage.parentPiSessionId }
290
+ : {}),
291
+ });
292
+ });
293
+
294
+ pi.on("agent_settled", async (_event, ctx) => {
295
+ const piSessionId = ctx.sessionManager?.getSessionId?.();
296
+ if (piSessionId) endCodexTurn(piSessionId);
297
+ });
298
+
299
+ pi.on("session_compact", async (event, ctx) => {
300
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
301
+ if (!ensure(typed)) return;
302
+ const identity = advanceCodexWindow(
303
+ typed.sessionManager.getSessionId(),
304
+ event.compactionEntry.id,
305
+ );
306
+ pi.appendEntry(
307
+ CODEX_IDENTITY_CUSTOM_TYPE,
308
+ structuredClone(identity),
309
+ );
310
+ });
311
+
312
+ pi.on("session_tree", async (_event, ctx) => {
313
+ const typed = ctx as unknown as { sessionManager: CodexIdentitySessionView };
314
+ const piSessionId = typed.sessionManager.getSessionId();
315
+ const branch = typed.sessionManager.getBranch?.()
316
+ ?? typed.sessionManager.getEntries();
317
+ const identity = readIdentity(branch, piSessionId)
318
+ ?? readIdentity(typed.sessionManager.getEntries(), piSessionId);
319
+ if (identity) registerCodexThreadIdentity(identity);
320
+ });
321
+
322
+ pi.on("session_shutdown", async (_event, ctx) => {
323
+ const piSessionId = ctx.sessionManager?.getSessionId?.();
324
+ if (piSessionId) endCodexTurn(piSessionId);
325
+ });
326
+ }
327
+
328
+ /** Named inline extension used by pi-subagent when normal inheritance is off. */
329
+ export function createCodexSubagentInlineExtension(
330
+ options: { parentSessionManager?: CodexIdentitySessionView } = {},
331
+ ): InlineExtension {
332
+ if (options.parentSessionManager) {
333
+ ensureCodexSessionIdentity(options.parentSessionManager);
334
+ }
335
+ return {
336
+ name: "pi-codex-subagent-identity",
337
+ factory: (pi) => {
338
+ installCodexIdentityLifecycle(pi);
339
+ },
340
+ };
341
+ }
@@ -0,0 +1,45 @@
1
+ import type { ReasoningSummary } from "./model-catalog/types.js";
2
+
3
+ export interface CodexRequestProfile {
4
+ responsesMode: "standard" | "lite";
5
+ reasoningSummary: ReasoningSummary;
6
+ systemPromptPlacement: "instructions" | "developer";
7
+ patchTransport: "function" | "custom";
8
+ supportsHostedTools: boolean;
9
+ supportsParallelTools: boolean;
10
+ }
11
+
12
+ /**
13
+ * Only modes implemented by the current provider shim are configurable.
14
+ * Custom transport remains unavailable until its complete wire parser lands.
15
+ */
16
+ export interface CodexRequestProfileOverride {
17
+ responsesMode?: "standard" | "lite";
18
+ reasoningSummary?: ReasoningSummary;
19
+ systemPromptPlacement?: "instructions" | "developer";
20
+ patchTransport?: "function" | "custom";
21
+ supportsHostedTools?: boolean;
22
+ supportsParallelTools?: boolean;
23
+ }
24
+
25
+ export const DEFAULT_CODEX_REQUEST_PROFILE: CodexRequestProfile = {
26
+ responsesMode: "standard",
27
+ reasoningSummary: "auto",
28
+ systemPromptPlacement: "instructions",
29
+ patchTransport: "function",
30
+ supportsHostedTools: true,
31
+ supportsParallelTools: true,
32
+ };
33
+
34
+ export function resolveCodexRequestProfile(override: CodexRequestProfileOverride = {}): CodexRequestProfile {
35
+ const responsesMode = override.responsesMode ?? DEFAULT_CODEX_REQUEST_PROFILE.responsesMode;
36
+ return {
37
+ responsesMode,
38
+ reasoningSummary: override.reasoningSummary
39
+ ?? (responsesMode === "lite" ? "none" : DEFAULT_CODEX_REQUEST_PROFILE.reasoningSummary),
40
+ systemPromptPlacement: responsesMode === "lite" ? "developer" : override.systemPromptPlacement ?? DEFAULT_CODEX_REQUEST_PROFILE.systemPromptPlacement,
41
+ patchTransport: override.patchTransport ?? DEFAULT_CODEX_REQUEST_PROFILE.patchTransport,
42
+ supportsHostedTools: responsesMode === "lite" ? false : override.supportsHostedTools ?? DEFAULT_CODEX_REQUEST_PROFILE.supportsHostedTools,
43
+ supportsParallelTools: responsesMode === "lite" ? false : override.supportsParallelTools ?? DEFAULT_CODEX_REQUEST_PROFILE.supportsParallelTools,
44
+ };
45
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * SPDX-FileCopyrightText: 2025 OpenAI
3
+ * SPDX-FileCopyrightText: 2026 oai404iao
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ *
6
+ * Modified TypeScript compatibility serialization derived from the namespace
7
+ * tool construction in OpenAI Codex at revision
8
+ * eb9dceba1a2e658142a456c5898836774835616b.
9
+ *
10
+ * This preserves the reviewed `web.run` and `image_gen.imagegen` declaration
11
+ * shapes for this package's internal Responses Lite compatibility path. It is
12
+ * not an OpenAI-supported public API contract. Immutable upstream blob IDs,
13
+ * source hashes, and local compatibility fingerprints are recorded in
14
+ * provenance/openai-codex-eb9dceba-reserved-tools.json.
15
+ *
16
+ * See THIRD_PARTY_NOTICES.md and LICENSES/Apache-2.0.txt.
17
+ */
18
+
19
+ import { WEB_SEARCH_NAMESPACE } from "./reserved-tools/web-search.js";
20
+ import { IMAGE_GENERATION_NAMESPACE } from "./reserved-tools/image-generation.js";
21
+ import type { CodexReservedToolName, CodexReservedNamespaceTool } from "./reserved-tools/types.js";
22
+ export type { CodexReservedToolName, CodexReservedNamespaceTool } from "./reserved-tools/types.js";
23
+
24
+ const CODEX_RESERVED_NAMESPACE_TOOLS: Record<CodexReservedToolName, CodexReservedNamespaceTool> = {
25
+ web_search: WEB_SEARCH_NAMESPACE,
26
+ image_generation: IMAGE_GENERATION_NAMESPACE,
27
+ };
28
+
29
+ export function createCodexReservedNamespaceTool(
30
+ name: CodexReservedToolName,
31
+ ): CodexReservedNamespaceTool {
32
+ return structuredClone(CODEX_RESERVED_NAMESPACE_TOOLS[name]);
33
+ }