| author | |
| committer | |
| log | def34c28b8841002bb97a3238b82ac758cbdaa0f |
| tree | 09877d4bb294b67d3d5d915005cb2b00d0fea8a7 |
| parent | 2679d279ce4b4879290bc756cf219b10f77e1873 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>7 files changed, 182 insertions(+), 61 deletions(-)
apps/server/src/provider/Layers/AntigravityAdapter.ts+3-3| ... | @@ -786,9 +786,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi | ... | @@ -786,9 +786,9 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi |
| 786 | stopOwned, | 786 | stopOwned, |
| 787 | Effect.gen(function* () { | 787 | Effect.gen(function* () { |
| 788 | const mcp = McpProviderSession.readMcpProviderSession(input.threadId); | 788 | const mcp = McpProviderSession.readMcpProviderSession(input.threadId); |
| 789 | // The attachments dir grant lets the agent read pasted files at | 789 | // The attachments dir grant lets the agent read path-only uploads |
| 790 | // the paths ProviderService injects into the turn text. It is a | 790 | // at the paths ProviderService injects into the turn text. It is |
| 791 | // leaf directory holding only uploads. | 791 | // a leaf directory holding only uploads. |
| 792 | const runtime = yield* options.makeRuntime({ | 792 | const runtime = yield* options.makeRuntime({ |
| 793 | cwd, | 793 | cwd, |
| 794 | clientInfo: { name: "t3-code", version: "0.0.0" }, | 794 | clientInfo: { name: "t3-code", version: "0.0.0" }, |
apps/server/src/provider/Layers/ProviderService.test.ts+51| ... | @@ -4801,6 +4801,57 @@ validation.layer("ProviderServiceLive validation", (it) => { | ... | @@ -4801,6 +4801,57 @@ validation.layer("ProviderServiceLive validation", (it) => { |
| 4801 | }), | 4801 | }), |
| 4802 | ); | 4802 | ); |
| 4803 | 4803 | ||
| 4804 | it.effect("rejects a file when its path cannot fit in the prompt", () => | ||
| 4805 | Effect.gen(function* () { | ||
| 4806 | const provider = yield* ProviderService.ProviderService; | ||
| 4807 | validation.codex.sendTurn.mockClear(); | ||
| 4808 | const failure = yield* provider | ||
| 4809 | .sendTurn({ | ||
| 4810 | threadId: asThreadId("thread-file-path-context-limit"), | ||
| 4811 | input: "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS), | ||
| 4812 | attachments: [ | ||
| 4813 | { | ||
| 4814 | type: "file", | ||
| 4815 | id: "thread-attach-12345678-1234-1234-1234-123456789abc-zip", | ||
| 4816 | name: "archive.zip", | ||
| 4817 | mimeType: "application/zip", | ||
| 4818 | sizeBytes: 1024, | ||
| 4819 | }, | ||
| 4820 | ], | ||
| 4821 | }) | ||
| 4822 | .pipe(Effect.flip); | ||
| 4823 | |||
| 4824 | assert.instanceOf(failure, ProviderValidationError); | ||
| 4825 | assert.include(failure.issue, String(PROVIDER_SEND_TURN_MAX_INPUT_CHARS)); | ||
| 4826 | assert.equal(validation.codex.sendTurn.mock.calls.length, 0); | ||
| 4827 | }), | ||
| 4828 | ); | ||
| 4829 | |||
| 4830 | it.effect("sends a native image when its path cannot fit in the prompt", () => | ||
| 4831 | Effect.gen(function* () { | ||
| 4832 | const provider = yield* ProviderService.ProviderService; | ||
| 4833 | const threadId = asThreadId("thread-image-path-context-limit"); | ||
| 4834 | yield* provider.startSession(threadId, { | ||
| 4835 | provider: ProviderDriverKind.make("codex"), | ||
| 4836 | providerInstanceId: codexInstanceId, | ||
| 4837 | threadId, | ||
| 4838 | runtimeMode: "full-access", | ||
| 4839 | }); | ||
| 4840 | validation.codex.sendTurn.mockClear(); | ||
| 4841 | const attachment = { | ||
| 4842 | type: "image" as const, | ||
| 4843 | id: "thread-attach-12345678-1234-1234-1234-123456789abc-png", | ||
| 4844 | name: "screen.png", | ||
| 4845 | mimeType: "image/png", | ||
| 4846 | sizeBytes: 1024, | ||
| 4847 | }; | ||
| 4848 | const input = "x".repeat(PROVIDER_SEND_TURN_MAX_INPUT_CHARS); | ||
| 4849 | yield* provider.sendTurn({ threadId, input, attachments: [attachment] }); | ||
| 4850 | assert.equal(validation.codex.sendTurn.mock.calls[0]?.[0].input, input); | ||
| 4851 | assert.deepEqual(validation.codex.sendTurn.mock.calls[0]?.[0].attachments, [attachment]); | ||
| 4852 | }), | ||
| 4853 | ); | ||
| 4854 | |||
| 4804 | it.effect("rejects citation-expanded input over the provider character limit", () => | 4855 | it.effect("rejects citation-expanded input over the provider character limit", () => |
| 4805 | Effect.gen(function* () { | 4856 | Effect.gen(function* () { |
| 4806 | const provider = yield* ProviderService.ProviderService; | 4857 | const provider = yield* ProviderService.ProviderService; |
apps/server/src/provider/Layers/ProviderService.ts+4-2| ... | @@ -1634,10 +1634,12 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( | ... | @@ -1634,10 +1634,12 @@ const makeProviderService = Effect.fn("makeProviderService")(function* ( |
| 1634 | ? `[Pasted text "${attachment.name}" is saved at: ${attachmentPath}. Inspect it as needed.]` | 1634 | ? `[Pasted text "${attachment.name}" is saved at: ${attachmentPath}. Inspect it as needed.]` |
| 1635 | : `[Attached ${attachment.type} "${attachment.name}" is saved at: ${attachmentPath}]`, | 1635 | : `[Attached ${attachment.type} "${attachment.name}" is saved at: ${attachmentPath}]`, |
| 1636 | ); | 1636 | ); |
| 1637 | if (isPastedText && !appended) { | 1637 | // Most adapters see generic files only through this path line, so a file |
| 1638 | // without one would be silently dropped. Images still go natively. | ||
| 1639 | if (!appended && attachment.type === "file") { | ||
| 1638 | return yield* toValidationError( | 1640 | return yield* toValidationError( |
| 1639 | "ProviderService.sendTurn", | 1641 | "ProviderService.sendTurn", |
| 1640 | `Input plus pasted-text attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`, | 1642 | `Input plus attachment context exceeds the ${PROVIDER_SEND_TURN_MAX_INPUT_CHARS} character limit`, |
| 1641 | ); | 1643 | ); |
| 1642 | } | 1644 | } |
| 1643 | } | 1645 | } |
apps/server/src/provider/acp/AntigravityAcpSupport.test.ts+94-38| ... | @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; | ... | @@ -2,7 +2,6 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; | 2 | import { describe, expect, it } from "@effect/vitest"; |
| 3 | import { | 3 | import { |
| 4 | ANTIGRAVITY_DEFAULT_MODEL, | 4 | ANTIGRAVITY_DEFAULT_MODEL, |
| 5 | PROVIDER_SEND_TURN_MAX_FILE_BYTES, | ||
| 6 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, | 5 | PROVIDER_SEND_TURN_MAX_IMAGE_BYTES, |
| 7 | type ChatAttachment, | 6 | type ChatAttachment, |
| 8 | type RuntimeMode, | 7 | type RuntimeMode, |
| ... | @@ -15,7 +14,6 @@ import type * as EffectAcpSchema from "effect-acp/schema"; | ... | @@ -15,7 +14,6 @@ import type * as EffectAcpSchema from "effect-acp/schema"; |
| 15 | 14 | ||
| 16 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; | 15 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 17 | import { | 16 | import { |
| 18 | ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES, | ||
| 19 | antigravityPermissionMode, | 17 | antigravityPermissionMode, |
| 20 | applyAntigravityAcpModelSelection, | 18 | applyAntigravityAcpModelSelection, |
| 21 | buildAntigravityPrompt, | 19 | buildAntigravityPrompt, |
| ... | @@ -324,6 +322,41 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { | ... | @@ -324,6 +322,41 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { |
| 324 | }), | 322 | }), |
| 325 | ); | 323 | ); |
| 326 | 324 | ||
| 325 | it.effect.each([ | ||
| 326 | { name: "archive.zip", mimeType: "application/zip" }, | ||
| 327 | { name: "clip.mp4", mimeType: "video/mp4" }, | ||
| 328 | { name: "recording.aiff", mimeType: "audio/aiff" }, | ||
| 329 | { name: "large.pdf", mimeType: "application/pdf", sizeBytes: 75_000_000 }, | ||
| 330 | { name: "large.txt", mimeType: "text/plain", sizeBytes: 1024 * 1024 + 1 }, | ||
| 331 | { name: "large.wav", mimeType: "audio/wav", sizeBytes: 20 * 1024 * 1024 + 1 }, | ||
| 332 | ])("keeps $name as a file path without reading or spending the native media budget", (file) => | ||
| 333 | Effect.gen(function* () { | ||
| 334 | const fixture = yield* makeAttachmentFixture(); | ||
| 335 | const attachment = { ...textAttachment, sizeBytes: 50 * 1024 * 1024, ...file }; | ||
| 336 | const upload = yield* fixture.write(attachment, ""); | ||
| 337 | yield* fixture.fs.truncate(upload.filePath, attachment.sizeBytes); | ||
| 338 | const pdf = yield* fixture.write(pdfAttachment, ""); | ||
| 339 | yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024); | ||
| 340 | const input = `Inspect the file at ${upload.filePath}`; | ||
| 341 | const prompt = yield* buildAntigravityPrompt({ | ||
| 342 | input, | ||
| 343 | attachments: [attachment, pdfAttachment], | ||
| 344 | attachmentsDir: fixture.attachmentsDir, | ||
| 345 | }).pipe( | ||
| 346 | Effect.provideService(FileSystem.FileSystem, { | ||
| 347 | ...fixture.fs, | ||
| 348 | stream: () => { | ||
| 349 | throw new Error("Path attachments must not be read into the prompt"); | ||
| 350 | }, | ||
| 351 | }), | ||
| 352 | ); | ||
| 353 | expect(prompt).toEqual([ | ||
| 354 | { type: "text", text: input }, | ||
| 355 | { type: "resource_link", uri: pdf.uri, name: "report.pdf", mimeType: "application/pdf" }, | ||
| 356 | ]); | ||
| 357 | }), | ||
| 358 | ); | ||
| 359 | |||
| 327 | it.effect("sends supported audio files as native audio content", () => | 360 | it.effect("sends supported audio files as native audio content", () => |
| 328 | Effect.gen(function* () { | 361 | Effect.gen(function* () { |
| 329 | const fixture = yield* makeAttachmentFixture(); | 362 | const fixture = yield* makeAttachmentFixture(); |
| ... | @@ -378,8 +411,6 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { | ... | @@ -378,8 +411,6 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { |
| 378 | 411 | ||
| 379 | it.effect.each([ | 412 | it.effect.each([ |
| 380 | { ...imageAttachment, name: "animation.gif", mimeType: "image/gif" }, | 413 | { ...imageAttachment, name: "animation.gif", mimeType: "image/gif" }, |
| 381 | { ...textAttachment, name: "archive.zip", mimeType: "application/zip" }, | ||
| 382 | { ...textAttachment, name: "recording.aiff", mimeType: "audio/aiff" }, | ||
| 383 | ] satisfies ReadonlyArray<ChatAttachment>)( | 414 | ] satisfies ReadonlyArray<ChatAttachment>)( |
| 384 | "rejects $name instead of silently dropping it from a valid prompt", | 415 | "rejects $name instead of silently dropping it from a valid prompt", |
| 385 | (attachment) => | 416 | (attachment) => |
| ... | @@ -400,32 +431,25 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { | ... | @@ -400,32 +431,25 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { |
| 400 | }), | 431 | }), |
| 401 | ); | 432 | ); |
| 402 | 433 | ||
| 403 | it.effect.each([ | 434 | it.effect("rejects oversized images using file size instead of upload metadata", () => |
| 404 | { attachment: textAttachment, bytes: ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES + 1 }, | 435 | Effect.gen(function* () { |
| 405 | { attachment: imageAttachment, bytes: PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1 }, | 436 | const fixture = yield* makeAttachmentFixture(); |
| 406 | { attachment: pdfAttachment, bytes: PROVIDER_SEND_TURN_MAX_FILE_BYTES + 1 }, | 437 | const upload = yield* fixture.write(imageAttachment, ""); |
| 407 | ])( | 438 | yield* fixture.fs.truncate(upload.filePath, PROVIDER_SEND_TURN_MAX_IMAGE_BYTES + 1); |
| 408 | "rejects oversized $attachment.name using file size instead of upload metadata", | 439 | const error = yield* buildAntigravityPrompt({ |
| 409 | ({ attachment, bytes }) => | 440 | input: "Read this attachment.", |
| 410 | Effect.gen(function* () { | 441 | attachments: [imageAttachment], |
| 411 | const fixture = yield* makeAttachmentFixture(); | 442 | attachmentsDir: fixture.attachmentsDir, |
| 412 | const upload = yield* fixture.write(attachment, ""); | 443 | }).pipe(Effect.flip); |
| 413 | yield* fixture.fs.truncate(upload.filePath, bytes); | 444 | expect(error).toMatchObject({ |
| 414 | const error = yield* buildAntigravityPrompt({ | 445 | _tag: "AcpRequestError", |
| 415 | input: "Read this attachment.", | 446 | code: -32602, |
| 416 | attachments: [attachment], | 447 | errorMessage: expect.stringContaining("'screen.png' is too large"), |
| 417 | attachmentsDir: fixture.attachmentsDir, | 448 | }); |
| 418 | }).pipe(Effect.flip); | 449 | }), |
| 419 | |||
| 420 | expect(error).toMatchObject({ | ||
| 421 | _tag: "AcpRequestError", | ||
| 422 | code: -32602, | ||
| 423 | errorMessage: expect.stringContaining(`'${attachment.name}' is too large`), | ||
| 424 | }); | ||
| 425 | }), | ||
| 426 | ); | 450 | ); |
| 427 | 451 | ||
| 428 | it.effect("accepts 50 MiB in total but rejects one byte more across files", () => | 452 | it.effect("keeps PDF overflow on the file-path route", () => |
| 429 | Effect.gen(function* () { | 453 | Effect.gen(function* () { |
| 430 | const fixture = yield* makeAttachmentFixture(); | 454 | const fixture = yield* makeAttachmentFixture(); |
| 431 | const secondAttachment = { | 455 | const secondAttachment = { |
| ... | @@ -435,25 +459,57 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { | ... | @@ -435,25 +459,57 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { |
| 435 | }; | 459 | }; |
| 436 | const first = yield* fixture.write(pdfAttachment, ""); | 460 | const first = yield* fixture.write(pdfAttachment, ""); |
| 437 | const second = yield* fixture.write(secondAttachment, ""); | 461 | const second = yield* fixture.write(secondAttachment, ""); |
| 438 | yield* fixture.fs.truncate(first.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2); | 462 | yield* fixture.fs.truncate(first.filePath, (50 * 1024 * 1024) / 2); |
| 439 | yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2); | 463 | yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2); |
| 440 | const input = { | 464 | const input = { |
| 441 | input: undefined, | 465 | input: `Read ${first.filePath} and ${second.filePath}`, |
| 442 | attachments: [pdfAttachment, secondAttachment], | 466 | attachments: [pdfAttachment, secondAttachment], |
| 443 | attachmentsDir: fixture.attachmentsDir, | 467 | attachmentsDir: fixture.attachmentsDir, |
| 444 | }; | 468 | }; |
| 445 | const prompt = yield* buildAntigravityPrompt(input); | 469 | const prompt = yield* buildAntigravityPrompt(input); |
| 446 | expect(prompt).toEqual([ | 470 | expect(prompt).toEqual([ |
| 447 | { type: "resource_link", uri: first.uri, name: "report.pdf", mimeType: "application/pdf" }, | 471 | { type: "text", text: input.input }, |
| 448 | { type: "resource_link", uri: second.uri, name: "second.pdf", mimeType: "application/pdf" }, | 472 | { |
| 473 | type: "resource_link", | ||
| 474 | uri: first.uri, | ||
| 475 | name: "report.pdf", | ||
| 476 | mimeType: "application/pdf", | ||
| 477 | }, | ||
| 478 | { | ||
| 479 | type: "resource_link", | ||
| 480 | uri: second.uri, | ||
| 481 | name: "second.pdf", | ||
| 482 | mimeType: "application/pdf", | ||
| 483 | }, | ||
| 449 | ]); | 484 | ]); |
| 450 | 485 | ||
| 451 | yield* fixture.fs.truncate(second.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES / 2 + 1); | 486 | yield* fixture.fs.truncate(second.filePath, (50 * 1024 * 1024) / 2 + 1); |
| 452 | const error = yield* buildAntigravityPrompt(input).pipe(Effect.flip); | 487 | expect(yield* buildAntigravityPrompt(input)).toEqual([ |
| 488 | { type: "text", text: input.input }, | ||
| 489 | { | ||
| 490 | type: "resource_link", | ||
| 491 | uri: first.uri, | ||
| 492 | name: "report.pdf", | ||
| 493 | mimeType: "application/pdf", | ||
| 494 | }, | ||
| 495 | ]); | ||
| 496 | }), | ||
| 497 | ); | ||
| 498 | |||
| 499 | it.effect("still rejects images when the native budget is full", () => | ||
| 500 | Effect.gen(function* () { | ||
| 501 | const fixture = yield* makeAttachmentFixture(); | ||
| 502 | const pdf = yield* fixture.write(pdfAttachment, ""); | ||
| 503 | yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024); | ||
| 504 | yield* fixture.write(imageAttachment, new Uint8Array([1])); | ||
| 505 | const error = yield* buildAntigravityPrompt({ | ||
| 506 | input: "Inspect both attachments.", | ||
| 507 | attachments: [pdfAttachment, imageAttachment], | ||
| 508 | attachmentsDir: fixture.attachmentsDir, | ||
| 509 | }).pipe(Effect.flip); | ||
| 453 | expect(error).toMatchObject({ | 510 | expect(error).toMatchObject({ |
| 454 | _tag: "AcpRequestError", | 511 | _tag: "AcpRequestError", |
| 455 | code: -32602, | 512 | errorMessage: expect.stringContaining("'screen.png' is too large"), |
| 456 | errorMessage: expect.stringContaining("'second.pdf' is too large"), | ||
| 457 | }); | 513 | }); |
| 458 | }), | 514 | }), |
| 459 | ); | 515 | ); |
| ... | @@ -463,7 +519,7 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { | ... | @@ -463,7 +519,7 @@ it.layer(NodeServices.layer)("buildAntigravityPrompt", (it) => { |
| 463 | const fixture = yield* makeAttachmentFixture(); | 519 | const fixture = yield* makeAttachmentFixture(); |
| 464 | const pdf = yield* fixture.write(pdfAttachment, ""); | 520 | const pdf = yield* fixture.write(pdfAttachment, ""); |
| 465 | const text = yield* fixture.write(textAttachment, "a"); | 521 | const text = yield* fixture.write(textAttachment, "a"); |
| 466 | yield* fixture.fs.truncate(pdf.filePath, PROVIDER_SEND_TURN_MAX_FILE_BYTES - 1); | 522 | yield* fixture.fs.truncate(pdf.filePath, 50 * 1024 * 1024 - 1); |
| 467 | const error = yield* buildAntigravityPrompt({ | 523 | const error = yield* buildAntigravityPrompt({ |
| 468 | input: undefined, | 524 | input: undefined, |
| 469 | attachments: [pdfAttachment, textAttachment], | 525 | attachments: [pdfAttachment, textAttachment], |
apps/server/src/provider/acp/AntigravityAcpSupport.ts+23-13| ... | @@ -243,10 +243,14 @@ const TEXT_FILE_EXTENSIONS = new Set([ | ... | @@ -243,10 +243,14 @@ const TEXT_FILE_EXTENSIONS = new Set([ |
| 243 | ".ini", | 243 | ".ini", |
| 244 | ".conf", | 244 | ".conf", |
| 245 | ]); | 245 | ]); |
| 246 | export const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024; | 246 | const ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES = 1024 * 1024; |
| 247 | const MAX_TOTAL_ATTACHMENT_BYTES = PROVIDER_SEND_TURN_MAX_FILE_BYTES; | 247 | const MAX_TOTAL_ATTACHMENT_BYTES = PROVIDER_SEND_TURN_MAX_FILE_BYTES; |
| 248 | 248 | ||
| 249 | /** Sends uploads as native ACP content instead of workspace path hints. */ | 249 | /** |
| 250 | * Sends supported uploads as native ACP content. Other files, and native | ||
| 251 | * candidates over their limits, reach the agent through the saved path | ||
| 252 | * ProviderService puts in the text block. | ||
| 253 | */ | ||
| 250 | export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(function* (input: { | 254 | export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(function* (input: { |
| 251 | readonly input: ProviderSendTurnInput["input"]; | 255 | readonly input: ProviderSendTurnInput["input"]; |
| 252 | readonly attachments: ProviderSendTurnInput["attachments"]; | 256 | readonly attachments: ProviderSendTurnInput["attachments"]; |
| ... | @@ -280,7 +284,9 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi | ... | @@ -280,7 +284,9 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi |
| 280 | (mimeType.startsWith("text/") || | 284 | (mimeType.startsWith("text/") || |
| 281 | TEXT_MIME_TYPES.has(mimeType) || | 285 | TEXT_MIME_TYPES.has(mimeType) || |
| 282 | TEXT_FILE_EXTENSIONS.has(path.extname(attachment.name).toLowerCase())); | 286 | TEXT_FILE_EXTENSIONS.has(path.extname(attachment.name).toLowerCase())); |
| 283 | if (!image && !audio && !pdf && !textFile) { | 287 | const isPathOnly = |
| 288 | attachment.type === "file" && (isPastedText || (!audio && !pdf && !textFile)); | ||
| 289 | if (attachment.type === "image" && !image) { | ||
| 284 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( | 290 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( |
| 285 | `Antigravity does not support '${attachment.name}' (${attachment.mimeType}). Attach a BMP, JPEG, PNG, WebP, PDF, audio, or text file.`, | 291 | `Antigravity does not support '${attachment.name}' (${attachment.mimeType}). Attach a BMP, JPEG, PNG, WebP, PDF, audio, or text file.`, |
| 286 | ); | 292 | ); |
| ... | @@ -303,26 +309,30 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi | ... | @@ -303,26 +309,30 @@ export const buildAntigravityPrompt = Effect.fn("buildAntigravityPrompt")(functi |
| 303 | ), | 309 | ), |
| 304 | ), | 310 | ), |
| 305 | ); | 311 | ); |
| 306 | if (isPastedText) { | 312 | if (info.type !== "File") { |
| 307 | if (info.type !== "File") { | 313 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( |
| 308 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( | 314 | `Could not read attachment '${attachment.name}'.`, |
| 309 | `Could not read attachment '${attachment.name}'.`, | 315 | ); |
| 310 | ); | ||
| 311 | } | ||
| 312 | continue; | ||
| 313 | } | 316 | } |
| 317 | if (isPathOnly) continue; | ||
| 314 | const size = Number(info.size); | 318 | const size = Number(info.size); |
| 315 | const limit = image | 319 | const limit = image |
| 316 | ? PROVIDER_SEND_TURN_MAX_IMAGE_BYTES | 320 | ? PROVIDER_SEND_TURN_MAX_IMAGE_BYTES |
| 317 | : audio | 321 | : audio |
| 318 | ? ANTIGRAVITY_MAX_AUDIO_ATTACHMENT_BYTES | 322 | ? ANTIGRAVITY_MAX_AUDIO_ATTACHMENT_BYTES |
| 319 | 323 | ||
| 320 | ? PROVIDER_SEND_TURN_MAX_FILE_BYTES | 324 | ? MAX_TOTAL_ATTACHMENT_BYTES |
| 321 | : ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES; | 325 | : ANTIGRAVITY_MAX_TEXT_ATTACHMENT_BYTES; |
| 326 | if ( | ||
| 327 | attachment.type === "file" && | ||
| 328 | (size > limit || totalBytes + size > MAX_TOTAL_ATTACHMENT_BYTES) | ||
| 329 | ) { | ||
| 330 | continue; | ||
| 331 | } | ||
| 322 | totalBytes += size; | 332 | totalBytes += size; |
| 323 | if (info.type !== "File" || size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) { | 333 | if (size > limit || totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) { |
| 324 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( | 334 | return yield* EffectAcpErrors.AcpRequestError.invalidParams( |
| 325 | `Attachment '${attachment.name}' is too large. Antigravity accepts text files up to 1 MiB, images up to 10 MiB, audio up to 20 MiB, and 50 MiB total attachments.`, | 335 | `Image '${attachment.name}' is too large. Antigravity accepts images up to 10 MiB and 50 MiB of native attachments per message.`, |
| 326 | ); | 336 | ); |
| 327 | } | 337 | } |
| 328 | const uri = yield* path.toFileUrl(attachmentPath).pipe( | 338 | const uri = yield* path.toFileUrl(attachmentPath).pipe( |
docs/user/composer.md+1-2| ... | @@ -19,8 +19,7 @@ Attach up to 100 files per message. Each image can be up to 10 MiB, with at most | ... | @@ -19,8 +19,7 @@ Attach up to 100 files per message. Each image can be up to 10 MiB, with at most |
| 19 | 50 MiB each, subject to the environment's upload support and limit. The agent | 19 | 50 MiB each, subject to the environment's upload support and limit. The agent |
| 20 | receives them on the environment's machine. Provider and model limits still | 20 | receives them on the environment's machine. Provider and model limits still |
| 21 | apply, including images already in the conversation. A video attachment gives | 21 | apply, including images already in the conversation. A video attachment gives |
| 22 | the agent a file path; it does not enable native video input. Antigravity does | 22 | the agent a file path; it does not enable native video input. |
| 23 | not accept video attachments. | ||
| 24 | 23 | ||
| 25 | Uploads begin when you add an attachment. All uploads must finish before the | 24 | Uploads begin when you add an attachment. All uploads must finish before the |
| 26 | message can send. Retry or remove a failed upload. On web and desktop, reloading | 25 | message can send. Retry or remove a failed upload. On web and desktop, reloading |
docs/user/providers-antigravity.md+6-3| ... | @@ -94,9 +94,12 @@ so a skill there only appears when the project itself is your home directory. | ... | @@ -94,9 +94,12 @@ so a skill there only appears when the project itself is your home directory. |
| 94 | 94 | ||
| 95 | Antigravity accepts images, PDFs, text files, and supported audio formats directly. | 95 | Antigravity accepts images, PDFs, text files, and supported audio formats directly. |
| 96 | Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and | 96 | Its limits are 1 MiB per text file, 10 MiB per image, 20 MiB per audio clip, and |
| 97 | 50 MiB total attachments per message. Unsupported formats are rejected. These | 97 | 50 MiB total native attachments per message. PDFs, text, and audio that exceed |
| 98 | limits can be lower than the general upload limit; uploading a file does not | 98 | their native limits or the remaining native budget are passed as file paths |
| 99 | mean this provider can use it. | 99 | for the agent to inspect with tools. ZIP archives and videos also use file paths. |
| 100 | These files do not count toward the native attachment budget or enable native video input. | ||
| 101 | Unsupported image formats are rejected. The native limits can be lower than | ||
| 102 | the general upload limit. | ||
| 100 | 103 | ||
| 101 | ### Subagents | 104 | ### Subagents |
| 102 | 105 |