fix(bridge): rawRPC direct polling + SDK analysis docs + trial-and-error log

- Root cause: getDiagnostics.lastStepIndex is stale, SDK EventMonitor cannot detect real-time step changes
- Fix: Direct rawRPC('GetCascadeTrajectorySteps') polling every 5s
- Relay: PLANNER_RESPONSE, NOTIFY_USER, TASK_BOUNDARY, WAITING steps
- Added: docs/discord-bridge-analysis.md (full SDK architecture analysis)
- Added: docs/devlog/entries/20260308-003.md (trial-and-error history)
- Added: antigravity-sdk-main/ source reference
- Vikunja: #252 done, #253 created, #251 commented
This commit is contained in:
2026-03-08 07:08:25 +09:00
parent 731dad35bf
commit c3964f8e7a
40 changed files with 11086 additions and 25 deletions

View File

@@ -0,0 +1,73 @@
/**
* Disposable pattern for resource cleanup.
*
* @module disposable
*/
/**
* An object that can release resources when no longer needed.
*/
export interface IDisposable {
dispose(): void;
}
/**
* Collects multiple disposables and disposes them all at once.
*
* @example
* ```typescript
* const store = new DisposableStore();
* store.add(someEventSub);
* store.add(anotherSub);
* // Later:
* store.dispose(); // cleans up everything
* ```
*/
export class DisposableStore implements IDisposable {
private readonly _disposables: IDisposable[] = [];
private _disposed = false;
/**
* Add a disposable to the store.
*
* @param disposable - The disposable to track
* @returns The same disposable (for chaining)
*/
add<T extends IDisposable>(disposable: T): T {
if (this._disposed) {
disposable.dispose();
console.warn('[AntigravitySDK] Adding disposable to already disposed store');
} else {
this._disposables.push(disposable);
}
return disposable;
}
/**
* Dispose all tracked disposables.
*/
dispose(): void {
if (this._disposed) {
return;
}
this._disposed = true;
for (const d of this._disposables) {
try {
d.dispose();
} catch (error) {
console.error('[AntigravitySDK] Dispose error:', error);
}
}
this._disposables.length = 0;
}
}
/**
* Creates a disposable from a cleanup function.
*
* @param fn - Cleanup function to call on dispose
*/
export function toDisposable(fn: () => void): IDisposable {
return { dispose: fn };
}

View File

@@ -0,0 +1,61 @@
/**
* SDK-specific error classes.
*
* @module errors
*/
/**
* Base error for all Antigravity SDK errors.
*/
export class AntigravitySDKError extends Error {
constructor(message: string) {
super(`[AntigravitySDK] ${message}`);
this.name = 'AntigravitySDKError';
}
}
/**
* Thrown when Antigravity IDE is not detected or not running.
*/
export class AntigravityNotFoundError extends AntigravitySDKError {
constructor() {
super('Antigravity IDE not detected. Make sure this extension is running inside Antigravity.');
this.name = 'AntigravityNotFoundError';
}
}
/**
* Thrown when a command fails to execute.
*/
export class CommandExecutionError extends AntigravitySDKError {
constructor(
public readonly command: string,
public readonly reason: string,
) {
super(`Command "${command}" failed: ${reason}`);
this.name = 'CommandExecutionError';
}
}
/**
* Thrown when the state database cannot be read.
*/
export class StateReadError extends AntigravitySDKError {
constructor(
public readonly key: string,
public readonly reason: string,
) {
super(`Failed to read state key "${key}": ${reason}`);
this.name = 'StateReadError';
}
}
/**
* Thrown when a session/conversation is not found.
*/
export class SessionNotFoundError extends AntigravitySDKError {
constructor(public readonly sessionId: string) {
super(`Session "${sessionId}" not found`);
this.name = 'SessionNotFoundError';
}
}

View File

