@assistant-ui/react-langchain 0.0.6 → 0.0.8

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.
@@ -5,6 +5,7 @@ import type {
5
5
  AppendMessage,
6
6
  AttachmentAdapter,
7
7
  FeedbackAdapter,
8
+ RemoteThreadListAdapter,
8
9
  SpeechSynthesisAdapter,
9
10
  } from "@assistant-ui/core";
10
11
  import {
@@ -18,8 +19,8 @@ import {
18
19
  import { useAui, useAuiState } from "@assistant-ui/store";
19
20
  import type { AssistantCloud } from "assistant-cloud";
20
21
  import { useStream, type UseStreamOptions } from "@langchain/react";
21
- import type { LangChainBaseMessage } from "./types";
22
- import { convertLangChainBaseMessage } from "./convertMessages";
22
+ import type { LangChainBaseMessage, LangChainToolCall } from "./types";
23
+ import { convertLangChainBaseMessage, getMessageType } from "./convertMessages";
23
24
 
24
25
  const symbolLangChainRuntimeExtras = Symbol("langchain-runtime-extras");
25
26
 
@@ -32,6 +33,7 @@ type LangChainRuntimeExtras = {
32
33
  options?: Record<string, unknown>,
33
34
  ) => Promise<void>;
34
35
  values: Record<string, unknown>;
36
+ messagesKey: string;
35
37
  };
36
38
 
37
39
  const asLangChainRuntimeExtras = (extras: unknown): LangChainRuntimeExtras => {
@@ -46,7 +48,7 @@ const asLangChainRuntimeExtras = (extras: unknown): LangChainRuntimeExtras => {
46
48
  return extras as LangChainRuntimeExtras;
47
49
  };
48
50
 
49
- export type UseStreamRuntimeOptions = UseStreamOptions & {
51
+ type LangChainRuntimeExtraOptions = {
50
52
  cloud?: AssistantCloud | undefined;
51
53
  adapters?:
52
54
  | {
@@ -55,10 +57,54 @@ export type UseStreamRuntimeOptions = UseStreamOptions & {
55
57
  feedback?: FeedbackAdapter | undefined;
56
58
  }
57
59
  | undefined;
58
- /** The key in the LangGraph state that contains messages. Defaults to "messages". */
59
- messagesKey?: string | undefined;
60
+ /**
61
+ * When the user sends a new message while previous tool calls are
62
+ * still pending, automatically submit `tool` messages that cancel
63
+ * them so the agent's tool-call accounting stays consistent.
64
+ * Defaults to `true`.
65
+ */
66
+ autoCancelPendingToolCalls?: boolean | undefined;
67
+ /**
68
+ * Routes the Cancel button's click to `useStream().stop()`. On by
69
+ * default. Pass `false` to disable the Cancel button.
70
+ */
71
+ unstable_allowCancellation?: boolean | undefined;
72
+ /**
73
+ * Custom `RemoteThreadListAdapter`. When provided, replaces the
74
+ * cloud-backed thread list adapter.
75
+ */
76
+ unstable_threadListAdapter?: RemoteThreadListAdapter | undefined;
77
+ /** Custom thread-creation hook, forwarded to the cloud adapter. */
78
+ create?: (() => Promise<{ externalId: string | undefined }>) | undefined;
79
+ /** Custom thread-deletion hook, forwarded to the cloud adapter. */
80
+ delete?: ((threadId: string) => Promise<void>) | undefined;
60
81
  };
61
82
 
83
+ const getPendingToolCalls = (
84
+ messages: readonly LangChainBaseMessage[],
85
+ ): LangChainToolCall[] => {
86
+ const pending = new Map<string, LangChainToolCall>();
87
+ for (const m of messages) {
88
+ const type = getMessageType(m);
89
+ if (type === "ai") {
90
+ for (const tc of m.tool_calls ?? []) pending.set(tc.id, tc);
91
+ } else if (type === "tool" && m.tool_call_id) {
92
+ pending.delete(m.tool_call_id);
93
+ }
94
+ }
95
+ return [...pending.values()];
96
+ };
97
+
98
+ // Distribute the intersection through the union arms of `UseStreamOptions`
99
+ // (`AgentServerOptions | CustomAdapterOptions`). Writing `UseStreamOptions & X`
100
+ // directly collapses arm tracking, so `Omit<…, "cloud">` and the like would
101
+ // produce a flattened structural type that no longer matches either arm.
102
+ export type UseStreamRuntimeOptions = UseStreamOptions extends infer O
103
+ ? O extends UseStreamOptions
104
+ ? O & LangChainRuntimeExtraOptions
105
+ : never
106
+ : never;
107
+
62
108
  const getMessageContent = (msg: AppendMessage) => {
63
109
  const allContent = [
64
110
  ...msg.content,
@@ -91,8 +137,12 @@ const getMessageContent = (msg: AppendMessage) => {
91
137
  case "tool-call":
92
138
  throw new Error("Tool call appends are not supported.");
93
139
  default: {
94
- const _exhaustiveCheck: "reasoning" | "source" | "audio" | "data" =
95
- type;
140
+ const _exhaustiveCheck:
141
+ | "reasoning"
142
+ | "source"
143
+ | "audio"
144
+ | "data"
145
+ | "generative-ui" = type;
96
146
  throw new Error(
97
147
  `Unsupported append message part type: ${_exhaustiveCheck}`,
98
148
  );
@@ -106,21 +156,31 @@ const getMessageContent = (msg: AppendMessage) => {
106
156
  return content;
107
157
  };
108
158
 
109
- const useStreamThreadRuntime = ({
110
- adapters,
111
- messagesKey = "messages",
112
- ...streamOptions
113
- }: Omit<UseStreamRuntimeOptions, "cloud">) => {
159
+ type DistributiveOmit<T, K extends keyof any> = T extends unknown
160
+ ? Omit<T, K>
161
+ : never;
162
+
163
+ const useStreamThreadRuntime = (
164
+ options: DistributiveOmit<
165
+ UseStreamRuntimeOptions,
166
+ "cloud" | "unstable_threadListAdapter" | "create" | "delete"
167
+ >,
168
+ ) => {
169
+ const { adapters, autoCancelPendingToolCalls, unstable_allowCancellation } =
170
+ options;
171
+ const messagesKey = options.messagesKey ?? "messages";
172
+
114
173
  // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
115
174
  const externalId = useAuiState((s) => s.threadListItem.externalId) as
116
175
  | string
117
176
  | null;
177
+ // Mutate in place rather than `{ ...options, threadId }`: spreading
178
+ // `UseStreamOptions` (a discriminated union on `transport`) into an object
179
+ // literal merges both arms' transport types, breaking arm assignment.
180
+ options.threadId = externalId;
118
181
 
119
182
  // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
120
- const stream = useStream({
121
- ...streamOptions,
122
- threadId: externalId,
123
- });
183
+ const stream = useStream(options);
124
184
 
125
185
  // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
126
186
  const [toolStatuses, setToolStatuses] = useState<
@@ -185,8 +245,15 @@ const useStreamThreadRuntime = ({
185
245
  interrupts: stream.interrupts,
186
246
  submit: stream.submit,
187
247
  values: stream.values,
248
+ messagesKey,
188
249
  }),
189
- [stream.interrupt, stream.interrupts, stream.submit, stream.values],
250
+ [
251
+ stream.interrupt,
252
+ stream.interrupts,
253
+ stream.submit,
254
+ stream.values,
255
+ messagesKey,
256
+ ],
190
257
  );
191
258
 
192
259
  // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
@@ -198,8 +265,20 @@ const useStreamThreadRuntime = ({
198
265
  onNew: async (msg) => {
199
266
  await toolInvocations.abort();
200
267
  const content = getMessageContent(msg);
268
+ const cancellations =
269
+ autoCancelPendingToolCalls !== false
270
+ ? getPendingToolCalls(
271
+ streamRef.current.messages as readonly LangChainBaseMessage[],
272
+ ).map((t) => ({
273
+ type: "tool" as const,
274
+ name: t.name,
275
+ tool_call_id: t.id,
276
+ content: JSON.stringify({ cancelled: true }),
277
+ status: "error" as const,
278
+ }))
279
+ : [];
201
280
  await stream.submit({
202
- [messagesKey]: [{ type: "human", content }],
281
+ [messagesKey]: [...cancellations, { type: "human", content }],
203
282
  });
204
283
  },
205
284
  onAddToolResult: async ({
@@ -222,10 +301,13 @@ const useStreamThreadRuntime = ({
222
301
  ],
223
302
  });
224
303
  },
225
- onCancel: async () => {
226
- await stream.stop();
227
- await toolInvocations.abort();
228
- },
304
+ onCancel:
305
+ unstable_allowCancellation !== false
306
+ ? async () => {
307
+ await stream.stop();
308
+ await toolInvocations.abort();
309
+ }
310
+ : undefined,
229
311
  });
230
312
 
231
313
  return runtime;
@@ -255,20 +337,31 @@ const useStreamThreadRuntime = ({
255
337
  * }
256
338
  * ```
257
339
  */
258
- export const useStreamRuntime = ({
259
- cloud,
260
- ...options
261
- }: UseStreamRuntimeOptions) => {
340
+ export const useStreamRuntime = (rawOptions: UseStreamRuntimeOptions) => {
341
+ const {
342
+ cloud,
343
+ unstable_threadListAdapter,
344
+ create,
345
+ delete: deleteFn,
346
+ ...options
347
+ } = rawOptions;
348
+
262
349
  const optionsRef = useRef(options);
263
350
  optionsRef.current = options;
264
351
 
265
- const cloudAdapter = useCloudThreadListAdapter({ cloud });
352
+ const cloudAdapter = useCloudThreadListAdapter({
353
+ cloud,
354
+ create,
355
+ delete: deleteFn,
356
+ });
357
+ const adapter = unstable_threadListAdapter ?? cloudAdapter;
358
+
266
359
  return useRemoteThreadListRuntime({
267
360
  runtimeHook: function RuntimeHook() {
268
361
  // biome-ignore lint/correctness/useHookAtTopLevel: intentional conditional/nested hook usage
269
362
  return useStreamThreadRuntime(optionsRef.current);
270
363
  },
271
- adapter: cloudAdapter,
364
+ adapter,
272
365
  allowNesting: true,
273
366
  });
274
367
  };
@@ -301,6 +394,36 @@ export const useLangChainSubmit = () => {
301
394
  };
302
395
  };
303
396
 
397
+ /**
398
+ * Submit a list of LangChain-shaped messages on the current thread.
399
+ * Parity helper for migrating from `useLangGraphSend`. Routes to
400
+ * `useStream().submit({ [messagesKey]: messages }, options)`.
401
+ */
402
+ export const useLangChainSend = () => {
403
+ const aui = useAui();
404
+ return (
405
+ messages: readonly LangChainBaseMessage[],
406
+ options?: Record<string, unknown>,
407
+ ) => {
408
+ const { submit, messagesKey } = asLangChainRuntimeExtras(
409
+ aui.thread().getState().extras,
410
+ );
411
+ return submit({ [messagesKey]: messages }, options);
412
+ };
413
+ };
414
+
415
+ /**
416
+ * Submit a `useStream` command (e.g. interrupt resume). Parity helper
417
+ * for migrating from `useLangGraphSendCommand`. Note that v1's command
418
+ * shape (`{ resume?, goto?, update? }`) differs from the legacy
419
+ * `{ resume: string }` form — to carry a payload, use the input or
420
+ * `stream.respond` instead.
421
+ */
422
+ export const useLangChainSendCommand = () => {
423
+ const submit = useLangChainSubmit();
424
+ return (command: Record<string, unknown>) => submit(null, { command });
425
+ };
426
+
304
427
  /**
305
428
  * Read a custom LangGraph state key from the current thread. Mirrors
306
429
  * `useStream().values[key]` from `@langchain/react` and updates when the
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,0BAA0B,EAC1B,iBAAiB,EACjB,kBAAkB,GACnB,8BAA2B;AAC5B,YAAY,EAAE,uBAAuB,EAAE,8BAA2B;AAElE,OAAO,EAAE,2BAA2B,EAAE,6BAA0B;AAEhE,YAAY,EACV,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,GAClB,mBAAgB"}
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,0BAA0B,EAC1B,iBAAiB,EACjB,kBAAkB,GACnB,8BAA2B;AAG5B,OAAO,EAAE,2BAA2B,EAAE,6BAA0B"}
package/dist/types.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}