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
|
// 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 ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue