fix(daemon): Antigravity adapter — stdin prompt, brand icon, form loop, empty-output guard

- 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
This commit is contained in:
qiongyu1999 2026-05-28 17:33:32 +08:00
parent 5294db4d71
commit fba1e40bbf
5 changed files with 52 additions and 70 deletions

View file

@ -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 "<prompt>"` 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',

View file

@ -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 <question-form> 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 `<question-form>` 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

View file

@ -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'],

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-2 -1 28 28"><path d="M21.751 22.607c1.34 1.005 3.35.335 1.508-1.508C17.73 15.74 18.904 1 12.037 1 5.17 1 6.342 15.74.815 21.1c-2.01 2.009.167 2.511 1.507 1.506 5.192-3.517 4.857-9.714 9.715-9.714 4.857 0 4.522 6.197 9.714 9.715z" fill="url(#ag)"/><defs><linearGradient id="ag" x1="2" y1="12" x2="22" y2="12" gradientUnits="userSpaceOnUse"><stop stop-color="#4285F4"/><stop offset=".33" stop-color="#EA4335"/><stop offset=".66" stop-color="#FBBC04"/><stop offset="1" stop-color="#34A853"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 569 B

View file

@ -29,6 +29,7 @@ const ICON_EXT: Record<string, 'svg' | 'png'> = {
kiro: 'svg',
kilo: 'svg',
vibe: 'svg',
antigravity: 'svg',
aider: 'png',
'trae-cli': 'png',
devin: 'png',