refactor: finalize @agor/types + TauriAdapter (agent completion)

This commit is contained in:
Hibryda 2026-03-22 03:36:48 +01:00
parent c86f669f96
commit df83b1df4d
8 changed files with 56 additions and 137 deletions

View file

@ -1,9 +1,9 @@
// BackendAdapter — abstraction layer for Tauri and Electrobun backends
import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent.ts';
import type { FileEntry, FileContent, PtyCreateOptions } from './protocol.ts';
import type { SettingsMap } from './settings.ts';
import type { GroupsFile } from './project.ts';
import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent';
import type { FileEntry, FileContent, PtyCreateOptions } from './protocol';
import type { SettingsMap } from './settings';
import type { GroupsFile } from './project';
// ── Backend capabilities ─────────────────────────────────────────────────────

View file

@ -1,6 +1,6 @@
// btmsg types — agent messaging system
import type { AgentId, GroupId } from './ids.ts';
import type { AgentId, GroupId } from './ids';
export interface BtmsgAgent {
id: AgentId;

View file

@ -1,6 +1,6 @@
// bttask types — task board system
import type { AgentId, GroupId } from './ids.ts';
import type { AgentId, GroupId } from './ids';
export type TaskStatus = 'todo' | 'progress' | 'review' | 'done' | 'blocked';
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';

View file

@ -1,6 +1,6 @@
// Health tracking types
import type { ProjectId, SessionId } from './ids.ts';
import type { ProjectId, SessionId } from './ids';
export type ActivityState = 'inactive' | 'running' | 'idle' | 'stalled';

View file

@ -1,11 +1,11 @@
// @agor/types — shared type definitions for Tauri and Electrobun frontends
export * from './ids.ts';
export * from './agent.ts';
export * from './project.ts';
export * from './settings.ts';
export * from './btmsg.ts';
export * from './bttask.ts';
export * from './health.ts';
export * from './protocol.ts';
export * from './backend.ts';
export * from './ids';
export * from './agent';
export * from './project';
export * from './settings';
export * from './btmsg';
export * from './bttask';
export * from './health';
export * from './protocol';
export * from './backend';

View file

@ -1,7 +1,7 @@
// Project and Group configuration types
import type { ProviderId } from './agent.ts';
import type { ProjectId, GroupId, AgentId } from './ids.ts';
import type { ProviderId } from './agent';
import type { ProjectId, GroupId, AgentId } from './ids';
// ── Anchor budget ────────────────────────────────────────────────────────────

View file

@ -1,7 +1,7 @@
// Unified RPC protocol types — covers both Tauri commands and Electrobun RPC schema
// These define the request/response shapes for all backend operations.
import type { AgentStartOptions, AgentStatus, AgentWireMessage } from './agent.ts';
import type { AgentWireMessage } from './agent';
// ── PTY ──────────────────────────────────────────────────────────────────────

View file

@ -4,21 +4,11 @@
import { invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type {
BackendAdapter,
BackendCapabilities,
UnsubscribeFn,
AgentStartOptions,
AgentMessage,
AgentStatus,
FileEntry,
FileContent,
PtyCreateOptions,
SettingsMap,
GroupsFile,
BackendAdapter, BackendCapabilities, UnsubscribeFn, AgentStartOptions,
AgentMessage, AgentStatus, FileEntry, FileContent, PtyCreateOptions,
SettingsMap, GroupsFile,
} from '@agor/types';
// ── Capabilities ─────────────────────────────────────────────────────────────
const TAURI_CAPABILITIES: BackendCapabilities = {
supportsPtyMultiplexing: true,
supportsPluginSandbox: true,
@ -30,40 +20,25 @@ const TAURI_CAPABILITIES: BackendCapabilities = {
supportsTelemetry: true,
};
// ── Wire format types (Tauri event payloads) ─────────────────────────────────
interface SidecarMessage {
type: string;
sessionId?: string;
event?: Record<string, unknown>;
message?: string;
exitCode?: number | null;
signal?: string | null;
}
interface TauriDirEntry {
name: string;
path: string;
is_dir: boolean;
size: number;
ext: string;
name: string; path: string; is_dir: boolean; size: number; ext: string;
}
// ── Adapter ──────────────────────────────────────────────────────────────────
export class TauriAdapter implements BackendAdapter {
readonly capabilities = TAURI_CAPABILITIES;
private unlisteners: UnlistenFn[] = [];
async init(): Promise<void> {
// Tauri IPC is ready as soon as the webview loads — no setup needed.
}
async init(): Promise<void> { /* Tauri IPC ready on webview load */ }
async destroy(): Promise<void> {
for (const unlisten of this.unlisteners) {
unlisten();
}
for (const fn of this.unlisteners) fn();
this.unlisteners = [];
}
@ -80,9 +55,7 @@ export class TauriAdapter implements BackendAdapter {
async getAllSettings(): Promise<SettingsMap> {
const pairs = await invoke<[string, string][]>('settings_list');
const map: SettingsMap = {};
for (const [k, v] of pairs) {
map[k] = v;
}
for (const [k, v] of pairs) map[k] = v;
return map;
}
@ -99,8 +72,7 @@ export class TauriAdapter implements BackendAdapter {
// ── Agent ────────────────────────────────────────────────────────────────
async startAgent(options: AgentStartOptions): Promise<{ ok: boolean; error?: string }> {
// Map camelCase options to snake_case for Tauri command
const tauriOptions = {
const tauriOpts = {
provider: options.provider ?? 'claude',
session_id: options.sessionId,
prompt: options.prompt,
@ -119,7 +91,7 @@ export class TauriAdapter implements BackendAdapter {
extra_env: options.extraEnv,
};
try {
await invoke('agent_query', { options: tauriOptions });
await invoke('agent_query', { options: tauriOpts });
return { ok: true };
} catch (err: unknown) {
return { ok: false, error: String(err) };
@ -136,14 +108,9 @@ export class TauriAdapter implements BackendAdapter {
}
async sendPrompt(sessionId: string, prompt: string): Promise<{ ok: boolean; error?: string }> {
// Tauri agent_query supports resume via resume_session_id
try {
await invoke('agent_query', {
options: {
session_id: sessionId,
prompt,
resume_session_id: sessionId,
},
options: { session_id: sessionId, prompt, resume_session_id: sessionId },
});
return { ok: true };
} catch (err: unknown) {
@ -155,13 +122,7 @@ export class TauriAdapter implements BackendAdapter {
async createPty(options: PtyCreateOptions): Promise<string> {
return invoke<string>('pty_spawn', {
options: {
shell: options.shell,
cwd: options.cwd,
args: options.args,
cols: options.cols,
rows: options.rows,
},
options: { shell: options.shell, cwd: options.cwd, args: options.args, cols: options.cols, rows: options.rows },
});
}
@ -182,11 +143,7 @@ export class TauriAdapter implements BackendAdapter {
async listDirectory(path: string): Promise<FileEntry[]> {
const entries = await invoke<TauriDirEntry[]>('list_directory_children', { path });
return entries.map((e) => ({
name: e.name,
path: e.path,
isDir: e.is_dir,
size: e.size,
ext: e.ext || undefined,
name: e.name, path: e.path, isDir: e.is_dir, size: e.size, ext: e.ext || undefined,
}));
}
@ -200,13 +157,24 @@ export class TauriAdapter implements BackendAdapter {
// ── Events ───────────────────────────────────────────────────────────────
onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn {
/** Subscribe to a Tauri event; tracks unlisten for cleanup. */
private listenTauri<T>(event: string, handler: (payload: T) => void): UnsubscribeFn {
let unlisten: UnlistenFn | null = null;
listen<T>(event, (e) => handler(e.payload)).then((fn) => {
unlisten = fn;
this.unlisteners.push(fn);
});
return () => {
if (unlisten) {
unlisten();
this.unlisteners = this.unlisteners.filter((f) => f !== unlisten);
}
};
}
listen<SidecarMessage>('sidecar-message', (event) => {
const msg = event.payload;
onAgentMessage(callback: (sessionId: string, messages: AgentMessage[]) => void): UnsubscribeFn {
return this.listenTauri<SidecarMessage>('sidecar-message', (msg) => {
if (msg.type !== 'message' || !msg.sessionId || !msg.event) return;
// The sidecar emits individual messages; wrap in array for uniform API
const agentMsg: AgentMessage = {
id: String(msg.event['id'] ?? Date.now()),
role: mapSidecarRole(String(msg.event['type'] ?? '')),
@ -216,78 +184,32 @@ export class TauriAdapter implements BackendAdapter {
timestamp: Number(msg.event['timestamp'] ?? Date.now()),
};
callback(msg.sessionId, [agentMsg]);
}).then((fn) => {
unlisten = fn;
this.unlisteners.push(fn);
});
return () => {
if (unlisten) {
unlisten();
this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten);
}
};
}
onAgentStatus(callback: (sessionId: string, status: AgentStatus, error?: string) => void): UnsubscribeFn {
let unlisten: UnlistenFn | null = null;
listen<SidecarMessage>('sidecar-message', (event) => {
const msg = event.payload;
return this.listenTauri<SidecarMessage>('sidecar-message', (msg) => {
if (msg.type !== 'status' || !msg.sessionId) return;
const status = normalizeAgentStatus(String(msg.event?.['status'] ?? 'idle'));
callback(msg.sessionId, status, msg.message);
}).then((fn) => {
unlisten = fn;
this.unlisteners.push(fn);
callback(msg.sessionId, normalizeStatus(String(msg.event?.['status'] ?? 'idle')), msg.message);
});
return () => {
if (unlisten) {
unlisten();
this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten);
}
};
}
onAgentCost(
callback: (sessionId: string, costUsd: number, inputTokens: number, outputTokens: number) => void,
): UnsubscribeFn {
let unlisten: UnlistenFn | null = null;
listen<SidecarMessage>('sidecar-message', (event) => {
const msg = event.payload;
return this.listenTauri<SidecarMessage>('sidecar-message', (msg) => {
if (msg.type !== 'cost' || !msg.sessionId || !msg.event) return;
callback(
msg.sessionId,
Number(msg.event['totalCostUsd'] ?? 0),
Number(msg.event['inputTokens'] ?? 0),
Number(msg.event['outputTokens'] ?? 0),
);
}).then((fn) => {
unlisten = fn;
this.unlisteners.push(fn);
callback(msg.sessionId, Number(msg.event['totalCostUsd'] ?? 0), Number(msg.event['inputTokens'] ?? 0), Number(msg.event['outputTokens'] ?? 0));
});
return () => {
if (unlisten) {
unlisten();
this.unlisteners = this.unlisteners.filter((fn) => fn !== unlisten);
}
};
}
onPtyOutput(callback: (sessionId: string, data: string) => void): UnsubscribeFn {
// Tauri PTY uses per-session event channels: pty-data-{id}
// This adapter requires the caller to register per-session; for the generic
// interface we listen to a global pty-data event if available, or this becomes
// a no-op that individual TerminalPane components handle directly.
// For Phase 1, PTY event wiring remains in pty-bridge.ts.
onPtyOutput(_callback: (sessionId: string, data: string) => void): UnsubscribeFn {
// Tauri PTY uses per-session event channels (pty-data-{id}).
// Phase 1: PTY event wiring remains in pty-bridge.ts per-session listeners.
return () => {};
}
onPtyClosed(callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn {
// Same as onPtyOutput — Tauri uses per-session events.
onPtyClosed(_callback: (sessionId: string, exitCode: number | null) => void): UnsubscribeFn {
return () => {};
}
}
@ -302,15 +224,12 @@ function mapSidecarRole(type: string): MsgRole {
case 'thinking': return 'thinking';
case 'tool_call': return 'tool-call';
case 'tool_result': return 'tool-result';
case 'init':
case 'error':
case 'status':
return 'system';
case 'init': case 'error': case 'status': return 'system';
default: return 'assistant';
}
}
function normalizeAgentStatus(status: string): AgentStatus {
function normalizeStatus(status: string): AgentStatus {
if (status === 'idle' || status === 'starting' || status === 'running' || status === 'done' || status === 'error') {
return status;
}