refactor: consolidate ai agent session flow
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user