| author | |
| committer | |
| log | 3fd21df62da3191f1abf12a0cd74df6de9ed30a2 |
| tree | 8a79cd92eb2343fd8f8f2e9465b7ac2ce4e652d4 |
| parent | 5f225395637cb347cfe416c649825a8c7c210014 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>4 files changed, 66 insertions(+), 28 deletions(-)
apps/server/src/provider/Layers/CodexAdapter.test.ts+48| ... | ... | @@ -448,6 +448,54 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => { |
| 448 | 448 | }), |
| 449 | 449 | ); |
| 450 | 450 | |
| 451 | it.effect("passes image attachments to Codex by path instead of base64", () => | |
| 452 | Effect.gen(function* () { | |
| 453 | const adapter = yield* CodexAdapter; | |
| 454 | const threadId = asThreadId("thread-image-attachment"); | |
| 455 | yield* adapter.startSession({ | |
| 456 | provider: ProviderDriverKind.make("codex"), | |
| 457 | threadId, | |
| 458 | runtimeMode: "full-access", | |
| 459 | }); | |
| 460 | const runtime = sessionRuntimeFactory.lastRuntime; | |
| 461 | NodeAssert.ok(runtime); | |
| 462 | runtime.sendTurnImpl.mockClear(); | |
| 463 | ||
| 464 | const serverConfig = yield* ServerConfig; | |
| 465 | const attachmentsDir = serverConfig.attachmentsDir; | |
| 466 | const attachmentId = "attachment-local-image-1"; | |
| 467 | const attachmentPath = NodePath.join(attachmentsDir, `${attachmentId}.png`); | |
| 468 | NodeFS.writeFileSync(attachmentPath, Buffer.alloc(4, 0x89)); | |
| 469 | ||
| 470 | try { | |
| 471 | yield* adapter.sendTurn({ | |
| 472 | threadId, | |
| 473 | input: "Use this image.", | |
| 474 | attachments: [ | |
| 475 | { | |
| 476 | type: "image", | |
| 477 | id: attachmentId, | |
| 478 | name: "generated.png", | |
| 479 | mimeType: "image/png", | |
| 480 | sizeBytes: NodeFS.statSync(attachmentPath).size, | |
| 481 | }, | |
| 482 | ], | |
| 483 | }); | |
| 484 | ||
| 485 | const input = runtime.sendTurnImpl.mock.calls[0]?.[0]; | |
| 486 | NodeAssert.ok(input); | |
| 487 | NodeAssert.deepStrictEqual(input.attachments, [ | |
| 488 | { | |
| 489 | type: "localImage", | |
| 490 | path: attachmentPath, | |
| 491 | }, | |
| 492 | ]); | |
| 493 | } finally { | |
| 494 | NodeFS.rmSync(attachmentPath, { force: true }); | |
| 495 | } | |
| 496 | }), | |
| 497 | ); | |
| 498 | ||
| 451 | 499 | it.effect("passes configured launch args into the session runtime", () => { |
| 452 | 500 | const runtimeFactory = makeRuntimeFactory(); |
| 453 | 501 | const layer = Layer.effect( |
apps/server/src/provider/Layers/CodexAdapter.ts+8-18| ... | ... | @@ -36,7 +36,6 @@ import * as NodeCrypto from "node:crypto"; |
| 36 | 36 | import * as Crypto from "effect/Crypto"; |
| 37 | 37 | import * as Exit from "effect/Exit"; |
| 38 | 38 | import * as Fiber from "effect/Fiber"; |
| 39 | import * as FileSystem from "effect/FileSystem"; | |
| 40 | 39 | import * as Queue from "effect/Queue"; |
| 41 | 40 | import * as Schema from "effect/Schema"; |
| 42 | 41 | import * as Scope from "effect/Scope"; |
| ... | ... | @@ -67,6 +66,7 @@ import { |
| 67 | 66 | makeCodexSessionRuntime, |
| 68 | 67 | type CodexSessionRuntimeError, |
| 69 | 68 | type CodexSessionRuntimeOptions, |
| 69 | type CodexSessionRuntimeSendTurnInput, | |
| 70 | 70 | type CodexSessionRuntimeShape, |
| 71 | 71 | } from "./CodexSessionRuntime.ts"; |
| 72 | 72 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| ... | ... | @@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2218 | 2218 | options?: CodexAdapterLiveOptions, |
| 2219 | 2219 | ) { |
| 2220 | 2220 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); |
| 2221 | const fileSystem = yield* FileSystem.FileSystem; | |
| 2222 | 2221 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 2223 | 2222 | const crypto = yield* Crypto.Crypto; |
| 2224 | 2223 | const serverConfig = yield* Effect.service(ServerConfig); |
| ... | ... | @@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2491 | 2490 | detail: `Invalid attachment id '${attachment.id}'.`, |
| 2492 | 2491 | }); |
| 2493 | 2492 | } |
| 2494 | const bytes = yield* fileSystem.readFile(attachmentPath).pipe( | |
| 2495 | Effect.mapError( | |
| 2496 | (cause) => | |
| 2497 | new ProviderAdapterRequestError({ | |
| 2498 | provider: PROVIDER, | |
| 2499 | method: "turn/start", | |
| 2500 | detail: `Failed to read attachment file: ${cause.message}.`, | |
| 2501 | cause, | |
| 2502 | }), | |
| 2503 | ), | |
| 2504 | ); | |
| 2505 | 2493 | return { |
| 2506 | type: "image" as const, | |
| 2507 | url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`, | |
| 2494 | type: "localImage" as const, | |
| 2495 | path: attachmentPath, | |
| 2508 | 2496 | }; |
| 2509 | 2497 | }); |
| 2510 | 2498 | |
| 2511 | 2499 | const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { |
| 2512 | // Codex ingests images only. Anything else would be base64-encoded as an | |
| 2513 | // image and rejected or misread; generic files reach the agent through the | |
| 2514 | // path line ProviderService puts in the prompt. | |
| 2500 | // Codex ingests images only. Anything else would be inlined as an image | |
| 2501 | // and rejected or misread; generic files reach the agent through the path | |
| 2502 | // line ProviderService puts in the prompt. Images are passed by path | |
| 2503 | // instead of base64 so the turn/start request does not scale with file | |
| 2504 | // size; the CLI reads the file itself. | |
| 2515 | 2505 | const codexAttachments = yield* Effect.forEach( |
| 2516 | 2506 | (input.attachments ?? []).filter((attachment) => attachment.type === "image"), |
| 2517 | 2507 | (attachment) => resolveAttachment(input, attachment), |
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+6-6| ... | ... | @@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => { |
| 179 | 179 | runtimeMode: "full-access", |
| 180 | 180 | attachments: [ |
| 181 | 181 | { |
| 182 | type: "image", | |
| 183 | url: { secret } as unknown as string, | |
| 182 | type: "localImage", | |
| 183 | path: { secret } as unknown as string, | |
| 184 | 184 | }, |
| 185 | 185 | ], |
| 186 | 186 | }).pipe(Effect.flip), |
| ... | ... | @@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => { |
| 248 | 248 | interactionMode: "default", |
| 249 | 249 | attachments: [ |
| 250 | 250 | { |
| 251 | type: "image", | |
| 252 | url: "data:image/png;base64,abc", | |
| 251 | type: "localImage", | |
| 252 | path: "/tmp/generated.png", | |
| 253 | 253 | }, |
| 254 | 254 | ], |
| 255 | 255 | }), |
| ... | ... | @@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => { |
| 268 | 268 | text: "Implement it", |
| 269 | 269 | }, |
| 270 | 270 | { |
| 271 | type: "image", | |
| 272 | url: "data:image/png;base64,abc", | |
| 271 | type: "localImage", | |
| 272 | path: "/tmp/generated.png", | |
| 273 | 273 | }, |
| 274 | 274 | ], |
| 275 | 275 | model: "gpt-5.3-codex", |
apps/server/src/provider/Layers/CodexSessionRuntime.ts+4-4| ... | ... | @@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions { |
| 186 | 186 | export interface CodexSessionRuntimeSendTurnInput { |
| 187 | 187 | readonly input?: string; |
| 188 | 188 | readonly attachments?: ReadonlyArray<{ |
| 189 | readonly type: "image"; | |
| 190 | readonly url: string; | |
| 189 | readonly type: "localImage"; | |
| 190 | readonly path: string; | |
| 191 | 191 | }>; |
| 192 | 192 | readonly model?: string; |
| 193 | 193 | readonly serviceTier?: CodexServiceTier | undefined; |
| ... | ... | @@ -614,8 +614,8 @@ export function buildTurnStartParams(input: { |
| 614 | 614 | readonly runtimeMode: RuntimeMode; |
| 615 | 615 | readonly prompt?: string; |
| 616 | 616 | readonly attachments?: ReadonlyArray<{ |
| 617 | readonly type: "image"; | |
| 618 | readonly url: string; | |
| 617 | readonly type: "localImage"; | |
| 618 | readonly path: string; | |
| 619 | 619 | }>; |
| 620 | 620 | readonly model?: string; |
| 621 | 621 | readonly serviceTier?: CodexServiceTier; |