@windmill-labs/chat-sdk 1.846.1 → 1.848.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
@@ -148,6 +148,45 @@ const chat = useWindmillChat({ flowPath: 'f/support/assistant' })
148
148
  - **`wmill app dev`**: there is no viewer session on the dev server, so pass
149
149
  `baseUrl`, `workspace` and `token` explicitly during development.
150
150
 
151
+ ### A saved agent, through an agent runnable
152
+
153
+ To chat with a saved AI agent (an `ai_agent` resource), give the app an **agent
154
+ runnable**: a path runnable of type `agent` naming the agent, picked under "AI agents"
155
+ in the editor, or in `raw_app.yaml`:
156
+
157
+ ```yaml
158
+ runnables:
159
+ chat:
160
+ type: agent
161
+ path: f/support/agent
162
+ ```
163
+
164
+ Each call runs one turn of the agent's chat as the app, so a sandboxed app declares no
165
+ frontend SDK scope, viewers get no consent prompt and need no access to the agent.
166
+ Name the runnable and pass the app's generated `wmill.ts`, which the chat goes through:
167
+
168
+ ```tsx
169
+ import { useWindmillChat } from '@windmill-labs/chat-sdk/react'
170
+ import * as wmill from './wmill'
171
+
172
+ const chat = useWindmillChat({ wmill, runnable: 'chat' })
173
+ ```
174
+
175
+ `runnable` is checked against the runnables `wmill.ts` declares, and names the local
176
+ history, so two agent runnables in one app keep separate conversations. Going through
177
+ `wmill.ts` is what makes the same code work in a deployed app and under `wmill app dev`.
178
+
179
+ `createChat` and the assistant-ui runtime take the same options; the AI SDK transport
180
+ does not. The runnable takes `user_message` and `memory_id`, the conversation id, which
181
+ the agent's memory is keyed by, so it remembers earlier turns (called from `wmill.ts`
182
+ without `memory_id`, it is a single run with no conversation). The agent needs managed
183
+ memory: one whose memory is off takes `previous_messages` instead, the history you keep. A signed-in viewer's turns
184
+ are filed under them, so nobody else can continue their conversations; an anonymous
185
+ visitor's are kept apart per app. The chat makes no request of its own: history is
186
+ `'local'` (the app's own storage), `stop()` stops following without cancelling the turn,
187
+ a message sent while it still runs is refused with `TurnRunningError`, and attachments
188
+ are refused.
189
+
151
190
  ## Any framework
152
191
 
153
192
  `createChat` returns a store: `subscribe` calls the listener immediately and on every
@@ -178,7 +217,7 @@ await chat.sendMessage('Hello')
178
217
 
179
218
  | Option | |
180
219
  |---|---|
181
- | `flowPath` | Path of the deployed flow, e.g. `f/support/assistant`. Required. |
220
+ | `flowPath` | Path of the deployed flow, e.g. `f/support/assistant`. Required unless `follow` is set. |
182
221
  | `baseUrl` | The Windmill origin. Detected inside a raw app. |
183
222
  | `workspace` | Detected inside a raw app. |
184
223
  | `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. |
@@ -190,6 +229,8 @@ await chat.sendMessage('Hello')
190
229
  | `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
230
  | `onFinish`, `onError` | Called when a turn has its answer, or could not run at all. |
192
231
  | `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. |
232
+ | `follow` | With `run`: follows the job through your own client, calling `onStream` with each chunk of its result stream and resolving with its result. The chat then makes no request of its own; `runnable` sets both for a raw app's agent runnable, see [A saved agent](#a-saved-agent-through-an-agent-runnable). |
233
+ | `runnable`, `wmill` | In a raw app, chat through the agent runnable of that name, going through the app's generated module (`import * as wmill from './wmill'`). Replaces `flowPath`, `run` and `follow`. |
193
234
 
194
235
  ## State
195
236
 
@@ -294,7 +335,8 @@ That store is keyed by the **Windmill user**, so it fits a viewer session or a t
294
335
  issued per user. With one token shared by every visitor of a site, all visitors would
295
336
  see each other's conversations. For that setup use `history: 'local'` (the default
296
337
  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
338
+ `localStorage`, per Windmill instance, workspace and flow, and the chat starts with that
339
+ list in `state.conversations`, without a `loadConversations()` call. `'none'` keeps nothing
298
340
  beyond the page.
299
341
 
300
342
  When the default `'server'` mode turns out unreadable (a token or sandboxed app
@@ -333,7 +375,8 @@ custom integrations.
333
375
  When asked to add a chat over a Windmill flow: the flow must be deployed with chat mode
334
376
  on. Pick the entry point from the table at the top (`useChat` → `@windmill-labs/chat-sdk/ai-sdk`,
335
377
  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
378
+ Windmill raw app pass only `flowPath`; for a saved agent, use an `agent` runnable and pass
379
+ `{ wmill, runnable }`. Elsewhere pass `baseUrl`, `workspace` and a
337
380
  `token`; for a public page use a token scoped to `jobs:run:flows:<flowPath>` and leave
338
381
  `history` at its default. Render `role`, `content`, `pending`, `success` and
339
382
  `tool.status`; never build the SSE handling yourself.
@@ -1,21 +1,21 @@
1
1
  import { type AssistantRuntime, type ThreadMessageLike } from '@assistant-ui/react';
2
- import type { ChatMessage, ChatOptions } from './types';
2
+ import type { ChatMessage, RawAppWmill, TypedChatOptions } from './types';
3
3
  /** One assistant turn: the assistant and tool rows between two user messages. */
4
4
  export interface WindmillTurn {
5
5
  id: string;
6
6
  role: 'user' | 'assistant' | 'system';
7
7
  messages: ChatMessage[];
8
8
  }
9
- export interface WindmillRuntimeOptions extends ChatOptions {
9
+ export type WindmillRuntimeOptions<W extends RawAppWmill = RawAppWmill> = TypedChatOptions<W> & {
10
10
  /** Load the conversation list on mount so `ThreadListPrimitive` has something to show. Default true. */
11
11
  threadList?: boolean;
12
- }
12
+ };
13
13
  /**
14
14
  * An assistant-ui runtime over a chat-mode flow, for `AssistantRuntimeProvider`.
15
15
  * Conversations become threads, so the thread list primitives switch, create and
16
16
  * delete Windmill conversations.
17
17
  */
18
- export declare function useWindmillRuntime(options: WindmillRuntimeOptions): AssistantRuntime;
18
+ export declare function useWindmillRuntime<W extends RawAppWmill>(options: WindmillRuntimeOptions<W>): AssistantRuntime;
19
19
  /** Folds the role-per-row message list into turns: one entry per user message, one per answer. */
20
20
  export declare function groupTurns(messages: ChatMessage[]): WindmillTurn[];
21
21
  /** A turn as assistant-ui content parts; the status reflects streaming and a failed flow. */
@@ -1,4 +1,29 @@
1
1
  const require_follow = require("./follow-Dug-Afeu.cjs");
2
+ //#region src/runnable.ts
3
+ /**
4
+ * `run` and `follow` over a raw app's agent runnable (a path runnable of type `agent`, naming
5
+ * a saved AI agent), behind the `runnable` option. `memory_id` is the conversation id, the
6
+ * agent's memory key.
7
+ */
8
+ function rawAppRunnable(start, streamJob) {
9
+ return {
10
+ run: async (args, { conversationId }) => {
11
+ try {
12
+ return String(await start({
13
+ ...args,
14
+ memory_id: conversationId
15
+ }));
16
+ } catch (e) {
17
+ const text = e instanceof Error ? e.message : String(e);
18
+ throw require_follow.turnRunningError(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)) ?? e;
19
+ }
20
+ },
21
+ follow: (jobId, onStream) => streamJob(jobId, (update) => {
22
+ if (update.new_result_stream) onStream(update.new_result_stream);
23
+ })
24
+ };
25
+ }
26
+ //#endregion
2
27
  //#region src/config.ts
3
28
  /**
4
29
  * The Windmill a raw app bundle runs in. A sandboxed app gets `window.process.env`
@@ -22,19 +47,32 @@ function detectRawApp() {
22
47
  };
23
48
  }
24
49
  function resolveConfig(options) {
25
- if (!options.flowPath) throw new Error("windmill-chat: flowPath is required");
50
+ if (options.runnable !== void 0) {
51
+ if (!options.wmill) throw new Error("windmill-chat: `runnable` needs `wmill`, the app's generated module");
52
+ if (options.flowPath || options.run || options.follow) throw new Error("windmill-chat: `runnable` replaces `flowPath`, `run` and `follow`");
53
+ const app = globalThis.ctx?.path;
54
+ options = {
55
+ ...options,
56
+ ...rawAppRunnable(options.wmill.backendAsync[options.runnable], options.wmill.streamJob),
57
+ flowPath: `runnable:${typeof app === "string" ? `${app}:` : ""}${options.runnable}`
58
+ };
59
+ }
60
+ const follow = options.follow;
61
+ if (follow && !options.run) throw new Error("windmill-chat: `follow` needs `run`, which starts the job it follows");
62
+ if (follow && options.history === "server") throw new Error("windmill-chat: server history is read with the SDK's own requests, which `follow` replaces");
63
+ if (!options.flowPath && !follow) throw new Error("windmill-chat: flowPath is required");
26
64
  const explicitToken = options.token !== void 0;
27
65
  const detected = options.baseUrl && options.workspace && explicitToken ? void 0 : detectRawApp();
28
66
  const baseUrl = options.baseUrl ?? detected?.baseUrl;
29
67
  const workspace = options.workspace ?? detected?.workspace;
30
- if (!baseUrl || !workspace) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
68
+ if ((!baseUrl || !workspace) && !follow) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
31
69
  const token = options.token ?? (options.baseUrl === void 0 || options.baseUrl === detected?.baseUrl ? detected?.token : void 0);
32
70
  return {
33
- flowPath: options.flowPath,
34
- baseUrl,
35
- workspace,
71
+ flowPath: options.flowPath ?? "",
72
+ baseUrl: baseUrl ?? "",
73
+ workspace: workspace ?? "",
36
74
  token,
37
- history: options.history ?? (explicitToken ? "local" : "server"),
75
+ history: options.history ?? (explicitToken || follow ? "local" : "server"),
38
76
  historyExplicit: options.history !== void 0,
39
77
  inputs: options.inputs ?? {},
40
78
  fetch: options.fetch,
@@ -43,6 +81,7 @@ function resolveConfig(options) {
43
81
  pageSize: options.pageSize ?? 50,
44
82
  pollDelayMs: options.pollDelayMs,
45
83
  run: options.run,
84
+ follow,
46
85
  onFinish: options.onFinish,
47
86
  onError: options.onError
48
87
  };
@@ -188,7 +227,8 @@ async function uploadAttachments(api, attachments, turnId, signal) {
188
227
  const results = await Promise.allSettled(attachments.map(async (attachment, index) => {
189
228
  try {
190
229
  const blob = attachmentBlob(attachment);
191
- const filename = storedAttachmentName(attachment.name || `attachment-${index + 1}`, blob.type);
230
+ const name = attachment.name?.split(/[\\/]/).pop()?.replace(/^\.+$/, "");
231
+ const filename = storedAttachmentName(name || `attachment-${index + 1}`, blob.type);
192
232
  const { file_key } = await api.uploadFile(`${prefix}/${index}/${filename}`, blob, {
193
233
  contentType: blob.type,
194
234
  signal: batch.signal
@@ -267,6 +307,7 @@ var ChatImpl = class {
267
307
  const attachments = options.attachments ?? [];
268
308
  const attachmentsInput = options.attachmentsInput;
269
309
  if (attachments.length > 0 && !attachmentsInput) throw new Error("windmill-chat: attachments need `attachmentsInput`, the flow input that takes them");
310
+ if (attachments.length > 0 && require_follow._classPrivateFieldGet2(_config, this).follow) throw new Error("windmill-chat: attachments upload with the SDK's own requests, which `follow` replaces");
270
311
  if (attachmentsInput && !attachmentsInput.multiple && attachments.length > 1) throw new Error(`windmill-chat: \`${attachmentsInput.name}\` holds one file; got ${attachments.length}`);
