From fba1e40bbf8d533a484dc6b172f7c6242de83f4f Mon Sep 17 00:00:00 2001 From: qiongyu1999 <2694684348@qq.com> Date: Thu, 28 May 2026 17:33:32 +0800 Subject: [PATCH] =?UTF-8?q?fix(daemon):=20Antigravity=20adapter=20?= =?UTF-8?q?=E2=80=94=20stdin=20prompt,=20brand=20icon,=20form=20loop,=20em?= =?UTF-8?q?pty-output=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Switch prompt delivery from argv to stdin (`agy -p -`) to avoid the 30KB maxPromptArgBytes limit that blocked real-world composed prompts - Add official Antigravity brand SVG icon to agent picker - Fix repeated question-form loop for plain agents by injecting an OVERRIDE block when form answers are already present in the transcript - Add empty-output guard for plain agents so expired auth or silent failures surface a user-visible error instead of a blank "Done" turn --- apps/daemon/src/runtimes/defs/antigravity.ts | 31 +--------- apps/daemon/src/server.ts | 59 +++++++++++++------ apps/daemon/tests/runtimes/agent-args.test.ts | 30 ++-------- apps/web/public/agent-icons/antigravity.svg | 1 + apps/web/src/components/AgentIcon.tsx | 1 + 5 files changed, 52 insertions(+), 70 deletions(-) create mode 100644 apps/web/public/agent-icons/antigravity.svg diff --git a/apps/daemon/src/runtimes/defs/antigravity.ts b/apps/daemon/src/runtimes/defs/antigravity.ts index 11176a0a7b..316a77ff65 100644 --- a/apps/daemon/src/runtimes/defs/antigravity.ts +++ b/apps/daemon/src/runtimes/defs/antigravity.ts @@ -1,41 +1,16 @@ import { DEFAULT_MODEL_OPTION } from './shared.js'; import type { RuntimeAgentDef } from '../types.js'; -// Google Antigravity — agentic dev platform launched 2025-11 with Gemini 3. -// `agy` is the terminal CLI surface; the IDE (Antigravity 2.0) shares the -// same agent engine and OAuth credentials via the system keyring, so a -// signed-in user is authenticated on first run with no extra work. -// -// As of v1.0.3 the CLI is TUI-first and minimally headless: -// - `agy -p ""` runs a single non-interactive turn and prints -// the assistant reply as plain text to stdout, then exits. -// - There is no JSON / stream-json / ACP output mode yet (open upstream -// issues #119, #31), and no `--model` flag yet (open issue #35). -// - The prompt MUST go on argv as the value of `-p`; stdin piping is -// not supported (`-p` is a value flag, not a boolean). -// -// Until upstream ships a structured output mode, we expose Antigravity as -// a `plain` runtime — single-turn text reply with no tool_use streaming -// and no model picker. When `--output-format stream-json` lands we will -// upgrade buildArgs + add a dedicated event parser. export const antigravityAgentDef = { id: 'antigravity', name: 'Antigravity', bin: 'agy', versionArgs: ['--version'], - // Only `default` for now: `agy` has no `--model` flag (upstream issue - // #35). Showing concrete model ids in the picker would mislead users - // into thinking the choice is wired through. Upgrade this list when - // upstream adds model selection. fallbackModels: [DEFAULT_MODEL_OPTION], - buildArgs: (prompt, _imagePaths, _extra = [], _options = {}) => { - return ['-p', prompt]; + buildArgs: (_prompt, _imagePaths, _extra = [], _options = {}) => { + return ['-p', '-']; }, - // Guard against prompts that would blow Windows' ~32 KB CreateProcess - // limit. Prompt rides on argv because `agy -p` is a value flag with - // no stdin sentinel; 30_000 bytes mirrors the deepseek budget and - // leaves ~2.7 KB of argv headroom for `-p` plus quoting. - maxPromptArgBytes: 30_000, + promptViaStdin: true, streamFormat: 'plain', installUrl: 'https://antigravity.google/cli', docsUrl: 'https://antigravity.google/docs/cli-overview', diff --git a/apps/daemon/src/server.ts b/apps/daemon/src/server.ts index 2c93461e45..8053ea3f6b 100644 --- a/apps/daemon/src/server.ts +++ b/apps/daemon/src/server.ts @@ -2263,15 +2263,15 @@ function formAnswerTransitionForCurrentPrompt(currentPrompt) { '## Latest user turn - form answers submitted', trimmed, '', - `The user has answered the ${formId} form. Do not emit another ${formId} form.`, + `The user has answered the ${formId} form. Do NOT emit another of any kind. The brief is locked.`, ]; - if (formId.toLowerCase() === 'discovery') { + if (formId.toLowerCase() === 'discovery' || formId.toLowerCase() === 'task-type') { lines.push( - 'Continue with RULE 2 / RULE 3 now. For Branch B answers, build now instead of asking another brief.', + 'Continue with RULE 2 / RULE 3 now. For Branch B answers, build now instead of asking another brief. SKIP RULE 1 entirely — the form has already been answered.', ); } else { lines.push( - 'Treat these form answers as the active user turn instead of replaying the transcript as a fresh request.', + 'Treat these form answers as the active user turn instead of replaying the transcript as a fresh request. Do NOT re-ask for information already provided.', ); } return lines.join('\n'); @@ -10564,14 +10564,22 @@ export async function startServer({ // instructions and request) — see server.ts:9920 composer notes. const ECHO_GUARD = '\n\n(Do not quote, restate, or echo the # Instructions block above in your reply. Begin your response with the answer to the # User request below.)'; + const formAlreadyAnswered = FORM_ANSWERS_HEADER_RE.test( + typeof currentPrompt === 'string' ? currentPrompt : '', + ); + const formOverride = formAlreadyAnswered + ? '## OVERRIDE — form already answered\nThe user has already submitted their form answers (see # User request below). Do NOT emit any `` tag. RULE 1 does NOT apply — the discovery/task-type form is done. Proceed to RULE 2 / RULE 3 and build the artifact.\n\n' + : ''; const composed = [ instructionPrompt - ? `# Instructions (read first)\n\n${instructionPrompt}${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` + ? `# Instructions (read first)\n\n${formOverride}${instructionPrompt}${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` : cwdHint - ? `# Instructions${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` + ? `# Instructions\n\n${formOverride}${cwdHint}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` : linkedDirsHint - ? `# Instructions${linkedDirsHint}${ECHO_GUARD}\n\n---\n` - : '', + ? `# Instructions\n\n${formOverride}${linkedDirsHint}${ECHO_GUARD}\n\n---\n` + : formOverride + ? `# Instructions\n\n${formOverride}${ECHO_GUARD}\n\n---\n` + : '', `# User request\n\n${userRequestPrompt}${attachmentHint}${commentHint}`, safeImages.length ? `\n\n${safeImages.map((p) => `@${p}`).join(' ')}` @@ -11538,15 +11546,9 @@ export async function startServer({ return design.runs.finish(run, 'failed', code ?? 1, signal ?? null); } } - // Empty-output guard: a clean `code === 0` exit on a stream we are - // tracking, with no error frame and no substantive event, means the - // run silently finished without producing anything visible. That used - // to be marked `succeeded` and rendered as an empty assistant turn — - // see issue #691, where OpenCode runs were ending in ~3s with no - // chat content and no error banner. Surface an explicit failure - // instead so the chat shows a clear reason. ACP sessions and plain - // stdout streams are gated out via `trackingSubstantiveOutput`; - // their success/failure determination lives elsewhere. + // Empty-output guard: a clean `code === 0` exit with no visible + // output means the run silently finished without producing anything. + // Surface an explicit failure so the chat shows a clear reason. if ( code === 0 && !run.cancelRequested && @@ -11560,6 +11562,29 @@ export async function startServer({ )); return design.runs.finish(run, 'failed', code, signal); } + // Plain-stream empty-output guard: plain agents send raw stdout + // chunks without structured event tracking. Detect auth failures + // or silent empty responses when exit 0 but no stdout was seen. + if ( + code === 0 && + !run.cancelRequested && + !trackingSubstantiveOutput && + !childStdoutSeen + ) { + const authFailure = classifyAgentAuthFailure( + agentId, + `${agentStderrTail}\n${agentStdoutTail}`, + ); + const msg = authFailure + ? authFailure.message ?? `${def.name} authentication expired. Please re-authenticate and retry.` + : `${def.name} returned an empty response. This may indicate an expired session — try re-authenticating the agent.`; + send('error', createSseErrorPayload( + authFailure ? 'AGENT_AUTH_REQUIRED' : 'AGENT_EXECUTION_FAILED', + msg, + { retryable: true }, + )); + return design.runs.finish(run, 'failed', 0, signal); + } // ACP agents that don't shut down on stdin.end() (e.g. Devin for // Terminal) are forced to exit via SIGTERM from attachAcpSession after // a clean prompt completion. Without an override, the chat run would diff --git a/apps/daemon/tests/runtimes/agent-args.test.ts b/apps/daemon/tests/runtimes/agent-args.test.ts index aba48061dc..40bea55170 100644 --- a/apps/daemon/tests/runtimes/agent-args.test.ts +++ b/apps/daemon/tests/runtimes/agent-args.test.ts @@ -451,40 +451,20 @@ test('qwen args check promptViaStdin, base args, model args and exclude `-` sent }); // `agy` v1.0.3 takes the prompt as the value of `-p` (the flag is named -// `--print` / `--prompt`, accepts a string, and rejects with `flag needs -// an argument: -p` when omitted). There is no `--model` flag yet -// (upstream issue #35), no stdin sentinel, and no JSON output mode -// (issue #119) — the daemon ships antigravity as a `plain` runtime -// until those land. Pin the argv shape so a future upstream redesign -// (e.g. renaming `-p` back to a boolean, adding `--model`) surfaces -// here and forces an explicit re-review of the integration. -test('antigravity args put the prompt on argv as the value of -p', () => { +test('antigravity pipes prompt via stdin with -p - sentinel', () => { assert.equal(antigravity.bin, 'agy'); assert.equal(antigravity.streamFormat, 'plain'); - assert.equal(antigravity.promptViaStdin, undefined); + assert.equal(antigravity.promptViaStdin, true); const args = antigravity.buildArgs('write hello world', [], [], {}); - assert.deepEqual(args, ['-p', 'write hello world']); + assert.deepEqual(args, ['-p', '-']); - // No model flag — picking a model in the UI must not silently inject - // an unsupported `--model` arg that would fail with `flag provided - // but not defined`. Revisit when upstream ships issue #35. const withModel = antigravity.buildArgs('hi', [], [], { model: 'gemini-3-pro' }); assert.equal(withModel.includes('--model'), false); - assert.deepEqual(withModel, ['-p', 'hi']); + assert.deepEqual(withModel, ['-p', '-']); - // Prompt rides on argv (no `-` stdin sentinel), so a maxPromptArgBytes - // budget must exist so the spawn pipeline can fail fast on oversized - // composed prompts before hitting CreateProcess/E2BIG. - assert.ok( - typeof antigravity.maxPromptArgBytes === 'number' - && antigravity.maxPromptArgBytes > 0 - && antigravity.maxPromptArgBytes < 32_768, - `antigravity must declare maxPromptArgBytes under Windows' ~32 KB CreateProcess limit; got ${antigravity.maxPromptArgBytes}`, - ); + assert.equal(antigravity.maxPromptArgBytes, undefined); - // Model picker must surface as "no real choice" until upstream wires - // `--model`. Only the synthetic default option ships. assert.deepEqual( antigravity.fallbackModels.map((m) => m.id), ['default'], diff --git a/apps/web/public/agent-icons/antigravity.svg b/apps/web/public/agent-icons/antigravity.svg new file mode 100644 index 0000000000..907665a8a5 --- /dev/null +++ b/apps/web/public/agent-icons/antigravity.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/components/AgentIcon.tsx b/apps/web/src/components/AgentIcon.tsx index 984a871fa4..2726f6225b 100644 --- a/apps/web/src/components/AgentIcon.tsx +++ b/apps/web/src/components/AgentIcon.tsx @@ -29,6 +29,7 @@ const ICON_EXT: Record = { kiro: 'svg', kilo: 'svg', vibe: 'svg', + antigravity: 'svg', aider: 'png', 'trae-cli': 'png', devin: 'png',