Files
windmill/chat-sdk/src/types.ts
T
GuilhemandClaude Opus 5 f367eaf6d0 feat: run turns in several flow chat conversations at once (#11202)
* feat: run turns in several flow chat conversations at once

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep finished turns finished and cached chats current in the flow chat pool

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: attribute a turn's rows by job id as well as sequence

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count only real stream updates and retry the job-id read

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a chat that holds an unsent draft, and take one back when its first turn is withdrawn

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: ignore a stale running-turn snapshot, keep a withdrawn chat's draft, poll after clean stream ends

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow the turn running now when the listing named one already over

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep replacement turns and SSE fallback moving

* fix: keep replacement turn handoffs active

* fix: preserve unread badge line height

* fix: settle local fallback handoffs

* fix: settle refused turn handoffs

* fix: scope turn handoffs to conversation

* fix: drop stale turn handoffs

* refactor: move the queued message and 409 handling into per-conversation turns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: address cubic's review of the parallel flow chat turns

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: clear a stale failure on refresh, and tighten the docs and test waits

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: recover running rows past the first page, and drop the failure a re-read disproves

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: check the running-turn query at compile time, and narrow what a refresh clears

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle a failed turn only from an answer that turn wrote

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: settle a failed turn from its own answer, and only while it is still the failure shown

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a failure whose answer arrived even when a newer turn owns the error

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: free an answered failure whatever the turn that started meanwhile is doing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a rows read that a turn outran, rather than merging it under newer messages

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: drop a rows read whose conversation was left and opened again

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hand over a file still being read when its composer goes

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: count a drop's routing as work in flight, so its file is handed over too

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: hold the send until every file a conversation is owed has landed

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: keep a panel mounted per conversation instead of handing its draft over

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the withdrawn chat whose composer was written in, not the empty one

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep the chat in front of the reader when both withdrawn composers were written in

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep a retry's own run arguments when a turn elsewhere refuses it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: name panels apart across pools, and read a flow's inputs when its chat is built

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 11:36:30 +02:00

210 lines
8.9 KiB
TypeScript

import type { RunningTurn } from './api'
export type ChatRole = 'user' | 'assistant' | 'tool' | 'system'
/**
* - `idle`: ready for a message
* - `submitted`: the message was sent, no answer has started streaming yet
* - `streaming`: the answer is arriving
* - `error`: the last turn failed; `error` holds why. Sending again is allowed.
*/
export type ChatStatus = 'idle' | 'submitted' | 'streaming' | 'error'
/**
* Where conversation history lives.
* - `server`: Windmill's conversation store. Each Windmill user only sees their own
* conversations, so use it with the viewer's own session or a per-user token.
* - `local`: the browser's storage. Right for a token shared by every visitor.
* - `none`: nothing is kept beyond the current page.
*/
export type HistoryMode = 'server' | 'local' | 'none'
export interface ToolInvocation {
callId?: string
name: string
/** The arguments the model passed, as a JSON string. */
arguments?: string
result?: string
status: 'running' | 'success' | 'error'
}
export interface ChatAttachment { input: string; s3: string; storage?: string; filename?: string }
export interface ChatMessage {
id: string
role: ChatRole
content: string
/** The model's reasoning summary, when the provider streams one. */
reasoning?: string
/** The call on a `tool` message, from the live stream or its stored row; `callId` is only known from the stream. */
tool?: ToolInvocation
success: boolean
createdAt: string
jobId?: string
/** The flow step that produced the message. */
stepName?: string
/** The files a user message carried, as object-storage references. */
attachments?: ChatAttachment[]
/** True while the message is optimistic or still streaming. */
pending: boolean
/** Id of the persisted row once the server has it; `id` itself never changes, so list keys stay stable. */
serverId?: string
/** The server's cursor for a persisted message; unset for one created on the client. */
seq?: number
}
export interface Conversation {
id: string
title: string | undefined
createdAt: string
updatedAt: string
/**
* Started from the flow editor's test panel rather than a deployed run. Known once the
* server has listed the conversation; unset for one only this client has seen.
*/
isTest?: boolean
/**
* The turn the conversation was still answering when the server listed it: started in
* another tab, or before this chat was created. Pass it to `resumeTurn` to follow it.
*/
runningTurn?: RunningTurn
}
export interface ChatState {
conversationId: string | undefined
messages: ChatMessage[]
status: ChatStatus
error: Error | undefined
conversations: Conversation[]
/** Where history is read from. Starts as configured; drops from `server` to `local` when the credential cannot read conversations. */
history: HistoryMode
loadingMessages: boolean
hasMoreMessages: boolean
}
export type TokenSource = string | (() => string | Promise<string>)
export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>
export interface ChatOptions {
/** Path of a deployed flow with chat mode enabled, e.g. `f/support/assistant`. */
flowPath: string
/** Windmill origin, e.g. `https://app.windmill.dev`. Detected inside a raw app. */
baseUrl?: string
/** Detected inside a raw app. */
workspace?: string
/**
* A Windmill token, or a function returning one (called before every request, so it
* can fetch a short-lived token from your backend). Omit it to use the viewer's
* session: the cookie on the Windmill origin, or a sandboxed raw app's SDK token.
*/
token?: TokenSource
/** Defaults to `server` with the viewer's session and `local` with an explicit token. */
history?: HistoryMode
/** Extra flow inputs sent with every message, next to `user_message`. */
inputs?: Record<string, unknown>
fetch?: FetchLike
/** Backing store for `local` history. Defaults to `localStorage`. */
storage?: StorageLike
/**
* Namespace for `local` history, e.g. the signed-in user's id. Local history is
* per browser, per flow; without this, users sharing a browser share it.
*/
storageKey?: string
/** Messages fetched per page of server history. */
pageSize?: number
/**
* How often, in milliseconds, the server polls a running turn for the stream
* (Enterprise; 50 at the fastest, other servers ignore it). Unset, the server
* relaxes from 100 ms to 3 s over a long turn.
*/
pollDelayMs?: number
/**
* Runs the flow for a turn and returns the job id, instead of the deployed flow at
* `flowPath`. `args` carries `user_message` and the extra inputs; the run must set
* `memory_id` to the conversation id for the conversation and its memory to line up.
* Windmill's own editor uses this to chat with an undeployed flow through a preview run.
*/
run?: (args: Record<string, unknown>, turn: { conversationId: string; signal: AbortSignal }) => Promise<string>
/** Called once a turn has its answer (a failed flow included: its error is the answer). */
onFinish?: (turn: { conversationId: string; jobId?: string; messages: ChatMessage[] }) => void
/** Called when a turn could not run or be followed; `state.error` holds the same error. */
onError?: (error: Error, turn: { conversationId: string; jobId?: string }) => void
}
/** A file sent with a message. It is uploaded to the workspace's object storage before the run starts. */
export interface AttachmentUpload {
/** Kept as the last segment of the stored key, its extension corrected to the media type for PNG, JPEG and PDF. */
name: string
/** The bytes: a Blob, or a `data:` URL of them. */
data: Blob | string
/** The file's media type. Defaults to the Blob's own type, or the data URL's. */
mediaType?: string
}
/** The flow input the uploaded attachments are handed to: an `s3object` (`multiple: false`) or an `s3object[]`. */
export interface AttachmentsInput {
name: string
multiple: boolean
}
export interface SendMessageOptions {
/** Extra flow inputs for this message, on top of `ChatOptions.inputs`. */
inputs?: Record<string, unknown>
/**
* Files to upload and hand to the flow as `{ s3, filename }` objects in `attachmentsInput`,
* the way an AI agent step reads `user_attachments`. A failed upload rejects `sendMessage`
* and the run never starts; `stop()` during the upload does the same with an `AbortError`.
*/
attachments?: AttachmentUpload[]
/** Required with `attachments`, which also need message text. With `multiple: false`, more than one attachment is refused before anything uploads. */
attachmentsInput?: AttachmentsInput
}
export interface Chat {
getState(): ChatState
/** Calls `listener` now and on every change; returns the unsubscribe function (Svelte store contract). */
subscribe(listener: (state: ChatState) => void): () => void
/**
* Sends a message in the current conversation, starting one when there is none. Resolves
* when the answer is complete. Rejects when the message could not be sent at all — a turn
* already running, an attachment that failed to upload — without touching the transcript.
* A conversation still answering a message sent elsewhere rejects with `TurnRunningError`:
* follow that turn with `resumeTurn`, then send again.
*/
sendMessage(text: string, options?: SendMessageOptions): Promise<void>
/**
* Follows a turn of the current conversation that this chat did not start, as named by
* `Conversation.runningTurn` or a `TurnRunningError`: its answer streams into `messages`
* from the start and the turn finishes like one sent here. Server history only; resolves
* when the answer is complete, at once when a turn is already being followed.
*/
resumeTurn(turn: RunningTurn): Promise<void>
/** Stops following the answer and asks Windmill to cancel the run. */
stop(): Promise<void>
newConversation(): void
selectConversation(conversationId: string): Promise<void>
/**
* `kind` narrows server history to the flow editor's test chats, the deployed flow's
* own (the server's default), or both. Local history has no test chats and ignores it.
*/
loadConversations(options?: {
page?: number
perPage?: number
kind?: 'test' | 'deployed' | 'all'
}): Promise<Conversation[]>
deleteConversation(conversationId: string): Promise<void>
/** Sets a conversation's title. The list keeps its order: only a turn moves a conversation. */
renameConversation(conversationId: string, title: string): Promise<void>
loadOlderMessages(): Promise<void>
/**
* Reads what the current conversation gained since its newest message held here, such
* as a turn another tab ran. Server history only; does nothing while a turn is followed.
*/
refreshMessages(): Promise<void>
/** Stops background work (stream, polling) and writes local history out. The chat stays usable. */
destroy(): void
}