@kodelyth/codex 2026.5.40 → 2026.5.42

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 (178) hide show
  1. package/dist/client-ChMX13_o.js +642 -0
  2. package/dist/client-factory-D3dIsp4Y.js +9 -0
  3. package/dist/command-formatters-BRW7_Nu7.js +519 -0
  4. package/dist/command-handlers-P2IqtXaZ.js +1462 -0
  5. package/dist/compact-baos5flR.js +329 -0
  6. package/dist/computer-use-VfLvTMaa.js +367 -0
  7. package/dist/config-CezENx_E.js +510 -0
  8. package/dist/doctor-contract-api.js +53 -0
  9. package/dist/harness.js +51 -0
  10. package/dist/index.js +1133 -0
  11. package/dist/media-understanding-provider.js +335 -0
  12. package/dist/models-B9DhrIwD.js +110 -0
  13. package/dist/node-cli-sessions-De4_DuFw.js +1216 -0
  14. package/dist/plugin-activation-BlMuJeXz.js +452 -0
  15. package/dist/prompt-overlay.js +12 -0
  16. package/dist/protocol-C9UWI98H.js +9 -0
  17. package/dist/protocol-validators-BGBspNmF.js +5988 -0
  18. package/dist/provider-catalog.js +84 -0
  19. package/dist/provider-discovery.js +33 -0
  20. package/dist/provider.js +150 -0
  21. package/dist/rate-limit-cache-CHuacE27.js +24 -0
  22. package/dist/request-CTQKUxaa.js +89 -0
  23. package/dist/rolldown-runtime-DUslC3ob.js +14 -0
  24. package/dist/run-attempt-DqV2OU1R.js +5366 -0
  25. package/dist/session-binding-3PzU7ZTW.js +222 -0
  26. package/dist/shared-client-Cnyr9dyT.js +631 -0
  27. package/dist/side-question-CP5XlA0U.js +667 -0
  28. package/dist/test-api.js +45 -0
  29. package/dist/thread-lifecycle-DBJetBuV.js +1561 -0
  30. package/dist/vision-tools-Cl_5a93K.js +1379 -0
  31. package/doctor-contract-api.test.ts +44 -0
  32. package/doctor-contract-api.ts +68 -0
  33. package/harness.ts +72 -0
  34. package/index.test.ts +230 -0
  35. package/index.ts +66 -0
  36. package/klaw.plugin.json +24 -85
  37. package/media-understanding-provider.test.ts +486 -0
  38. package/media-understanding-provider.ts +521 -0
  39. package/package.json +3 -3
  40. package/prompt-overlay-runtime-contract.test.ts +48 -0
  41. package/prompt-overlay.ts +21 -0
  42. package/provider-catalog.ts +83 -0
  43. package/provider-discovery.ts +45 -0
  44. package/provider.test.ts +384 -0
  45. package/provider.ts +243 -0
  46. package/src/app-server/app-inventory-cache.test.ts +176 -0
  47. package/src/app-server/app-inventory-cache.ts +324 -0
  48. package/src/app-server/approval-bridge.test.ts +1471 -0
  49. package/src/app-server/approval-bridge.ts +1211 -0
  50. package/src/app-server/auth-bridge.test.ts +1449 -0
  51. package/src/app-server/auth-bridge.ts +614 -0
  52. package/src/app-server/auth-profile-runtime-contract.test.ts +239 -0
  53. package/src/app-server/capabilities.ts +27 -0
  54. package/src/app-server/client-factory.ts +24 -0
  55. package/src/app-server/client.test.ts +563 -0
  56. package/src/app-server/client.ts +715 -0
  57. package/src/app-server/compact.test.ts +710 -0
  58. package/src/app-server/compact.ts +500 -0
  59. package/src/app-server/computer-use.test.ts +788 -0
  60. package/src/app-server/computer-use.ts +683 -0
  61. package/src/app-server/config.test.ts +879 -0
  62. package/src/app-server/config.ts +1038 -0
  63. package/src/app-server/context-engine-projection.test.ts +252 -0
  64. package/src/app-server/context-engine-projection.ts +403 -0
  65. package/src/app-server/delivery-no-reply-runtime-contract.test.ts +80 -0
  66. package/src/app-server/dynamic-tool-diagnostics.ts +73 -0
  67. package/src/app-server/dynamic-tool-profile.ts +69 -0
  68. package/src/app-server/dynamic-tools.test.ts +1302 -0
  69. package/src/app-server/dynamic-tools.ts +623 -0
  70. package/src/app-server/elicitation-bridge.test.ts +1056 -0
  71. package/src/app-server/elicitation-bridge.ts +783 -0
  72. package/src/app-server/event-projector.test.ts +2668 -0
  73. package/src/app-server/event-projector.ts +2057 -0
  74. package/src/app-server/image-payload-sanitizer.test.ts +49 -0
  75. package/src/app-server/image-payload-sanitizer.ts +167 -0
  76. package/src/app-server/klaw-owned-tool-runtime-contract.test.ts +456 -0
  77. package/src/app-server/local-runtime-attribution.ts +39 -0
  78. package/src/app-server/managed-binary.test.ts +139 -0
  79. package/src/app-server/managed-binary.ts +193 -0
  80. package/src/app-server/models.test.ts +246 -0
  81. package/src/app-server/models.ts +172 -0
  82. package/src/app-server/native-hook-relay.test.ts +271 -0
  83. package/src/app-server/native-hook-relay.ts +150 -0
  84. package/src/app-server/native-subagent-task-mirror.test.ts +573 -0
  85. package/src/app-server/native-subagent-task-mirror.ts +497 -0
  86. package/src/app-server/outcome-fallback-runtime-contract.test.ts +404 -0
  87. package/src/app-server/plugin-activation.test.ts +336 -0
  88. package/src/app-server/plugin-activation.ts +283 -0
  89. package/src/app-server/plugin-app-cache-key.ts +74 -0
  90. package/src/app-server/plugin-approval-roundtrip.ts +122 -0
  91. package/src/app-server/plugin-inventory.test.ts +355 -0
  92. package/src/app-server/plugin-inventory.ts +357 -0
  93. package/src/app-server/plugin-thread-config.test.ts +865 -0
  94. package/src/app-server/plugin-thread-config.ts +455 -0
  95. package/src/app-server/protocol-generated/json/DynamicToolCallParams.json +33 -0
  96. package/src/app-server/protocol-generated/json/v2/ErrorNotification.json +199 -0
  97. package/src/app-server/protocol-generated/json/v2/GetAccountResponse.json +102 -0
  98. package/src/app-server/protocol-generated/json/v2/ModelListResponse.json +227 -0
  99. package/src/app-server/protocol-generated/json/v2/ThreadResumeResponse.json +2630 -0
  100. package/src/app-server/protocol-generated/json/v2/ThreadStartResponse.json +2630 -0
  101. package/src/app-server/protocol-generated/json/v2/TurnCompletedNotification.json +1659 -0
  102. package/src/app-server/protocol-generated/json/v2/TurnStartResponse.json +1655 -0
  103. package/src/app-server/protocol-validators.test.ts +75 -0
  104. package/src/app-server/protocol-validators.ts +203 -0
  105. package/src/app-server/protocol.ts +520 -0
  106. package/src/app-server/rate-limit-cache.ts +48 -0
  107. package/src/app-server/rate-limits.test.ts +202 -0
  108. package/src/app-server/rate-limits.ts +583 -0
  109. package/src/app-server/request.ts +73 -0
  110. package/src/app-server/run-attempt.context-engine.test.ts +1004 -0
  111. package/src/app-server/run-attempt.test.ts +9477 -0
  112. package/src/app-server/run-attempt.ts +4683 -0
  113. package/src/app-server/run-attempt.vision-tools.test.ts +35 -0
  114. package/src/app-server/schema-normalization-runtime-contract.test.ts +206 -0
  115. package/src/app-server/session-binding.test.ts +303 -0
  116. package/src/app-server/session-binding.ts +398 -0
  117. package/src/app-server/session-history.ts +44 -0
  118. package/src/app-server/shared-client.test.ts +589 -0
  119. package/src/app-server/shared-client.ts +289 -0
  120. package/src/app-server/side-question.test.ts +1175 -0
  121. package/src/app-server/side-question.ts +1007 -0
  122. package/src/app-server/test-support.ts +48 -0
  123. package/src/app-server/thread-lifecycle.test.ts +447 -0
  124. package/src/app-server/thread-lifecycle.ts +939 -0
  125. package/src/app-server/thread-lifecycle.user-mcp-servers.test.ts +442 -0
  126. package/src/app-server/timeout.ts +9 -0
  127. package/src/app-server/tool-progress-normalization.ts +77 -0
  128. package/src/app-server/trajectory.test.ts +205 -0
  129. package/src/app-server/trajectory.ts +365 -0
  130. package/src/app-server/transcript-mirror.test.ts +524 -0
  131. package/src/app-server/transcript-mirror.ts +208 -0
  132. package/src/app-server/transcript-repair-runtime-contract.test.ts +44 -0
  133. package/src/app-server/transport-stdio.test.ts +171 -0
  134. package/src/app-server/transport-stdio.ts +107 -0
  135. package/src/app-server/transport-websocket.test.ts +69 -0
  136. package/src/app-server/transport-websocket.ts +90 -0
  137. package/src/app-server/transport.ts +117 -0
  138. package/src/app-server/user-input-bridge.test.ts +249 -0
  139. package/src/app-server/user-input-bridge.ts +316 -0
  140. package/src/app-server/version.ts +4 -0
  141. package/src/app-server/vision-tools.ts +12 -0
  142. package/src/command-account.ts +544 -0
  143. package/src/command-formatters.ts +425 -0
  144. package/src/command-handlers.ts +2004 -0
  145. package/src/command-rpc.test.ts +16 -0
  146. package/src/command-rpc.ts +142 -0
  147. package/src/commands.test.ts +3312 -0
  148. package/src/commands.ts +65 -0
  149. package/src/conversation-binding-data.ts +124 -0
  150. package/src/conversation-binding.test.ts +599 -0
  151. package/src/conversation-binding.ts +561 -0
  152. package/src/conversation-control.test.ts +126 -0
  153. package/src/conversation-control.ts +303 -0
  154. package/src/conversation-turn-collector.test.ts +191 -0
  155. package/src/conversation-turn-collector.ts +186 -0
  156. package/src/conversation-turn-input.test.ts +141 -0
  157. package/src/conversation-turn-input.ts +106 -0
  158. package/src/manifest.test.ts +20 -0
  159. package/src/migration/apply.ts +501 -0
  160. package/src/migration/helpers.ts +55 -0
  161. package/src/migration/plan.ts +461 -0
  162. package/src/migration/provider.test.ts +1741 -0
  163. package/src/migration/provider.ts +41 -0
  164. package/src/migration/source.ts +643 -0
  165. package/src/migration/targets.ts +25 -0
  166. package/src/node-cli-sessions.test.ts +180 -0
  167. package/src/node-cli-sessions.ts +711 -0
  168. package/test-api.ts +82 -0
  169. package/tsconfig.json +16 -0
  170. package/doctor-contract-api.js +0 -7
  171. package/harness.js +0 -7
  172. package/index.js +0 -7
  173. package/media-understanding-provider.js +0 -7
  174. package/prompt-overlay.js +0 -7
  175. package/provider-catalog.js +0 -7
  176. package/provider-discovery.js +0 -7
  177. package/provider.js +0 -7
  178. package/test-api.js +0 -7
