@windmill-labs/chat-sdk 0.0.0-stage → 1.830.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,339 @@
1
- # Temporary Holding Version
1
+ # @windmill-labs/chat-sdk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Build a chat interface on a Windmill flow deployed in **chat mode**, from any frontend
4
+ or from a Windmill raw app. The library is headless: it runs the flow, follows the
5
+ answer as it streams, keeps the conversation history, and hands you state to render.
6
+
7
+ ```
8
+ npm install @windmill-labs/chat-sdk
9
+ ```
10
+
11
+ No runtime dependencies. Optional peers: `react` for `@windmill-labs/chat-sdk/react`, `ai` for
12
+ `@windmill-labs/chat-sdk/ai-sdk`, `@assistant-ui/react` for `@windmill-labs/chat-sdk/assistant-ui`.
13
+
14
+ | You build the UI with | Import | You get |
15
+ |---|---|---|
16
+ | Vercel AI SDK `useChat`, AI Elements | `@windmill-labs/chat-sdk/ai-sdk` | a `ChatTransport`: `useChat({ transport })`, nothing else changes |
17
+ | assistant-ui | `@windmill-labs/chat-sdk/assistant-ui` | a runtime for `AssistantRuntimeProvider`, threads included |
18
+ | Your own components | `@windmill-labs/chat-sdk/react` or `@windmill-labs/chat-sdk` | a hook / a store with messages, status and actions |
19
+
20
+ ## The flow
21
+
22
+ Any deployed flow with **Chat mode** enabled in its settings works. Windmill passes the
23
+ message as the `user_message` input and threads the conversation through `memory_id`,
24
+ so an AI agent step remembers earlier turns. The answer is:
25
+
26
+ - what the last step streams, when it is an AI agent step;
27
+ - otherwise the flow's result: its `windmill_chat_answer` field when it has one, a
28
+ string as is, anything else as JSON.
29
+
30
+ ## Vercel AI SDK (`useChat`, AI Elements)
31
+
32
+ ```tsx
33
+ import { useChat } from '@ai-sdk/react'
34
+ import { createWindmillChatTransport } from '@windmill-labs/chat-sdk/ai-sdk'
35
+
36
+ const transport = createWindmillChatTransport({
37
+ baseUrl: 'https://app.windmill.dev',
38
+ workspace: 'acme',
39
+ flowPath: 'f/support/assistant',
40
+ token: () => fetch('/api/windmill-token').then((r) => r.text())
41
+ })
42
+
43
+ export function Support() {
44
+ const { messages, status, sendMessage, stop } = useChat({ id: conversationId, transport })
45
+ // render `messages[i].parts`: text, reasoning and dynamic-tool parts, as with any AI SDK backend
46
+ }
47
+ ```
48
+
49
+ The chat `id` is the conversation: reuse it to continue one, and pass a UUID when you
50
+ also read server history, so it matches what `flow_conversations` stores (any other id
51
+ maps to a fixed UUID). `sendMessage(msg, { body })` sends extra flow inputs. Tool calls
52
+ arrive as `dynamic-tool` parts (`input-available → output-available | output-error`),
53
+ which AI Elements' `<Tool>` renders as is. A failed flow surfaces as `error`.
54
+ `regenerate()` runs the flow again with the same message: a new turn on the server,
55
+ not a replacement of the previous answer.
56
+
57
+ The transport also carries the history helpers: `transport.loadMessages(id)` returns
58
+ `UIMessage`s for `useChat({ messages })` or `setMessages`, `transport.listConversations()`
59
+ and `transport.deleteConversation(id)`. A loaded user message lists the files it carried
60
+ in `metadata.attachments`; `WindmillChatApi.attachmentUrl` gives each one's download URL.
61
+ Sending attachments is not supported: `sendMessage` with `files` is refused with an
62
+ explanatory error.
63
+
64
+ ## assistant-ui
65
+
66
+ ```tsx
67
+ import { AssistantRuntimeProvider } from '@assistant-ui/react'
68
+ import { useWindmillRuntime } from '@windmill-labs/chat-sdk/assistant-ui'
69
+
70
+ export function Support() {
71
+ const runtime = useWindmillRuntime({ baseUrl, workspace, flowPath, token })
72
+ return (
73
+ <AssistantRuntimeProvider runtime={runtime}>
74
+ <Thread /> {/* your assistant-ui components, thread list included */}
75
+ </AssistantRuntimeProvider>
76
+ )
77
+ }
78
+ ```
79
+
80
+ Conversations are threads: `ThreadListPrimitive` switches, creates and deletes them.
81
+ Tool calls render through your `tools` components (`MessagePrimitive.Parts`), reasoning
82
+ through `Reasoning`. It takes the same options as `useWindmillChat` below.
83
+
84
+ ## React
85
+
86
+ ```tsx
87
+ import { useWindmillChat } from '@windmill-labs/chat-sdk/react'
88
+
89
+ export function Support() {
90
+ const chat = useWindmillChat({
91
+ baseUrl: 'https://app.windmill.dev',
92
+ workspace: 'acme',
93
+ flowPath: 'f/support/assistant',
94
+ token: () => fetch('/api/windmill-token').then((r) => r.text())
95
+ })
96
+ const [draft, setDraft] = useState('')
97
+
98
+ return (
99
+ <div>
100
+ {chat.messages.map((m) => (
101
+ <p key={m.id} data-role={m.role} data-pending={m.pending}>
102
+ {m.content}
103
+ </p>
104
+ ))}
105
+ <form
106
+ onSubmit={(e) => {
107
+ e.preventDefault()
108
+ chat.sendMessage(draft)
109
+ setDraft('')
110
+ }}
111
+ >
112
+ <input value={draft} onChange={(e) => setDraft(e.target.value)} />
113
+ <button disabled={chat.status !== 'idle' && chat.status !== 'error'}>Send</button>
114
+ {chat.status === 'streaming' && (
115
+ <button type="button" onClick={chat.stop}>
116
+ Stop
117
+ </button>
118
+ )}
119
+ </form>
120
+ </div>
121
+ )
122
+ }
123
+ ```
124
+
125
+ The hook returns the [state](#state) plus the chat's methods. It recreates the chat
126
+ (fresh state, old one destroyed) when `flowPath`, `baseUrl`, `workspace`, `history`,
127
+ `storageKey` or the credential change: a different token string, or a switch between
128
+ no token, a string and a function; and when a `run` callback appears or goes away.
129
+ A token function is called through a ref, so passing a new closure on every render
130
+ is fine and never resets the chat, and so are `run` and the callbacks; when users
131
+ sign in and out behind a token function, change `storageKey` (their id) so local
132
+ history and state start over with them.
133
+
134
+ ## Raw apps
135
+
136
+ Inside a Windmill raw app nothing needs configuring: the chat runs as the viewer,
137
+ against the Windmill the app is served from.
138
+
139
+ ```tsx
140
+ const chat = useWindmillChat({ flowPath: 'f/support/assistant' })
141
+ ```
142
+
143
+ - **Unsandboxed app** (the default): the viewer's session is used. Viewers need
144
+ permission to run the flow.
145
+ - **Sandboxed app**: declare `jobs:run` in the app's frontend SDK scopes, and
146
+ `flow_conversations:write` for server-side history. The viewer consents once and
147
+ the app receives a token restricted to those scopes.
148
+ - **`wmill app dev`**: there is no viewer session on the dev server, so pass
149
+ `baseUrl`, `workspace` and `token` explicitly during development.
150
+
151
+ ## Any framework
152
+
153
+ `createChat` returns a store: `subscribe` calls the listener immediately and on every
154
+ change, and returns the unsubscribe function. That is the Svelte store contract, so
155
+ `$chat` works as is; other frameworks wrap it in a few lines.
156
+
157
+ ```ts
158
+ import { createChat } from '@windmill-labs/chat-sdk'
159
+
160
+ const chat = createChat({ baseUrl, workspace, flowPath, token })
161
+ chat.subscribe((state) => render(state))
162
+ await chat.sendMessage('Hello')
163
+ ```
164
+
165
+ ```svelte
166
+ <script>
167
+ import { createChat } from '@windmill-labs/chat-sdk'
168
+ const chat = createChat({ flowPath: 'f/support/assistant' })
169
+ </script>
170
+
171
+ {#each $chat.messages as m (m.id)}
172
+ <p>{m.content}</p>
173
+ {/each}
174
+ <button onclick={() => chat.sendMessage(draft)}>Send</button>
175
+ ```
176
+
177
+ ## Options
178
+
179
+ | Option | |
180
+ |---|---|
181
+ | `flowPath` | Path of the deployed flow, e.g. `f/support/assistant`. Required. |
182
+ | `baseUrl` | The Windmill origin. Detected inside a raw app. |
183
+ | `workspace` | Detected inside a raw app. |
184
+ | `token` | A token, or a function returning one (called before every request, so it can fetch a short-lived token from your backend). Omit it inside a raw app. |
185
+ | `history` | `'server'`, `'local'` or `'none'`, see [History](#history). Defaults to `'server'` with a viewer session and `'local'` with an explicit `token`. |
186
+ | `inputs` | Extra flow inputs sent with every message. `sendMessage(text, { inputs })` adds per-message ones, and `{ attachments, attachmentsInput }` files, see [Attachments](#attachments). |
187
+ | `storageKey` | Namespace for `local` history, e.g. the signed-in user's id. Local history is per browser and per flow; without it, users sharing a browser share it. |
188
+ | `fetch`, `storage` | Replacements for the globals, for tests and unusual runtimes. |
189
+ | `pageSize` | Messages and conversations per page of server history. Default 50. |
190
+ | `pollDelayMs` | How often, in ms, 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; set it when tokens must keep flowing at that pace. |
191
+ | `onFinish`, `onError` | Called when a turn has its answer, or could not run at all. |
192
+ | `run` | Runs the flow for a turn yourself and returns the job id, instead of the deployed flow at `flowPath` (Windmill's editor chats with an undeployed flow through a preview run this way). Pass `memory_id` = the conversation id. |
193
+
194
+ ## State
195
+
196
+ ```ts
197
+ interface ChatState {
198
+ conversationId: string | undefined
199
+ messages: ChatMessage[]
200
+ status: 'idle' | 'submitted' | 'streaming' | 'error'
201
+ error: Error | undefined
202
+ conversations: Conversation[]
203
+ history: 'server' | 'local' | 'none'
204
+ loadingMessages: boolean
205
+ hasMoreMessages: boolean
206
+ }
207
+
208
+ interface ChatMessage {
209
+ id: string
210
+ role: 'user' | 'assistant' | 'tool' | 'system'
211
+ content: string
212
+ reasoning?: string // the model's reasoning summary, when streamed
213
+ tool?: { callId?: string; name: string; arguments?: string; result?: string; status: 'running' | 'success' | 'error' }
214
+ success: boolean // false for a failed flow or tool
215
+ pending: boolean // still streaming, or not yet confirmed by the server
216
+ createdAt: string
217
+ jobId?: string
218
+ stepName?: string
219
+ serverId?: string // the persisted row; `id` itself never changes, so list keys are stable
220
+ }
221
+ ```
222
+
223
+ A turn goes `submitted` (the flow is queued) → `streaming` (the answer is arriving) →
224
+ `idle`. Tool calls appear as `tool` messages whose `status` moves from `running` to
225
+ `success` or `error`. A flow that fails still completes the turn: its error is the
226
+ answer, an `assistant` message with `success: false`. `status: 'error'` (with `error`
227
+ set) means the turn could not run or be followed at all, such as a refused request.
228
+
229
+ Methods: `sendMessage(text, { inputs?, attachments?, attachmentsInput? })`, `stop()`,
230
+ `newConversation()`, `selectConversation(id)`, `loadConversations({ page?, perPage?, kind? })`,
231
+ `deleteConversation(id)`, `renameConversation(id, title)`, `loadOlderMessages()`,
232
+ `resumeTurn(turn)`, `refreshMessages()` (reads what another tab added to the open
233
+ conversation), `destroy()`. `kind` lists the flow editor's test chats (`'test'`), the deployed flow's
234
+ own (`'deployed'`, the server's default) or both (`'all'`); each `Conversation` carries
235
+ `isTest`. A rename keeps the conversation's place in the list. Switching conversations
236
+ stops following the current answer; the flow keeps running and, with server history,
237
+ its answer is there when you come back.
238
+
239
+ ## Attachments
240
+
241
+ A flow whose AI agent step reads `user_attachments` from an `s3object[]` (or a single
242
+ `s3object`) flow input takes files with a message:
243
+
244
+ ```ts
245
+ await chat.sendMessage('What does this contract say?', {
246
+ attachments: [{ name: file.name, data: file }], // a Blob/File, or a `data:` URL
247
+ attachmentsInput: { name: 'files', multiple: true }
248
+ })
249
+ ```
250
+
251
+ Each file is uploaded to the workspace's object storage under
252
+ `windmill_uploads/chat/<turn>/<index>/<name>` and handed to the input as `{ s3, filename }`
253
+ objects (the object for a single-file input). Once the uploads return, the pending user
254
+ message lists them in `attachments`, as `{ input, s3, filename }` references. The name's
255
+ extension is corrected to the file's media type for PNG, JPEG and PDF, because the worker
256
+ reads the type off the key.
257
+ Files need message text to go with them. A failed upload rejects `sendMessage` before any
258
+ run starts, and `stop()` during the upload aborts it; both leave the transcript as it was.
259
+ The chat never deletes uploads, so files of a send that did not run stay in storage. The
260
+ workspace needs object storage set up. With Enterprise advanced storage permissions, the
261
+ user needs read and write on `windmill_uploads/*`, which the default rules grant. The upload goes through
262
+ `job_helpers`, so a restricted token needs `job_helpers:write`; a sandboxed raw app cannot
263
+ request that scope today, so attachments are not available there yet.
264
+
265
+ ## One turn at a time
266
+
267
+ A conversation answers one message at a time, and Windmill enforces it: a message sent
268
+ while its previous turn still runs (from another tab, or before a reload) is refused.
269
+ `sendMessage` then rejects with a `TurnRunningError` and shows nothing of the message.
270
+ With server history, a listed `Conversation` also carries `runningTurn` while it is
271
+ answering. Either way, `resumeTurn(turn)` follows that turn in the selected
272
+ conversation: its answer streams in from the start and the turn finishes as if it had
273
+ been sent here, after which the message can be sent again.
274
+
275
+ ```ts
276
+ // `runningTurn` comes from the listing, so read it before selecting.
277
+ await chat.loadConversations()
278
+ const running = chat.getState().conversations.find((c) => c.id === id)?.runningTurn
279
+ await chat.selectConversation(id)
280
+ if (running) await chat.resumeTurn(running)
281
+ ```
282
+
283
+ A dropped connection to the answer is retried; when it keeps failing, the chat stops
284
+ streaming and waits for the flow's result instead, so the turn still ends.
285
+
286
+ ## History
287
+
288
+ Windmill stores every conversation of a chat-mode flow, and each Windmill user sees
289
+ only their own. `history: 'server'` reads that store: `loadConversations()` lists
290
+ them, `selectConversation(id)` loads one, `loadOlderMessages()` pages back. Every
291
+ message rendered from the server carries its `jobId` and `stepName`.
292
+
293
+ That store is keyed by the **Windmill user**, so it fits a viewer session or a token
294
+ issued per user. With one token shared by every visitor of a site, all visitors would
295
+ see each other's conversations. For that setup use `history: 'local'` (the default
296
+ with an explicit `token`): the conversation list and messages stay in the browser's
297
+ `localStorage`, per Windmill instance, workspace and flow. `'none'` keeps nothing
298
+ beyond the page.
299
+
300
+ When the default `'server'` mode turns out unreadable (a token or sandboxed app
301
+ without `flow_conversations` scopes), the chat switches itself to `'local'` and
302
+ `state.history` says so. Passing `history` explicitly disables that fallback.
303
+
304
+ ## Tokens
305
+
306
+ Anything a browser holds can be read by its user, so give a chat token exactly what
307
+ the chat needs:
308
+
309
+ | Setup | Scopes |
310
+ |---|---|
311
+ | Public site, one token for everyone | `jobs:run:flows:f/support/assistant`, and `history: 'local'`. The token can run that one flow and follow its jobs, nothing else. |
312
+ | Per-user tokens minted by your backend | The above plus `flow_conversations:write`, with `history: 'server'` (an explicit token defaults to local history). Return them from an endpoint and pass `token: () => fetch(...)`. |
313
+ | A Windmill user in the browser (raw app, embedded Windmill) | No token: the session is used. |
314
+
315
+ The token's user must be allowed to run the flow. `stop()` closes the stream in any
316
+ case; cancelling the run on the server as well needs `jobs:write`, which also lets the
317
+ token read every job its user can see, so leave it out unless that matters.
318
+
319
+ Anyone holding the token can run the flow with inputs of their choosing, so a flow
320
+ exposed this way should treat `user_message` and the other inputs as untrusted.
321
+
322
+ ## Lower level
323
+
324
+ `WindmillChatApi` wraps the endpoints (`runFlow`, `streamJob`, `listConversations`,
325
+ `listMessages`, `deleteConversation`, `cancelJob`), `followJob` follows a run to
326
+ completion across the server's stream timeouts, `parseStreamEvents` decodes the AI
327
+ agent stream, `extractChatAnswer` turns a flow result into the text a chat shows, and
328
+ `conversationIdFor` maps any chat id to its conversation UUID. They are exported for
329
+ custom integrations.
330
+
331
+ ## For AI coding agents
332
+
333
+ When asked to add a chat over a Windmill flow: the flow must be deployed with chat mode
334
+ on. Pick the entry point from the table at the top (`useChat` → `@windmill-labs/chat-sdk/ai-sdk`,
335
+ assistant-ui → `@windmill-labs/chat-sdk/assistant-ui`, otherwise `@windmill-labs/chat-sdk/react`). Inside a
336
+ Windmill raw app pass only `flowPath`. Elsewhere pass `baseUrl`, `workspace` and a
337
+ `token`; for a public page use a token scoped to `jobs:run:flows:<flowPath>` and leave
338
+ `history` at its default. Render `role`, `content`, `pending`, `success` and
339
+ `tool.status`; never build the SSE handling yourself.
@@ -0,0 +1,344 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_follow = require("./follow-Dug-Afeu.cjs");
3
+ //#region src/ai-sdk.ts
4
+ /**
5
+ * A Vercel AI SDK `ChatTransport` over a chat-mode flow: `useChat({ transport })`
6
+ * (and AI Elements, which builds on it) then work against Windmill unchanged.
7
+ * The chat id is the conversation, so a UUID id lines up with server history.
8
+ */
9
+ function createWindmillChatTransport(options) {
10
+ const api = new require_follow.WindmillChatApi(options);
11
+ const jobs = /* @__PURE__ */ new Map();
12
+ return {
13
+ conversationId: require_follow.conversationIdFor,
14
+ async sendMessages({ chatId, messages, abortSignal, body }) {
15
+ const last = messages[messages.length - 1];
16
+ if (!last || last.role !== "user") throw new Error("windmill-chat: the last message must be a user message");
17
+ if (last.parts.some((p) => p.type === "file")) throw new Error("windmill-chat: attachments are not supported; upload the file yourself and pass its reference through `body`");
18
+ const text = last.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
19
+ const memoryId = require_follow.conversationIdFor(chatId);
20
+ const entry = {
21
+ jobId: await api.runFlow(options.flowPath, {
22
+ ...options.inputs,
23
+ ...body,
24
+ user_message: text
25
+ }, {
26
+ memoryId,
27
+ signal: abortSignal
28
+ }),
29
+ done: false
30
+ };
31
+ jobs.set(chatId, entry);
32
+ return chunkStream(api, entry, abortSignal);
33
+ },
34
+ async reconnectToStream({ chatId, abortSignal }) {
35
+ const entry = jobs.get(chatId);
36
+ if (!entry || entry.done) return null;
37
+ return chunkStream(api, entry, abortSignal);
38
+ },
39
+ async loadMessages(chatId, pagination) {
40
+ return toUIMessages((await api.listMessages(require_follow.conversationIdFor(chatId), pagination).catch((e) => {
41
+ if (e instanceof require_follow.WindmillApiError && e.status === 404) return [];
42
+ throw e;
43
+ })).map((row) => ({
44
+ id: row.id,
45
+ serverId: row.id,
46
+ role: row.message_type,
47
+ content: row.content,
48
+ success: row.success ?? true,
49
+ createdAt: row.created_at,
50
+ jobId: row.job_id ?? void 0,
51
+ stepName: row.step_name ?? void 0,
52
+ pending: false,
53
+ seq: row.created_seq,
54
+ reasoning: row.reasoning ?? void 0,
55
+ attachments: row.attachments ?? void 0,
56
+ tool: toolFromRow(row)
57
+ })));
58
+ },
59
+ async listConversations(pagination) {
60
+ return (await api.listConversations(options.flowPath, pagination)).map((row) => ({
61
+ id: row.id,
62
+ title: row.title ?? void 0,
63
+ createdAt: row.created_at,
64
+ updatedAt: row.updated_at
65
+ }));
66
+ },
67
+ async deleteConversation(chatId) {
68
+ await api.deleteConversation(require_follow.conversationIdFor(chatId));
69
+ jobs.delete(chatId);
70
+ }
71
+ };
72
+ }
73
+ /** The call a stored tool row carries: its tool, named by the sentence the worker words
74
+ * every tool row from, and the model's arguments and what it got back — for a failed
75
+ * tool, the result is what it failed with. */
76
+ function toolFromRow(row) {
77
+ if (row.message_type !== "tool") return void 0;
78
+ const name = /^Used (.+) tool$/.exec(row.content)?.[1] ?? /^Error executing (.+)$/.exec(row.content)?.[1];
79
+ if (!name) return void 0;
80
+ return {
81
+ name,
82
+ status: row.success ?? true ? "success" : "error",
83
+ arguments: row.tool_arguments ?? void 0,
84
+ result: row.tool_result ?? void 0
85
+ };
86
+ }
87
+ /** Streams a job's answer as AI SDK chunks; resumes from `entry.offset` when the job is already running. */
88
+ function chunkStream(api, entry, signal) {
89
+ return new ReadableStream({ async start(controller) {
90
+ const parts = new PartWriter((chunk) => controller.enqueue(chunk));
91
+ parts.emit({ type: "start" });
92
+ try {
93
+ let failure;
94
+ for await (const event of require_follow.followJob(api, entry.jobId, {
95
+ signal,
96
+ streamOffset: entry.offset,
97
+ onOffset: (offset) => {
98
+ entry.offset = offset;
99
+ }
100
+ })) {
101
+ if (event.type === "stream") {
102
+ for (const e of event.events) parts.apply(e);
103
+ continue;
104
+ }
105
+ const cut = parts.openText ?? "";
106
+ parts.closeOpen();
107
+ failure = await failureText(api, entry.jobId, event.result, signal);
108
+ if (failure === void 0 && event.streamLost) {
109
+ const answer = require_follow.extractChatAnswer(event.result);
110
+ if (answer !== void 0 && answer !== cut) {
111
+ parts.text(answer.startsWith(cut) ? answer.slice(cut.length) : answer);
112
+ parts.closeOpen();
113
+ }
114
+ } else if (failure === void 0 && !parts.streamedText) {
115
+ const answer = require_follow.extractChatAnswer(event.result);
116
+ if (answer !== void 0) {
117
+ parts.text(answer);
118
+ parts.closeOpen();
119
+ }
120
+ }
121
+ }
122
+ entry.done = true;
123
+ parts.emit(failure === void 0 ? { type: "finish" } : {
124
+ type: "error",
125
+ errorText: failure
126
+ });
127
+ } catch (e) {
128
+ if (!require_follow.isAbortError(e)) {
129
+ parts.closeOpen();
130
+ parts.emit({
131
+ type: "error",
132
+ errorText: e instanceof Error ? e.message : String(e)
133
+ });
134
+ }
135
+ } finally {
136
+ controller.close();
137
+ }
138
+ } });
139
+ }
140
+ /** A completed flow's error, when the job did fail (the envelope alone is a legitimate result). */
141
+ async function failureText(api, jobId, result, signal) {
142
+ if (!require_follow.isErrorResult(result)) return void 0;
143
+ return await api.getCompletedResult(jobId, signal).then((r) => r.success === false).catch(() => true) ? require_follow.errorResultMessage(result) : void 0;
144
+ }
145
+ var _textId = /* @__PURE__ */ new WeakMap();
146
+ var _reasoningId = /* @__PURE__ */ new WeakMap();
147
+ var _started = /* @__PURE__ */ new WeakMap();
148
+ var _inputSent = /* @__PURE__ */ new WeakMap();
149
+ var _PartWriter_brand = /* @__PURE__ */ new WeakSet();
150
+ /**
151
+ * Turns agent events into AI SDK chunks. Text and reasoning are open parts that a
152
+ * tool call closes (a new round starts new parts); tool calls are `dynamic-tool`
153
+ * parts, since the UI declares no tools of its own.
154
+ */
155
+ var PartWriter = class {
156
+ constructor(emit) {
157
+ this.emit = emit;
158
+ require_follow._classPrivateMethodInitSpec(this, _PartWriter_brand);
159
+ this.streamedText = false;
160
+ require_follow._classPrivateFieldInitSpec(this, _textId, void 0);
161
+ require_follow._classPrivateFieldInitSpec(this, _reasoningId, void 0);
162
+ require_follow._classPrivateFieldInitSpec(this, _started, /* @__PURE__ */ new Set());
163
+ require_follow._classPrivateFieldInitSpec(this, _inputSent, /* @__PURE__ */ new Set());
164
+ }
165
+ text(delta) {
166
+ this.streamedText = true;
167
+ if (!require_follow._classPrivateFieldGet2(_textId, this)) {
168
+ require_follow._classPrivateFieldSet2(_textId, this, require_follow.randomId());
169
+ this.openText = "";
170
+ this.emit({
171
+ type: "text-start",
172
+ id: require_follow._classPrivateFieldGet2(_textId, this)
173
+ });
174
+ }
175
+ this.openText += delta;
176
+ this.emit({
177
+ type: "text-delta",
178
+ id: require_follow._classPrivateFieldGet2(_textId, this),
179
+ delta
180
+ });
181
+ }
182
+ reasoning(delta) {
183
+ if (!require_follow._classPrivateFieldGet2(_reasoningId, this)) {
184
+ require_follow._classPrivateFieldSet2(_reasoningId, this, require_follow.randomId());
185
+ this.emit({
186
+ type: "reasoning-start",
187
+ id: require_follow._classPrivateFieldGet2(_reasoningId, this)
188
+ });
189
+ }
190
+ this.emit({
191
+ type: "reasoning-delta",
192
+ id: require_follow._classPrivateFieldGet2(_reasoningId, this),
193
+ delta
194
+ });
195
+ }
196
+ closeOpen() {
197
+ if (require_follow._classPrivateFieldGet2(_reasoningId, this)) {
198
+ this.emit({
199
+ type: "reasoning-end",
200
+ id: require_follow._classPrivateFieldGet2(_reasoningId, this)
201
+ });
202
+ require_follow._classPrivateFieldSet2(_reasoningId, this, void 0);
203
+ }
204
+ if (require_follow._classPrivateFieldGet2(_textId, this)) {
205
+ this.emit({
206
+ type: "text-end",
207
+ id: require_follow._classPrivateFieldGet2(_textId, this)
208
+ });
209
+ require_follow._classPrivateFieldSet2(_textId, this, void 0);
210
+ this.openText = void 0;
211
+ }
212
+ }
213
+ apply(event) {
214
+ switch (event.type) {
215
+ case "token_delta":
216
+ this.text(event.content);
217
+ break;
218
+ case "reasoning_token_delta":
219
+ this.reasoning(event.content);
220
+ break;
221
+ case "tool_call":
222
+ this.closeOpen();
223
+ require_follow._assertClassBrand(_PartWriter_brand, this, _toolStart).call(this, event.call_id, event.function_name);
224
+ break;
225
+ case "tool_call_arguments":
226
+ this.closeOpen();
227
+ require_follow._assertClassBrand(_PartWriter_brand, this, _toolStart).call(this, event.call_id, event.function_name);
228
+ require_follow._classPrivateFieldGet2(_inputSent, this).add(event.call_id);
229
+ this.emit({
230
+ type: "tool-input-available",
231
+ toolCallId: event.call_id,
232
+ toolName: event.function_name,
233
+ input: require_follow.parseJsonOr(event.arguments),
234
+ dynamic: true
235
+ });
236
+ break;
237
+ case "tool_execution":
238
+ this.closeOpen();
239
+ require_follow._assertClassBrand(_PartWriter_brand, this, _toolStart).call(this, event.call_id, event.function_name);
240
+ break;
241
+ case "tool_result":
242
+ require_follow._assertClassBrand(_PartWriter_brand, this, _toolStart).call(this, event.call_id, event.function_name);
243
+ if (!require_follow._classPrivateFieldGet2(_inputSent, this).has(event.call_id)) {
244
+ require_follow._classPrivateFieldGet2(_inputSent, this).add(event.call_id);
245
+ this.emit({
246
+ type: "tool-input-available",
247
+ toolCallId: event.call_id,
248
+ toolName: event.function_name,
249
+ input: void 0,
250
+ dynamic: true
251
+ });
252
+ }
253
+ this.emit(event.success ? {
254
+ type: "tool-output-available",
255
+ toolCallId: event.call_id,
256
+ output: require_follow.parseJsonOr(event.result),
257
+ dynamic: true
258
+ } : {
259
+ type: "tool-output-error",
260
+ toolCallId: event.call_id,
261
+ errorText: event.result,
262
+ dynamic: true
263
+ });
264
+ }
265
+ }
266
+ };
267
+ function _toolStart(callId, name) {
268
+ if (require_follow._classPrivateFieldGet2(_started, this).has(callId)) return;
269
+ require_follow._classPrivateFieldGet2(_started, this).add(callId);
270
+ this.emit({
271
+ type: "tool-input-start",
272
+ toolCallId: callId,
273
+ toolName: name,
274
+ dynamic: true
275
+ });
276
+ }
277
+ /**
278
+ * `ChatMessage`s (Windmill's role-per-row model) as `UIMessage`s: an assistant
279
+ * turn becomes one message whose parts carry its text, reasoning and tool calls. A
280
+ * user message's attachments ride in `metadata.attachments`, as references for
281
+ * `WindmillChatApi.attachmentUrl`: a `file` part would need a URL the browser can
282
+ * load unauthenticated.
283
+ */
284
+ function toUIMessages(messages) {
285
+ const out = [];
286
+ for (const m of messages) {
287
+ if (m.role === "user" || m.role === "system") {
288
+ out.push({
289
+ id: m.id,
290
+ role: m.role,
291
+ ...m.attachments?.length ? { metadata: { attachments: m.attachments } } : {},
292
+ parts: [{
293
+ type: "text",
294
+ text: m.content
295
+ }]
296
+ });
297
+ continue;
298
+ }
299
+ let target = out[out.length - 1];
300
+ if (!target || target.role !== "assistant") {
301
+ target = {
302
+ id: m.id,
303
+ role: "assistant",
304
+ parts: []
305
+ };
306
+ out.push(target);
307
+ }
308
+ if (m.reasoning) target.parts.push({
309
+ type: "reasoning",
310
+ text: m.reasoning,
311
+ state: "done"
312
+ });
313
+ if (m.role === "tool") {
314
+ const toolCallId = m.tool?.callId ?? m.id;
315
+ const toolName = m.tool?.name ?? "tool";
316
+ const input = require_follow.parseJsonOr(m.tool?.arguments);
317
+ target.parts.push(m.success ? {
318
+ type: "dynamic-tool",
319
+ toolName,
320
+ toolCallId,
321
+ state: "output-available",
322
+ input,
323
+ output: m.tool?.result !== void 0 ? require_follow.parseJsonOr(m.tool.result) : m.content
324
+ } : {
325
+ type: "dynamic-tool",
326
+ toolName,
327
+ toolCallId,
328
+ state: "output-error",
329
+ input,
330
+ errorText: m.tool?.result ?? m.content
331
+ });
332
+ continue;
333
+ }
334
+ if (m.content) target.parts.push({
335
+ type: "text",
336
+ text: m.content,
337
+ state: "done"
338
+ });
339
+ }
340
+ return out;
341
+ }
342
+ //#endregion
343
+ exports.createWindmillChatTransport = createWindmillChatTransport;
344
+ exports.toUIMessages = toUIMessages;