mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
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:
parent
5294db4d71
commit
fba1e40bbf
5 changed files with 52 additions and 70 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
1
apps/web/public/agent-icons/antigravity.svg
Normal file
1
apps/web/public/agent-icons/antigravity.svg
Normal 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 |
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue