Files
windmill/chat-sdk/src/stream.ts
T
Ruben FiszelandClaude Fable 5.1 e8c02c04cd feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps (#11117)
* feat: windmill-chat sdk for chat-mode flows in external frontends and raw apps

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J

* fix: keep streamed answers until persisted, finish turns after history fallback

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018aQiZNAU8g17kWkyTryS5J

* feat: ai sdk transport and assistant-ui runtime for windmill-chat

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: finish a turn from the flow result until its answer row lands, hash chat ids without crypto.subtle

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: judge a turn answered by a persisted assistant row, wherever it was fetched

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: attribute a turn's answer to its own jobs, keep a local turn when switching conversations

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: mirror local history on every change, attribute failure-handler answers to the turn

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: new chat per token string in the React hook, idle after destroy, no reorder on view

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: recreate the hook's chat on any credential change, namespace local history per user

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix: send the latest inputs from the React hook

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 22:32:45 +02:00

66 lines
2.0 KiB
TypeScript

/** The events an AI agent step streams, one JSON object per line of the job's result stream. */
export type AgentStreamEvent =
| { type: 'token_delta'; content: string }
| { type: 'reasoning_token_delta'; content: string }
| { type: 'tool_call'; call_id: string; function_name: string }
| { type: 'tool_call_arguments'; call_id: string; function_name: string; arguments: string }
| { type: 'tool_execution'; call_id: string; function_name: string }
| {
type: 'tool_result'
call_id: string
function_name: string
result: string
success: boolean
}
const KNOWN_TYPES = new Set([
'token_delta',
'reasoning_token_delta',
'tool_call',
'tool_call_arguments',
'tool_execution',
'tool_result'
])
/**
* Incremental parser for the `new_result_stream` chunks of a job update. A chunk is
* not guaranteed to end on a line boundary, so an incomplete last line waits for the
* next `push` (or `flush` once the job completes).
*/
export function createStreamEventParser() {
let pending = ''
return {
push(chunk: string): AgentStreamEvent[] {
pending += chunk
const lastNewline = pending.lastIndexOf('\n')
if (lastNewline === -1) return []
const complete = pending.slice(0, lastNewline)
pending = pending.slice(lastNewline + 1)
return parseStreamEvents(complete)
},
flush(): AgentStreamEvent[] {
const rest = pending
pending = ''
return parseStreamEvents(rest)
}
}
}
/** Parses complete NDJSON lines; lines that aren't agent events are skipped. */
export function parseStreamEvents(ndjson: string): AgentStreamEvent[] {
const events: AgentStreamEvent[] = []
for (const line of ndjson.split('\n')) {
const trimmed = line.trim()
if (!trimmed) continue
try {
const parsed = JSON.parse(trimmed)
if (parsed && typeof parsed === 'object' && KNOWN_TYPES.has(parsed.type)) {
events.push(parsed as AgentStreamEvent)
}
} catch {
// not an agent event
}
}
return events
}