@@ -0,0 +1,99 @@
/**
* Lightweight event system for SDK.
*
* Follows VS Code's `Event<T>` / `EventEmitter<T>` pattern.
* Supports subscription, disposal, and one-shot listeners.
*
* @module events
*/
import type { IDisposable } from './disposable';
/**
* A function that represents a subscription to an event.
* Call the returned disposable to unsubscribe.
*/
export type Event<T> = (listener: (e: T) => void) => IDisposable;
/**
* Emits events to registered listeners.
*
* @example
* ```typescript
* const emitter = new EventEmitter<string>();
*
* const sub = emitter.event((msg) => console.log(msg));
* emitter.fire('hello'); // logs: hello
* sub.dispose();
* emitter.fire('world'); // nothing happens
* ```
*/
export class EventEmitter<T> implements IDisposable {
private _listeners: Set<(e: T) => void> = new Set();
private _disposed = false;
/**
* The event that listeners can subscribe to.
*/
readonly event: Event<T> = (listener: (e: T) => void): IDisposable => {
if (this._disposed) {
throw new Error('EventEmitter has been disposed');
}
this._listeners.add(listener);
return {
dispose: () => {
this._listeners.delete(listener);
},
};
};
/**
* Fire the event, notifying all listeners.
*
* @param data - The event data to send to listeners
*/
fire(data: T): void {
if (this._disposed) {
return;
}
for (const listener of this._listeners) {
try {
listener(data);
} catch (error) {
console.error('[AntigravitySDK] Event listener error:', error);
}
}
}
/**
* Subscribe to the event, but only fire once.
*
* @param listener - Callback to invoke once
* @returns Disposable to cancel before the event fires
*/
once(listener: (e: T) => void): IDisposable {
const sub = this.event((data) => {
sub.dispose();
listener(data);
});
return sub;
}
/**
* Get the current number of listeners.
*/
get listenerCount(): number {
return this._listeners.size;
}
/**
* Dispose of the emitter and all listeners.
*/
dispose(): void {
this._disposed = true;
this._listeners.clear();
}
}

View File

@@ -0,0 +1,11 @@
/**
* Core module — types, events, disposables, errors, logging.
*
* @module core
*/
export * from './types';
export * from './events';
export * from './disposable';
export * from './errors';
export { Logger, LogLevel } from './logger';

View File

@@ -0,0 +1,84 @@
/**
* Debug logger for SDK internals.
*
* Respects the `antigravitySDK.debug` setting.
*
* @module logger
*/
/**
* Log levels for SDK logging.
*/
export enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
Off = 4,
}
/**
* SDK logger with level-based filtering.
*
* @example
* ```typescript
* const log = new Logger('CascadeManager');
* log.debug('Loading sessions...');
* log.info('Found 5 sessions');
* log.error('Failed to load', err);
* ```
*/
export class Logger {
private static _globalLevel: LogLevel = LogLevel.Warn;
/**
* Set the global log level for all SDK loggers.
*
* @param level - Minimum level to output
*/
static setLevel(level: LogLevel): void {
Logger._globalLevel = level;
}
/**
* Create a logger for a specific module.
*
* @param module - Module name (shown in log prefix)
*/
constructor(private readonly module: string) { }
/** Log a debug message. */
debug(message: string, ...args: unknown[]): void {
this._log(LogLevel.Debug, message, args);
}
/** Log an informational message. */
info(message: string, ...args: unknown[]): void {
this._log(LogLevel.Info, message, args);
}
/** Log a warning. */
warn(message: string, ...args: unknown[]): void {
this._log(LogLevel.Warn, message, args);
}
/** Log an error. */
error(message: string, ...args: unknown[]): void {
this._log(LogLevel.Error, message, args);
}
private _log(level: LogLevel, message: string, args: unknown[]): void {
if (level < Logger._globalLevel) {
return;
}
const prefix = `[AntigravitySDK:${this.module}]`;
const fn =
level === LogLevel.Error ? console.error
: level === LogLevel.Warn ? console.warn
: level === LogLevel.Info ? console.info
: console.debug;
fn(prefix, message, ...args);
}
}

View File

