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 // BackendAdapter — abstraction layer for Tauri and Electrobun backends
import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent.ts'; import type { AgentStartOptions, AgentMessage, AgentStatus } from './agent';
import type { FileEntry, FileContent, PtyCreateOptions } from './protocol.ts'; import type { FileEntry, FileContent, PtyCreateOptions } from './protocol';
import type { SettingsMap } from './settings.ts'; import type { SettingsMap } from './settings';
import type { GroupsFile } from './project.ts'; import type { GroupsFile } from './project';
// ── Backend capabilities ───────────────────────────────────────────────────── // ── Backend capabilities ─────────────────────────────────────────────────────

View file

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

View file

@ -1,6 +1,6 @@
// bttask types — task board system // 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 TaskStatus = 'todo' | 'progress' | 'review' | 'done' | 'blocked';
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical'; export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';

View file

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

View file

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

View file

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

View file

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

View file

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