Skip to content

feat(agy): queue turns, stream output, expose usage and image attachments - #1

Merged
Mchicao merged 13 commits into
mainfrom
feat/antigravity-stream-queue-usage-attachments
Aug 24, 2026
Merged

feat(agy): queue turns, stream output, expose usage and image attachments#1
Mchicao merged 13 commits into
mainfrom
feat/antigravity-stream-queue-usage-attachments

Conversation

@Mchicao

@Mchicao Mchicao commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Overview

This PR enhances the Antigravity (agy) provider integration and the surrounding orchestration/UI with four related improvements: FIFO turn queueing, native streaming assistant output, granular token usage reporting, and image attachment support in headless text mode.

Key Changes

1. Queued turns: each prompt gets its own turn lifecycle

  • The Agy adapter previously merged multiple prompts into a single active turn via "steering". It now maintains a FIFO queue (queuedTurns) so each user message becomes its own T3 turn, started only after the previous Antigravity turn emits its terminal result (SUCCESS/CANCELED/FAILED).
  • Interrupts clear the queue; failures drop queued prompts.
  • The persisted projection_turns pending-start table was updated from a replace-style "latest row" model to an append-style FIFO queue: getPendingTurnStartByThreadId now returns the oldest pending start, and deletePendingTurnStartByThreadId consumes only the oldest pending start while a session is running (clears all when the session is not running). A new deletePendingTurnStartByMessageId was added for targeted cleanup.

2. Streaming assistant output

  • Provider adapters can now declare assistantDeliveryMode: "streaming" in their capabilities. The Agy adapter advertises this capability.
  • ProviderRuntimeIngestion now resolves the delivery mode per event: it streams immediately if the legacy streaming setting is enabled, otherwise it consults the provider's declared capability (falling back to buffered when unknown).
  • This allows Agy assistant text deltas to be projected to clients in real time rather than buffered until turn completion.

3. Token usage exposure

  • usageFromAgy now:
    • Adds Antigravity's cache_read_tokens back into canonical input/context token counts (matching how input context is commonly reported).
    • Accepts cumulative result snapshots and subtracts the previous cumulative counters so each terminal result yields a per-turn usage delta.
    • Emits live usage snapshots on agent_response steps and per-turn deltas on turn results.
    • Populates last* fields, which the UI now prefers over aggregate counters.
  • The Context Window meter now displays a token breakdown (Input, Cached with cache-hit %, Output, Reasoning) using the latest-turn counters, and shows the active model name in the tooltip. The provider display name mapper recognizes agy as "Antigravity".

4. Image attachments in headless text mode

  • Since agy --input-format stream-json accepts text only, image attachments are resolved to validated local file paths and appended to the prompt as a delimited <t3_attached_images> manifest with metadata (name, MIME type, size, absolute path). Antigravity's native file/media tools can then inspect the referenced files.
  • Attachment files are stat-checked (must exist and be regular files); invalid attachments produce a validation error instead of being silently dropped.
  • The runtimeMode === "full-access" session now spawns the process with --dangerously-skip-permissions, enabling tool access in headless setups.

Impact

  • Users composing follow-up messages now get a clean, separately tracked turn per prompt (with per-turn lifecycle and usage) instead of merged continuations.
  • The UI shows accurate per-turn token usage, including cache reads, helping users understand context consumption.
  • Image attachments work with the Antigravity CLI despite its text-only stream interface.
  • Assistant responses stream live for compatible providers, improving perceived latency.

The changes are localized to the Agy adapter, provider ingestion, projection turn persistence, and the context-window meter UI with matching unit tests.

@kody-ai