@@ -0,0 +1,486 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { buildCodexMediaUnderstandingProvider } from "./media-understanding-provider.js";
3
+ import type { CodexAppServerClient } from "./src/app-server/client.js";
4
+ import type { CodexServerNotification, JsonValue } from "./src/app-server/protocol.js";
5
+
6
+ function codexModel(inputModalities: string[] = ["text", "image"]) {
7
+ return {
8
+ id: "gpt-5.4",
9
+ model: "gpt-5.4",
10
+ upgrade: null,
11
+ upgradeInfo: null,
12
+ availabilityNux: null,
13
+ displayName: "gpt-5.4",
14
+ description: "GPT-5.4",
15
+ hidden: false,
16
+ supportedReasoningEfforts: [{ reasoningEffort: "low", description: "fast" }],
17
+ defaultReasoningEffort: "low",
18
+ inputModalities,
19
+ supportsPersonality: false,
20
+ additionalSpeedTiers: [],
21
+ isDefault: true,
22
+ };
23
+ }
24
+
25
+ function threadStartResult() {
26
+ return {
27
+ thread: {
28
+ id: "thread-1",
29
+ sessionId: "session-1",
30
+ forkedFromId: null,
31
+ preview: "",
32
+ ephemeral: true,
33
+ modelProvider: "openai",
34
+ createdAt: 1,
35
+ updatedAt: 1,
36
+ status: { type: "idle" },
37
+ path: null,
38
+ cwd: "/tmp/klaw-agent",
39
+ cliVersion: "0.125.0",
40
+ source: "unknown",
41
+ agentNickname: null,
42
+ agentRole: null,
43
+ gitInfo: null,
44
+ name: null,
45
+ turns: [],
46
+ },
47
+ model: "gpt-5.4",
48
+ modelProvider: "openai",
49
+ serviceTier: null,
50
+ cwd: "/tmp/klaw-agent",
51
+ instructionSources: [],
52
+ approvalPolicy: "on-request",
53
+ approvalsReviewer: "user",
54
+ sandbox: { type: "dangerFullAccess" },
55
+ permissionProfile: null,
56
+ reasoningEffort: null,
57
+ };
58
+ }
59
+
60
+ function turnStartResult(status = "inProgress", items: JsonValue[] = []) {
61
+ return {
62
+ turn: {
63
+ id: "turn-1",
64
+ status,
65
+ items,
66
+ error: null,
67
+ startedAt: null,
68
+ completedAt: null,
69
+ durationMs: null,
70
+ },
71
+ };
72
+ }
73
+
74
+ function createFakeClient(options?: {
75
+ inputModalities?: string[];
76
+ completeWithItems?: boolean;
77
+ notifyError?: string;
78
+ approvalRequestMethod?: string;
79
+ responseText?: string;
80
+ }) {
81
+ const notifications = new Set<(notification: CodexServerNotification) => void>();
82
+ const requestHandlers = new Set<(request: { method: string }) => JsonValue | undefined>();
83
+ const requests: Array<{ method: string; params?: JsonValue }> = [];
84
+ const approvalResponses: JsonValue[] = [];
85
+ const request = vi.fn(async (method: string, params?: JsonValue) => {
86
+ requests.push({ method, params });
87
+ if (method === "model/list") {
88
+ return {
89
+ data: [codexModel(options?.inputModalities)],
90
+ nextCursor: null,
91
+ };
92
+ }
93
+ if (method === "thread/start") {
94
+ return threadStartResult();
95
+ }
96
+ if (method === "turn/start") {
97
+ if (options?.approvalRequestMethod) {
98
+ for (const handler of requestHandlers) {
99
+ const response = handler({ method: options.approvalRequestMethod });
100
+ if (response !== undefined) {
101
+ approvalResponses.push(response);
102
+ }
103
+ }
104
+ }
105
+ if (options?.notifyError) {
106
+ for (const notify of notifications) {
107
+ notify({
108
+ method: "error",
109
+ params: {
110
+ threadId: "thread-1",
111
+ turnId: "turn-1",
112
+ error: {
113
+ message: options.notifyError,
114
+ codexErrorInfo: null,
115
+ additionalDetails: null,
116
+ },
117
+ willRetry: false,
118
+ },
119
+ });
120
+ }
121
+ } else if (!options?.completeWithItems) {
122
+ for (const notify of notifications) {
123
+ notify({
124
+ method: "item/agentMessage/delta",
125
+ params: {
126
+ threadId: "thread-1",
127
+ turnId: "turn-1",
128
+ itemId: "msg-1",
129
+ delta: options?.responseText ?? "A red square.",
130
+ },
131
+ });
132
+ notify({
133
+ method: "turn/completed",
134
+ params: {
135
+ threadId: "thread-1",
136
+ turnId: "turn-1",
137
+ turn: turnStartResult("completed").turn,
138
+ },
139
+ });
140
+ }
141
+ }
142
+ return turnStartResult(
143
+ options?.completeWithItems ? "completed" : "inProgress",
144
+ options?.completeWithItems
145
+ ? [
146
+ {
147
+ id: "msg-1",
148
+ type: "agentMessage",
149
+ text: options?.responseText ?? "A blue circle.",
150
+ phase: null,
151
+ memoryCitation: null,
152
+ },
153
+ ]
154
+ : [],
155
+ );
156
+ }
157
+ return {};
158
+ });
159
+
160
+ const client = {
161
+ request,
162
+ addNotificationHandler(handler: (notification: CodexServerNotification) => void) {
163
+ notifications.add(handler);
164
+ return () => notifications.delete(handler);
165
+ },
166
+ addRequestHandler(handler: (request: { method: string }) => JsonValue | undefined) {
167
+ requestHandlers.add(handler);
168
+ return () => requestHandlers.delete(handler);
169
+ },
170
+ } as unknown as CodexAppServerClient;
171
+
172
+ return { client, requests, approvalResponses };
173
+ }
174
+
175
+ describe("codex media understanding provider", () => {
176
+ it("runs image understanding through a bounded Codex app-server turn", async () => {
177
+ const { client, requests } = createFakeClient();
178
+ const provider = buildCodexMediaUnderstandingProvider({
179
+ clientFactory: async () => client,
180
+ });
181
+
182
+ const result = await provider.describeImage?.({
183
+ buffer: Buffer.from("image-bytes"),
184
+ fileName: "image.png",
185
+ mime: "image/png",
186
+ provider: "codex",
187
+ model: "gpt-5.4",
188
+ prompt: "Describe briefly.",
189
+ timeoutMs: 30_000,
190
+ cfg: {},
191
+ agentDir: "/tmp/klaw-agent",
192
+ });
193
+
194
+ expect(result).toEqual({ text: "A red square.", model: "gpt-5.4" });
195
+ expect(requests.map((entry) => entry.method)).toEqual([
196
+ "model/list",
197
+ "thread/start",
198
+ "turn/start",
199
+ ]);
200
+ expect(requests[1]?.params).toEqual({
201
+ model: "gpt-5.4",
202
+ modelProvider: "openai",
203
+ cwd: "/tmp/klaw-agent",
204
+ approvalPolicy: "on-request",
205
+ sandbox: "read-only",
206
+ serviceName: "Klaw",
207
+ developerInstructions:
208
+ "You are Klaw's bounded image-understanding worker. Describe only the provided image content. Do not call tools, edit files, or ask follow-up questions.",
209
+ dynamicTools: [],
210
+ experimentalRawEvents: true,
211
+ ephemeral: true,
212
+ persistExtendedHistory: false,
213
+ });
214
+ expect(requests[2]?.params).toEqual({
215
+ threadId: "thread-1",
216
+ input: [
217
+ { type: "text", text: "Describe briefly.", text_elements: [] },
218
+ { type: "image", url: "data:image/png;base64,aW1hZ2UtYnl0ZXM=" },
219
+ ],
220
+ cwd: "/tmp/klaw-agent",
221
+ approvalPolicy: "on-request",
222
+ model: "gpt-5.4",
223
+ effort: "low",
224
+ });
225
+ });
226
+
227
+ it("declines approval requests during image understanding", async () => {
228
+ const { client, approvalResponses } = createFakeClient({
229
+ approvalRequestMethod: "item/permissions/requestApproval",
230
+ });
231
+ const provider = buildCodexMediaUnderstandingProvider({
232
+ clientFactory: async () => client,
233
+ });
234
+
235
+ await provider.describeImage?.({
236
+ buffer: Buffer.from("image-bytes"),
237
+ fileName: "image.png",
238
+ mime: "image/png",
239
+ provider: "codex",
240
+ model: "gpt-5.4",
241
+ prompt: "Describe briefly.",
242
+ timeoutMs: 30_000,
243
+ cfg: {},
244
+ agentDir: "/tmp/klaw-agent",
245
+ });
246
+
247
+ expect(approvalResponses).toEqual([{ permissions: {}, scope: "turn" }]);
248
+ });
249
+
250
+ it("extracts text from terminal turn items", async () => {
251
+ const { client } = createFakeClient({ completeWithItems: true });
252
+ const provider = buildCodexMediaUnderstandingProvider({
253
+ clientFactory: async () => client,
254
+ });
255
+
256
+ const result = await provider.describeImages?.({
257
+ images: [{ buffer: Buffer.from("image-bytes"), fileName: "image.png", mime: "image/png" }],
258
+ provider: "codex",
259
+ model: "gpt-5.4",
260
+ prompt: "Describe briefly.",
261
+ timeoutMs: 30_000,
262
+ cfg: {},
263
+ agentDir: "/tmp/klaw-agent",
264
+ });
265
+
266
+ expect(result).toEqual({ text: "A blue circle.", model: "gpt-5.4" });
267
+ });
268
+
269
+ it("rejects text-only Codex app-server models before starting a turn", async () => {
270
+ const { client, requests } = createFakeClient({ inputModalities: ["text"] });
271
+ const provider = buildCodexMediaUnderstandingProvider({
272
+ clientFactory: async () => client,
273
+ });
274
+
275
+ await expect(
276
+ provider.describeImage?.({
277
+ buffer: Buffer.from("image-bytes"),
278
+ fileName: "image.png",
279
+ mime: "image/png",
280
+ provider: "codex",
281
+ model: "gpt-5.4",
282
+ timeoutMs: 30_000,
283
+ cfg: {},
284
+ agentDir: "/tmp/klaw-agent",
285
+ }),
286
+ ).rejects.toThrow("Codex app-server model does not support images: gpt-5.4");
287
+ expect(requests.map((entry) => entry.method)).toEqual(["model/list"]);
288
+ });
289
+
290
+ it("surfaces Codex app-server turn errors", async () => {
291
+ const { client } = createFakeClient({ notifyError: "vision unavailable" });
292
+ const provider = buildCodexMediaUnderstandingProvider({
293
+ clientFactory: async () => client,
294
+ });
295
+
296
+ await expect(
297
+ provider.describeImage?.({
298
+ buffer: Buffer.from("image-bytes"),
299
+ fileName: "image.png",
300
+ mime: "image/png",
301
+ provider: "codex",
302
+ model: "gpt-5.4",
303
+ timeoutMs: 30_000,
304
+ cfg: {},
305
+ agentDir: "/tmp/klaw-agent",
306
+ }),
307
+ ).rejects.toThrow("vision unavailable");
308
+ });
309
+
310
+ it("runs structured extraction through the same bounded Codex app-server path", async () => {
311
+ const { client, requests } = createFakeClient({
312
+ responseText: '{"summary":"red square","tags":["shape"]}',
313
+ });
314
+ const provider = buildCodexMediaUnderstandingProvider({
315
+ clientFactory: async () => client,
316
+ });
317
+
318
+ const result = await provider.extractStructured?.({
319
+ input: [
320
+ { type: "text", text: "Extract searchable evidence." },
321
+ {
322
+ type: "image",
323
+ buffer: Buffer.from("image-bytes"),
324
+ fileName: "image.png",
325
+ mime: "image/png",
326
+ },
327
+ ],
328
+ instructions: "Return a compact evidence object.",
329
+ schemaName: "example.media",
330
+ jsonSchema: {
331
+ type: "object",
332
+ properties: {
333
+ summary: { type: "string" },
334
+ tags: { type: "array", items: { type: "string" } },
335
+ },
336
+ required: ["summary"],
337
+ },
338
+ provider: "codex",
339
+ model: "gpt-5.4",
340
+ timeoutMs: 30_000,
341
+ cfg: {},
342
+ agentDir: "/tmp/klaw-agent",
343
+ });
344
+
345
+ expect(result).toEqual({
346
+ text: '{"summary":"red square","tags":["shape"]}',
347
+ parsed: { summary: "red square", tags: ["shape"] },
348
+ model: "gpt-5.4",
349
+ provider: "codex",
350
+ contentType: "json",
351
+ });
352
+ expect(requests.map((entry) => entry.method)).toEqual([
353
+ "model/list",
354
+ "thread/start",
355
+ "turn/start",
356
+ ]);
357
+ expect(requests[1]?.params).toEqual({
358
+ model: "gpt-5.4",
359
+ modelProvider: "openai",
360
+ cwd: "/tmp/klaw-agent",
361
+ approvalPolicy: "on-request",
362
+ sandbox: "read-only",
363
+ serviceName: "Klaw",
364
+ developerInstructions:
365
+ "You are Klaw's bounded structured-extraction worker. Return only the requested extraction. Do not call tools, edit files, ask follow-up questions, or include secrets.",
366
+ dynamicTools: [],
367
+ experimentalRawEvents: true,
368
+ ephemeral: true,
369
+ persistExtendedHistory: false,
370
+ });
371
+ const turnParams = requests[2]?.params as
372
+ | {
373
+ threadId?: unknown;
374
+ approvalPolicy?: unknown;
375
+ model?: unknown;
376
+ input?: Array<{ type?: unknown; text?: unknown; text_elements?: unknown; url?: unknown }>;
377
+ cwd?: unknown;
378
+ effort?: unknown;
379
+ }
380
+ | undefined;
381
+ expect(turnParams?.threadId).toBe("thread-1");
382
+ expect(turnParams?.approvalPolicy).toBe("on-request");
383
+ expect(turnParams?.model).toBe("gpt-5.4");
384
+ expect(turnParams?.cwd).toBe("/tmp/klaw-agent");
385
+ expect(turnParams?.effort).toBe("low");
386
+ expect(turnParams?.input).toHaveLength(3);
387
+ expect(turnParams?.input?.[0]?.type).toBe("text");
388
+ expect(turnParams?.input?.[0]?.text).toContain("Return valid JSON only");
389
+ expect(turnParams?.input?.[0]?.text_elements).toStrictEqual([]);
390
+ expect(turnParams?.input?.[1]).toStrictEqual({
391
+ type: "text",
392
+ text: "Extract searchable evidence.",
393
+ text_elements: [],
394
+ });
395
+ expect(turnParams?.input?.[2]).toStrictEqual({
396
+ type: "image",
397
+ url: "data:image/png;base64,aW1hZ2UtYnl0ZXM=",
398
+ });
399
+ });
400
+
401
+ it("rejects text-only structured extraction before starting a turn", async () => {
402
+ const { client, requests } = createFakeClient({
403
+ inputModalities: ["text"],
404
+ responseText: '{"summary":"only text"}',
405
+ });
406
+ const provider = buildCodexMediaUnderstandingProvider({
407
+ clientFactory: async () => client,
408
+ });
409
+
410
+ await expect(
411
+ provider.extractStructured?.({
412
+ input: [{ type: "text", text: "The answer is only text." }],
413
+ instructions: "Return summary JSON.",
414
+ provider: "codex",
415
+ model: "gpt-5.4",
416
+ timeoutMs: 30_000,
417
+ cfg: {},
418
+ agentDir: "/tmp/klaw-agent",
419
+ }),
420
+ ).rejects.toThrow("Codex structured extraction requires at least one image input.");
421
+ expect(requests).toEqual([]);
422
+ });
423
+
424
+ it("returns a controlled error when structured JSON parsing fails", async () => {
425
+ const { client } = createFakeClient({ responseText: "not json" });
426
+ const provider = buildCodexMediaUnderstandingProvider({
427
+ clientFactory: async () => client,
428
+ });
429
+
430
+ await expect(
431
+ provider.extractStructured?.({
432
+ input: [
433
+ { type: "text", text: "Extract JSON." },
434
+ {
435
+ type: "image",
436
+ buffer: Buffer.from("image-bytes"),
437
+ fileName: "image.png",
438
+ mime: "image/png",
439
+ },
440
+ ],
441
+ instructions: "Return summary JSON.",
442
+ provider: "codex",
443
+ model: "gpt-5.4",
444
+ timeoutMs: 30_000,
445
+ cfg: {},
446
+ agentDir: "/tmp/klaw-agent",
447
+ }),
448
+ ).rejects.toThrow("Codex structured extraction returned invalid JSON.");
449
+ });
450
+
451
+ it("validates structured extraction JSON against the requested schema", async () => {
452
+ const { client } = createFakeClient({
453
+ responseText: '{"summary":123,"tags":["shape"]}',
454
+ });
455
+ const provider = buildCodexMediaUnderstandingProvider({
456
+ clientFactory: async () => client,
457
+ });
458
+
459
+ await expect(
460
+ provider.extractStructured?.({
461
+ input: [
462
+ { type: "text", text: "Extract JSON." },
463
+ {
464
+ type: "image",
465
+ buffer: Buffer.from("image-bytes"),
466
+ fileName: "image.png",
467
+ mime: "image/png",
468
+ },
469
+ ],
470
+ instructions: "Return summary JSON.",
471
+ jsonSchema: {
472
+ type: "object",
473
+ properties: {
474
+ summary: { type: "string" },
475
+ },
476
+ required: ["summary"],
477
+ },
478
+ provider: "codex",
479
+ model: "gpt-5.4",
480
+ timeoutMs: 30_000,
481
+ cfg: {},
482
+ agentDir: "/tmp/klaw-agent",
483
+ }),
484
+ ).rejects.toThrow("Codex structured extraction JSON did not match schema");
485
+ });
486
+ });