@@ -0,0 +1,381 @@
/**
* Core type definitions for Antigravity SDK.
*
* These types mirror the internal protobuf schemas used by Antigravity's
* Language Server, extracted via reverse engineering of the minified source.
*
* @module types
*/
// ─── Enums ──────────────────────────────────────────────────────────────────
/**
* Terminal command auto-execution policy.
*
* Controls how terminal commands are handled when the agent requests execution.
*/
export enum TerminalExecutionPolicy {
/** Always ask user before running */
OFF = 1,
/** Auto-run safe commands, ask for potentially dangerous ones */
AUTO = 2,
/** Always auto-run without asking */
EAGER = 3,
}
/**
* Artifact review policy for code changes.
*/
export enum ArtifactReviewPolicy {
/** Always show diff review */
ALWAYS = 1,
/** Skip review for simple changes */
TURBO = 2,
/** Automatically decide based on change complexity */
AUTO = 3,
}
/**
* Type of a Cortex step (tool call) in a trajectory.
*/
export enum CortexStepType {
RunCommand = 'RunCommand',
WriteToFile = 'WriteToFile',
ViewFile = 'ViewFile',
ViewFileOutline = 'ViewFileOutline',
ViewCodeItem = 'ViewCodeItem',
SearchWeb = 'SearchWeb',
ReadUrlContent = 'ReadUrlContent',
OpenBrowserUrl = 'OpenBrowserUrl',
ReadBrowserPage = 'ReadBrowserPage',
ListBrowserPages = 'ListBrowserPages',
ListDirectory = 'ListDirectory',
FindByName = 'FindByName',
CodebaseSearch = 'CodebaseSearch',
GrepSearch = 'GrepSearch',
SendCommandInput = 'SendCommandInput',
ReadTerminal = 'ReadTerminal',
ShellExec = 'ShellExec',
McpTool = 'McpTool',
InvokeSubagent = 'InvokeSubagent',
Memory = 'Memory',
KnowledgeGeneration = 'KnowledgeGeneration',
UserInput = 'UserInput',
SystemMessage = 'SystemMessage',
PlannerResponse = 'PlannerResponse',
Wait = 'Wait',
ProposeCode = 'ProposeCode',
WriteCascadeEdit = 'WriteCascadeEdit',
}
/**
* Status of a Cortex step.
*/
export enum StepStatus {
/** Step is being processed */
Running = 'running',
/** Step completed successfully */
Completed = 'completed',
/** Step failed */
Failed = 'failed',
/** Step is waiting for user interaction */
WaitingForUser = 'waiting_for_user',
/** Step was cancelled */
Cancelled = 'cancelled',
}
/**
* Type of trajectory (conversation).
*/
export enum TrajectoryType {
/** Standard chat conversation */
Chat = 'chat',
/** Agent mode (Cascade) */
Cascade = 'cascade',
}
// ─── Interfaces ─────────────────────────────────────────────────────────────
/**
* A single step (tool call) in a Cascade trajectory.
*/
export interface ICortexStep {
/** Unique step identifier */
readonly id: string;
/** Step index within the trajectory */
readonly index: number;
/** Type of tool call */
readonly type: CortexStepType;
/** Current status */
readonly status: StepStatus;
/** Human-readable summary of what this step does */
readonly summary: string;
/** Step-specific data (command line, file path, etc.) */
readonly data: Record<string, unknown>;
/** Internal metadata not shown in UI */
readonly metadata: IStepMetadata;
/** Timestamp when step was created */
readonly createdAt: Date;
/** Timestamp when step completed (if completed) */
readonly completedAt?: Date;
}
/**
* Internal metadata attached to each step.
*/
export interface IStepMetadata {
/** Raw protobuf fields from the server response */
readonly rawFields: Record<string, unknown>;
/** Token count for this step's input */
readonly inputTokens?: number;
/** Token count for this step's output */
readonly outputTokens?: number;
/** Model used for this step */
readonly model?: string;
/** Whether this step was auto-approved */
readonly autoApproved?: boolean;
}
/**
* A chat message in a conversation.
*/
export interface IChatMessage {
/** Message role */
readonly role: 'user' | 'assistant' | 'system';
/** Message content */
readonly content: string;
/** Message ID */
readonly id: string;
/** Timestamp */
readonly createdAt: Date;
/** Hidden metadata */
readonly metadata: Record<string, unknown>;
}
/**
* Information about the current context window usage.
*/
export interface IContextInfo {
/** Total tokens currently in context */
readonly totalTokens: number;
/** Maximum context window size */
readonly maxTokens: number;
/** Usage as percentage (0-100) */
readonly usagePercent: number;
/** Token breakdown by category */
readonly breakdown: ITokenBreakdown;
}
/**
* Token usage breakdown.
*/
export interface ITokenBreakdown {
/** System prompt tokens */
readonly system: number;
/** User message tokens */
readonly userMessages: number;
/** Assistant response tokens */
readonly assistantMessages: number;
/** Tool call input tokens */
readonly toolCalls: number;
/** Tool result tokens */
readonly toolResults: number;
}
/**
* A Cascade session (conversation/trajectory).
*/
export interface ISessionInfo {
/** Unique session/cascade ID */
readonly id: string;
/** Session title (auto-generated or user-set) */
readonly title: string;
/** When the session was created */
readonly createdAt: Date;
/** When the session was last active */
readonly lastActiveAt: Date;
/** Type of trajectory */
readonly type: TrajectoryType;
/** Whether the session is currently active */
readonly isActive: boolean;
/** Tags applied to this session */
readonly tags: string[];
}
/**
* Agent preferences from USS (Unified State Sync).
*
* All 16 sentinel keys verified from live state.vscdb on 2026-02-28.
*/
export interface IAgentPreferences {
/** Terminal command auto-execution policy (terminalAutoExecutionPolicySentinelKey) */
readonly terminalExecutionPolicy: TerminalExecutionPolicy;
/** Code change review policy (artifactReviewPolicySentinelKey) */
readonly artifactReviewPolicy: ArtifactReviewPolicy;
/** Planning mode (planningModeSentinelKey) */
readonly planningMode: number;
/** Whether strict/secure mode is enabled (secureModeSentinelKey) */
readonly secureModeEnabled: boolean;
/** Whether terminal sandbox is enabled (enableTerminalSandboxSentinelKey) */
readonly terminalSandboxEnabled: boolean;
/** Whether sandbox allows network access (sandboxAllowNetworkSentinelKey) */
readonly sandboxAllowNetwork: boolean;
/** Whether shell integration is enabled (enableShellIntegrationSentinelKey) */
readonly shellIntegrationEnabled: boolean;
/** Allow agent to access files outside workspace (allowAgentAccessNonWorkspaceFilesSentinelKey) */
readonly allowNonWorkspaceFiles: boolean;
/** Allow Cascade to read .gitignore files (allowCascadeAccessGitignoreFilesSentinelKey) */
readonly allowGitignoreAccess: boolean;
/** Explain and fix in current conversation (explainAndFixInCurrentConversationSentinelKey) */
readonly explainFixInCurrentConvo: boolean;
/** Auto-continue on max generator invocations (autoContinueOnMaxGeneratorInvocationsSentinelKey) */
readonly autoContinueOnMax: number;
/** Disable auto-open of edited files (disableAutoOpenEditedFilesSentinelKey) */
readonly disableAutoOpenEdited: boolean;
/** Enable sounds for special events (enableSoundsForSpecialEventsSentinelKey) */
readonly enableSounds: boolean;
/** Disable Cascade auto-fix for lint errors (disableCascadeAutoFixLintsSentinelKey) */
readonly disableAutoFixLints: boolean;
/** Explicitly allowed terminal commands (terminalAllowedCommandsSentinelKey) */
readonly allowedCommands: string[];
/** Explicitly denied terminal commands (terminalDeniedCommandsSentinelKey) */
readonly deniedCommands: string[];
}
/**
* Model configuration.
*/
export interface IModelConfig {
/** Model identifier */
readonly id: string;
/** Human-readable model name */
readonly name: string;
/** Whether this model is currently selected */
readonly isActive: boolean;
/** Maximum context window size in tokens */
readonly maxContextTokens: number;
}
/**
* Options for creating a new Cascade session.
*/
export interface ICreateSessionOptions {
/** Initial task/message to send */
readonly task: string;
/** Whether to run in background (don't focus the panel) */
readonly background?: boolean;
/** Model to use (defaults to current) */
readonly model?: string;
}
/**
* Agent state from the Agent Manager.
*/
export interface IAgentState {
/** Whether the agent manager is enabled */
readonly isEnabled: boolean;
/** Whether the agent is currently processing */
readonly isProcessing: boolean;
/** Active cascade/conversation ID */
readonly activeCascadeId: string | null;
/** Current model in use */
readonly currentModel: string;
}
/**
* Trajectory entry from getDiagnostics.recentTrajectories.
*
* VERIFIED 2026-02-28: getDiagnostics returns clean JSON array with:
* { googleAgentId, trajectoryId, summary, lastStepIndex, lastModifiedTime }
*/
export interface ITrajectoryEntry {
/** Conversation UUID = googleAgentId */
readonly id: string;
/** Human-readable title = summary field */
readonly title: string;
/** Current step index in this conversation */
readonly stepCount: number;
/** Workspace URI (from USS protobuf fallback) */
readonly workspaceUri: string;
/** Internal trajectory UUID (from getDiagnostics) */
readonly trajectoryId?: string;
/** ISO timestamp of last modification (from getDiagnostics) */
readonly lastModifiedTime?: string;
}
/**
* Diagnostics info from `antigravity.getDiagnostics`.
*
* VERIFIED: returns 176KB JSON string with 8 top-level keys:
* isRemote, systemInfo, extensionLogs, rendererLogs,
* mainThreadLogs, agentWindowConsoleLogs, languageServerLogs,
* recentTrajectories.
*/
export interface IDiagnosticsInfo {
/** Whether IDE is running remotely (SSH) */
readonly isRemote: boolean;
/** System info */
readonly systemInfo: {
readonly operatingSystem: string;
readonly timestamp: string;
readonly userEmail: string;
readonly userName: string;
};
/** Raw JSON for fields not yet typed */
readonly raw: Record<string, unknown>;
}