refactor: finalize @agor/types + TauriAdapter (agent completion)
This commit is contained in:
parent
c86f669f96
commit
df83b1df4d
8 changed files with 56 additions and 137 deletions
|
|
@ -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 ─────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 ────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue