| 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,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 | it.effect("passes configured launch args into the session runtime", () => { | 499 | it.effect("passes configured launch args into the session runtime", () => { |
| 452 | const runtimeFactory = makeRuntimeFactory(); | 500 | const runtimeFactory = makeRuntimeFactory(); |
| 453 | const layer = Layer.effect( | 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,7 +36,6 @@ import * as NodeCrypto from "node:crypto"; |
| 36 | import * as Crypto from "effect/Crypto"; | 36 | import * as Crypto from "effect/Crypto"; |
| 37 | import * as Exit from "effect/Exit"; | 37 | import * as Exit from "effect/Exit"; |
| 38 | import * as Fiber from "effect/Fiber"; | 38 | import * as Fiber from "effect/Fiber"; |
| 39 | import * as FileSystem from "effect/FileSystem"; | ||
| 40 | import * as Queue from "effect/Queue"; | 39 | import * as Queue from "effect/Queue"; |
| 41 | import * as Schema from "effect/Schema"; | 40 | import * as Schema from "effect/Schema"; |
| 42 | import * as Scope from "effect/Scope"; | 41 | import * as Scope from "effect/Scope"; |
| ... | @@ -67,6 +66,7 @@ import { | ... | @@ -67,6 +66,7 @@ import { |
| 67 | makeCodexSessionRuntime, | 66 | makeCodexSessionRuntime, |
| 68 | type CodexSessionRuntimeError, | 67 | type CodexSessionRuntimeError, |
| 69 | type CodexSessionRuntimeOptions, | 68 | type CodexSessionRuntimeOptions, |
| 69 | type CodexSessionRuntimeSendTurnInput, | ||
| 70 | type CodexSessionRuntimeShape, | 70 | type CodexSessionRuntimeShape, |
| 71 | } from "./CodexSessionRuntime.ts"; | 71 | } from "./CodexSessionRuntime.ts"; |
| 72 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; | 72 | import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; |
| ... | @@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -2218,7 +2218,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2218 | options?: CodexAdapterLiveOptions, | 2218 | options?: CodexAdapterLiveOptions, |
| 2219 | ) { | 2219 | ) { |
| 2220 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); | 2220 | const boundInstanceId = options?.instanceId ?? ProviderInstanceId.make("codex"); |
| 2221 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 2222 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 2221 | const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 2223 | const crypto = yield* Crypto.Crypto; | 2222 | const crypto = yield* Crypto.Crypto; |
| 2224 | const serverConfig = yield* Effect.service(ServerConfig); | 2223 | const serverConfig = yield* Effect.service(ServerConfig); |
| ... | @@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( | ... | @@ -2491,27 +2490,18 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* ( |
| 2491 | detail: `Invalid attachment id '${attachment.id}'.`, | 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 | return { | 2493 | return { |
| 2506 | type: "image" as const, | 2494 | type: "localImage" as const, |
| 2507 | url: `data:${attachment.mimeType};base64,${Buffer.from(bytes).toString("base64")}`, | 2495 | path: attachmentPath, |
| 2508 | }; | 2496 | }; |
| 2509 | }); | 2497 | }); |
| 2510 | 2498 | ||
| 2511 | const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { | 2499 | const sendTurn: CodexAdapterShape["sendTurn"] = Effect.fn("sendTurn")(function* (input) { |
| 2512 | // Codex ingests images only. Anything else would be base64-encoded as an | 2500 | // Codex ingests images only. Anything else would be inlined as an image |
| 2513 | // image and rejected or misread; generic files reach the agent through the | 2501 | // and rejected or misread; generic files reach the agent through the path |
| 2514 | // path line ProviderService puts in the prompt. | 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 | const codexAttachments = yield* Effect.forEach( | 2505 | const codexAttachments = yield* Effect.forEach( |
| 2516 | (input.attachments ?? []).filter((attachment) => attachment.type === "image"), | 2506 | (input.attachments ?? []).filter((attachment) => attachment.type === "image"), |
| 2517 | (attachment) => resolveAttachment(input, attachment), | 2507 | (attachment) => resolveAttachment(input, attachment), |
apps/server/src/provider/Layers/CodexSessionRuntime.test.ts+6-6| ... | @@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => { | ... | @@ -179,8 +179,8 @@ describe("buildTurnStartParams", () => { |
| 179 | runtimeMode: "full-access", | 179 | runtimeMode: "full-access", |
| 180 | attachments: [ | 180 | attachments: [ |
| 181 | { | 181 | { |
| 182 | type: "image", | 182 | type: "localImage", |
| 183 | url: { secret } as unknown as string, | 183 | path: { secret } as unknown as string, |
| 184 | }, | 184 | }, |
| 185 | ], | 185 | ], |
| 186 | }).pipe(Effect.flip), | 186 | }).pipe(Effect.flip), |
| ... | @@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => { | ... | @@ -248,8 +248,8 @@ describe("buildTurnStartParams", () => { |
| 248 | interactionMode: "default", | 248 | interactionMode: "default", |
| 249 | attachments: [ | 249 | attachments: [ |
| 250 | { | 250 | { |
| 251 | type: "image", | 251 | type: "localImage", |
| 252 | url: "data:image/png;base64,abc", | 252 | path: "/tmp/generated.png", |
| 253 | }, | 253 | }, |
| 254 | ], | 254 | ], |
| 255 | }), | 255 | }), |
| ... | @@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => { | ... | @@ -268,8 +268,8 @@ describe("buildTurnStartParams", () => { |
| 268 | text: "Implement it", | 268 | text: "Implement it", |
| 269 | }, | 269 | }, |
| 270 | { | 270 | { |
| 271 | type: "image", | 271 | type: "localImage", |
| 272 | url: "data:image/png;base64,abc", | 272 | path: "/tmp/generated.png", |
| 273 | }, | 273 | }, |
| 274 | ], | 274 | ], |
| 275 | model: "gpt-5.3-codex", | 275 | model: "gpt-5.3-codex", |
apps/server/src/provider/Layers/CodexSessionRuntime.ts+4-4| ... | @@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions { | ... | @@ -186,8 +186,8 @@ export interface CodexSessionRuntimeOptions { |
| 186 | export interface CodexSessionRuntimeSendTurnInput { | 186 | export interface CodexSessionRuntimeSendTurnInput { |
| 187 | readonly input?: string; | 187 | readonly input?: string; |
| 188 | readonly attachments?: ReadonlyArray<{ | 188 | readonly attachments?: ReadonlyArray<{ |
| 189 | readonly type: "image"; | 189 | readonly type: "localImage"; |
| 190 | readonly url: string; | 190 | readonly path: string; |
| 191 | }>; | 191 | }>; |
| 192 | readonly model?: string; | 192 | readonly model?: string; |
| 193 | readonly serviceTier?: CodexServiceTier | undefined; | 193 | readonly serviceTier?: CodexServiceTier | undefined; |
| ... | @@ -614,8 +614,8 @@ export function buildTurnStartParams(input: { | ... | @@ -614,8 +614,8 @@ export function buildTurnStartParams(input: { |
| 614 | readonly runtimeMode: RuntimeMode; | 614 | readonly runtimeMode: RuntimeMode; |
| 615 | readonly prompt?: string; | 615 | readonly prompt?: string; |
| 616 | readonly attachments?: ReadonlyArray<{ | 616 | readonly attachments?: ReadonlyArray<{ |
| 617 | readonly type: "image"; | 617 | readonly type: "localImage"; |
| 618 | readonly url: string; | 618 | readonly path: string; |
| 619 | }>; | 619 | }>; |
| 620 | readonly model?: string; | 620 | readonly model?: string; |
| 621 | readonly serviceTier?: CodexServiceTier; | 621 | readonly serviceTier?: CodexServiceTier; |