feat: stream Reginald's replies token-by-token
The 26b is slow (~30s/call); the chat now shows the answer forming instead of freezing until it's done. The final prose streams over SSE; tool-calling turns stay structured (no partial tokens), so streaming kicks in for the narration. core (@commitea/core): - chat-client.complete gains an optional onToken — when set, it requests stream:true and parses the OpenAI SSE stream, emitting content deltas and assembling streamed tool-call argument fragments into the final result. - GiteaHttpResponse exposes the optional `body` stream (real fetch has it; stubs don't). agent-loop threads onToken to each completion. app: - model:chat forwards each delta to the renderer (event.sender.send); preload exposes model.onToken(cb) → unsubscribe. useChat accumulates the live stream into a growing bubble (with a cursor), replaced by the authoritative final content when the turn resolves. Unconfigured → scripted reply, unchanged. Verified: 118 core tests green (2 streaming: SSE content deltas + tool-call fragment assembly), desktop typecheck clean, 14 fixture e2e green. Live: a real turn against gemma-4-26b assembles the correct answer via the streaming path (live-reginald green) — the reply now renders token-by-token. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,18 +31,20 @@ function stringify(result: unknown): string {
|
||||
}
|
||||
|
||||
export async function runAgentTurn(opts: {
|
||||
complete: (messages: ChatMessage[], tools?: ToolDecl[]) => Promise<CompletionResult>
|
||||
complete: (messages: ChatMessage[], tools?: ToolDecl[], onToken?: (delta: string) => void) => Promise<CompletionResult>
|
||||
messages: ChatMessage[]
|
||||
tools: ToolDecl[]
|
||||
execute: ToolExecutor
|
||||
maxSteps?: number
|
||||
/** Streams content deltas as the model produces prose (final-answer streaming). */
|
||||
onToken?: (delta: string) => void
|
||||
}): Promise<AgentTurn> {
|
||||
const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS
|
||||
const convo: ChatMessage[] = [...opts.messages]
|
||||
const steps: AgentStep[] = []
|
||||
|
||||
for (let step = 0; step < maxSteps; step++) {
|
||||
const { content, toolCalls } = await opts.complete(convo, opts.tools)
|
||||
const { content, toolCalls } = await opts.complete(convo, opts.tools, opts.onToken)
|
||||
if (toolCalls.length === 0) {
|
||||
convo.push({ role: 'assistant', content })
|
||||
return { content, steps, messages: convo }
|
||||
@@ -63,7 +65,7 @@ export async function runAgentTurn(opts: {
|
||||
}
|
||||
|
||||
// Out of tool budget — force a final prose answer with tools withheld.
|
||||
const final = await opts.complete(convo, [])
|
||||
const final = await opts.complete(convo, [], opts.onToken)
|
||||
convo.push({ role: 'assistant', content: final.content })
|
||||
return { content: final.content, steps, messages: convo }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user