kody-ai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XXL labels Aug 24, 2026
Comment on lines +327 to +335
const deletePendingTurnStartByMessageId: ProjectionTurnRepositoryShape["deletePendingTurnStartByMessageId"] =
(input) =>
deletePendingProjectionTurnByMessageId(input).pipe(
Effect.mapError(
toPersistenceSqlError(
"ProjectionTurnRepository.deletePendingTurnStartByMessageId:query",
),
),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

Stale pending-start placeholders can survive because deletePendingTurnStartByMessageId in apps/server/src/persistence/Layers/ProjectionTurns.ts has no production caller, so canceled or deleted queued prompts remain in projection_turns. getPendingTurnStartByThreadId then reuses the oldest pending row even after its message is gone, misattributing pendingMessageId and sourcePlans to a later turn; invoke deletePendingTurnStartByMessageId({ threadId, messageId }) on queued prompt deletion or filter for message liveness.

// Call deletePendingTurnStartByMessageId({ threadId, messageId }) whenever a
// queued user prompt is deleted/canceled so its pending placeholder cannot be
// consumed by a later turn; otherwise the FIFO getPendingTurnStartByThreadId
// keeps returning the stale row.
Prompt for LLM

File apps/server/src/persistence/Layers/ProjectionTurns.ts:

Line 327 to 335:

Stale pending-start placeholders can survive because `deletePendingTurnStartByMessageId` in `apps/server/src/persistence/Layers/ProjectionTurns.ts` has no production caller, so canceled or deleted queued prompts remain in `projection_turns`. `getPendingTurnStartByThreadId` then reuses the oldest pending row even after its message is gone, misattributing `pendingMessageId` and `sourcePlans` to a later turn; invoke `deletePendingTurnStartByMessageId({ threadId, messageId })` on queued prompt deletion or filter for message liveness.

Suggested Code:

// Call deletePendingTurnStartByMessageId({ threadId, messageId }) whenever a
// queued user prompt is deleted/canceled so its pending placeholder cannot be
// consumed by a later turn; otherwise the FIFO getPendingTurnStartByThreadId
// keeps returning the stale row.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment on lines 628 to +640
if (stepType === "agent_response") {
const liveUsage = usageFromAgy(step.usage);
if (liveUsage !== undefined) {
yield* offerRuntimeEvent({
type: "thread.token-usage.updated",
...(yield* makeEventStamp()),
provider: PROVIDER,
providerInstanceId: boundInstanceId,
threadId: ctx.threadId,
turnId: turn.turnId,
payload: { usage: liveUsage },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

Cumulative token accounting in the live agent_response path is incorrect because usageFromAgy(step.usage) ignores { cumulativeResult: true, previousCumulative: ctx.lastCumulativeUsage }, so mid-turn thread.token-usage.updated events overwrite lastInputTokens and lastCachedInputTokens with session-wide totals instead of per-turn deltas. Use usageFromAgy(step.usage, { cumulativeResult: true, previousCumulative: ctx.lastCumulativeUsage }) and refresh ctx.lastCumulativeUsage from agyUsageCounters(step.usage) when defined, matching the result handling.

const currentStepUsage = agyUsageCounters(step.usage);
const liveUsage = usageFromAgy(step.usage, {
  cumulativeResult: true,
  previousCumulative: ctx.lastCumulativeUsage,
});
if (currentStepUsage !== undefined) ctx.lastCumulativeUsage = currentStepUsage;
Prompt for LLM

File apps/server/src/provider/Layers/AgyAdapter.ts:

Line 628 to 640:

Cumulative token accounting in the live `agent_response` path is incorrect because `usageFromAgy(step.usage)` ignores `{ cumulativeResult: true, previousCumulative: ctx.lastCumulativeUsage }`, so mid-turn `thread.token-usage.updated` events overwrite `lastInputTokens` and `lastCachedInputTokens` with session-wide totals instead of per-turn deltas. Use `usageFromAgy(step.usage, { cumulativeResult: true, previousCumulative: ctx.lastCumulativeUsage })` and refresh `ctx.lastCumulativeUsage` from `agyUsageCounters(step.usage)` when defined, matching the `result` handling.

Suggested Code:

const currentStepUsage = agyUsageCounters(step.usage);
const liveUsage = usageFromAgy(step.usage, {
  cumulativeResult: true,
  previousCumulative: ctx.lastCumulativeUsage,
});
if (currentStepUsage !== undefined) ctx.lastCumulativeUsage = currentStepUsage;

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (status === "SUCCESS") {
yield* settleTurn(ctx, turn, { state: "completed", usage });
yield* startNextQueuedTurn(ctx);
} else if (status === "CANCELED" || status === "INTERRUPTED") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug medium

Queued turn loss in apps/server/src/provider/Layers/AgyAdapter.ts occurs when ctx.queuedTurns.length = 0 clears interrupted, cancelled, or failed prompts and clearOrConsumePendingProjectionTurnsByThread removes their pending rows without emitting any turn.completed or turn.failed event. Settle each dropped queued turn before clearing the queue, or emit a capped failure event per discarded prompt, so the UI surfaces an error instead of silently losing accepted input.

} else if (status === "CANCELED" || status === "INTERRUPTED") {
  const dropped = [...ctx.queuedTurns];
  ctx.queuedTurns.length = 0;
  yield* Effect.forEach(dropped, (queued) =>
    settleTurn(ctx, queued, { state: status === "CANCELED" ? "cancelled" : "interrupted" }),
  );
  yield* settleTurn(ctx, turn, {...});
Prompt for LLM

File apps/server/src/provider/Layers/AgyAdapter.ts:

Line 601:

Queued turn loss in `apps/server/src/provider/Layers/AgyAdapter.ts` occurs when `ctx.queuedTurns.length = 0` clears interrupted, cancelled, or failed prompts and `clearOrConsumePendingProjectionTurnsByThread` removes their pending rows without emitting any `turn.completed` or `turn.failed` event. Settle each dropped queued turn before clearing the queue, or emit a capped failure event per discarded prompt, so the UI surfaces an error instead of silently losing accepted input.

Suggested Code:

} else if (status === "CANCELED" || status === "INTERRUPTED") {
  const dropped = [...ctx.queuedTurns];
  ctx.queuedTurns.length = 0;
  yield* Effect.forEach(dropped, (queued) =>
    settleTurn(ctx, queued, { state: status === "CANCELED" ? "cancelled" : "interrupted" }),
  );
  yield* settleTurn(ctx, turn, {...});

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Mchicao
Mchicao marked this pull request as ready for review August 24, 2026 22:36
@Mchicao
Mchicao merged commit 42854bb into main Aug 24, 2026
7 of 15 checks passed
Mchicao added a commit that referenced this pull request Aug 25, 2026
Resolved conflicts keeping both sides: fork Codex-session-import overlay
(ws.ts, rpc.ts, server.test.ts, providers-codex.md) + upstream provider
feedback upload (pingdotgg#7949) and app-approval docs (pingdotgg#8058).

Fixed two latent PR #1 type errors surfaced by tsgo:
- ContextWindowMeter.logic.ts: coalesce optional snapshot fields with ?? null
- ProviderRuntimeIngestion.ts: drop Effect.fn.Return annotation pinning the
  error channel to never (ServerSettingsError now propagates like before)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant