dsh-agy 0.4.2 → 0.4.3

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.
@@ -1 +1 @@
1
- {"version":3,"file":"translate-YhGDUuVa.mjs","names":[],"sources":["../src/runtime/signature-cache.ts","../src/adapter/dsh-view.ts","../src/adapter/multimodal.ts","../src/adapter/translate.ts"],"sourcesContent":["/**\n * Gemini thought-signature cache for Antigravity tool calls.\n *\n * The Antigravity backend requires every outbound `functionCall` part to carry\n * a `thoughtSignature` (400 \"Function call is missing a thought_signature\"\n * otherwise). Signatures arrive on the *response* side — as a sibling field of\n * functionCall parts, and on thought parts — and must be replayed on the next\n * turn for the same tool call id. When nothing is cached, the translator\n * injects the `skip_thought_signature_validator` sentinel (the established\n * bypass both reference implementations use by default).\n *\n * Process-local with TTL; a cold process falls back to the sentinel, which the\n * upstream accepts, so cross-process persistence is not required for\n * correctness.\n */\n\nexport const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator'\n\ninterface CacheEntry {\n signature: string\n expiresAt: number\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\nconst MAX_ENTRIES = 2000\n\nconst store = new Map<string, CacheEntry>()\n\nfunction pruneExpired(now = Date.now()): void {\n if (store.size < MAX_ENTRIES / 2) return\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key)\n }\n}\n\n/** Store the signature observed for one tool call id (response side). */\nexport function setThoughtSignature(toolCallId: string, signature: string, ttlMs = DEFAULT_TTL_MS): void {\n if (!toolCallId || !signature) return\n pruneExpired()\n if (store.size >= MAX_ENTRIES) {\n // Evict the oldest entry to keep the cache bounded.\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of store) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey) store.delete(oldestKey)\n }\n store.set(toolCallId, { signature, expiresAt: Date.now() + ttlMs })\n}\n\n/** Resolve the signature for one tool call id, or null when unknown/expired. */\nexport function getThoughtSignature(toolCallId: string): string | null {\n if (!toolCallId) return null\n const entry = store.get(toolCallId)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n store.delete(toolCallId)\n return null\n }\n return entry.signature\n}\n","/**\n * The adapter's canonical view of a DSH message, normalized at the seam.\n *\n * WHY THIS FILE EXISTS. `@deepseek-ai/dsh-llm` changed its message vocabulary\n * between the release lines this plugin supports, and the change is not a\n * superset — the SAME tool result is described by a different shape:\n *\n * - 0.1.5: a tool result is a `tool-result` CONTENT BLOCK, carried inside a\n * `user` message (`ContentBlockMap['tool-result']`), and `Message['role']`\n * is `'system' | 'user' | 'assistant'`.\n * - 0.2.0: `ContentBlockMap` has NO `tool-result` entry; the result is\n * promoted to a first-class `tool`-ROLE message (`ToolResultMessage`) that\n * carries `toolCallId`/`isError` at message level, and `role` gains\n * `'developer'`. `GenerateOptions.messages` becomes `RequestMessage[]`\n * (`Message | RequestUserInput`), where `RequestUserInput` is an\n * identity-free user turn.\n *\n * Neither union is assignable to the other, so the adapter cannot switch on the\n * harness type directly: on 0.1.5 a `tool-result` case narrows fine, and on\n * 0.2.0 that same case is a type error against a union that no longer has it.\n *\n * The fix is a boundary, not a cast scattered through the translator: the raw\n * harness list is projected ONCE into the union below, which keeps BOTH\n * vocabularies as separate, explicitly handled cases. Downstream code then has\n * one vocabulary to reason about, and `pnpm run verify:dsh-llm-compat` pins\n * that `src/` still compiles against each supported line's published types.\n *\n * The projection is deliberately tolerant: an unreadable or unknown block is\n * dropped rather than guessed at, which matches the translator's existing\n * \"unknown block types are skipped\" policy for merge-extensible content.\n */\n\n/** A text part of a message. */\nexport interface AgyTextBlockView {\n type: 'text'\n text: string\n}\n\n/** A reasoning/thinking part of a message. */\nexport interface AgyReasoningBlockView {\n type: 'reasoning'\n text: string\n}\n\n/** An image reference; bytes are resolved separately through the attachment store. */\nexport interface AgyImageBlockView {\n type: 'image'\n attachment: { attachmentId: string }\n}\n\n/** A file reference; handled by the multimodal path, not by the translator. */\nexport interface AgyFileBlockView {\n type: 'file'\n attachment: { attachmentId: string }\n}\n\n/** A tool invocation requested by the model. */\nexport interface AgyToolCallBlockView {\n type: 'tool-call'\n id: string\n name: string\n arguments: unknown\n}\n\n/**\n * A tool result in the 0.1.5 shape: a content block inside a `user` message.\n *\n * Still modelled even on the 0.2.0 line, because the shape is what the\n * translator's shared `functionResponse` builder consumes — see\n * `normalizeMessages` for how a 0.2.0 `tool`-role message is kept distinct.\n */\nexport interface AgyToolResultBlockView {\n type: 'tool-result'\n toolCallId: string\n content: AgyBlockView[]\n isError?: boolean\n}\n\n/** One content block, in the union of both supported vocabularies. */\nexport type AgyBlockView =\n | AgyTextBlockView\n | AgyReasoningBlockView\n | AgyImageBlockView\n | AgyFileBlockView\n | AgyToolCallBlockView\n | AgyToolResultBlockView\n\n/**\n * One message.\n *\n * `role` stays `string` rather than a closed union: 0.2.0 introduced\n * `'developer'`, and the harness declares the role maps merge-extensible, so a\n * future role must degrade to \"no parts\" instead of failing a cast. Comparison\n * against the roles this adapter understands happens at each use site.\n */\nexport interface AgyMessageView {\n readonly role: string\n /** Durable identity; absent on 0.2.0's `RequestUserInput`. */\n readonly id?: string\n readonly content: AgyBlockView[]\n /**\n * 0.2.0 `ToolResultMessage` correlation, carried at MESSAGE level.\n *\n * Set only on a `tool`-role message; the 0.1.5 shape carries the same fact on\n * the `tool-result` block instead.\n */\n readonly toolCallId?: string\n /** 0.2.0 `ToolResultMessage`: whether the tool invocation failed. */\n readonly isError?: boolean\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n}\n\nfunction normalizeBlocks(raw: unknown): AgyBlockView[] {\n if (!Array.isArray(raw)) return []\n const blocks: AgyBlockView[] = []\n for (const entry of raw) {\n const block = asRecord(entry)\n if (!block) continue\n switch (block.type) {\n case 'text':\n case 'reasoning': {\n const text = asString(block.text)\n if (text !== undefined) blocks.push({ type: block.type, text })\n break\n }\n case 'image':\n case 'file': {\n const attachment = asRecord(block.attachment)\n const attachmentId = attachment ? asString(attachment.attachmentId) : undefined\n if (attachmentId !== undefined) blocks.push({ type: block.type, attachment: { attachmentId } })\n break\n }\n case 'tool-call': {\n const id = asString(block.id)\n const name = asString(block.name)\n if (id !== undefined && name !== undefined) {\n blocks.push({ type: 'tool-call', id, name, arguments: block.arguments })\n }\n break\n }\n case 'tool-result': {\n const toolCallId = asString(block.toolCallId)\n if (toolCallId !== undefined) {\n blocks.push({\n type: 'tool-result',\n toolCallId,\n content: normalizeBlocks(block.content),\n ...(block.isError === true ? { isError: true } : {}),\n })\n }\n break\n }\n default:\n // Merge-extensible: tool-addition/tool-removal and any future block are\n // projected away here, exactly as the translator's `default` case\n // already skipped them on the wire.\n break\n }\n }\n return blocks\n}\n\n/**\n * Project a raw harness message list into {@link AgyMessageView}s.\n *\n * Both vocabularies survive the projection as their own shapes — a 0.1.5\n * `tool-result` block and a 0.2.0 `tool`-role message each keep the fields the\n * translator needs — so the wire shape is decided in ONE place (`toolResultPart`\n * in `translate.ts`) rather than duplicated per release line.\n *\n * @param raw - the harness's `GenerateOptions.messages` (or any list of them).\n * @returns one normalized view per readable message, in order.\n */\nexport function normalizeMessages(raw: readonly unknown[]): AgyMessageView[] {\n const messages: AgyMessageView[] = []\n for (const entry of raw) {\n const message = asRecord(entry)\n if (!message) continue\n const role = asString(message.role)\n if (role === undefined) continue\n const id = asString(message.id)\n const toolCallId = asString(message.toolCallId)\n messages.push({\n role,\n ...(id === undefined ? {} : { id }),\n content: normalizeBlocks(message.content),\n ...(toolCallId === undefined ? {} : { toolCallId }),\n ...(message.isError === true ? { isError: true } : {}),\n })\n }\n return messages\n}\n\n/**\n * Messages that project to a wire turn.\n *\n * `system` and `developer` are excluded because they are not conversation\n * turns: the system prompt becomes `systemInstruction` and a developer message\n * carries tool-addition/removal bookkeeping, which this channel expresses\n * through the tool declarations themselves.\n */\nexport function conversationMessages(messages: readonly AgyMessageView[]): AgyMessageView[] {\n return messages.filter((message) => message.role !== 'system' && message.role !== 'developer')\n}\n\n/**\n * The system prompt text carried by `system`-role messages, in order.\n *\n * `GenerateOptions.system` is documented as the one-shot channel and is\n * undefined for loop-built requests, whose derived history carries the prompt\n * as a leading `system`-role message on BOTH supported lines. Reading only\n * `options.system` therefore sent no system prompt at all for a real agent\n * turn, and mapping the message through the conversation path sent it as a USER\n * turn instead — this is the text that belongs in `systemInstruction`.\n */\nexport function systemTextFromMessages(messages: readonly AgyMessageView[]): string | undefined {\n const parts: string[] = []\n for (const message of messages) {\n if (message.role !== 'system') continue\n for (const block of message.content) {\n if (block.type === 'text' && block.text !== '') parts.push(block.text)\n }\n }\n return parts.length > 0 ? parts.join('\\n\\n') : undefined\n}\n","/**\n * Multimodal file resolution for Google Gemini on Antigravity.\n *\n * In DeepSeek Harness (DSH), non-image files (PDF, audio, video, etc.)\n * uploaded by users are converted by DSH's LLM runtime into deterministic\n * text handles:\n *\n * `[File \"${name}\" (${bytes} bytes, sha256:${digest}): verbatim read-only copy saved at \"${readonlyPath}\". ...]`\n *\n * Gemini models natively support these files as base64-encoded `inlineData`\n * parts alongside text. Claude models running behind the Antigravity Vertex\n * proxy reject non-image `inlineData` with 500, so injection is scoped strictly\n * to Gemini (non-Claude, vision-capable) models.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport type { GenerateOptions } from '@deepseek-ai/dsh-llm'\nimport { catalogModel } from './catalog.ts'\nimport { normalizeMessages } from './dsh-view.ts'\n\n/**\n * MIME type mapping for Gemini supported multimodal formats.\n *\n * Covered formats (PDF observed live end-to-end; the rest follow Gemini's\n * documented multimodal support and are not yet live-verified):\n * - Document: .pdf -> 'application/pdf'\n * - Audio: .mp3 -> 'audio/mp3', .wav -> 'audio/wav', .m4a -> 'audio/m4a', .aac -> 'audio/aac', .ogg -> 'audio/ogg', .flac -> 'audio/flac'\n * - Video: .mp4 -> 'video/mp4', .mov -> 'video/quicktime', .webm -> 'video/webm'\n * - Extended image: .bmp -> 'image/bmp', .heic -> 'image/heic', .heif -> 'image/heif'\n */\nexport const GEMINI_MULTIMODAL_MIMES: Readonly<Record<string, string>> = Object.freeze({\n pdf: 'application/pdf',\n mp3: 'audio/mp3',\n wav: 'audio/wav',\n m4a: 'audio/m4a',\n aac: 'audio/aac',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n mp4: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n bmp: 'image/bmp',\n heic: 'image/heic',\n heif: 'image/heif',\n '.pdf': 'application/pdf',\n '.mp3': 'audio/mp3',\n '.wav': 'audio/wav',\n '.m4a': 'audio/m4a',\n '.aac': 'audio/aac',\n '.ogg': 'audio/ogg',\n '.flac': 'audio/flac',\n '.mp4': 'video/mp4',\n '.mov': 'video/quicktime',\n '.webm': 'video/webm',\n '.bmp': 'image/bmp',\n '.heic': 'image/heic',\n '.heif': 'image/heif',\n})\n\n/**\n * Regex matching DSH's deterministic read-only file handle blocks.\n * Capture groups:\n * 1: file name\n * 2: file size in bytes\n * 3: local read-only filesystem path\n */\nexport const DSH_FILE_HANDLE_REGEX =\n /\\[File \"([^\"]+)\" \\((\\d+) bytes, sha256:[a-f0-9]+\\): verbatim read-only copy saved at \"([^\"]+)\"\\./g\n\nexport const FILE_HANDLE_REGEX = DSH_FILE_HANDLE_REGEX\n\n/** Maximum file size for inlining (20MB). */\nexport const MAX_MULTIMODAL_FILE_BYTES = 20 * 1024 * 1024\nexport const MAX_MULTIMODAL_FILE_SIZE = MAX_MULTIMODAL_FILE_BYTES\n\nexport interface ExtractedFileHandle {\n name: string\n bytes: number\n readonlyPath: string\n}\n\n/** Extract file handle descriptors from text content. */\nexport function extractFileHandles(text: string): ExtractedFileHandle[] {\n const regex = new RegExp(DSH_FILE_HANDLE_REGEX.source, 'g')\n const results: ExtractedFileHandle[] = []\n for (const match of text.matchAll(regex)) {\n const [, name, bytesStr, readonlyPath] = match\n if (name && bytesStr && readonlyPath) {\n const bytes = parseInt(bytesStr, 10)\n if (!Number.isNaN(bytes)) {\n results.push({ name, bytes, readonlyPath })\n }\n }\n }\n return results\n}\n\n/** Resolve MIME type for a file name or path based on extension. */\nexport function getMultimodalMimeType(filenameOrPath: string): string | undefined {\n const dotIndex = filenameOrPath.lastIndexOf('.')\n if (dotIndex === -1) return undefined\n const ext = filenameOrPath.slice(dotIndex + 1).toLowerCase()\n return GEMINI_MULTIMODAL_MIMES[ext]\n}\n\n/** Whether a model id belongs to a Claude-branded model (Vertex-hosted). */\nexport function isClaudeModel(model: string): boolean {\n return model.startsWith('claude-') || model.includes('/claude')\n}\n\n/**\n * Check whether a model supports Gemini multimodal file inlineData.\n * Deny-by-default: Claude models are always excluded (Vertex 500 on\n * non-image inlineData), catalog models must be vision-capable, and ids\n * unknown to the catalog are only allowed when they carry the `gemini-`\n * prefix (so future tiered ids keep working without a catalog bump).\n */\nexport function supportsMultimodalFiles(model: string): boolean {\n if (isClaudeModel(model)) return false\n const meta = catalogModel(model)\n if (meta) return meta.supportsVision === true\n return model.startsWith('gemini-')\n}\n\nexport const isMultimodalSupported = supportsMultimodalFiles\nexport const isMultimodalModel = supportsMultimodalFiles\n\nexport interface AgyResolvedMultimodalFile {\n mimeType: string\n /** Pure base64 (no data: prefix). */\n data: string\n name?: string\n path?: string\n bytes?: number\n}\n\nexport interface ResolveMultimodalOptions {\n readFile?: (path: string) => Promise<Buffer | Uint8Array>\n}\n\n/**\n * Resolve multimodal files referenced in user messages.\n *\n * Reads local files up to 20MB into base64 strings. Silently ignores\n * missing/unreadable files or files exceeding the size limit so the original\n * text handle block remains intact in the prompt.\n */\nexport async function resolveMultimodalFiles(\n optionsOrMessages: GenerateOptions | readonly unknown[],\n modelOrOptions?: string | ResolveMultimodalOptions,\n extraOptions?: ResolveMultimodalOptions,\n): Promise<Map<string, AgyResolvedMultimodalFile[]>> {\n let raw: readonly unknown[]\n let model: string\n let customOptions: ResolveMultimodalOptions | undefined\n\n if ('messages' in optionsOrMessages) {\n raw = optionsOrMessages.messages ?? []\n model = optionsOrMessages.model ?? ''\n customOptions = typeof modelOrOptions === 'object' ? modelOrOptions : extraOptions\n } else {\n raw = optionsOrMessages\n if (typeof modelOrOptions === 'string') {\n model = modelOrOptions\n customOptions = extraOptions\n } else {\n model = ''\n customOptions = modelOrOptions\n }\n }\n\n // Normalized through the same boundary the translator uses, so this path sees\n // one vocabulary whichever dsh-llm line fed the request (see dsh-view.ts).\n const messages = normalizeMessages(raw)\n\n const result = new Map<string, AgyResolvedMultimodalFile[]>()\n if (!supportsMultimodalFiles(model)) {\n return result\n }\n\n const readFn = customOptions?.readFile ?? readFile\n\n for (let i = 0; i < messages.length; i++) {\n const message = messages[i]\n if (!message || message.role !== 'user') continue\n\n const resolvedForMessage: AgyResolvedMultimodalFile[] = []\n for (const block of message.content) {\n if (block.type !== 'text' || typeof block.text !== 'string') continue\n\n const handles = extractFileHandles(block.text)\n for (const handle of handles) {\n if (handle.bytes > MAX_MULTIMODAL_FILE_BYTES) continue\n\n const mimeType = getMultimodalMimeType(handle.name) ?? getMultimodalMimeType(handle.readonlyPath)\n if (!mimeType) continue\n\n try {\n const fileBuffer = await readFn(handle.readonlyPath)\n if (fileBuffer.length > MAX_MULTIMODAL_FILE_BYTES) continue\n resolvedForMessage.push({\n mimeType,\n data: Buffer.from(fileBuffer).toString('base64'),\n name: handle.name,\n path: handle.readonlyPath,\n bytes: fileBuffer.length,\n })\n } catch {\n // Error resilience: catch error silently and do not throw\n continue\n }\n }\n }\n\n if (resolvedForMessage.length > 0) {\n const key = message.id ?? `msg-${i}`\n result.set(key, resolvedForMessage)\n }\n }\n\n return result\n}\n","/**\n * Translate a DSH GenerateOptions into the Antigravity wrapped request.\n *\n * Envelope shape follows the actively-maintained OmniRoute wire format\n * (the archived opencode reference predates it): top-level `project`,\n * `requestId`, `model`, `userAgent`, `requestType`, with the Gemini-style\n * body under `request` (contents/systemInstruction/tools/generationConfig/\n * sessionId). `toolConfig` VALIDATED is attached when tools are present, and\n * Claude-path requests strip trailing model turns (Vertex rejects \"assistant\n * message prefill\").\n *\n * Thinking blocks are carried as-is on the Gemini path (Gemini `thought`\n * parts); no thought is ever re-signed — that signature dance was an artifact\n * of the reference plugin's interception architecture (see\n * docs/ARCHITECTURE.md). The Claude path is stricter and drops thought parts\n * entirely, because its validator demands a real thinking signature that only\n * the originating model can produce (docs/ANTIGRAVITY-API.md §3.3).\n */\n\nimport { createHash } from 'node:crypto'\nimport type { GenerateOptions, ToolSchema } from '@deepseek-ai/dsh-llm'\nimport { generateAntigravityRequestId } from '../runtime/identity.ts'\nimport { getThoughtSignature, THOUGHT_SIGNATURE_SENTINEL } from '../runtime/signature-cache.ts'\nimport { catalogModel, isLevelThinkingModel } from './catalog.ts'\nimport { isClaudeModel, supportsMultimodalFiles, type AgyResolvedMultimodalFile } from './multimodal.ts'\nimport {\n conversationMessages,\n normalizeMessages,\n systemTextFromMessages,\n type AgyBlockView,\n type AgyMessageView,\n} from './dsh-view.ts'\n\nexport { isClaudeModel, supportsMultimodalFiles }\nexport type { AgyResolvedMultimodalFile }\n\nexport type AgyPart =\n | { text: string }\n | { thought: true; text: string }\n | { thoughtSignature: string; functionCall: { id: string; name: string; args: unknown } }\n | { functionResponse: { id: string; name: string; response: unknown } }\n | { inlineData: { mimeType: string; data: string } }\n\n/** Image bytes pre-resolved from the durable attachment store, keyed by attachment id. */\nexport interface AgyResolvedImage {\n mediaType: string\n /** Pure base64 (no data: prefix). */\n data: string\n}\n\nexport interface AgyContent {\n role: 'user' | 'model'\n parts: AgyPart[]\n}\n\nexport interface AgyRequestBody {\n project?: string\n requestId?: string\n model: string\n userAgent?: string\n requestType?: 'agent'\n request: {\n contents: AgyContent[]\n systemInstruction?: { parts: Array<{ text: string }> }\n tools?: Array<{ functionDeclarations: Array<{ name: string; description: string; parameters: unknown }> }>\n toolConfig?: { functionCallingConfig: { mode: 'VALIDATED' } }\n generationConfig?: {\n temperature?: number\n maxOutputTokens?: number\n stopSequences?: string[]\n thinkingConfig?: { thinkingLevel?: string; includeThoughts?: boolean; thinkingBudget?: number }\n }\n sessionId?: string\n }\n}\n\n// isClaudeModel is imported from ./multimodal.ts and re-exported above\n\n/**\n * Vertex (the Antigravity Claude backend) rejects conversations ending on an\n * assistant/model turn (\"assistant message prefill\"); never strip to empty.\n */\nexport function stripTrailingModelTurn(contents: AgyContent[]): AgyContent[] {\n while (contents.length > 1 && contents[contents.length - 1]?.role === 'model') {\n contents.pop()\n }\n return contents\n}\n\n/**\n * The Antigravity backend parses tool `parameters` as a strict protobuf\n * schema and rejects ANY unknown keyword with 400 (verified empirically:\n * `$schema`, `propertyNames`, `pattern`, `minLength`, ... each fail in turn).\n * Denylisting is whack-a-mole, so keep only the keywords the upstream\n * accepts. Container shapes are handled distinctly: `properties` is a\n * name->schema map (keys preserved), `items`/`additionalProperties` are\n * nested schemas (additionalProperties also accepts a boolean — live-verified\n * against the Antigravity upstream), `required`/`enum` are plain arrays.\n *\n * Keyword VALUES are also constrained by the protobuf shape (verified\n * empirically): `type` must be a single enum string (union arrays like\n * `[\"string\",\"number\"]` are rejected) and every `enum` item must be a\n * non-empty string (booleans/numbers/empty strings are rejected). Values are\n * normalized to the nearest valid form instead of being dropped wholesale.\n */\n// Exported for the contract invariant test (tests/adapter.test.ts); not part of\n// the package public API (translate.ts is an internal module).\nexport const AGY_SCHEMA_ALLOWLIST = new Set([\n 'type', 'format', 'title', 'description', 'nullable',\n 'items', 'enum', 'default', 'properties', 'required', 'additionalProperties',\n])\nconst AGY_SCHEMA_MAP_KEYS = new Set(['properties'])\nconst AGY_SCHEMA_NESTED_KEYS = new Set(['items', 'additionalProperties'])\nconst AGY_SCHEMA_LIST_KEYS = new Set(['required', 'enum'])\n\nfunction sanitizeToolSchema(schema: unknown): unknown {\n if (!schema || typeof schema !== 'object') return schema\n if (Array.isArray(schema)) return schema.map((entry) => sanitizeToolSchema(entry))\n\n // Normalize union types: upstream `type` is a single enum string. Pick the\n // first non-null string type (`\"null\"` maps to the `nullable` keyword);\n // fall back to `string` when no usable type remains.\n let normalized = schema as Record<string, unknown>\n if (Array.isArray(normalized.type)) {\n const types = normalized.type.filter((t): t is string => typeof t === 'string' && t !== 'null')\n normalized = { ...normalized, type: types[0] ?? 'string' }\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(normalized)) {\n if (!AGY_SCHEMA_ALLOWLIST.has(key)) continue\n if (AGY_SCHEMA_MAP_KEYS.has(key)) {\n const map: Record<string, unknown> = {}\n for (const [name, child] of Object.entries(value as Record<string, unknown>)) {\n map[name] = sanitizeToolSchema(child)\n }\n result[key] = map\n continue\n }\n if (AGY_SCHEMA_NESTED_KEYS.has(key)) {\n result[key] = sanitizeToolSchema(value)\n continue\n }\n if (AGY_SCHEMA_LIST_KEYS.has(key)) {\n // Upstream `enum` items must be non-empty strings; filter the rest\n // and omit an empty enum entirely (an empty array would be rejected too).\n if (key === 'enum' && Array.isArray(value)) {\n const filtered = value.filter((v): v is string => typeof v === 'string' && v.length > 0)\n if (filtered.length > 0) result[key] = filtered\n } else {\n result[key] = value\n }\n continue\n }\n result[key] = value\n }\n return result\n}\n\n/** Collect tool-call names by id so tool results can name their function. */\nfunction buildToolNameIndex(messages: readonly AgyMessageView[]): Map<string, string> {\n const index = new Map<string, string>()\n for (const message of messages) {\n for (const block of message.content) {\n if (block.type === 'tool-call') {\n index.set(block.id, block.name)\n }\n }\n }\n return index\n}\n\n/**\n * The single producer of a `functionResponse` part.\n *\n * Both supported dsh-llm vocabularies funnel through here: a 0.1.5\n * `tool-result` CONTENT BLOCK and a 0.2.0 `tool`-ROLE message describe the same\n * result, and this channel's wire shape must not depend on which one arrived.\n */\nfunction toolResultPart(\n toolCallId: string,\n isError: boolean,\n content: readonly AgyBlockView[],\n toolNames: Map<string, string>,\n): AgyPart {\n const name = toolNames.get(toolCallId) ?? toolCallId\n const text = content\n .filter((block): block is Extract<AgyBlockView, { type: 'text' }> => block.type === 'text')\n .map((block) => block.text)\n .join('\\n')\n return {\n functionResponse: {\n // The Anthropic-backed Claude path requires tool_result.tool_use_id\n // and 400s without it (\"messages.N.content.M.tool_result.tool_use_id:\n // Field required\"); the Gemini path accepts the id too, so it is\n // always carried rather than branched per family (live-verified).\n id: toolCallId,\n name,\n response: { result: text, is_error: isError },\n },\n }\n}\n\nfunction blockToParts(\n block: AgyBlockView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n /** Claude path: replayed thought blocks are rejected outright (see below). */\n dropThoughts = false,\n): AgyPart[] {\n switch (block.type) {\n case 'text':\n // An empty text part is rejected by the Anthropic-backed Claude path\n // (\"messages.N.content.M.text.text: Field required\") while the Gemini\n // path tolerates it. Upstream's own parts normalization drops empty\n // text, so drop it here for every family (live-verified). DSH emits\n // these as trailing zero-length blocks after a tool call.\n return block.text === '' ? [] : [{ text: block.text }]\n case 'reasoning':\n // Empty thought: same rejection class as `text` (\"thinking.thinking:\n // Field required\").\n if (block.text === '') return []\n // A replayed thought block cannot be sent to the Claude path at all: the\n // backend demands a thinking `signature`, and the\n // `skip_thought_signature_validator` sentinel that works for functionCall\n // parts is rejected here as an invalid signature (live-verified). Only\n // the model that produced the thought could re-sign it, so a thought that\n // came from another family (e.g. a mid-session switch from a Gemini\n // tiered model) has no valid form — drop it rather than 400. Gemini\n // accepts replayed thoughts, so this is Claude-only.\n if (dropThoughts) return []\n return [{ thought: true, text: block.text }]\n case 'tool-call': {\n // Upstream parses functionCall.args as google.protobuf.Struct and\n // rejects a raw string with 400. Guarantee an object: parse the string\n // form, fall back to {} when it is truncated/malformed (the failure\n // mode for long multi-turn histories).\n let args: unknown = {}\n if (typeof block.arguments === 'object' && block.arguments !== null && !Array.isArray(block.arguments)) {\n args = block.arguments\n } else if (typeof block.arguments === 'string') {\n try {\n const parsed: unknown = JSON.parse(block.arguments)\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n args = parsed\n }\n } catch {\n // truncated/malformed JSON -> empty object\n }\n }\n // Antigravity rejects functionCall parts without a thoughtSignature\n // (400). Replay the signature captured for this tool call id on the\n // previous turn; the sentinel is the established bypass when nothing is\n // cached (both reference implementations default to it).\n //\n // OPEN QUESTION, deliberately not changed yet: another implementation of\n // this client stamps the sentinel ONLY on the FIRST functionCall of a model\n // turn and leaves sibling calls unsigned (\"unsigned sibling functionCalls\n // preserve native parallel-call shape\"), and never synthesizes a bypass\n // signature anywhere else. This path stamps every call. Which is right is\n // unverified for THIS channel — the sentinel is live-verified to work here,\n // and a wrong change turns working parallel tool calls into 400s — so it\n // needs one real multi-tool turn measured before any edit.\n const signature = getThoughtSignature(block.id) ?? THOUGHT_SIGNATURE_SENTINEL\n return [{\n thoughtSignature: signature,\n functionCall: { id: block.id, name: block.name, args },\n }]\n }\n case 'tool-result':\n return [toolResultPart(\n block.toolCallId,\n block.isError === true,\n block.content,\n toolNames,\n )]\n case 'image': {\n // Bytes are pre-resolved by the adapter before translation; a missing\n // entry breaks that invariant and must fail loudly, never silently drop.\n const resolved = images.get(block.attachment.attachmentId)\n if (!resolved) {\n throw new Error(`agy translate: unresolved image attachment \"${block.attachment.attachmentId}\"`)\n }\n return [{ inlineData: { mimeType: resolved.mediaType, data: resolved.data } }]\n }\n default:\n return [] // unknown block types (merge-extensible) are skipped\n }\n}\n\nfunction messageToContent(\n message: AgyMessageView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>,\n messageIndex?: number,\n dropThoughts = false,\n): AgyContent | null {\n // 0.2.0 promotes a tool result to a `tool`-ROLE message, with the correlation\n // (`toolCallId`/`isError`) on the message instead of on a `tool-result` block.\n // It projects to the same wire part, and a `functionResponse` always belongs\n // to a user-side turn on this channel.\n if (message.role === 'tool') {\n if (message.toolCallId === undefined) {\n // Uncorrelated: the Claude path 400s without an id, and inventing one\n // would mis-address a real call. Dropped like any other unreadable shape.\n return null\n }\n return {\n role: 'user',\n parts: [toolResultPart(message.toolCallId, message.isError === true, message.content, toolNames)],\n }\n }\n\n const parts = message.content.flatMap((block) =>\n // Non-user images are out of scope by policy (docs ANTIGRAVITY-API §3.2):\n // skip them like any other untranslatable block instead of tripping the\n // unresolved-map guard, which protects only the user-image invariant.\n block.type === 'image' && message.role !== 'user'\n ? []\n : blockToParts(block, toolNames, images, dropThoughts),\n )\n\n if (message.role === 'user' && multimodalFiles) {\n const files =\n (message.id ? multimodalFiles.get(message.id) : undefined) ??\n (multimodalFiles as Map<unknown, AgyResolvedMultimodalFile[]>).get(message) ??\n (messageIndex !== undefined ? multimodalFiles.get(`msg-${messageIndex}`) : undefined) ??\n (messageIndex !== undefined ? multimodalFiles.get(String(messageIndex)) : undefined)\n\n if (files) {\n for (const file of files) {\n parts.push({ inlineData: { mimeType: file.mimeType, data: file.data } })\n }\n }\n }\n\n if (parts.length === 0) return null\n const role = message.role === 'assistant' ? 'model' : 'user'\n return { role, parts }\n}\n\n/**\n * Builtin Gemini tools must not shadow functionDeclarations names (upstream\n * treats them as native tools; verified by OmniRoute's GEMINI_BUILTIN_TOOL_NAMES).\n */\nconst AGY_BUILTIN_TOOL_NAMES = new Set(['google_search', 'web_search', 'search_web', 'googleSearch'])\n\n/** Level-thinking: single id + selectable low/medium/high via thinkingLevel (catalog thinking:'level'). */\nconst LEVEL_THINKING_LEVELS = new Set(['low', 'medium', 'high'])\n\n/**\n * Claude-family output ceiling on the Antigravity channel (live-measured).\n *\n * The Claude models are served through the Gemini-style\n * `generationConfig.maxOutputTokens` field here, and this channel rejects the\n * Claude family above 64000: 64000 answers 200, 64001 answers 400\n * `INVALID_ARGUMENT: Request contains an invalid argument` (deterministic\n * across both Claude ids, with and without the full 87-tool payload). The\n * Gemini family accepts 65536 on the same endpoint, so the limit is\n * model-family-specific, not endpoint-wide.\n *\n * Do NOT reason about this number from Anthropic's public API limits. Agy may\n * front a self-hosted or otherwise gated Claude deployment whose capacity and\n * validation rules are its own; the only authority is what this channel\n * accepts, which is what the probe measures. The value here is that\n * measurement, nothing more.\n *\n * The catalog's `maxOutputTokens` is the harness-injected default\n * (`LlmResolvedModelInfo.defaultMaxTokens`), so a wrong value there makes every\n * Claude request fail. This clamp is the second line of defense: it also covers\n * an explicit `maxTokens` (agent preset / call config) and a dynamically\n * discovered Claude id absent from the pinned catalog.\n */\nexport const AGY_CLAUDE_MAX_OUTPUT_TOKENS = 64_000\n\n/** Upstream functionDeclarations names are `[a-zA-Z0-9_]` and ≤64 chars (OmniRoute-verified). */\nconst AGY_TOOL_NAME_MAX_LENGTH = 64\n\n/** Sanitize a tool name to the upstream charset/length; dedupe via a short hash. */\nfunction sanitizeToolName(name: string, seen: Set<string>): string {\n let candidate = name.replace(/[^a-zA-Z0-9_]/g, '_') || 'tool'\n if (candidate.length > AGY_TOOL_NAME_MAX_LENGTH || seen.has(candidate)) {\n const hash = createHash('sha256').update(candidate).digest('hex').slice(0, 8)\n const prefix = candidate.slice(0, AGY_TOOL_NAME_MAX_LENGTH - hash.length - 1)\n candidate = `${prefix}_${hash}`\n let i = 2\n while (seen.has(candidate)) candidate = `${prefix}_${i++}_${hash}`\n }\n seen.add(candidate)\n return candidate\n}\n\nfunction toolsToDeclarations(tools: ToolSchema[] | undefined): AgyRequestBody['request']['tools'] {\n if (!tools || tools.length === 0) return undefined\n const seenNames = new Set<string>()\n const declarations = []\n for (const tool of tools) {\n if (AGY_BUILTIN_TOOL_NAMES.has(tool.name)) continue\n declarations.push({\n name: sanitizeToolName(tool.name, seenNames),\n description: tool.description,\n parameters: sanitizeToolSchema(tool.parameters),\n })\n }\n if (declarations.length === 0) return undefined\n return [{ functionDeclarations: declarations }]\n}\n\n/** Build the wrapped Antigravity request body for one call. */\nexport function toAgyRequestBody(\n options: GenerateOptions,\n context: {\n projectId?: string\n sessionId?: string\n images?: Map<string, AgyResolvedImage>\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>\n /**\n * The request id, when the caller also stamps it on the wire\n * (`x-goog-request-id`). Generating it here as well produced TWO different\n * ids for one request — the body and the header disagreed, a shape no client\n * produces. Callers that send the header must pass the same value.\n */\n requestId?: string\n /**\n * A configured token budget for a reasoning level, or undefined to leave the\n * level's own budget to upstream.\n *\n * When this returns a number the budget REPLACES `thinkingLevel` rather than\n * joining it, because measurement shows the level wins when both are sent:\n * `{thinkingLevel:\"low\", thinkingBudget:16000}` spends what `low` alone\n * spends (~180 thoughts, against ~330 for the budget alone), and\n * `{thinkingLevel:\"high\", thinkingBudget:1000}` likewise tracks `high`\n * (~316 vs ~173). Sending both would therefore make a configured number\n * silently inert, which is worse than not offering the setting.\n */\n thinkingBudgetFor?: (level: string) => number | undefined\n /**\n * The configured Claude thinking budget, or undefined when unset.\n *\n * A single value, not a per-level map: the Claude family is id-bound (each\n * capability is its own model id with no level selector), so there is no\n * level to key by.\n */\n claudeBudgetFor?: () => number | undefined\n /**\n * The configured budget for the TIERED slot (the selector's \"Default\"\n * effort), or undefined when unset.\n *\n * That effort arrives with NO level id, so it cannot go through\n * `thinkingBudgetFor`. A value here sends a bare `thinkingBudget` with no\n * `thinkingLevel` — the tiered model's adaptive entry, given an explicit cap.\n */\n tieredBudgetFor?: () => number | undefined\n },\n): AgyRequestBody {\n const messages = normalizeMessages(options.messages)\n const toolNames = buildToolNameIndex(messages)\n const images = context.images ?? new Map<string, AgyResolvedImage>()\n const multimodalFiles = supportsMultimodalFiles(options.model) ? context.multimodalFiles : undefined\n const claude = isClaudeModel(options.model)\n let contents = conversationMessages(messages)\n .map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude))\n .filter((c): c is AgyContent => c !== null)\n if (claude) {\n contents = stripTrailingModelTurn(contents)\n }\n\n // `options.system` is the one-shot channel and is undefined for a loop-built\n // request, whose derived history carries the prompt as a `system`-role message\n // (documented identically on both supported dsh-llm lines). Both sources feed\n // the system slot; routing the message through `contents` instead would put\n // the system prompt on the wire as a USER turn.\n const systemText = [options.system, systemTextFromMessages(messages)]\n .filter((text): text is string => typeof text === 'string' && text !== '')\n .join('\\n\\n')\n\n const tools = toolsToDeclarations(options.tools)\n const generationConfig: NonNullable<AgyRequestBody['request']['generationConfig']> = {}\n if (options.temperature !== undefined) generationConfig.temperature = options.temperature\n if (options.maxTokens !== undefined) {\n // Claude family: the platform rejects >64000 with 400, and the value the\n // harness injects comes from the catalog default, so clamp before the wire.\n generationConfig.maxOutputTokens = isClaudeModel(options.model)\n ? Math.min(options.maxTokens, AGY_CLAUDE_MAX_OUTPUT_TOKENS)\n : options.maxTokens\n }\n if (options.stop !== undefined && options.stop.length > 0) generationConfig.stopSequences = options.stop\n // Level-thinking: map the DSH reasoning effort to thinkingConfig.\n // Id-bound models (thinking !== 'level') never emit it, and a tiered model\n // emits NOTHING when no effort is requested — there is deliberately no default\n // level, because \"no level chosen\" is what lets the model allocate its own\n // thinking (see `LEVEL_REASONING` for why declaring a default broke that).\n // When purpose is 'session-title' or reasoning is off, thinkingBudget: 0 prevents\n // default thinking tokens from exhausting tight output caps (e.g. maxTokens: 64).\n const effort = options.reasoningEffort?.toLowerCase()\n if (isLevelThinkingModel(options.model)) {\n if (options.purpose === 'session-title' || effort === 'none' || effort === 'off') {\n generationConfig.thinkingConfig = { thinkingBudget: 0 }\n } else if (effort === undefined) {\n // The selector's \"Default\" effort: no level was chosen. A configured tiered\n // budget turns this into Max by sending a bare `thinkingBudget` (no\n // `thinkingLevel`, so the number alone decides). Unset sends nothing at all,\n // which leaves upstream's own adaptive allocation in charge.\n const tiered = context.tieredBudgetFor?.()\n if (tiered !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: tiered, includeThoughts: true }\n }\n } else if (LEVEL_THINKING_LEVELS.has(effort)) {\n // A configured number for this level takes the place of the level token:\n // both together would let the level win (see `thinkingBudgetFor`).\n const configured = context.thinkingBudgetFor?.(effort)\n generationConfig.thinkingConfig = configured === undefined\n ? { thinkingLevel: effort, includeThoughts: true }\n : { thinkingBudget: configured, includeThoughts: true }\n }\n } else if (claude && options.purpose !== 'session-title') {\n // Claude thinking models are id-bound (no level selector), so their budget is\n // a single configured value rather than one per level. Measured constraints,\n // all of which must hold or the request is a 400:\n // - `max_tokens` must be STRICTLY greater than the budget. `budget=1024`\n // with `max_tokens=1024` is rejected, and so is a budget sent with no\n // `maxOutputTokens` at all.\n // - the floor is 1024 (not `-1`); the store validates that on save.\n // So a budget that does not leave room is DROPPED rather than forced through\n // by raising `maxTokens`: silently enlarging the caller's output cap would\n // change the request's cost and truncation behaviour, while omitting the\n // budget merely means this turn thinks with upstream's default. A 400 would\n // be worse than either.\n const claudeBudget = context.claudeBudgetFor?.()\n const outputCap = generationConfig.maxOutputTokens\n if (claudeBudget !== undefined && outputCap !== undefined && outputCap > claudeBudget) {\n generationConfig.thinkingConfig = { thinkingBudget: claudeBudget, includeThoughts: true }\n }\n }\n\n return {\n project: context.projectId || undefined,\n requestId: context.requestId ?? generateAntigravityRequestId(),\n model: options.model,\n userAgent: 'antigravity',\n requestType: 'agent',\n request: {\n contents,\n ...(systemText ? { systemInstruction: { parts: [{ text: systemText }] } } : {}),\n ...(tools ? { tools } : {}),\n ...(tools ? { toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } } } : {}),\n ...(Object.keys(generationConfig).length > 0 ? { generationConfig } : {}),\n ...(context.sessionId ? { sessionId: context.sessionId } : {}),\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"translate-B9ZOauqw.mjs","names":[],"sources":["../src/runtime/signature-cache.ts","../src/adapter/dsh-view.ts","../src/adapter/multimodal.ts","../src/adapter/translate.ts"],"sourcesContent":["/**\n * Gemini thought-signature cache for Antigravity tool calls.\n *\n * The Antigravity backend requires every outbound `functionCall` part to carry\n * a `thoughtSignature` (400 \"Function call is missing a thought_signature\"\n * otherwise). Signatures arrive on the *response* side — as a sibling field of\n * functionCall parts, and on thought parts — and must be replayed on the next\n * turn for the same tool call id. When nothing is cached, the translator\n * injects the `skip_thought_signature_validator` sentinel (the established\n * bypass both reference implementations use by default).\n *\n * Process-local with TTL; a cold process falls back to the sentinel, which the\n * upstream accepts, so cross-process persistence is not required for\n * correctness.\n */\n\nexport const THOUGHT_SIGNATURE_SENTINEL = 'skip_thought_signature_validator'\n\ninterface CacheEntry {\n signature: string\n expiresAt: number\n}\n\nconst DEFAULT_TTL_MS = 60 * 60 * 1000\nconst MAX_ENTRIES = 2000\n\nconst store = new Map<string, CacheEntry>()\n\nfunction pruneExpired(now = Date.now()): void {\n if (store.size < MAX_ENTRIES / 2) return\n for (const [key, entry] of store) {\n if (entry.expiresAt <= now) store.delete(key)\n }\n}\n\n/** Store the signature observed for one tool call id (response side). */\nexport function setThoughtSignature(toolCallId: string, signature: string, ttlMs = DEFAULT_TTL_MS): void {\n if (!toolCallId || !signature) return\n pruneExpired()\n if (store.size >= MAX_ENTRIES) {\n // Evict the oldest entry to keep the cache bounded.\n let oldestKey: string | null = null\n let oldestExpiry = Infinity\n for (const [key, entry] of store) {\n if (entry.expiresAt < oldestExpiry) {\n oldestExpiry = entry.expiresAt\n oldestKey = key\n }\n }\n if (oldestKey) store.delete(oldestKey)\n }\n store.set(toolCallId, { signature, expiresAt: Date.now() + ttlMs })\n}\n\n/** Resolve the signature for one tool call id, or null when unknown/expired. */\nexport function getThoughtSignature(toolCallId: string): string | null {\n if (!toolCallId) return null\n const entry = store.get(toolCallId)\n if (!entry) return null\n if (entry.expiresAt <= Date.now()) {\n store.delete(toolCallId)\n return null\n }\n return entry.signature\n}\n","/**\n * The adapter's canonical view of a DSH message, normalized at the seam.\n *\n * WHY THIS FILE EXISTS. `@deepseek-ai/dsh-llm` changed its message vocabulary\n * between the release lines this plugin supports, and the change is not a\n * superset — the SAME tool result is described by a different shape:\n *\n * - 0.1.5: a tool result is a `tool-result` CONTENT BLOCK, carried inside a\n * `user` message (`ContentBlockMap['tool-result']`), and `Message['role']`\n * is `'system' | 'user' | 'assistant'`.\n * - 0.2.0: `ContentBlockMap` has NO `tool-result` entry; the result is\n * promoted to a first-class `tool`-ROLE message (`ToolResultMessage`) that\n * carries `toolCallId`/`isError` at message level, and `role` gains\n * `'developer'`. `GenerateOptions.messages` becomes `RequestMessage[]`\n * (`Message | RequestUserInput`), where `RequestUserInput` is an\n * identity-free user turn.\n *\n * Neither union is assignable to the other, so the adapter cannot switch on the\n * harness type directly: on 0.1.5 a `tool-result` case narrows fine, and on\n * 0.2.0 that same case is a type error against a union that no longer has it.\n *\n * The fix is a boundary, not a cast scattered through the translator: the raw\n * harness list is projected ONCE into the union below, which keeps BOTH\n * vocabularies as separate, explicitly handled cases. Downstream code then has\n * one vocabulary to reason about, and `pnpm run verify:dsh-llm-compat` pins\n * that `src/` still compiles against each supported line's published types.\n *\n * The projection is deliberately tolerant: an unreadable or unknown block is\n * dropped rather than guessed at, which matches the translator's existing\n * \"unknown block types are skipped\" policy for merge-extensible content.\n */\n\n/** A text part of a message. */\nexport interface AgyTextBlockView {\n type: 'text'\n text: string\n}\n\n/** A reasoning/thinking part of a message. */\nexport interface AgyReasoningBlockView {\n type: 'reasoning'\n text: string\n}\n\n/** An image reference; bytes are resolved separately through the attachment store. */\nexport interface AgyImageBlockView {\n type: 'image'\n attachment: { attachmentId: string }\n}\n\n/** A file reference; handled by the multimodal path, not by the translator. */\nexport interface AgyFileBlockView {\n type: 'file'\n attachment: { attachmentId: string }\n}\n\n/** A tool invocation requested by the model. */\nexport interface AgyToolCallBlockView {\n type: 'tool-call'\n id: string\n name: string\n arguments: unknown\n}\n\n/**\n * A tool result in the 0.1.5 shape: a content block inside a `user` message.\n *\n * Still modelled even on the 0.2.0 line, because the shape is what the\n * translator's shared `functionResponse` builder consumes — see\n * `normalizeMessages` for how a 0.2.0 `tool`-role message is kept distinct.\n */\nexport interface AgyToolResultBlockView {\n type: 'tool-result'\n toolCallId: string\n content: AgyBlockView[]\n isError?: boolean\n}\n\n/** One content block, in the union of both supported vocabularies. */\nexport type AgyBlockView =\n | AgyTextBlockView\n | AgyReasoningBlockView\n | AgyImageBlockView\n | AgyFileBlockView\n | AgyToolCallBlockView\n | AgyToolResultBlockView\n\n/**\n * One message.\n *\n * `role` stays `string` rather than a closed union: 0.2.0 introduced\n * `'developer'`, and the harness declares the role maps merge-extensible, so a\n * future role must degrade to \"no parts\" instead of failing a cast. Comparison\n * against the roles this adapter understands happens at each use site.\n */\nexport interface AgyMessageView {\n readonly role: string\n /** Durable identity; absent on 0.2.0's `RequestUserInput`. */\n readonly id?: string\n readonly content: AgyBlockView[]\n /**\n * 0.2.0 `ToolResultMessage` correlation, carried at MESSAGE level.\n *\n * Set only on a `tool`-role message; the 0.1.5 shape carries the same fact on\n * the `tool-result` block instead.\n */\n readonly toolCallId?: string\n /** 0.2.0 `ToolResultMessage`: whether the tool invocation failed. */\n readonly isError?: boolean\n}\n\nfunction asString(value: unknown): string | undefined {\n return typeof value === 'string' ? value : undefined\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n ? value as Record<string, unknown>\n : undefined\n}\n\nfunction normalizeBlocks(raw: unknown): AgyBlockView[] {\n if (!Array.isArray(raw)) return []\n const blocks: AgyBlockView[] = []\n for (const entry of raw) {\n const block = asRecord(entry)\n if (!block) continue\n switch (block.type) {\n case 'text':\n case 'reasoning': {\n const text = asString(block.text)\n if (text !== undefined) blocks.push({ type: block.type, text })\n break\n }\n case 'image':\n case 'file': {\n const attachment = asRecord(block.attachment)\n const attachmentId = attachment ? asString(attachment.attachmentId) : undefined\n if (attachmentId !== undefined) blocks.push({ type: block.type, attachment: { attachmentId } })\n break\n }\n case 'tool-call': {\n const id = asString(block.id)\n const name = asString(block.name)\n if (id !== undefined && name !== undefined) {\n blocks.push({ type: 'tool-call', id, name, arguments: block.arguments })\n }\n break\n }\n case 'tool-result': {\n const toolCallId = asString(block.toolCallId)\n if (toolCallId !== undefined) {\n blocks.push({\n type: 'tool-result',\n toolCallId,\n content: normalizeBlocks(block.content),\n ...(block.isError === true ? { isError: true } : {}),\n })\n }\n break\n }\n default:\n // Merge-extensible: tool-addition/tool-removal and any future block are\n // projected away here, exactly as the translator's `default` case\n // already skipped them on the wire.\n break\n }\n }\n return blocks\n}\n\n/**\n * Project a raw harness message list into {@link AgyMessageView}s.\n *\n * Both vocabularies survive the projection as their own shapes — a 0.1.5\n * `tool-result` block and a 0.2.0 `tool`-role message each keep the fields the\n * translator needs — so the wire shape is decided in ONE place (`toolResultPart`\n * in `translate.ts`) rather than duplicated per release line.\n *\n * @param raw - the harness's `GenerateOptions.messages` (or any list of them).\n * @returns one normalized view per readable message, in order.\n */\nexport function normalizeMessages(raw: readonly unknown[]): AgyMessageView[] {\n const messages: AgyMessageView[] = []\n for (const entry of raw) {\n const message = asRecord(entry)\n if (!message) continue\n const role = asString(message.role)\n if (role === undefined) continue\n const id = asString(message.id)\n const toolCallId = asString(message.toolCallId)\n messages.push({\n role,\n ...(id === undefined ? {} : { id }),\n content: normalizeBlocks(message.content),\n ...(toolCallId === undefined ? {} : { toolCallId }),\n ...(message.isError === true ? { isError: true } : {}),\n })\n }\n return messages\n}\n\n/**\n * Messages that project to a wire turn.\n *\n * `system` and `developer` are excluded because they are not conversation\n * turns: the system prompt becomes `systemInstruction` and a developer message\n * carries tool-addition/removal bookkeeping, which this channel expresses\n * through the tool declarations themselves.\n */\nexport function conversationMessages(messages: readonly AgyMessageView[]): AgyMessageView[] {\n return messages.filter((message) => message.role !== 'system' && message.role !== 'developer')\n}\n\n/**\n * The system prompt text carried by `system`-role messages, in order.\n *\n * `GenerateOptions.system` is documented as the one-shot channel and is\n * undefined for loop-built requests, whose derived history carries the prompt\n * as a leading `system`-role message on BOTH supported lines. Reading only\n * `options.system` therefore sent no system prompt at all for a real agent\n * turn, and mapping the message through the conversation path sent it as a USER\n * turn instead — this is the text that belongs in `systemInstruction`.\n */\nexport function systemTextFromMessages(messages: readonly AgyMessageView[]): string | undefined {\n const parts: string[] = []\n for (const message of messages) {\n if (message.role !== 'system') continue\n for (const block of message.content) {\n if (block.type === 'text' && block.text !== '') parts.push(block.text)\n }\n }\n return parts.length > 0 ? parts.join('\\n\\n') : undefined\n}\n","/**\n * Multimodal file resolution for Google Gemini on Antigravity.\n *\n * In DeepSeek Harness (DSH), non-image files (PDF, audio, video, etc.)\n * uploaded by users are converted by DSH's LLM runtime into deterministic\n * text handles:\n *\n * `[File \"${name}\" (${bytes} bytes, sha256:${digest}): verbatim read-only copy saved at \"${readonlyPath}\". ...]`\n *\n * Gemini models natively support these files as base64-encoded `inlineData`\n * parts alongside text. Claude models running behind the Antigravity Vertex\n * proxy reject non-image `inlineData` with 500, so injection is scoped strictly\n * to Gemini (non-Claude, vision-capable) models.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport type { GenerateOptions } from '@deepseek-ai/dsh-llm'\nimport { catalogModel } from './catalog.ts'\nimport { normalizeMessages } from './dsh-view.ts'\n\n/**\n * MIME type mapping for Gemini supported multimodal formats.\n *\n * Covered formats (PDF observed live end-to-end; the rest follow Gemini's\n * documented multimodal support and are not yet live-verified):\n * - Document: .pdf -> 'application/pdf'\n * - Audio: .mp3 -> 'audio/mp3', .wav -> 'audio/wav', .m4a -> 'audio/m4a', .aac -> 'audio/aac', .ogg -> 'audio/ogg', .flac -> 'audio/flac'\n * - Video: .mp4 -> 'video/mp4', .mov -> 'video/quicktime', .webm -> 'video/webm'\n * - Extended image: .bmp -> 'image/bmp', .heic -> 'image/heic', .heif -> 'image/heif'\n */\nexport const GEMINI_MULTIMODAL_MIMES: Readonly<Record<string, string>> = Object.freeze({\n pdf: 'application/pdf',\n mp3: 'audio/mp3',\n wav: 'audio/wav',\n m4a: 'audio/m4a',\n aac: 'audio/aac',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n mp4: 'video/mp4',\n mov: 'video/quicktime',\n webm: 'video/webm',\n bmp: 'image/bmp',\n heic: 'image/heic',\n heif: 'image/heif',\n '.pdf': 'application/pdf',\n '.mp3': 'audio/mp3',\n '.wav': 'audio/wav',\n '.m4a': 'audio/m4a',\n '.aac': 'audio/aac',\n '.ogg': 'audio/ogg',\n '.flac': 'audio/flac',\n '.mp4': 'video/mp4',\n '.mov': 'video/quicktime',\n '.webm': 'video/webm',\n '.bmp': 'image/bmp',\n '.heic': 'image/heic',\n '.heif': 'image/heif',\n})\n\n/**\n * Regex matching DSH's deterministic read-only file handle blocks.\n * Capture groups:\n * 1: file name\n * 2: file size in bytes\n * 3: local read-only filesystem path\n */\nexport const DSH_FILE_HANDLE_REGEX =\n /\\[File \"([^\"]+)\" \\((\\d+) bytes, sha256:[a-f0-9]+\\): verbatim read-only copy saved at \"([^\"]+)\"\\./g\n\nexport const FILE_HANDLE_REGEX = DSH_FILE_HANDLE_REGEX\n\n/** Maximum file size for inlining (20MB). */\nexport const MAX_MULTIMODAL_FILE_BYTES = 20 * 1024 * 1024\nexport const MAX_MULTIMODAL_FILE_SIZE = MAX_MULTIMODAL_FILE_BYTES\n\nexport interface ExtractedFileHandle {\n name: string\n bytes: number\n readonlyPath: string\n}\n\n/** Extract file handle descriptors from text content. */\nexport function extractFileHandles(text: string): ExtractedFileHandle[] {\n const regex = new RegExp(DSH_FILE_HANDLE_REGEX.source, 'g')\n const results: ExtractedFileHandle[] = []\n for (const match of text.matchAll(regex)) {\n const [, name, bytesStr, readonlyPath] = match\n if (name && bytesStr && readonlyPath) {\n const bytes = parseInt(bytesStr, 10)\n if (!Number.isNaN(bytes)) {\n results.push({ name, bytes, readonlyPath })\n }\n }\n }\n return results\n}\n\n/** Resolve MIME type for a file name or path based on extension. */\nexport function getMultimodalMimeType(filenameOrPath: string): string | undefined {\n const dotIndex = filenameOrPath.lastIndexOf('.')\n if (dotIndex === -1) return undefined\n const ext = filenameOrPath.slice(dotIndex + 1).toLowerCase()\n return GEMINI_MULTIMODAL_MIMES[ext]\n}\n\n/** Whether a model id belongs to a Claude-branded model (Vertex-hosted). */\nexport function isClaudeModel(model: string): boolean {\n return model.startsWith('claude-') || model.includes('/claude')\n}\n\n/**\n * Check whether a model supports Gemini multimodal file inlineData.\n * Deny-by-default: Claude models are always excluded (Vertex 500 on\n * non-image inlineData), catalog models must be vision-capable, and ids\n * unknown to the catalog are only allowed when they carry the `gemini-`\n * prefix (so future tiered ids keep working without a catalog bump).\n */\nexport function supportsMultimodalFiles(model: string): boolean {\n if (isClaudeModel(model)) return false\n const meta = catalogModel(model)\n if (meta) return meta.supportsVision === true\n return model.startsWith('gemini-')\n}\n\nexport const isMultimodalSupported = supportsMultimodalFiles\nexport const isMultimodalModel = supportsMultimodalFiles\n\nexport interface AgyResolvedMultimodalFile {\n mimeType: string\n /** Pure base64 (no data: prefix). */\n data: string\n name?: string\n path?: string\n bytes?: number\n}\n\nexport interface ResolveMultimodalOptions {\n readFile?: (path: string) => Promise<Buffer | Uint8Array>\n}\n\n/**\n * Resolve multimodal files referenced in user messages.\n *\n * Reads local files up to 20MB into base64 strings. Silently ignores\n * missing/unreadable files or files exceeding the size limit so the original\n * text handle block remains intact in the prompt.\n */\nexport async function resolveMultimodalFiles(\n optionsOrMessages: GenerateOptions | readonly unknown[],\n modelOrOptions?: string | ResolveMultimodalOptions,\n extraOptions?: ResolveMultimodalOptions,\n): Promise<Map<string, AgyResolvedMultimodalFile[]>> {\n let raw: readonly unknown[]\n let model: string\n let customOptions: ResolveMultimodalOptions | undefined\n\n if ('messages' in optionsOrMessages) {\n raw = optionsOrMessages.messages ?? []\n model = optionsOrMessages.model ?? ''\n customOptions = typeof modelOrOptions === 'object' ? modelOrOptions : extraOptions\n } else {\n raw = optionsOrMessages\n if (typeof modelOrOptions === 'string') {\n model = modelOrOptions\n customOptions = extraOptions\n } else {\n model = ''\n customOptions = modelOrOptions\n }\n }\n\n // Normalized through the same boundary the translator uses, so this path sees\n // one vocabulary whichever dsh-llm line fed the request (see dsh-view.ts).\n const messages = normalizeMessages(raw)\n\n const result = new Map<string, AgyResolvedMultimodalFile[]>()\n if (!supportsMultimodalFiles(model)) {\n return result\n }\n\n const readFn = customOptions?.readFile ?? readFile\n\n for (let i = 0; i < messages.length; i++) {\n const message = messages[i]\n if (!message || message.role !== 'user') continue\n\n const resolvedForMessage: AgyResolvedMultimodalFile[] = []\n for (const block of message.content) {\n if (block.type !== 'text' || typeof block.text !== 'string') continue\n\n const handles = extractFileHandles(block.text)\n for (const handle of handles) {\n if (handle.bytes > MAX_MULTIMODAL_FILE_BYTES) continue\n\n const mimeType = getMultimodalMimeType(handle.name) ?? getMultimodalMimeType(handle.readonlyPath)\n if (!mimeType) continue\n\n try {\n const fileBuffer = await readFn(handle.readonlyPath)\n if (fileBuffer.length > MAX_MULTIMODAL_FILE_BYTES) continue\n resolvedForMessage.push({\n mimeType,\n data: Buffer.from(fileBuffer).toString('base64'),\n name: handle.name,\n path: handle.readonlyPath,\n bytes: fileBuffer.length,\n })\n } catch {\n // Error resilience: catch error silently and do not throw\n continue\n }\n }\n }\n\n if (resolvedForMessage.length > 0) {\n const key = message.id ?? `msg-${i}`\n result.set(key, resolvedForMessage)\n }\n }\n\n return result\n}\n","/**\n * Translate a DSH GenerateOptions into the Antigravity wrapped request.\n *\n * Envelope shape follows the actively-maintained OmniRoute wire format\n * (the archived opencode reference predates it): top-level `project`,\n * `requestId`, `model`, `userAgent`, `requestType`, with the Gemini-style\n * body under `request` (contents/systemInstruction/tools/generationConfig/\n * sessionId). `toolConfig` VALIDATED is attached when tools are present, and\n * Claude-path requests strip trailing model turns (Vertex rejects \"assistant\n * message prefill\").\n *\n * Thinking blocks are carried as-is on the Gemini path (Gemini `thought`\n * parts); no thought is ever re-signed — that signature dance was an artifact\n * of the reference plugin's interception architecture (see\n * docs/ARCHITECTURE.md). The Claude path is stricter and drops thought parts\n * entirely, because its validator demands a real thinking signature that only\n * the originating model can produce (docs/ANTIGRAVITY-API.md §3.3).\n */\n\nimport { createHash } from 'node:crypto'\nimport type { GenerateOptions, ToolSchema } from '@deepseek-ai/dsh-llm'\nimport { generateAntigravityRequestId } from '../runtime/identity.ts'\nimport { getThoughtSignature, THOUGHT_SIGNATURE_SENTINEL } from '../runtime/signature-cache.ts'\nimport { catalogModel, isLevelThinkingModel } from './catalog.ts'\nimport { isClaudeModel, supportsMultimodalFiles, type AgyResolvedMultimodalFile } from './multimodal.ts'\nimport {\n conversationMessages,\n normalizeMessages,\n systemTextFromMessages,\n type AgyBlockView,\n type AgyMessageView,\n} from './dsh-view.ts'\n\nexport { isClaudeModel, supportsMultimodalFiles }\nexport type { AgyResolvedMultimodalFile }\n\nexport type AgyPart =\n | { text: string }\n | { thought: true; text: string }\n | { thoughtSignature: string; functionCall: { id: string; name: string; args: unknown } }\n | { functionResponse: { id: string; name: string; response: unknown } }\n | { inlineData: { mimeType: string; data: string } }\n\n/** Image bytes pre-resolved from the durable attachment store, keyed by attachment id. */\nexport interface AgyResolvedImage {\n mediaType: string\n /** Pure base64 (no data: prefix). */\n data: string\n}\n\nexport interface AgyContent {\n role: 'user' | 'model'\n parts: AgyPart[]\n}\n\nexport interface AgyRequestBody {\n project?: string\n requestId?: string\n model: string\n userAgent?: string\n requestType?: 'agent'\n request: {\n contents: AgyContent[]\n systemInstruction?: { parts: Array<{ text: string }> }\n tools?: Array<{ functionDeclarations: Array<{ name: string; description: string; parameters: unknown }> }>\n toolConfig?: { functionCallingConfig: { mode: 'VALIDATED' } }\n generationConfig?: {\n temperature?: number\n maxOutputTokens?: number\n stopSequences?: string[]\n thinkingConfig?: { thinkingLevel?: string; includeThoughts?: boolean; thinkingBudget?: number }\n }\n sessionId?: string\n }\n}\n\n// isClaudeModel is imported from ./multimodal.ts and re-exported above\n\n/**\n * Vertex (the Antigravity Claude backend) rejects conversations ending on an\n * assistant/model turn (\"assistant message prefill\"); never strip to empty.\n */\nexport function stripTrailingModelTurn(contents: AgyContent[]): AgyContent[] {\n while (contents.length > 1 && contents[contents.length - 1]?.role === 'model') {\n contents.pop()\n }\n return contents\n}\n\n/**\n * The Antigravity backend parses tool `parameters` as a strict protobuf\n * schema and rejects ANY unknown keyword with 400 (verified empirically:\n * `$schema`, `propertyNames`, `pattern`, `minLength`, ... each fail in turn).\n * Denylisting is whack-a-mole, so keep only the keywords the upstream\n * accepts. Container shapes are handled distinctly: `properties` is a\n * name->schema map (keys preserved), `items`/`additionalProperties` are\n * nested schemas (additionalProperties also accepts a boolean — live-verified\n * against the Antigravity upstream), `required`/`enum` are plain arrays.\n *\n * Keyword VALUES are also constrained by the protobuf shape (verified\n * empirically): `type` must be a single enum string (union arrays like\n * `[\"string\",\"number\"]` are rejected) and every `enum` item must be a\n * non-empty string (booleans/numbers/empty strings are rejected). Values are\n * normalized to the nearest valid form instead of being dropped wholesale.\n */\n// Exported for the contract invariant test (tests/adapter.test.ts); not part of\n// the package public API (translate.ts is an internal module).\nexport const AGY_SCHEMA_ALLOWLIST = new Set([\n 'type', 'format', 'title', 'description', 'nullable',\n 'items', 'enum', 'default', 'properties', 'required', 'additionalProperties',\n])\nconst AGY_SCHEMA_MAP_KEYS = new Set(['properties'])\nconst AGY_SCHEMA_NESTED_KEYS = new Set(['items', 'additionalProperties'])\nconst AGY_SCHEMA_LIST_KEYS = new Set(['required', 'enum'])\n\nfunction sanitizeToolSchema(schema: unknown): unknown {\n if (!schema || typeof schema !== 'object') return schema\n if (Array.isArray(schema)) return schema.map((entry) => sanitizeToolSchema(entry))\n\n // Normalize union types: upstream `type` is a single enum string. Pick the\n // first non-null string type (`\"null\"` maps to the `nullable` keyword);\n // fall back to `string` when no usable type remains.\n let normalized = schema as Record<string, unknown>\n if (Array.isArray(normalized.type)) {\n const types = normalized.type.filter((t): t is string => typeof t === 'string' && t !== 'null')\n normalized = { ...normalized, type: types[0] ?? 'string' }\n }\n\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(normalized)) {\n if (!AGY_SCHEMA_ALLOWLIST.has(key)) continue\n if (AGY_SCHEMA_MAP_KEYS.has(key)) {\n const map: Record<string, unknown> = {}\n for (const [name, child] of Object.entries(value as Record<string, unknown>)) {\n map[name] = sanitizeToolSchema(child)\n }\n result[key] = map\n continue\n }\n if (AGY_SCHEMA_NESTED_KEYS.has(key)) {\n result[key] = sanitizeToolSchema(value)\n continue\n }\n if (AGY_SCHEMA_LIST_KEYS.has(key)) {\n // Upstream `enum` items must be non-empty strings; filter the rest\n // and omit an empty enum entirely (an empty array would be rejected too).\n if (key === 'enum' && Array.isArray(value)) {\n const filtered = value.filter((v): v is string => typeof v === 'string' && v.length > 0)\n if (filtered.length > 0) result[key] = filtered\n } else {\n result[key] = value\n }\n continue\n }\n result[key] = value\n }\n return result\n}\n\n/** Collect tool-call names by id so tool results can name their function. */\nfunction buildToolNameIndex(messages: readonly AgyMessageView[]): Map<string, string> {\n const index = new Map<string, string>()\n for (const message of messages) {\n for (const block of message.content) {\n if (block.type === 'tool-call') {\n index.set(block.id, block.name)\n }\n }\n }\n return index\n}\n\n/**\n * The single producer of a `functionResponse` part.\n *\n * Both supported dsh-llm vocabularies funnel through here: a 0.1.5\n * `tool-result` CONTENT BLOCK and a 0.2.0 `tool`-ROLE message describe the same\n * result, and this channel's wire shape must not depend on which one arrived.\n */\nfunction toolResultPart(\n toolCallId: string,\n isError: boolean,\n content: readonly AgyBlockView[],\n toolNames: Map<string, string>,\n): AgyPart {\n const name = toolNames.get(toolCallId) ?? toolCallId\n const text = content\n .filter((block): block is Extract<AgyBlockView, { type: 'text' }> => block.type === 'text')\n .map((block) => block.text)\n .join('\\n')\n return {\n functionResponse: {\n // The Anthropic-backed Claude path requires tool_result.tool_use_id\n // and 400s without it (\"messages.N.content.M.tool_result.tool_use_id:\n // Field required\"); the Gemini path accepts the id too, so it is\n // always carried rather than branched per family (live-verified).\n id: toolCallId,\n name,\n response: { result: text, is_error: isError },\n },\n }\n}\n\nfunction blockToParts(\n block: AgyBlockView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n /** Claude path: replayed thought blocks are rejected outright (see below). */\n dropThoughts = false,\n): AgyPart[] {\n switch (block.type) {\n case 'text':\n // An empty text part is rejected by the Anthropic-backed Claude path\n // (\"messages.N.content.M.text.text: Field required\") while the Gemini\n // path tolerates it. Upstream's own parts normalization drops empty\n // text, so drop it here for every family (live-verified). DSH emits\n // these as trailing zero-length blocks after a tool call.\n return block.text === '' ? [] : [{ text: block.text }]\n case 'reasoning':\n // Empty thought: same rejection class as `text` (\"thinking.thinking:\n // Field required\").\n if (block.text === '') return []\n // A replayed thought block cannot be sent to the Claude path at all: the\n // backend demands a thinking `signature`, and the\n // `skip_thought_signature_validator` sentinel that works for functionCall\n // parts is rejected here as an invalid signature (live-verified). Only\n // the model that produced the thought could re-sign it, so a thought that\n // came from another family (e.g. a mid-session switch from a Gemini\n // tiered model) has no valid form — drop it rather than 400. Gemini\n // accepts replayed thoughts, so this is Claude-only.\n if (dropThoughts) return []\n return [{ thought: true, text: block.text }]\n case 'tool-call': {\n // Upstream parses functionCall.args as google.protobuf.Struct and\n // rejects a raw string with 400. Guarantee an object: parse the string\n // form, fall back to {} when it is truncated/malformed (the failure\n // mode for long multi-turn histories).\n let args: unknown = {}\n if (typeof block.arguments === 'object' && block.arguments !== null && !Array.isArray(block.arguments)) {\n args = block.arguments\n } else if (typeof block.arguments === 'string') {\n try {\n const parsed: unknown = JSON.parse(block.arguments)\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n args = parsed\n }\n } catch {\n // truncated/malformed JSON -> empty object\n }\n }\n // Antigravity rejects functionCall parts without a thoughtSignature\n // (400). Replay the signature captured for this tool call id on the\n // previous turn; the sentinel is the established bypass when nothing is\n // cached (both reference implementations default to it).\n //\n // OPEN QUESTION, deliberately not changed yet: another implementation of\n // this client stamps the sentinel ONLY on the FIRST functionCall of a model\n // turn and leaves sibling calls unsigned (\"unsigned sibling functionCalls\n // preserve native parallel-call shape\"), and never synthesizes a bypass\n // signature anywhere else. This path stamps every call. Which is right is\n // unverified for THIS channel — the sentinel is live-verified to work here,\n // and a wrong change turns working parallel tool calls into 400s — so it\n // needs one real multi-tool turn measured before any edit.\n const signature = getThoughtSignature(block.id) ?? THOUGHT_SIGNATURE_SENTINEL\n return [{\n thoughtSignature: signature,\n functionCall: { id: block.id, name: block.name, args },\n }]\n }\n case 'tool-result':\n return [toolResultPart(\n block.toolCallId,\n block.isError === true,\n block.content,\n toolNames,\n )]\n case 'image': {\n // Bytes are pre-resolved by the adapter before translation; a missing\n // entry breaks that invariant and must fail loudly, never silently drop.\n const resolved = images.get(block.attachment.attachmentId)\n if (!resolved) {\n throw new Error(`agy translate: unresolved image attachment \"${block.attachment.attachmentId}\"`)\n }\n return [{ inlineData: { mimeType: resolved.mediaType, data: resolved.data } }]\n }\n default:\n return [] // unknown block types (merge-extensible) are skipped\n }\n}\n\nfunction messageToContent(\n message: AgyMessageView,\n toolNames: Map<string, string>,\n images: Map<string, AgyResolvedImage>,\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>,\n messageIndex?: number,\n dropThoughts = false,\n): AgyContent | null {\n // 0.2.0 promotes a tool result to a `tool`-ROLE message, with the correlation\n // (`toolCallId`/`isError`) on the message instead of on a `tool-result` block.\n // It projects to the same wire part, and a `functionResponse` always belongs\n // to a user-side turn on this channel.\n if (message.role === 'tool') {\n if (message.toolCallId === undefined) {\n // Uncorrelated: the Claude path 400s without an id, and inventing one\n // would mis-address a real call. Dropped like any other unreadable shape.\n return null\n }\n return {\n role: 'user',\n parts: [toolResultPart(message.toolCallId, message.isError === true, message.content, toolNames)],\n }\n }\n\n const parts = message.content.flatMap((block) =>\n // Non-user images are out of scope by policy (docs ANTIGRAVITY-API §3.2):\n // skip them like any other untranslatable block instead of tripping the\n // unresolved-map guard, which protects only the user-image invariant.\n block.type === 'image' && message.role !== 'user'\n ? []\n : blockToParts(block, toolNames, images, dropThoughts),\n )\n\n if (message.role === 'user' && multimodalFiles) {\n const files =\n (message.id ? multimodalFiles.get(message.id) : undefined) ??\n (multimodalFiles as Map<unknown, AgyResolvedMultimodalFile[]>).get(message) ??\n (messageIndex !== undefined ? multimodalFiles.get(`msg-${messageIndex}`) : undefined) ??\n (messageIndex !== undefined ? multimodalFiles.get(String(messageIndex)) : undefined)\n\n if (files) {\n for (const file of files) {\n parts.push({ inlineData: { mimeType: file.mimeType, data: file.data } })\n }\n }\n }\n\n if (parts.length === 0) return null\n const role = message.role === 'assistant' ? 'model' : 'user'\n return { role, parts }\n}\n\n/**\n * Builtin Gemini tools must not shadow functionDeclarations names (upstream\n * treats them as native tools; verified by OmniRoute's GEMINI_BUILTIN_TOOL_NAMES).\n */\nconst AGY_BUILTIN_TOOL_NAMES = new Set(['google_search', 'web_search', 'search_web', 'googleSearch'])\n\n/** Level-thinking: single id + selectable low/medium/high via thinkingLevel (catalog thinking:'level'). */\nconst LEVEL_THINKING_LEVELS = new Set(['low', 'medium', 'high'])\n\n/**\n * Claude-family output ceiling on the Antigravity channel (live-measured).\n *\n * The Claude models are served through the Gemini-style\n * `generationConfig.maxOutputTokens` field here, and this channel rejects the\n * Claude family above 64000: 64000 answers 200, 64001 answers 400\n * `INVALID_ARGUMENT: Request contains an invalid argument` (deterministic\n * across both Claude ids, with and without the full 87-tool payload). The\n * Gemini family accepts 65536 on the same endpoint, so the limit is\n * model-family-specific, not endpoint-wide.\n *\n * Do NOT reason about this number from Anthropic's public API limits. Agy may\n * front a self-hosted or otherwise gated Claude deployment whose capacity and\n * validation rules are its own; the only authority is what this channel\n * accepts, which is what the probe measures. The value here is that\n * measurement, nothing more.\n *\n * The catalog's `maxOutputTokens` is the harness-injected default\n * (`LlmResolvedModelInfo.defaultMaxTokens`), so a wrong value there makes every\n * Claude request fail. This clamp is the second line of defense: it also covers\n * an explicit `maxTokens` (agent preset / call config) and a dynamically\n * discovered Claude id absent from the pinned catalog.\n */\nexport const AGY_CLAUDE_MAX_OUTPUT_TOKENS = 64_000\n\n/** Upstream functionDeclarations names are `[a-zA-Z0-9_]` and ≤64 chars (OmniRoute-verified). */\nconst AGY_TOOL_NAME_MAX_LENGTH = 64\n\n/** Sanitize a tool name to the upstream charset/length; dedupe via a short hash. */\nfunction sanitizeToolName(name: string, seen: Set<string>): string {\n let candidate = name.replace(/[^a-zA-Z0-9_]/g, '_') || 'tool'\n if (candidate.length > AGY_TOOL_NAME_MAX_LENGTH || seen.has(candidate)) {\n const hash = createHash('sha256').update(candidate).digest('hex').slice(0, 8)\n const prefix = candidate.slice(0, AGY_TOOL_NAME_MAX_LENGTH - hash.length - 1)\n candidate = `${prefix}_${hash}`\n let i = 2\n while (seen.has(candidate)) candidate = `${prefix}_${i++}_${hash}`\n }\n seen.add(candidate)\n return candidate\n}\n\nfunction toolsToDeclarations(tools: ToolSchema[] | undefined): AgyRequestBody['request']['tools'] {\n if (!tools || tools.length === 0) return undefined\n const seenNames = new Set<string>()\n const declarations = []\n for (const tool of tools) {\n if (AGY_BUILTIN_TOOL_NAMES.has(tool.name)) continue\n declarations.push({\n name: sanitizeToolName(tool.name, seenNames),\n description: tool.description,\n parameters: sanitizeToolSchema(tool.parameters),\n })\n }\n if (declarations.length === 0) return undefined\n return [{ functionDeclarations: declarations }]\n}\n\n/** Build the wrapped Antigravity request body for one call. */\nexport function toAgyRequestBody(\n options: GenerateOptions,\n context: {\n projectId?: string\n sessionId?: string\n images?: Map<string, AgyResolvedImage>\n multimodalFiles?: Map<string, AgyResolvedMultimodalFile[]>\n /**\n * The request id, when the caller also stamps it on the wire\n * (`x-goog-request-id`). Generating it here as well produced TWO different\n * ids for one request — the body and the header disagreed, a shape no client\n * produces. Callers that send the header must pass the same value.\n */\n requestId?: string\n /**\n * A configured token budget for a reasoning level, or undefined to leave the\n * level's own budget to upstream.\n *\n * When this returns a number the budget REPLACES `thinkingLevel` rather than\n * joining it, because measurement shows the level wins when both are sent:\n * `{thinkingLevel:\"low\", thinkingBudget:16000}` spends what `low` alone\n * spends (~180 thoughts, against ~330 for the budget alone), and\n * `{thinkingLevel:\"high\", thinkingBudget:1000}` likewise tracks `high`\n * (~316 vs ~173). Sending both would therefore make a configured number\n * silently inert, which is worse than not offering the setting.\n */\n thinkingBudgetFor?: (level: string) => number | undefined\n /**\n * The configured Claude thinking budget, or undefined when unset.\n *\n * A single value, not a per-level map: the Claude family is id-bound (each\n * capability is its own model id with no level selector), so there is no\n * level to key by.\n */\n claudeBudgetFor?: () => number | undefined\n /**\n * The configured budget for the TIERED slot (the selector's \"Default\"\n * effort), or undefined when unset.\n *\n * That effort arrives with NO level id, so it cannot go through\n * `thinkingBudgetFor`. A value here sends a bare `thinkingBudget` with no\n * `thinkingLevel` — the tiered model's adaptive entry, given an explicit cap.\n */\n tieredBudgetFor?: () => number | undefined\n },\n): AgyRequestBody {\n const messages = normalizeMessages(options.messages)\n const toolNames = buildToolNameIndex(messages)\n const images = context.images ?? new Map<string, AgyResolvedImage>()\n const multimodalFiles = supportsMultimodalFiles(options.model) ? context.multimodalFiles : undefined\n const claude = isClaudeModel(options.model)\n let contents = conversationMessages(messages)\n .map((message, index) => messageToContent(message, toolNames, images, multimodalFiles, index, claude))\n .filter((c): c is AgyContent => c !== null)\n if (claude) {\n contents = stripTrailingModelTurn(contents)\n }\n\n // `options.system` is the one-shot channel and is undefined for a loop-built\n // request, whose derived history carries the prompt as a `system`-role message\n // (documented identically on both supported dsh-llm lines). Both sources feed\n // the system slot; routing the message through `contents` instead would put\n // the system prompt on the wire as a USER turn.\n const systemText = [options.system, systemTextFromMessages(messages)]\n .filter((text): text is string => typeof text === 'string' && text !== '')\n .join('\\n\\n')\n\n const tools = toolsToDeclarations(options.tools)\n const generationConfig: NonNullable<AgyRequestBody['request']['generationConfig']> = {}\n if (options.temperature !== undefined) generationConfig.temperature = options.temperature\n if (options.maxTokens !== undefined) {\n // Claude family: the platform rejects >64000 with 400, and the value the\n // harness injects comes from the catalog default, so clamp before the wire.\n generationConfig.maxOutputTokens = isClaudeModel(options.model)\n ? Math.min(options.maxTokens, AGY_CLAUDE_MAX_OUTPUT_TOKENS)\n : options.maxTokens\n }\n if (options.stop !== undefined && options.stop.length > 0) generationConfig.stopSequences = options.stop\n // Level-thinking: map the DSH reasoning effort to thinkingConfig.\n // Id-bound models (thinking !== 'level') never emit it, and a tiered model\n // emits NOTHING when no effort is requested — there is deliberately no default\n // level, because \"no level chosen\" is what lets the model allocate its own\n // thinking (see `LEVEL_REASONING` for why declaring a default broke that).\n // When purpose is 'session-title' or reasoning is off, thinkingBudget: 0 prevents\n // default thinking tokens from exhausting tight output caps (e.g. maxTokens: 64).\n const effort = options.reasoningEffort?.toLowerCase()\n if (isLevelThinkingModel(options.model)) {\n if (options.purpose === 'session-title' || effort === 'none' || effort === 'off') {\n generationConfig.thinkingConfig = { thinkingBudget: 0 }\n } else if (effort === undefined) {\n // The selector's \"Default\" effort: no level was chosen. A configured tiered\n // budget turns this into Max by sending a bare `thinkingBudget` (no\n // `thinkingLevel`, so the number alone decides). Unset sends nothing at all,\n // which leaves upstream's own adaptive allocation in charge.\n const tiered = context.tieredBudgetFor?.()\n if (tiered !== undefined) {\n generationConfig.thinkingConfig = { thinkingBudget: tiered, includeThoughts: true }\n }\n } else if (LEVEL_THINKING_LEVELS.has(effort)) {\n // A configured number for this level takes the place of the level token:\n // both together would let the level win (see `thinkingBudgetFor`).\n const configured = context.thinkingBudgetFor?.(effort)\n generationConfig.thinkingConfig = configured === undefined\n ? { thinkingLevel: effort, includeThoughts: true }\n : { thinkingBudget: configured, includeThoughts: true }\n }\n } else if (claude && options.purpose !== 'session-title') {\n // Claude thinking models are id-bound (no level selector), so their budget is\n // a single configured value rather than one per level. Measured constraints,\n // all of which must hold or the request is a 400:\n // - `max_tokens` must be STRICTLY greater than the budget. `budget=1024`\n // with `max_tokens=1024` is rejected, and so is a budget sent with no\n // `maxOutputTokens` at all.\n // - the floor is 1024 (not `-1`); the store validates that on save.\n // So a budget that does not leave room is DROPPED rather than forced through\n // by raising `maxTokens`: silently enlarging the caller's output cap would\n // change the request's cost and truncation behaviour, while omitting the\n // budget merely means this turn thinks with upstream's default. A 400 would\n // be worse than either.\n const claudeBudget = context.claudeBudgetFor?.()\n const outputCap = generationConfig.maxOutputTokens\n if (claudeBudget !== undefined && outputCap !== undefined && outputCap > claudeBudget) {\n generationConfig.thinkingConfig = { thinkingBudget: claudeBudget, includeThoughts: true }\n }\n }\n\n return {\n project: context.projectId || undefined,\n requestId: context.requestId ?? generateAntigravityRequestId(),\n model: options.model,\n userAgent: 'antigravity',\n requestType: 'agent',\n request: {\n contents,\n ...(systemText ? { systemInstruction: { parts: [{ text: systemText }] } } : {}),\n ...(tools ? { tools } : {}),\n ...(tools ? { toolConfig: { functionCallingConfig: { mode: 'VALIDATED' } } } : {}),\n ...(Object.keys(generationConfig).length > 0 ? { generationConfig } : {}),\n ...(context.sessionId ? { sessionId: context.sessionId } : {}),\n },\n }\n}\n"],"mappings":"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"}
@@ -1,10 +1,97 @@
1
1
  import { p as currentAgyVersion } from "../constants-CmA9wXNe.mjs";
2
2
  import { i as normalizeProxyUrl, n as isProxyReachable, s as proxyUrlForLogs } from "../proxy-BbuQX5U9.mjs";
3
- import { a as generateFingerprint, i as isAgyDisabled, l as clearExpiredState, n as maskProxyUrl, o as recordFingerprintVersion } from "../accounts-D605JE2i.mjs";
4
- import "../models-B6bD7zPi.mjs";
5
- import { a as CLAUDE_BUDGET_MIN, i as CLAUDE_BUDGET_MAX, n as dayKey, o as THINKING_BUDGET_MAX, r as foldWindowBreakdown, t as createAgyRuntime } from "../plugin-common-kSBwIaFi.mjs";
3
+ import { a as generateFingerprint, d as agyDataFile, i as isAgyDisabled, l as clearExpiredState, n as maskProxyUrl, o as recordFingerprintVersion } from "../accounts-CB4blXpP.mjs";
4
+ import "../models-CjB0rXtI.mjs";
5
+ import { a as CLAUDE_BUDGET_MIN, i as CLAUDE_BUDGET_MAX, n as dayKey, o as THINKING_BUDGET_MAX, r as foldWindowBreakdown, t as createAgyRuntime } from "../plugin-common-D2NWMsX6.mjs";
6
6
  import { t as exchangeAntigravity } from "../exchange-C-tndv7j.mjs";
7
7
  import { n as upsertImportedAccount, r as authorizeAntigravity, t as importManySources } from "../import-SxPDjH3u.mjs";
8
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
9
+ import { dirname } from "node:path";
10
+ const UI_PREFS_FILE = "agy-ui-prefs.json";
11
+ function sanitizeUiPrefs(raw) {
12
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return { version: 1 };
13
+ const obj = raw;
14
+ const doc = { version: 1 };
15
+ if (typeof obj.conversationBadge === "boolean") doc.conversationBadge = obj.conversationBadge;
16
+ return doc;
17
+ }
18
+ function parseUiPrefs(text) {
19
+ let raw;
20
+ try {
21
+ raw = JSON.parse(text);
22
+ } catch {
23
+ return { version: 1 };
24
+ }
25
+ return sanitizeUiPrefs(raw);
26
+ }
27
+ var UiPrefsStore = class UiPrefsStore {
28
+ file;
29
+ doc;
30
+ raw;
31
+ constructor(file = agyDataFile(UI_PREFS_FILE)) {
32
+ this.file = file;
33
+ const { doc, raw } = this.readWithRaw();
34
+ this.doc = doc;
35
+ this.raw = raw;
36
+ }
37
+ get() {
38
+ this.reloadIfChanged();
39
+ return { conversationBadge: this.doc.conversationBadge === true };
40
+ }
41
+ set(prefs) {
42
+ this.reload();
43
+ const next = {
44
+ version: 1,
45
+ ...this.doc.conversationBadge !== void 0 ? { conversationBadge: this.doc.conversationBadge } : {}
46
+ };
47
+ if (typeof prefs.conversationBadge === "boolean") next.conversationBadge = prefs.conversationBadge;
48
+ this.write(next);
49
+ this.doc = next;
50
+ this.raw = UiPrefsStore.serialize(next);
51
+ return { conversationBadge: next.conversationBadge === true };
52
+ }
53
+ reload() {
54
+ const fresh = this.readWithRaw();
55
+ this.doc = fresh.doc;
56
+ this.raw = fresh.raw;
57
+ }
58
+ reloadIfChanged() {
59
+ let current;
60
+ try {
61
+ current = readFileSync(this.file, "utf8");
62
+ } catch {
63
+ current = "";
64
+ }
65
+ if (current === this.raw) return;
66
+ this.reload();
67
+ }
68
+ readWithRaw() {
69
+ let text;
70
+ try {
71
+ text = readFileSync(this.file, "utf8");
72
+ } catch {
73
+ return {
74
+ doc: { version: 1 },
75
+ raw: ""
76
+ };
77
+ }
78
+ return {
79
+ doc: parseUiPrefs(text),
80
+ raw: text
81
+ };
82
+ }
83
+ write(doc) {
84
+ const serialized = UiPrefsStore.serialize(doc);
85
+ const tmp = `${this.file}.tmp.${process.pid}.${Date.now()}`;
86
+ mkdirSync(dirname(this.file), { recursive: true });
87
+ writeFileSync(tmp, serialized, { mode: 384 });
88
+ renameSync(tmp, this.file);
89
+ }
90
+ static serialize(doc) {
91
+ return `${JSON.stringify(doc, null, 2)}\n`;
92
+ }
93
+ };
94
+ //#endregion
8
95
  //#region src/web/management.ts
9
96
  /**
10
97
  * agy management API: one method dispatcher shared by the RPC transport.
@@ -46,6 +133,7 @@ function toAccountUsageView(usage) {
46
133
  function createAgyManagement(options) {
47
134
  const { store, sessions, stats, recentRequests, modelVisibility, listAllModels, invalidateModelCache, baseUrl, notifyModelsChanged } = options;
48
135
  const thinkingBudget = options.thinkingBudget;
136
+ const uiPrefs = options.uiPrefs ?? new UiPrefsStore();
49
137
  /** Authorizations issued by `auth.url`, keyed by raw state. */
50
138
  const pendingAuth = /* @__PURE__ */ new Map();
51
139
  const PENDING_AUTH_TTL_MS = 6e5;
@@ -423,7 +511,13 @@ function createAgyManagement(options) {
423
511
  fail(error instanceof Error ? error.message : String(error));
424
512
  }
425
513
  },
426
- "stats.get": async () => statsView()
514
+ "stats.get": async () => statsView(),
515
+ "ui.prefs.get": async () => uiPrefs.get(),
516
+ "ui.prefs.set": async (payload) => {
517
+ const body = payload;
518
+ if (body?.conversationBadge !== void 0 && typeof body.conversationBadge !== "boolean") fail("conversationBadge must be a boolean");
519
+ return uiPrefs.set({ conversationBadge: body?.conversationBadge });
520
+ }
427
521
  };
428
522
  return {
429
523
  async call(method, payload) {