271
312
  const isNew = require_follow._classPrivateFieldGet2(_state, this).conversationId === void 0;
272
313
  const conversationId = require_follow._classPrivateFieldGet2(_state, this).conversationId ?? require_follow.randomId();
@@ -429,7 +470,7 @@ var ChatImpl = class {
429
470
  });
430
471
  require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
431
472
  }
432
- if (turn.jobId) await require_follow._classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
473
+ if (turn.jobId && !require_follow._classPrivateFieldGet2(_config, this).follow) await require_follow._classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
433
474
  if (require_follow._classPrivateFieldGet2(_state, this).history !== "server") return;
434
475
  await require_follow.sleep(RECONCILE_DELAY_MS).catch(() => {});
435
476
  if (require_follow._classPrivateFieldGet2(_turn, this) || require_follow._classPrivateFieldGet2(_state, this).conversationId !== turn.conversationId) return;
@@ -560,7 +601,7 @@ var ChatImpl = class {
560
601
  messages: [],
561
602
  status: "idle",
562
603
  error: void 0,
563
- conversations: [],
604
+ conversations: require_follow._classPrivateFieldGet2(_config, this).history === "local" ? require_follow._classPrivateFieldGet2(_local, this).listConversations() : [],
564
605
  history: require_follow._classPrivateFieldGet2(_config, this).history,
565
606
  loadingMessages: false,
566
607
  hasMoreMessages: false
@@ -635,6 +676,26 @@ async function _select(conversationId) {
635
676
  }
636
677
  async function _follow(turn, onStreamStart) {
637
678
  let started = false;
679
+ const { follow } = require_follow._classPrivateFieldGet2(_config, this);
680
+ if (follow) {
681
+ const { signal } = turn.controller;
682
+ const parser = require_follow.createStreamEventParser();
683
+ const onStream = (chunk) => {
684
+ if (signal.aborted) return;
685
+ if (!started) {
686
+ started = true;
687
+ onStreamStart();
688
+ }
689
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, parser.push(chunk));
690
+ };
691
+ const aborted = new Promise((_, reject) => {
692
+ if (signal.aborted) reject(require_follow.abortError());
693
+ signal.addEventListener("abort", () => reject(require_follow.abortError()), { once: true });
694
+ });
695
+ const result = await Promise.race([follow(turn.jobId, onStream, { signal }), aborted]);
696
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, parser.flush());
697
+ return result;
698
+ }
638
699
  for await (const event of require_follow.followJob(require_follow._classPrivateFieldGet2(_api, this), turn.jobId, { signal: turn.controller.signal })) {
639
700
  if (event.type === "completed") {
640
701
  if (event.streamLost) require_follow._assertClassBrand(_ChatImpl_brand, this, _dropCutRound).call(this, turn);
@@ -772,7 +833,7 @@ async function _finishTurn(turn, result, isNew) {
772
833
  let messages = require_follow._classPrivateFieldGet2(_state, this).messages;
773
834
  let failed = false;
774
835
  if (require_follow.isErrorResult(result)) {
775
- failed = await require_follow._classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
836
+ failed = require_follow._classPrivateFieldGet2(_config, this).follow ? true : await require_follow._classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
776
837
  if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
777
838
  if (failed) messages = [...messages, assistantMessage(require_follow.errorResultMessage(result), false, turn.jobId)];
778
839
  }
@@ -1,4 +1,29 @@
1
- import { C as _assertClassBrand, S as _classPrivateFieldSet2, T as _classPrivateMethodInitSpec, _ as WindmillApiError, a as errorResultMessage, c as isErrorResult, d as randomId, f as sleep, g as TurnRunningError, i as conversationTitle, l as now, n as abortError, o as extractChatAnswer, p as truncateTitle, s as isAbortError, t as followJob, v as WindmillChatApi, w as _classPrivateFieldInitSpec, x as _classPrivateFieldGet2 } from "./follow-DXfmfPfr.js";
1
+ import { C as _assertClassBrand, S as _classPrivateFieldSet2, T as _classPrivateMethodInitSpec, _ as WindmillApiError, a as errorResultMessage, b as turnRunningError, c as isErrorResult, d as randomId, f as sleep, g as TurnRunningError, i as conversationTitle, l as now, m as createStreamEventParser, n as abortError, o as extractChatAnswer, p as truncateTitle, s as isAbortError, t as followJob, v as WindmillChatApi, w as _classPrivateFieldInitSpec, x as _classPrivateFieldGet2 } from "./follow-DXfmfPfr.js";
2
+ //#region src/runnable.ts
3
+ /**
4
+ * `run` and `follow` over a raw app's agent runnable (a path runnable of type `agent`, naming
5
+ * a saved AI agent), behind the `runnable` option. `memory_id` is the conversation id, the
6
+ * agent's memory key.
7
+ */
8
+ function rawAppRunnable(start, streamJob) {
9
+ return {
10
+ run: async (args, { conversationId }) => {
11
+ try {
12
+ return String(await start({
13
+ ...args,
14
+ memory_id: conversationId
15
+ }));
16
+ } catch (e) {
17
+ const text = e instanceof Error ? e.message : String(e);
18
+ throw turnRunningError(text.slice(text.indexOf("{"), text.lastIndexOf("}") + 1)) ?? e;
19
+ }
20
+ },
21
+ follow: (jobId, onStream) => streamJob(jobId, (update) => {
22
+ if (update.new_result_stream) onStream(update.new_result_stream);
23
+ })
24
+ };
25
+ }
26
+ //#endregion
2
27
  //#region src/config.ts
3
28
  /**
4
29
  * The Windmill a raw app bundle runs in. A sandboxed app gets `window.process.env`
@@ -22,19 +47,32 @@ function detectRawApp() {
22
47
  };
23
48
  }
24
49
  function resolveConfig(options) {
25
- if (!options.flowPath) throw new Error("windmill-chat: flowPath is required");
50
+ if (options.runnable !== void 0) {
51
+ if (!options.wmill) throw new Error("windmill-chat: `runnable` needs `wmill`, the app's generated module");
52
+ if (options.flowPath || options.run || options.follow) throw new Error("windmill-chat: `runnable` replaces `flowPath`, `run` and `follow`");
53
+ const app = globalThis.ctx?.path;
54
+ options = {
55
+ ...options,
56
+ ...rawAppRunnable(options.wmill.backendAsync[options.runnable], options.wmill.streamJob),
57
+ flowPath: `runnable:${typeof app === "string" ? `${app}:` : ""}${options.runnable}`
58
+ };
59
+ }
60
+ const follow = options.follow;
61
+ if (follow && !options.run) throw new Error("windmill-chat: `follow` needs `run`, which starts the job it follows");
62
+ if (follow && options.history === "server") throw new Error("windmill-chat: server history is read with the SDK's own requests, which `follow` replaces");
63
+ if (!options.flowPath && !follow) throw new Error("windmill-chat: flowPath is required");
26
64
  const explicitToken = options.token !== void 0;
27
65
  const detected = options.baseUrl && options.workspace && explicitToken ? void 0 : detectRawApp();
28
66
  const baseUrl = options.baseUrl ?? detected?.baseUrl;
29
67
  const workspace = options.workspace ?? detected?.workspace;
30
- if (!baseUrl || !workspace) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
68
+ if ((!baseUrl || !workspace) && !follow) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
31
69
  const token = options.token ?? (options.baseUrl === void 0 || options.baseUrl === detected?.baseUrl ? detected?.token : void 0);
32
70
  return {
33
- flowPath: options.flowPath,
34
- baseUrl,
35
- workspace,
71
+ flowPath: options.flowPath ?? "",
72
+ baseUrl: baseUrl ?? "",
73
+ workspace: workspace ?? "",
36
74
  token,
37
- history: options.history ?? (explicitToken ? "local" : "server"),
75
+ history: options.history ?? (explicitToken || follow ? "local" : "server"),
38
76
  historyExplicit: options.history !== void 0,
39
77
  inputs: options.inputs ?? {},
40
78
  fetch: options.fetch,
@@ -43,6 +81,7 @@ function resolveConfig(options) {
43
81
  pageSize: options.pageSize ?? 50,
44
82
  pollDelayMs: options.pollDelayMs,
45
83
  run: options.run,
84
+ follow,
46
85
  onFinish: options.onFinish,
47
86
  onError: options.onError
48
87
  };
@@ -188,7 +227,8 @@ async function uploadAttachments(api, attachments, turnId, signal) {
188
227
  const results = await Promise.allSettled(attachments.map(async (attachment, index) => {
189
228
  try {
190
229
  const blob = attachmentBlob(attachment);
191
- const filename = storedAttachmentName(attachment.name || `attachment-${index + 1}`, blob.type);
230
+ const name = attachment.name?.split(/[\\/]/).pop()?.replace(/^\.+$/, "");
231
+ const filename = storedAttachmentName(name || `attachment-${index + 1}`, blob.type);
192
232
  const { file_key } = await api.uploadFile(`${prefix}/${index}/${filename}`, blob, {
193
233
  contentType: blob.type,
194
234
  signal: batch.signal
@@ -267,6 +307,7 @@ var ChatImpl = class {
267
307
  const attachments = options.attachments ?? [];
268
308
  const attachmentsInput = options.attachmentsInput;
269
309
  if (attachments.length > 0 && !attachmentsInput) throw new Error("windmill-chat: attachments need `attachmentsInput`, the flow input that takes them");
310
+ if (attachments.length > 0 && _classPrivateFieldGet2(_config, this).follow) throw new Error("windmill-chat: attachments upload with the SDK's own requests, which `follow` replaces");
270
311
  if (attachmentsInput && !attachmentsInput.multiple && attachments.length > 1) throw new Error(`windmill-chat: \`${attachmentsInput.name}\` holds one file; got ${attachments.length}`);
271
312
  const isNew = _classPrivateFieldGet2(_state, this).conversationId === void 0;
272
313
  const conversationId = _classPrivateFieldGet2(_state, this).conversationId ?? randomId();
@@ -429,7 +470,7 @@ var ChatImpl = class {
429
470
  });
430
471
  _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
431
472
  }
432
- if (turn.jobId) await _classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
473
+ if (turn.jobId && !_classPrivateFieldGet2(_config, this).follow) await _classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
433
474
  if (_classPrivateFieldGet2(_state, this).history !== "server") return;
434
475
  await sleep(RECONCILE_DELAY_MS).catch(() => {});
435
476
  if (_classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).conversationId !== turn.conversationId) return;
@@ -560,7 +601,7 @@ var ChatImpl = class {
560
601
  messages: [],
561
602
  status: "idle",
562
603
  error: void 0,
563
- conversations: [],
604
+ conversations: _classPrivateFieldGet2(_config, this).history === "local" ? _classPrivateFieldGet2(_local, this).listConversations() : [],
564
605
  history: _classPrivateFieldGet2(_config, this).history,
565
606
  loadingMessages: false,
566
607
  hasMoreMessages: false
@@ -635,6 +676,26 @@ async function _select(conversationId) {
635
676
  }
636
677
  async function _follow(turn, onStreamStart) {
637
678
  let started = false;
679
+ const { follow } = _classPrivateFieldGet2(_config, this);
680
+ if (follow) {
681
+ const { signal } = turn.controller;
682
+ const parser = createStreamEventParser();
683
+ const onStream = (chunk) => {
684
+ if (signal.aborted) return;
685
+ if (!started) {
686
+ started = true;
687
+ onStreamStart();
688
+ }
689
+ _assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, parser.push(chunk));
690
+ };
691
+ const aborted = new Promise((_, reject) => {
692
+ if (signal.aborted) reject(abortError());
693
+ signal.addEventListener("abort", () => reject(abortError()), { once: true });
694
+ });
695
+ const result = await Promise.race([follow(turn.jobId, onStream, { signal }), aborted]);
696
+ _assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, parser.flush());
697
+ return result;
698
+ }
638
699
  for await (const event of followJob(_classPrivateFieldGet2(_api, this), turn.jobId, { signal: turn.controller.signal })) {
639
700
  if (event.type === "completed") {
640
701
  if (event.streamLost) _assertClassBrand(_ChatImpl_brand, this, _dropCutRound).call(this, turn);
@@ -772,7 +833,7 @@ async function _finishTurn(turn, result, isNew) {
772
833
  let messages = _classPrivateFieldGet2(_state, this).messages;
773
834
  let failed = false;
774
835
  if (isErrorResult(result)) {
775
- failed = await _classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
836
+ failed = _classPrivateFieldGet2(_config, this).follow ? true : await _classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
776
837
  if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
777
838
  if (failed) messages = [...messages, assistantMessage(errorResultMessage(result), false, turn.jobId)];
778
839
  }
package/dist/chat.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import type { Chat, ChatOptions } from './types';
2
- export declare function createChat(options: ChatOptions): Chat;
1
+ import type { Chat, RawAppWmill, TypedChatOptions } from './types';
2
+ export declare function createChat<W extends RawAppWmill>(options: TypedChatOptions<W>): Chat;
package/dist/config.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface ResolvedConfig {
14
14
  pageSize: number;
15
15
  pollDelayMs: number | undefined;
16
16
  run: ChatOptions['run'];
17
+ follow: ChatOptions['follow'];
17
18
  onFinish: ChatOptions['onFinish'];
18
19
  onError: ChatOptions['onError'];
19
20
  }
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_follow = require("./follow-Dug-Afeu.cjs");
3
- const require_chat = require("./chat-u1tspn7Q.cjs");
3
+ const require_chat = require("./chat-C0iOSX6H.cjs");
4
4
  exports.CHAT_UPLOADS_PREFIX = require_chat.CHAT_UPLOADS_PREFIX;
5
5
  exports.TurnRunningError = require_follow.TurnRunningError;
6
6
  exports.WindmillApiError = require_follow.WindmillApiError;
package/dist/index.d.ts CHANGED
@@ -5,4 +5,4 @@ export { parseStreamEvents, createStreamEventParser, type AgentStreamEvent } fro
5
5
  export { followJob, type FollowEvent } from './follow';
6
6
  export { extractChatAnswer, conversationIdFor } from './utils';
7
7
  export { storedAttachmentName, uploadAttachments, CHAT_UPLOADS_PREFIX, type UploadedAttachment } from './attachments';
8
- export type { AttachmentsInput, AttachmentUpload, Chat, ChatAttachment, ChatMessage, ChatOptions, ChatRole, ChatState, ChatStatus, Conversation, FetchLike, HistoryMode, SendMessageOptions, StorageLike, TokenSource, ToolInvocation } from './types';
8
+ export type { AttachmentsInput, AttachmentUpload, Chat, ChatAttachment, ChatMessage, ChatOptions, ChatRole, ChatState, ChatStatus, Conversation, FetchLike, HistoryMode, RawAppWmill, TypedChatOptions, SendMessageOptions, StorageLike, TokenSource, ToolInvocation } from './types';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { _ as WindmillApiError, b as turnRunningError, g as TurnRunningError, h as parseStreamEvents, m as createStreamEventParser, o as extractChatAnswer, r as conversationIdFor, t as followJob, v as WindmillChatApi, y as readServerSentEvents } from "./follow-DXfmfPfr.js";
2
- import { a as detectRawApp, i as uploadAttachments, n as CHAT_UPLOADS_PREFIX, r as storedAttachmentName, t as createChat } from "./chat-DgINfcUU.js";
2
+ import { a as detectRawApp, i as uploadAttachments, n as CHAT_UPLOADS_PREFIX, r as storedAttachmentName, t as createChat } from "./chat-DjonfE4j.js";
3
3
  export { CHAT_UPLOADS_PREFIX, TurnRunningError, WindmillApiError, WindmillChatApi, conversationIdFor, createChat, createStreamEventParser, detectRawApp, extractChatAnswer, followJob, parseStreamEvents, readServerSentEvents, storedAttachmentName, turnRunningError, uploadAttachments };
package/dist/react.cjs CHANGED
@@ -1,16 +1,16 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_chat = require("./chat-u1tspn7Q.cjs");
2
+ const require_chat = require("./chat-C0iOSX6H.cjs");
3
3
  let react = require("react");
4
4
  //#region src/react.ts
5
5
  /**
6
6
  * A chat on a chat-mode flow. The chat is created once per `flowPath`, `baseUrl`,
7
- * `workspace`, `history`, `storageKey`, credential and presence of `run`, and
7
+ * `workspace`, `history`, `storageKey`, credential and presence of `run` and `follow`, and
8
8
  * destroyed on unmount. A credential change is a new user, whose chat must not
9
9
  * carry the previous one's state: a different token string, or a switch between no
10
10
  * token, a token string and a token function, all recreate it. A token function is
11
11
  * read through a ref on every call, so a new closure per render changes what the
12
12
  * next call runs and nothing else; pass a `storageKey` per user when local history
13
- * must not be shared. `run`, the callbacks and `inputs` are read the same way: the
13
+ * must not be shared. `run`, `follow`, the callbacks and `inputs` are read the same way: the
14
14
  * latest render's values go with the next message.
15
15
  */
16
16
  function useWindmillChat(options) {
@@ -18,6 +18,7 @@ function useWindmillChat(options) {
18
18
  latest.current = options;
19
19
  const credential = typeof options.token === "function" ? "fn" : typeof options.token === "string" ? `str:${options.token}` : "none";
20
20
  const customRun = options.run !== void 0;
21
+ const customFollow = options.follow !== void 0;
21
22
  const chat = (0, react.useMemo)(() => require_chat.createChat({
22
23
  ...options,
23
24
  inputs: void 0,
@@ -26,16 +27,20 @@ function useWindmillChat(options) {
26
27
  return typeof token === "function" ? token() : token ?? "";
27
28
  } : options.token,
28
29
  run: customRun ? (args, turn) => (latest.current.run ?? options.run)(args, turn) : void 0,
30
+ follow: customFollow ? (jobId, onStream, turn) => (latest.current.follow ?? options.follow)(jobId, onStream, turn) : void 0,
29
31
  onFinish: (turn) => latest.current.onFinish?.(turn),
30
32
  onError: (error, turn) => latest.current.onError?.(error, turn)
31
33
  }), [
32
34
  options.flowPath,
35
+ options.runnable,
36
+ options.wmill,
33
37
  options.baseUrl,
34
38
  options.workspace,
35
39
  options.history,
36
40
  options.storageKey,
37
41
  credential,
38
- customRun
42
+ customRun,
43
+ customFollow
39
44
  ]);
40
45
  (0, react.useEffect)(() => () => chat.destroy(), [chat]);
41
46
  const state = (0, react.useSyncExternalStore)(chat.subscribe, chat.getState, chat.getState);
package/dist/react.d.ts CHANGED
@@ -1,16 +1,16 @@
1
- import type { Chat, ChatOptions, ChatState } from './types';
1
+ import type { Chat, ChatState, RawAppWmill, TypedChatOptions } from './types';
2
2
  export type UseWindmillChat = ChatState & Pick<Chat, 'sendMessage' | 'resumeTurn' | 'stop' | 'newConversation' | 'selectConversation' | 'loadConversations' | 'deleteConversation' | 'renameConversation' | 'loadOlderMessages' | 'refreshMessages'> & {
3
3
  chat: Chat;
4
4
  };
5
5
  /**
6
6
  * A chat on a chat-mode flow. The chat is created once per `flowPath`, `baseUrl`,
7
- * `workspace`, `history`, `storageKey`, credential and presence of `run`, and
7
+ * `workspace`, `history`, `storageKey`, credential and presence of `run` and `follow`, and
8
8
  * destroyed on unmount. A credential change is a new user, whose chat must not
9
9
  * carry the previous one's state: a different token string, or a switch between no
10
10
  * token, a token string and a token function, all recreate it. A token function is
11
11
  * read through a ref on every call, so a new closure per render changes what the
12
12
  * next call runs and nothing else; pass a `storageKey` per user when local history
13
- * must not be shared. `run`, the callbacks and `inputs` are read the same way: the
13
+ * must not be shared. `run`, `follow`, the callbacks and `inputs` are read the same way: the
14
14
  * latest render's values go with the next message.
15
15
  */
16
- export declare function useWindmillChat(options: ChatOptions): UseWindmillChat;
16
+ export declare function useWindmillChat<W extends RawAppWmill>(options: TypedChatOptions<W>): UseWindmillChat;
package/dist/react.js CHANGED
@@ -1,15 +1,15 @@
1
- import { t as createChat } from "./chat-DgINfcUU.js";
1
+ import { t as createChat } from "./chat-DjonfE4j.js";
2
2
  import { useEffect, useMemo, useRef, useSyncExternalStore } from "react";
3
3
  //#region src/react.ts
4
4
  /**
5
5
  * A chat on a chat-mode flow. The chat is created once per `flowPath`, `baseUrl`,
6
- * `workspace`, `history`, `storageKey`, credential and presence of `run`, and
6
+ * `workspace`, `history`, `storageKey`, credential and presence of `run` and `follow`, and
7
7
  * destroyed on unmount. A credential change is a new user, whose chat must not
8
8
  * carry the previous one's state: a different token string, or a switch between no
9
9
  * token, a token string and a token function, all recreate it. A token function is
10
10
  * read through a ref on every call, so a new closure per render changes what the
11
11
  * next call runs and nothing else; pass a `storageKey` per user when local history
12
- * must not be shared. `run`, the callbacks and `inputs` are read the same way: the
12
+ * must not be shared. `run`, `follow`, the callbacks and `inputs` are read the same way: the
13
13
  * latest render's values go with the next message.
14
14
  */
15
15
  function useWindmillChat(options) {
@@ -17,6 +17,7 @@ function useWindmillChat(options) {
17
17
  latest.current = options;
18
18
  const credential = typeof options.token === "function" ? "fn" : typeof options.token === "string" ? `str:${options.token}` : "none";
19
19
  const customRun = options.run !== void 0;
20
+ const customFollow = options.follow !== void 0;
20
21
  const chat = useMemo(() => createChat({
21
22
  ...options,
22
23
  inputs: void 0,
@@ -25,16 +26,20 @@ function useWindmillChat(options) {
25
26
  return typeof token === "function" ? token() : token ?? "";
26
27
  } : options.token,
27
28
  run: customRun ? (args, turn) => (latest.current.run ?? options.run)(args, turn) : void 0,
29
+ follow: customFollow ? (jobId, onStream, turn) => (latest.current.follow ?? options.follow)(jobId, onStream, turn) : void 0,
28
30
  onFinish: (turn) => latest.current.onFinish?.(turn),
29
31
  onError: (error, turn) => latest.current.onError?.(error, turn)
30
32
  }), [
31
33
  options.flowPath,
34
+ options.runnable,
35
+ options.wmill,
32
36
  options.baseUrl,
33
37
  options.workspace,
34
38
  options.history,
35
39
  options.storageKey,
36
40
  credential,
37
- customRun
41
+ customRun,
42
+ customFollow
38
43
  ]);
39
44
  useEffect(() => () => chat.destroy(), [chat]);
40
45
  const state = useSyncExternalStore(chat.subscribe, chat.getState, chat.getState);
@@ -0,0 +1,9 @@
1
+ import type { ChatOptions } from './types';
2
+ /**
3
+ * `run` and `follow` over a raw app's agent runnable (a path runnable of type `agent`, naming
4
+ * a saved AI agent), behind the `runnable` option. `memory_id` is the conversation id, the
5
+ * agent's memory key.
6
+ */
7
+ export declare function rawAppRunnable(start: (args: any) => Promise<string>, streamJob: (jobId: string, onUpdate?: (update: {
8
+ new_result_stream?: string;
9
+ }) => void) => Promise<unknown>): Required<Pick<ChatOptions, 'run' | 'follow'>>;
package/dist/types.d.ts CHANGED
@@ -81,9 +81,33 @@ export interface ChatState {
81
81
  export type TokenSource = string | (() => string | Promise<string>);
82
82
  export type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
83
83
  export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
84
+ /** A raw app's generated `wmill.ts`, as far as a chat uses it. */
85
+ export interface RawAppWmill {
86
+ backendAsync: Record<string, (args: any) => Promise<string>>;
87
+ streamJob: (jobId: string, onUpdate?: (update: {
88
+ new_result_stream?: string;
89
+ }) => void) => Promise<unknown>;
90
+ }
91
+ /** `ChatOptions` whose `runnable` must name one of `wmill`'s runnables. */
92
+ export type TypedChatOptions<W extends RawAppWmill> = ChatOptions & {
93
+ wmill?: W;
94
+ runnable?: keyof W['backendAsync'] & string;
95
+ };
84
96
  export interface ChatOptions {
85
- /** Path of a deployed flow with chat mode enabled, e.g. `f/support/assistant`. */
86
- flowPath: string;
97
+ /**
98
+ * Path of a deployed flow with chat mode enabled, e.g. `f/support/assistant`. Required
99
+ * unless `runnable` or `follow` is set, where it only names the local history.
100
+ */
101
+ flowPath?: string;
102
+ /**
103
+ * In a raw app, chat through its agent runnable of this name instead of a flow:
104
+ * `{ wmill, runnable: 'chat' }`. Each turn runs as the app, so a sandboxed app needs no
105
+ * frontend SDK scope and its viewers no access to the agent; the chat follows it as
106
+ * with `follow`. It also names the local history, so each runnable keeps its own.
107
+ */
108
+ runnable?: string;
109
+ /** The app's generated module, `import * as wmill from './wmill'`, which `runnable` goes through. */
110
+ wmill?: RawAppWmill;
87
111
  /** Windmill origin, e.g. `https://app.windmill.dev`. Detected inside a raw app. */
88
112
  baseUrl?: string;
89
113
  /** Detected inside a raw app. */
@@ -124,6 +148,17 @@ export interface ChatOptions {
124
148
  conversationId: string;
125
149
  signal: AbortSignal;
126
150
  }) => Promise<string>;
151
+ /**
152
+ * Follows the job `run` returned through your own client instead of the SDK's requests:
153
+ * call `onStream` with each chunk of the job's result stream and resolve with its result.
154
+ * With it the chat makes no request of its own, so it needs no credential, `baseUrl` or
155
+ * `workspace`, and in exchange history is `local` (or `none`), `stop()` stops following
156
+ * without cancelling the job, and attachments are refused. `runnable` sets both for a raw
157
+ * app's agent runnable.
158
+ */
159
+ follow?: (jobId: string, onStream: (chunk: string) => void, turn: {
160
+ signal: AbortSignal;
161
+ }) => Promise<unknown>;
127
162
  /** Called once a turn has its answer (a failed flow included: its error is the answer). */
128
163
  onFinish?: (turn: {
129
164
  conversationId: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@windmill-labs/chat-sdk",
3
3
  "description": "Build chat interfaces on Windmill flows deployed in chat mode, from any frontend or raw app",
4
- "version": "1.846.1",
4
+ "version": "1.848.0",
5
5
  "author": "Ruben Fiszel",
6
6
  "license": "Apache-2.0",
7
7
  "homepage": "https://github.com/windmill-labs/windmill/tree/main/chat-sdk#readme",