refactor: consolidate ai agent session flow

This commit is contained in:
lucaronin
2026-04-28 04:20:40 +02:00
parent c72d833624
commit db82aee172
23 changed files with 377 additions and 957 deletions

View File

@@ -1,11 +1,6 @@
import { describe, it, expect, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
// Mock the mock-tauri module before importing ai-agent
vi.mock('../mock-tauri', () => ({
isTauri: () => false,
}))
import { buildAgentSystemPrompt, streamClaudeAgent } from './ai-agent'
import { buildAgentSystemPrompt } from './ai-agent'
// --- buildAgentSystemPrompt ---
@@ -32,29 +27,3 @@ describe('buildAgentSystemPrompt', () => {
expect(prompt).toMatch(/wikilink/i)
})
})
// --- streamClaudeAgent ---
describe('streamClaudeAgent', () => {
it('calls onText and onDone in non-Tauri environment', async () => {
const onText = vi.fn()
const onToolStart = vi.fn()
const onDone = vi.fn()
const onError = vi.fn()
await streamClaudeAgent('test message', 'system prompt', '/tmp/vault', {
onText,
onToolStart,
onError,
onDone,
})
// Wait for the setTimeout mock response
await new Promise(r => setTimeout(r, 400))
expect(onText).toHaveBeenCalledWith(expect.stringContaining('Build Laputa App'))
expect(onDone).toHaveBeenCalled()
expect(onError).not.toHaveBeenCalled()
expect(onToolStart).not.toHaveBeenCalled()
})
})

View File

@@ -6,8 +6,6 @@
* The frontend receives streaming events for text, tool calls, and completion.
*/
import { isTauri } from '../mock-tauri'
// --- Agent system prompt ---
const AGENT_SYSTEM_PREAMBLE = `You are working inside Tolaria, a personal knowledge management app.
@@ -25,125 +23,3 @@ export function buildAgentSystemPrompt(vaultContext?: string): string {
if (!vaultContext) return AGENT_SYSTEM_PREAMBLE
return `${AGENT_SYSTEM_PREAMBLE}\n\nVault context:\n${vaultContext}`
}
// --- Claude CLI agent streaming ---
type ClaudeAgentStreamEvent =
| { kind: 'Init'; session_id: string }
| { kind: 'TextDelta'; text: string }
| { kind: 'ThinkingDelta'; text: string }
| { kind: 'ToolStart'; tool_name: string; tool_id: string; input?: string }
| { kind: 'ToolDone'; tool_id: string; output?: string }
| { kind: 'Result'; text: string; session_id: string }
| { kind: 'Error'; message: string }
| { kind: 'Done' }
export interface AgentStreamCallbacks {
onText: (text: string) => void
onThinking: (text: string) => void
onToolStart: (toolName: string, toolId: string, input?: string) => void
onToolDone: (toolId: string, output?: string) => void
onError: (message: string) => void
onDone: () => void
}
/**
* Generate a mock response for browser/test mode.
* Inspects the message for conversation history so Playwright tests
* can verify that history is actually being sent.
*/
function mockAgentResponse(message: string): string {
if (message.includes('<conversation_history>')) {
const allUserLines = message.match(/\[user\]: .+/g) ?? []
const turnCount = allUserLines.length
const lastLine = allUserLines[allUserLines.length - 1] ?? ''
const lastUserMsg = lastLine.replace('[user]: ', '')
return `[mock-with-history turns=${turnCount}] You asked: "${lastUserMsg}" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].`
}
return `[mock-no-history] You said: "${message}" — This note is related to [[Build Laputa App]] and [[Matteo Cellini]].`
}
function emitMockAgentResponse(message: string, callbacks: AgentStreamCallbacks): void {
setTimeout(() => {
callbacks.onText(mockAgentResponse(message))
callbacks.onDone()
}, 300)
}
function createStreamCloser(callbacks: AgentStreamCallbacks): () => void {
let closed = false
return () => {
if (closed) return
closed = true
callbacks.onDone()
}
}
function handleClaudeStreamEvent(
data: ClaudeAgentStreamEvent,
callbacks: AgentStreamCallbacks,
closeStream: () => void,
): void {
switch (data.kind) {
case 'TextDelta':
callbacks.onText(data.text)
return
case 'ThinkingDelta':
callbacks.onThinking(data.text)
return
case 'ToolStart':
callbacks.onToolStart(data.tool_name, data.tool_id, data.input)
return
case 'ToolDone':
callbacks.onToolDone(data.tool_id, data.output)
return
case 'Error':
callbacks.onError(data.message)
return
case 'Done':
closeStream()
return
default:
return
}
}
/**
* Stream an agent task through the Claude CLI subprocess with scoped tool access.
* The CLI handles the tool-use loop; we receive events for UI updates.
*/
export async function streamClaudeAgent(
message: string,
systemPrompt: string | undefined,
vaultPath: string,
callbacks: AgentStreamCallbacks,
): Promise<void> {
if (!isTauri()) {
emitMockAgentResponse(message, callbacks)
return
}
const { invoke } = await import('@tauri-apps/api/core')
const { listen } = await import('@tauri-apps/api/event')
const closeStream = createStreamCloser(callbacks)
const unlisten = await listen<ClaudeAgentStreamEvent>('claude-agent-stream', (event) => {
handleClaudeStreamEvent(event.payload, callbacks, closeStream)
})
try {
await invoke<string>('stream_claude_agent', {
request: {
message,
system_prompt: systemPrompt || null,
vault_path: vaultPath,
},
})
closeStream()
} catch (err) {
callbacks.onError(err instanceof Error ? err.message : String(err))
closeStream()
} finally {
unlisten()
}
}