mirror of
https://github.com/kennethnym/freya
synced 2026-07-03 14:51:13 +01:00
126 lines
3.0 KiB
TypeScript
126 lines
3.0 KiB
TypeScript
import type { AgentEvent, SendMessageResult } from "@freya/agent-protocol"
|
|
|
|
import type { QueryAgent, QueryAgentAsk } from "./query-agent.ts"
|
|
|
|
export type AgentResponseStreamItem =
|
|
| { type: "event"; event: AgentEvent }
|
|
| { type: "result"; result: SendMessageResult }
|
|
|
|
export async function* streamAgentResponse({
|
|
agent,
|
|
input,
|
|
}: {
|
|
agent: QueryAgent
|
|
input: QueryAgentAsk
|
|
}): AsyncGenerator<AgentResponseStreamItem, void, void> {
|
|
let message = ""
|
|
let conversationId: string | null = null
|
|
const splitter = new AgentMessageSplitter()
|
|
|
|
function messageEvent(text: string): AgentResponseStreamItem | null {
|
|
if (text.trim() === "") return null
|
|
|
|
return { type: "event", event: { type: "message_created", text } }
|
|
}
|
|
|
|
function flushPendingMessage(): AgentResponseStreamItem | null {
|
|
const text = splitter.flush()
|
|
if (text === null) return null
|
|
|
|
return messageEvent(text)
|
|
}
|
|
|
|
for await (const event of agent.ask(input)) {
|
|
switch (event.type) {
|
|
case "conversation":
|
|
conversationId = event.conversationId
|
|
yield { type: "event", event: { type: "conversation_started", conversationId } }
|
|
break
|
|
|
|
case "text_delta":
|
|
message += event.text
|
|
for (const line of splitter.push(event.text)) {
|
|
const item = messageEvent(line)
|
|
if (item) yield item
|
|
}
|
|
break
|
|
|
|
case "tool_start":
|
|
{
|
|
const item = flushPendingMessage()
|
|
if (item) yield item
|
|
}
|
|
yield { type: "event", event: { type: "tool_started", toolName: event.toolName } }
|
|
break
|
|
|
|
case "tool_end":
|
|
{
|
|
const item = flushPendingMessage()
|
|
if (item) yield item
|
|
}
|
|
yield {
|
|
type: "event",
|
|
event: {
|
|
type: "tool_finished",
|
|
toolName: event.toolName,
|
|
ok: event.ok,
|
|
},
|
|
}
|
|
break
|
|
|
|
case "error":
|
|
{
|
|
const item = flushPendingMessage()
|
|
if (item) yield item
|
|
}
|
|
yield { type: "event", event: { type: "message_failed", error: event.message } }
|
|
throw new Error(event.message)
|
|
|
|
case "done":
|
|
{
|
|
const item = flushPendingMessage()
|
|
if (item) yield item
|
|
}
|
|
const result = createResult(message, conversationId)
|
|
yield { type: "event", event: { type: "message_finished" } }
|
|
yield { type: "result", result }
|
|
return
|
|
}
|
|
}
|
|
|
|
const item = flushPendingMessage()
|
|
if (item) yield item
|
|
const result = createResult(message, conversationId)
|
|
yield { type: "event", event: { type: "message_finished" } }
|
|
yield { type: "result", result }
|
|
}
|
|
|
|
function createResult(message: string, conversationId: string | null): SendMessageResult {
|
|
if (!conversationId) {
|
|
throw new Error("Agent response stream ended without a conversation id")
|
|
}
|
|
|
|
return { message, conversationId }
|
|
}
|
|
|
|
class AgentMessageSplitter {
|
|
private pending = ""
|
|
|
|
push(text: string): string[] {
|
|
this.pending += text
|
|
|
|
const lines = this.pending.split(/\r?\n/)
|
|
this.pending = lines.pop() ?? ""
|
|
|
|
return lines
|
|
}
|
|
|
|
flush(): string | null {
|
|
if (this.pending === "") return null
|
|
|
|
const text = this.pending
|
|
this.pending = ""
|
|
return text
|
|
}
|
|
}
|