@assistant-ui/react-langchain 0.0.32 → 0.0.34

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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/attachSubagentTranscripts.d.ts +13 -14
  3. package/dist/attachSubagentTranscripts.d.ts.map +1 -1
  4. package/dist/convertMessages.d.ts +6 -9
  5. package/dist/convertMessages.d.ts.map +1 -1
  6. package/dist/convertMessages.js +57 -15
  7. package/dist/convertMessages.js.map +1 -1
  8. package/dist/converter.d.ts +128 -128
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/converter.js +2 -1
  11. package/dist/converter.js.map +1 -1
  12. package/dist/findForkCheckpointInHistory.d.ts +15 -16
  13. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  14. package/dist/hooks.d.ts +5 -7
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/index.d.ts +7 -8
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/resolveForkCheckpoint.d.ts +4 -5
  19. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  20. package/dist/runtimeExtras.d.ts +1 -3
  21. package/dist/runtimeExtras.d.ts.map +1 -1
  22. package/dist/sdkIdentity.d.ts +1 -3
  23. package/dist/sdkIdentity.d.ts.map +1 -1
  24. package/dist/sdkIdentity.js +1 -1
  25. package/dist/streamingTiming.d.ts +2 -4
  26. package/dist/streamingTiming.d.ts.map +1 -1
  27. package/dist/subagentMessagesProjection.d.ts +17 -0
  28. package/dist/subagentMessagesProjection.d.ts.map +1 -0
  29. package/dist/subagentMessagesProjection.js +40 -0
  30. package/dist/subagentMessagesProjection.js.map +1 -0
  31. package/dist/types.d.ts +100 -103
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/uiMessages.d.ts +18 -6
  34. package/dist/uiMessages.d.ts.map +1 -1
  35. package/dist/uiMessages.js +33 -1
  36. package/dist/uiMessages.js.map +1 -1
  37. package/dist/useStreamRuntime.d.ts +7 -10
  38. package/dist/useStreamRuntime.d.ts.map +1 -1
  39. package/dist/useStreamRuntime.js +100 -26
  40. package/dist/useStreamRuntime.js.map +1 -1
  41. package/dist/useSubagentTranscripts.d.ts +3 -5
  42. package/dist/useSubagentTranscripts.d.ts.map +1 -1
  43. package/dist/useSubagentTranscripts.js +5 -3
  44. package/dist/useSubagentTranscripts.js.map +1 -1
  45. package/package.json +16 -9
  46. package/src/convertMessages.test.ts +153 -0
  47. package/src/convertMessages.ts +54 -8
  48. package/src/converter.ts +6 -0
  49. package/src/subagentMessagesProjection.test.ts +175 -0
  50. package/src/subagentMessagesProjection.ts +50 -0
  51. package/src/tests/settleOutsideAct.ts +15 -0
  52. package/src/uiMessages.test.ts +106 -0
  53. package/src/uiMessages.ts +43 -0
  54. package/src/useStreamRuntime.test.tsx +147 -7
  55. package/src/useStreamRuntime.ts +197 -45
  56. package/src/useStreamRuntime.voice.test.tsx +713 -0
  57. package/src/useSubagentTranscripts.fast-refresh.test.tsx +152 -0
  58. package/src/useSubagentTranscripts.test.tsx +2 -0
  59. package/src/useSubagentTranscripts.ts +5 -7
@@ -1 +1 @@
1
- {"version":3,"file":"useSubagentTranscripts.js","names":[],"sources":["../src/useSubagentTranscripts.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useSyncExternalStore } from \"react\";\nimport {\n stepStreamingTiming,\n type MessageTiming,\n type StreamingTimingState,\n type ThreadMessage,\n type ToolCallTiming,\n} from \"@assistant-ui/core\";\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n type ExternalMessageConversionCache,\n type useExternalMessageConverter,\n} from \"@assistant-ui/core/react\";\nimport { STREAM_CONTROLLER, type AnyStream } from \"@langchain/react\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport {\n channelProjection,\n messagesProjection,\n type Event,\n} from \"@langchain/langgraph-sdk/stream\";\nimport type { SubagentDiscoverySnapshot } from \"@langchain/react\";\nimport {\n attachSubagentTranscripts,\n type AttachMemo,\n createAttachMemo,\n type SubagentTranscript,\n} from \"./attachSubagentTranscripts\";\nimport { convertLangChainBaseMessage } from \"./convertMessages\";\nimport { groupUIMessagesByParent } from \"./converter\";\nimport { langChainStreamingTimingAccessors } from \"./streamingTiming\";\nimport type { LangChainBaseMessage, UIMessage } from \"./types\";\nimport {\n createUIFoldMemo,\n foldUIUpdates,\n mergeUIMessages,\n UI_CUSTOM_CHANNELS,\n type UIFoldMemo,\n} from \"./uiMessages\";\n\nexport const MAX_SUBAGENT_DEPTH = 16;\n\nconst ROOT_UI_CHANNEL_DEPTH = 1;\n\nconst TRANSCRIPT_METADATA = {};\n\nconst NO_UI_MESSAGES: readonly UIMessage[] = [];\n\nconst NO_MESSAGE_TIMING: Record<string, MessageTiming> = {};\n\ntype ProjectionStore<T> = {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n};\n\ntype ProjectionResource = {\n snapshot: SubagentDiscoverySnapshot;\n namespace: readonly string[];\n store: ProjectionStore<BaseMessage[]>;\n uiStore: ProjectionStore<readonly Event[]> | undefined;\n dispose: () => void;\n storeSnapshot: BaseMessage[] | undefined;\n status: SubagentDiscoverySnapshot[\"status\"] | undefined;\n uiFoldMemo: UIFoldMemo;\n localUiMessages: readonly UIMessage[];\n rootUiMessagesByParent: Map<string, UIMessage[]>;\n uiMessagesByParent: Map<string, UIMessage[]>;\n timingState: StreamingTimingState | null;\n messageTiming: Record<string, MessageTiming>;\n convertedMessageTiming: Record<string, MessageTiming> | undefined;\n convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;\n uiMessages: readonly UIMessage[];\n converted: readonly ThreadMessage[] | undefined;\n childTranscripts: ReadonlyMap<string, SubagentTranscript> | undefined;\n transcript: readonly ThreadMessage[] | undefined;\n timing: ToolCallTiming | undefined;\n entry: SubagentTranscript | undefined;\n memo: AttachMemo;\n cache: ExternalMessageConversionCache;\n};\n\ntype MeasuredTiming = {\n timing: ToolCallTiming | undefined;\n timingState: StreamingTimingState | null;\n messageTiming: Record<string, MessageTiming>;\n};\n\ntype NamespaceRequest = {\n id: string;\n attempts: number;\n pending: boolean;\n retryQueued: boolean;\n status: SubagentDiscoverySnapshot[\"status\"];\n};\n\ntype SubagentTranscriptSource = {\n resources: Map<string, ProjectionResource>;\n namespaceRequests: Map<string, NamespaceRequest>;\n snapshot: ReadonlyMap<string, SubagentTranscript>;\n listeners: Set<() => void>;\n controller: AnyStream[typeof STREAM_CONTROLLER] | undefined;\n uiMessagesByParent: Map<string, UIMessage[]>;\n subscribe(listener: () => void): () => void;\n getSnapshot(): ReadonlyMap<string, SubagentTranscript>;\n reconcile(\n controller: AnyStream[typeof STREAM_CONTROLLER],\n subagents: AnyStream[\"subagents\"],\n uiMessagesByParent: Map<string, UIMessage[]>,\n ): void;\n dispose(): void;\n};\n\nconst sameNamespace = (a: readonly string[], b: readonly string[]) =>\n a.length === b.length && a.every((segment, index) => segment === b[index]);\n\nconst needsNamespaceResolution = (snapshot: SubagentDiscoverySnapshot) =>\n snapshot.namespace.length === 1 &&\n snapshot.namespace[0] === `tools:${snapshot.id}`;\n\nconst requestSubagentNamespace = (\n source: SubagentTranscriptSource,\n controller: AnyStream[typeof STREAM_CONTROLLER],\n request: NamespaceRequest,\n) => {\n request.attempts += 1;\n request.pending = true;\n request.retryQueued = false;\n void controller\n .resolveSubagentNamespace(request.id)\n .catch(() => {})\n .finally(() => {\n if (\n source.controller !== controller ||\n source.namespaceRequests.get(request.id) !== request\n )\n return;\n\n request.pending = false;\n if (\n !request.retryQueued &&\n request.attempts === 1 &&\n request.status !== \"running\"\n ) {\n request.retryQueued = true;\n }\n if (!request.retryQueued || request.attempts >= 2) {\n request.retryQueued = false;\n return;\n }\n requestSubagentNamespace(source, controller, request);\n });\n};\n\nconst sameTranscriptEntries = (\n a: ReadonlyMap<string, SubagentTranscript> | undefined,\n b: ReadonlyMap<string, SubagentTranscript>,\n) =>\n a?.size === b.size &&\n [...b].every(([id, transcript]) => a.get(id) === transcript);\n\nconst collectUIMessages = (\n messages: readonly BaseMessage[],\n uiMessagesByParent: Map<string, UIMessage[]>,\n) => {\n const collected: UIMessage[] = [];\n if (uiMessagesByParent.size === 0) return collected;\n for (const message of messages) {\n const uiMessages = message.id && uiMessagesByParent.get(message.id);\n if (uiMessages) collected.push(...uiMessages);\n }\n return collected;\n};\n\nconst sameUIMessages = (a: readonly UIMessage[], b: readonly UIMessage[]) =>\n a.length === b.length && a.every((ui, index) => ui === b[index]);\n\nconst foldLocalUIMessages = (resource: ProjectionResource) => {\n const events = resource.uiStore?.getSnapshot();\n if (events === undefined) return NO_UI_MESSAGES;\n return foldUIUpdates(events, resource.uiFoldMemo);\n};\n\nconst mergeLocalUIMessages = (\n uiMessagesByParent: Map<string, UIMessage[]>,\n localUiMessages: readonly UIMessage[],\n) => {\n const merged = new Map(uiMessagesByParent);\n for (const [parentId, messages] of groupUIMessagesByParent<UIMessage>(\n localUiMessages,\n )) {\n merged.set(parentId, mergeUIMessages(messages, merged.get(parentId)));\n }\n return merged;\n};\n\nconst createConverter =\n (\n uiMessagesByParent: Map<string, UIMessage[]>,\n messageTiming: Record<string, MessageTiming>,\n ): useExternalMessageConverter.Callback<LangChainBaseMessage> =>\n (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n\nconst updateTiming = (resource: ProjectionResource) => {\n if (resource.timing === undefined) return;\n const startedAt = resource.snapshot.startedAt.getTime();\n const completedAt = resource.snapshot.completedAt?.getTime();\n if (\n resource.timing.startedAt === startedAt &&\n resource.timing.completedAt === completedAt\n )\n return;\n resource.timing = {\n startedAt,\n ...(completedAt !== undefined && { completedAt }),\n };\n};\n\nconst createSubagentTranscriptSource = (): SubagentTranscriptSource => {\n const uiMessagesByParent = new Map<string, UIMessage[]>();\n const source: SubagentTranscriptSource = {\n resources: new Map(),\n namespaceRequests: new Map(),\n snapshot: new Map(),\n listeners: new Set(),\n controller: undefined,\n uiMessagesByParent,\n subscribe(listener) {\n source.listeners.add(listener);\n return () => source.listeners.delete(listener);\n },\n getSnapshot() {\n return source.snapshot;\n },\n reconcile(controller, subagents, uiMessagesByParent) {\n source.uiMessagesByParent = uiMessagesByParent;\n // A resource is rebuilt from scratch when its namespace resolves, which\n // routinely happens after the subagent finished. Its timing was measured\n // while the client watched the task run and cannot be measured again.\n const rebound = new Map<string, MeasuredTiming>();\n\n if (source.controller !== controller) {\n source.dispose();\n source.controller = controller;\n }\n\n for (const id of source.namespaceRequests.keys()) {\n if (!subagents.has(id)) source.namespaceRequests.delete(id);\n }\n\n for (const [id, resource] of source.resources) {\n const snapshot = subagents.get(id);\n if (\n !snapshot ||\n snapshot.depth > MAX_SUBAGENT_DEPTH ||\n !sameNamespace(resource.namespace, snapshot.namespace)\n ) {\n if (snapshot && snapshot.depth <= MAX_SUBAGENT_DEPTH)\n rebound.set(id, {\n timing: resource.timing,\n timingState: resource.timingState,\n messageTiming: resource.messageTiming,\n });\n resource.dispose();\n source.resources.delete(id);\n continue;\n }\n resource.snapshot = snapshot;\n }\n\n for (const snapshot of subagents.values()) {\n if (snapshot.depth > MAX_SUBAGENT_DEPTH) continue;\n if (!needsNamespaceResolution(snapshot)) {\n source.namespaceRequests.delete(snapshot.id);\n } else {\n const request = source.namespaceRequests.get(snapshot.id);\n if (!request) {\n const nextRequest: NamespaceRequest = {\n id: snapshot.id,\n attempts: 0,\n pending: false,\n retryQueued: false,\n status: snapshot.status,\n };\n source.namespaceRequests.set(snapshot.id, nextRequest);\n requestSubagentNamespace(source, controller, nextRequest);\n } else if (request.status !== snapshot.status) {\n request.status = snapshot.status;\n if (request.pending) {\n request.retryQueued = request.attempts < 2;\n } else if (request.attempts < 2) {\n requestSubagentNamespace(source, controller, request);\n }\n }\n }\n if (source.resources.has(snapshot.id)) continue;\n const acquired = controller.registry.acquire(\n messagesProjection(snapshot.namespace),\n );\n const acquiredUI =\n snapshot.namespace.length > ROOT_UI_CHANNEL_DEPTH\n ? controller.registry.acquire(\n channelProjection(UI_CUSTOM_CHANNELS, snapshot.namespace),\n )\n : undefined;\n const resource: ProjectionResource = {\n snapshot,\n namespace: snapshot.namespace,\n store: acquired.store,\n uiStore: acquiredUI?.store,\n dispose: () => {},\n storeSnapshot: undefined,\n status: undefined,\n uiFoldMemo: createUIFoldMemo(),\n localUiMessages: NO_UI_MESSAGES,\n rootUiMessagesByParent: source.uiMessagesByParent,\n uiMessagesByParent: source.uiMessagesByParent,\n timingState: rebound.get(snapshot.id)?.timingState ?? null,\n messageTiming:\n rebound.get(snapshot.id)?.messageTiming ?? NO_MESSAGE_TIMING,\n convertedMessageTiming: undefined,\n convert: createConverter(\n source.uiMessagesByParent,\n NO_MESSAGE_TIMING,\n ),\n uiMessages: [],\n converted: undefined,\n childTranscripts: undefined,\n transcript: undefined,\n timing: rebound.has(snapshot.id)\n ? rebound.get(snapshot.id)!.timing\n : snapshot.status === \"running\"\n ? { startedAt: snapshot.startedAt.getTime() }\n : undefined,\n entry: undefined,\n memo: createAttachMemo(),\n cache: createExternalMessageConversionCache(),\n };\n const unsubscribe = resource.store.subscribe(() => rebuild());\n const unsubscribeUI = resource.uiStore?.subscribe(() => rebuild());\n resource.dispose = () => {\n unsubscribe();\n unsubscribeUI?.();\n acquired.release();\n acquiredUI?.release();\n };\n source.resources.set(snapshot.id, resource);\n }\n\n rebuild();\n },\n dispose() {\n for (const resource of source.resources.values()) resource.dispose();\n source.resources.clear();\n source.namespaceRequests.clear();\n source.snapshot = new Map();\n for (const listener of source.listeners) listener();\n },\n };\n\n const rebuild = () => {\n const { uiMessagesByParent } = source;\n const resources = [...source.resources.values()];\n const childrenByParent = new Map<string, ProjectionResource[]>();\n\n for (const resource of resources) {\n const parentId = resource.snapshot.parentId;\n if (parentId == null) continue;\n const children = childrenByParent.get(parentId);\n if (children) children.push(resource);\n else childrenByParent.set(parentId, [resource]);\n }\n\n const transcripts = new Map<string, SubagentTranscript>();\n let changed = source.snapshot.size !== resources.length;\n const built = new Set<string>();\n\n const build = (resource: ProjectionResource, depth: number) => {\n if (built.has(resource.snapshot.id)) return;\n built.add(resource.snapshot.id);\n const children =\n depth < MAX_SUBAGENT_DEPTH\n ? (childrenByParent.get(resource.snapshot.id) ?? [])\n : [];\n for (const child of children) build(child, depth + 1);\n const childTranscripts = new Map(\n children.flatMap((child) =>\n child.entry ? [[child.snapshot.id, child.entry] as const] : [],\n ),\n );\n const storeSnapshot = resource.store.getSnapshot();\n const status = resource.snapshot.status;\n const stepped = stepStreamingTiming(\n resource.timingState,\n storeSnapshot as LangChainBaseMessage[],\n status === \"running\",\n langChainStreamingTimingAccessors,\n undefined,\n );\n resource.timingState = stepped.state;\n if (Object.keys(stepped.timings).length > 0) {\n resource.messageTiming = {\n ...resource.messageTiming,\n ...stepped.timings,\n };\n }\n const localUiMessages = foldLocalUIMessages(resource);\n if (\n resource.localUiMessages !== localUiMessages ||\n resource.rootUiMessagesByParent !== uiMessagesByParent ||\n resource.convertedMessageTiming !== resource.messageTiming\n ) {\n resource.localUiMessages = localUiMessages;\n resource.rootUiMessagesByParent = uiMessagesByParent;\n resource.uiMessagesByParent =\n localUiMessages.length === 0\n ? uiMessagesByParent\n : mergeLocalUIMessages(uiMessagesByParent, localUiMessages);\n resource.convert = createConverter(\n resource.uiMessagesByParent,\n resource.messageTiming,\n );\n }\n const uiMessages = collectUIMessages(\n storeSnapshot,\n resource.uiMessagesByParent,\n );\n\n const conversionChanged =\n resource.converted === undefined ||\n resource.storeSnapshot !== storeSnapshot ||\n resource.status !== status ||\n resource.convertedMessageTiming !== resource.messageTiming ||\n !sameUIMessages(resource.uiMessages, uiMessages);\n if (conversionChanged) {\n resource.converted = convertExternalMessages(\n storeSnapshot as LangChainBaseMessage[],\n resource.convert,\n status === \"running\",\n TRANSCRIPT_METADATA,\n resource.cache,\n );\n resource.storeSnapshot = storeSnapshot;\n resource.status = status;\n resource.uiMessages = uiMessages;\n resource.convertedMessageTiming = resource.messageTiming;\n }\n\n if (\n resource.transcript === undefined ||\n conversionChanged ||\n !sameTranscriptEntries(resource.childTranscripts, childTranscripts)\n ) {\n const transcript = attachSubagentTranscripts(\n resource.converted!,\n childTranscripts,\n resource.memo,\n );\n changed ||= resource.transcript !== transcript;\n resource.transcript = transcript;\n resource.childTranscripts = childTranscripts;\n }\n\n updateTiming(resource);\n if (\n resource.entry === undefined ||\n resource.entry.messages !== resource.transcript ||\n resource.entry.timing !== resource.timing\n ) {\n resource.entry = {\n messages: resource.transcript!,\n ...(resource.timing && { timing: resource.timing }),\n };\n changed = true;\n }\n\n if (!source.snapshot.has(resource.snapshot.id)) changed = true;\n transcripts.set(resource.snapshot.id, resource.entry);\n };\n\n for (const resource of resources) {\n if (\n resource.snapshot.parentId == null ||\n !source.resources.has(resource.snapshot.parentId)\n )\n build(resource, 1);\n }\n for (const resource of resources) build(resource, MAX_SUBAGENT_DEPTH);\n\n if (!changed) return;\n source.snapshot = transcripts;\n for (const listener of source.listeners) listener();\n };\n\n return source;\n};\n\nexport const useSubagentTranscripts = (\n stream: AnyStream,\n uiMessagesByParent: Map<string, UIMessage[]>,\n): ReadonlyMap<string, SubagentTranscript> => {\n const sourceRef = useRef<SubagentTranscriptSource | undefined>(undefined);\n if (!sourceRef.current) {\n sourceRef.current = createSubagentTranscriptSource();\n }\n const source = sourceRef.current;\n const controller = stream[STREAM_CONTROLLER];\n\n useEffect(() => {\n source.reconcile(controller, stream.subagents, uiMessagesByParent);\n }, [controller, source, stream.subagents, uiMessagesByParent]);\n\n useEffect(() => () => source.dispose(), [source]);\n\n return useSyncExternalStore(\n source.subscribe,\n source.getSnapshot,\n source.getSnapshot,\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useSubagentTranscripts.js","names":[],"sources":["../src/useSubagentTranscripts.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useRef, useSyncExternalStore } from \"react\";\nimport {\n stepStreamingTiming,\n type MessageTiming,\n type StreamingTimingState,\n type ThreadMessage,\n type ToolCallTiming,\n} from \"@assistant-ui/core\";\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n type ExternalMessageConversionCache,\n type useExternalMessageConverter,\n} from \"@assistant-ui/core/react\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { STREAM_CONTROLLER, type AnyStream } from \"@langchain/react\";\nimport type { BaseMessage } from \"@langchain/core/messages\";\nimport { channelProjection, type Event } from \"@langchain/langgraph-sdk/stream\";\nimport type { SubagentDiscoverySnapshot } from \"@langchain/react\";\nimport {\n attachSubagentTranscripts,\n type AttachMemo,\n createAttachMemo,\n type SubagentTranscript,\n} from \"./attachSubagentTranscripts\";\nimport { convertLangChainBaseMessage } from \"./convertMessages\";\nimport { groupUIMessagesByParent } from \"./converter\";\nimport { langChainStreamingTimingAccessors } from \"./streamingTiming\";\nimport { subagentMessagesProjection } from \"./subagentMessagesProjection\";\nimport type { LangChainBaseMessage, UIMessage } from \"./types\";\nimport {\n createUIFoldMemo,\n foldUIUpdates,\n mergeUIMessages,\n UI_CUSTOM_CHANNELS,\n type UIFoldMemo,\n} from \"./uiMessages\";\n\nexport const MAX_SUBAGENT_DEPTH = 16;\n\nconst ROOT_UI_CHANNEL_DEPTH = 1;\n\nconst TRANSCRIPT_METADATA = {};\n\nconst NO_UI_MESSAGES: readonly UIMessage[] = [];\n\nconst NO_MESSAGE_TIMING: Record<string, MessageTiming> = {};\n\ntype ProjectionStore<T> = {\n getSnapshot(): T;\n subscribe(listener: () => void): () => void;\n};\n\ntype ProjectionResource = {\n snapshot: SubagentDiscoverySnapshot;\n namespace: readonly string[];\n store: ProjectionStore<BaseMessage[]>;\n uiStore: ProjectionStore<readonly Event[]> | undefined;\n dispose: () => void;\n storeSnapshot: BaseMessage[] | undefined;\n status: SubagentDiscoverySnapshot[\"status\"] | undefined;\n uiFoldMemo: UIFoldMemo;\n localUiMessages: readonly UIMessage[];\n rootUiMessagesByParent: Map<string, UIMessage[]>;\n uiMessagesByParent: Map<string, UIMessage[]>;\n timingState: StreamingTimingState | null;\n messageTiming: Record<string, MessageTiming>;\n convertedMessageTiming: Record<string, MessageTiming> | undefined;\n convert: useExternalMessageConverter.Callback<LangChainBaseMessage>;\n uiMessages: readonly UIMessage[];\n converted: readonly ThreadMessage[] | undefined;\n childTranscripts: ReadonlyMap<string, SubagentTranscript> | undefined;\n transcript: readonly ThreadMessage[] | undefined;\n timing: ToolCallTiming | undefined;\n entry: SubagentTranscript | undefined;\n memo: AttachMemo;\n cache: ExternalMessageConversionCache;\n};\n\ntype MeasuredTiming = {\n timing: ToolCallTiming | undefined;\n timingState: StreamingTimingState | null;\n messageTiming: Record<string, MessageTiming>;\n};\n\ntype NamespaceRequest = {\n id: string;\n attempts: number;\n pending: boolean;\n retryQueued: boolean;\n status: SubagentDiscoverySnapshot[\"status\"];\n};\n\ntype SubagentTranscriptSource = {\n resources: Map<string, ProjectionResource>;\n namespaceRequests: Map<string, NamespaceRequest>;\n snapshot: ReadonlyMap<string, SubagentTranscript>;\n listeners: Set<() => void>;\n controller: AnyStream[typeof STREAM_CONTROLLER] | undefined;\n uiMessagesByParent: Map<string, UIMessage[]>;\n subscribe(listener: () => void): () => void;\n getSnapshot(): ReadonlyMap<string, SubagentTranscript>;\n reconcile(\n controller: AnyStream[typeof STREAM_CONTROLLER],\n subagents: AnyStream[\"subagents\"],\n uiMessagesByParent: Map<string, UIMessage[]>,\n ): void;\n dispose(): void;\n};\n\nconst sameNamespace = (a: readonly string[], b: readonly string[]) =>\n a.length === b.length && a.every((segment, index) => segment === b[index]);\n\nconst needsNamespaceResolution = (snapshot: SubagentDiscoverySnapshot) =>\n snapshot.namespace.length === 1 &&\n snapshot.namespace[0] === `tools:${snapshot.id}`;\n\nconst requestSubagentNamespace = (\n source: SubagentTranscriptSource,\n controller: AnyStream[typeof STREAM_CONTROLLER],\n request: NamespaceRequest,\n) => {\n request.attempts += 1;\n request.pending = true;\n request.retryQueued = false;\n void controller\n .resolveSubagentNamespace(request.id)\n .catch(() => {})\n .finally(() => {\n if (\n source.controller !== controller ||\n source.namespaceRequests.get(request.id) !== request\n )\n return;\n\n request.pending = false;\n if (\n !request.retryQueued &&\n request.attempts === 1 &&\n request.status !== \"running\"\n ) {\n request.retryQueued = true;\n }\n if (!request.retryQueued || request.attempts >= 2) {\n request.retryQueued = false;\n return;\n }\n requestSubagentNamespace(source, controller, request);\n });\n};\n\nconst sameTranscriptEntries = (\n a: ReadonlyMap<string, SubagentTranscript> | undefined,\n b: ReadonlyMap<string, SubagentTranscript>,\n) =>\n a?.size === b.size &&\n [...b].every(([id, transcript]) => a.get(id) === transcript);\n\nconst collectUIMessages = (\n messages: readonly BaseMessage[],\n uiMessagesByParent: Map<string, UIMessage[]>,\n) => {\n const collected: UIMessage[] = [];\n if (uiMessagesByParent.size === 0) return collected;\n for (const message of messages) {\n const uiMessages = message.id && uiMessagesByParent.get(message.id);\n if (uiMessages) collected.push(...uiMessages);\n }\n return collected;\n};\n\nconst sameUIMessages = (a: readonly UIMessage[], b: readonly UIMessage[]) =>\n a.length === b.length && a.every((ui, index) => ui === b[index]);\n\nconst foldLocalUIMessages = (resource: ProjectionResource) => {\n const events = resource.uiStore?.getSnapshot();\n if (events === undefined) return NO_UI_MESSAGES;\n return foldUIUpdates(events, resource.uiFoldMemo);\n};\n\nconst mergeLocalUIMessages = (\n uiMessagesByParent: Map<string, UIMessage[]>,\n localUiMessages: readonly UIMessage[],\n) => {\n const merged = new Map(uiMessagesByParent);\n for (const [parentId, messages] of groupUIMessagesByParent<UIMessage>(\n localUiMessages,\n )) {\n merged.set(parentId, mergeUIMessages(messages, merged.get(parentId)));\n }\n return merged;\n};\n\nconst createConverter =\n (\n uiMessagesByParent: Map<string, UIMessage[]>,\n messageTiming: Record<string, MessageTiming>,\n ): useExternalMessageConverter.Callback<LangChainBaseMessage> =>\n (message, metadata) =>\n convertLangChainBaseMessage(message, {\n ...metadata,\n uiMessagesByParent,\n messageTiming,\n });\n\nconst updateTiming = (resource: ProjectionResource) => {\n if (resource.timing === undefined) return;\n const startedAt = resource.snapshot.startedAt.getTime();\n const completedAt = resource.snapshot.completedAt?.getTime();\n if (\n resource.timing.startedAt === startedAt &&\n resource.timing.completedAt === completedAt\n )\n return;\n resource.timing = {\n startedAt,\n ...(completedAt !== undefined && { completedAt }),\n };\n};\n\nconst createSubagentTranscriptSource = (): SubagentTranscriptSource => {\n const uiMessagesByParent = new Map<string, UIMessage[]>();\n const source: SubagentTranscriptSource = {\n resources: new Map(),\n namespaceRequests: new Map(),\n snapshot: new Map(),\n listeners: new Set(),\n controller: undefined,\n uiMessagesByParent,\n subscribe(listener) {\n source.listeners.add(listener);\n return () => source.listeners.delete(listener);\n },\n getSnapshot() {\n return source.snapshot;\n },\n reconcile(controller, subagents, uiMessagesByParent) {\n source.uiMessagesByParent = uiMessagesByParent;\n // A resource is rebuilt from scratch when its namespace resolves, which\n // routinely happens after the subagent finished. Its timing was measured\n // while the client watched the task run and cannot be measured again.\n const rebound = new Map<string, MeasuredTiming>();\n\n if (source.controller !== controller) {\n source.dispose();\n source.controller = controller;\n }\n\n for (const id of source.namespaceRequests.keys()) {\n if (!subagents.has(id)) source.namespaceRequests.delete(id);\n }\n\n for (const [id, resource] of source.resources) {\n const snapshot = subagents.get(id);\n if (\n !snapshot ||\n snapshot.depth > MAX_SUBAGENT_DEPTH ||\n !sameNamespace(resource.namespace, snapshot.namespace)\n ) {\n if (snapshot && snapshot.depth <= MAX_SUBAGENT_DEPTH)\n rebound.set(id, {\n timing: resource.timing,\n timingState: resource.timingState,\n messageTiming: resource.messageTiming,\n });\n resource.dispose();\n source.resources.delete(id);\n continue;\n }\n resource.snapshot = snapshot;\n }\n\n for (const snapshot of subagents.values()) {\n if (snapshot.depth > MAX_SUBAGENT_DEPTH) continue;\n if (!needsNamespaceResolution(snapshot)) {\n source.namespaceRequests.delete(snapshot.id);\n } else {\n const request = source.namespaceRequests.get(snapshot.id);\n if (!request) {\n const nextRequest: NamespaceRequest = {\n id: snapshot.id,\n attempts: 0,\n pending: false,\n retryQueued: false,\n status: snapshot.status,\n };\n source.namespaceRequests.set(snapshot.id, nextRequest);\n requestSubagentNamespace(source, controller, nextRequest);\n } else if (request.status !== snapshot.status) {\n request.status = snapshot.status;\n if (request.pending) {\n request.retryQueued = request.attempts < 2;\n } else if (request.attempts < 2) {\n requestSubagentNamespace(source, controller, request);\n }\n }\n }\n if (source.resources.has(snapshot.id)) continue;\n const acquired = controller.registry.acquire(\n subagentMessagesProjection(snapshot.namespace),\n );\n const acquiredUI =\n snapshot.namespace.length > ROOT_UI_CHANNEL_DEPTH\n ? controller.registry.acquire(\n channelProjection(UI_CUSTOM_CHANNELS, snapshot.namespace),\n )\n : undefined;\n const resource: ProjectionResource = {\n snapshot,\n namespace: snapshot.namespace,\n store: acquired.store,\n uiStore: acquiredUI?.store,\n dispose: () => {},\n storeSnapshot: undefined,\n status: undefined,\n uiFoldMemo: createUIFoldMemo(),\n localUiMessages: NO_UI_MESSAGES,\n rootUiMessagesByParent: source.uiMessagesByParent,\n uiMessagesByParent: source.uiMessagesByParent,\n timingState: rebound.get(snapshot.id)?.timingState ?? null,\n messageTiming:\n rebound.get(snapshot.id)?.messageTiming ?? NO_MESSAGE_TIMING,\n convertedMessageTiming: undefined,\n convert: createConverter(\n source.uiMessagesByParent,\n NO_MESSAGE_TIMING,\n ),\n uiMessages: [],\n converted: undefined,\n childTranscripts: undefined,\n transcript: undefined,\n timing: rebound.has(snapshot.id)\n ? rebound.get(snapshot.id)!.timing\n : snapshot.status === \"running\"\n ? { startedAt: snapshot.startedAt.getTime() }\n : undefined,\n entry: undefined,\n memo: createAttachMemo(),\n cache: createExternalMessageConversionCache(),\n };\n const unsubscribe = resource.store.subscribe(() => rebuild());\n const unsubscribeUI = resource.uiStore?.subscribe(() => rebuild());\n resource.dispose = () => {\n unsubscribe();\n unsubscribeUI?.();\n acquired.release();\n acquiredUI?.release();\n };\n source.resources.set(snapshot.id, resource);\n }\n\n rebuild();\n },\n dispose() {\n for (const resource of source.resources.values()) resource.dispose();\n source.resources.clear();\n source.namespaceRequests.clear();\n source.snapshot = new Map();\n for (const listener of source.listeners) listener();\n },\n };\n\n const rebuild = () => {\n const { uiMessagesByParent } = source;\n const resources = [...source.resources.values()];\n const childrenByParent = new Map<string, ProjectionResource[]>();\n\n for (const resource of resources) {\n const parentId = resource.snapshot.parentId;\n if (parentId == null) continue;\n const children = childrenByParent.get(parentId);\n if (children) children.push(resource);\n else childrenByParent.set(parentId, [resource]);\n }\n\n const transcripts = new Map<string, SubagentTranscript>();\n let changed = source.snapshot.size !== resources.length;\n const built = new Set<string>();\n\n const build = (resource: ProjectionResource, depth: number) => {\n if (built.has(resource.snapshot.id)) return;\n built.add(resource.snapshot.id);\n const children =\n depth < MAX_SUBAGENT_DEPTH\n ? (childrenByParent.get(resource.snapshot.id) ?? [])\n : [];\n for (const child of children) build(child, depth + 1);\n const childTranscripts = new Map(\n children.flatMap((child) =>\n child.entry ? [[child.snapshot.id, child.entry] as const] : [],\n ),\n );\n const storeSnapshot = resource.store.getSnapshot();\n const status = resource.snapshot.status;\n const stepped = stepStreamingTiming(\n resource.timingState,\n storeSnapshot as LangChainBaseMessage[],\n status === \"running\",\n langChainStreamingTimingAccessors,\n undefined,\n );\n resource.timingState = stepped.state;\n if (Object.keys(stepped.timings).length > 0) {\n resource.messageTiming = {\n ...resource.messageTiming,\n ...stepped.timings,\n };\n }\n const localUiMessages = foldLocalUIMessages(resource);\n if (\n resource.localUiMessages !== localUiMessages ||\n resource.rootUiMessagesByParent !== uiMessagesByParent ||\n resource.convertedMessageTiming !== resource.messageTiming\n ) {\n resource.localUiMessages = localUiMessages;\n resource.rootUiMessagesByParent = uiMessagesByParent;\n resource.uiMessagesByParent =\n localUiMessages.length === 0\n ? uiMessagesByParent\n : mergeLocalUIMessages(uiMessagesByParent, localUiMessages);\n resource.convert = createConverter(\n resource.uiMessagesByParent,\n resource.messageTiming,\n );\n }\n const uiMessages = collectUIMessages(\n storeSnapshot,\n resource.uiMessagesByParent,\n );\n\n const conversionChanged =\n resource.converted === undefined ||\n resource.storeSnapshot !== storeSnapshot ||\n resource.status !== status ||\n resource.convertedMessageTiming !== resource.messageTiming ||\n !sameUIMessages(resource.uiMessages, uiMessages);\n if (conversionChanged) {\n resource.converted = convertExternalMessages(\n storeSnapshot as LangChainBaseMessage[],\n resource.convert,\n status === \"running\",\n TRANSCRIPT_METADATA,\n resource.cache,\n );\n resource.storeSnapshot = storeSnapshot;\n resource.status = status;\n resource.uiMessages = uiMessages;\n resource.convertedMessageTiming = resource.messageTiming;\n }\n\n if (\n resource.transcript === undefined ||\n conversionChanged ||\n !sameTranscriptEntries(resource.childTranscripts, childTranscripts)\n ) {\n const transcript = attachSubagentTranscripts(\n resource.converted!,\n childTranscripts,\n resource.memo,\n );\n changed ||= resource.transcript !== transcript;\n resource.transcript = transcript;\n resource.childTranscripts = childTranscripts;\n }\n\n updateTiming(resource);\n if (\n resource.entry === undefined ||\n resource.entry.messages !== resource.transcript ||\n resource.entry.timing !== resource.timing\n ) {\n resource.entry = {\n messages: resource.transcript!,\n ...(resource.timing && { timing: resource.timing }),\n };\n changed = true;\n }\n\n if (!source.snapshot.has(resource.snapshot.id)) changed = true;\n transcripts.set(resource.snapshot.id, resource.entry);\n };\n\n for (const resource of resources) {\n if (\n resource.snapshot.parentId == null ||\n !source.resources.has(resource.snapshot.parentId)\n )\n build(resource, 1);\n }\n for (const resource of resources) build(resource, MAX_SUBAGENT_DEPTH);\n\n if (!changed) return;\n source.snapshot = transcripts;\n for (const listener of source.listeners) listener();\n };\n\n return source;\n};\n\nexport const useSubagentTranscripts = (\n stream: AnyStream,\n uiMessagesByParent: Map<string, UIMessage[]>,\n): ReadonlyMap<string, SubagentTranscript> => {\n const sourceRef = useRef<SubagentTranscriptSource | undefined>(undefined);\n if (!sourceRef.current) {\n sourceRef.current = createSubagentTranscriptSource();\n }\n const source = sourceRef.current;\n const controller = stream[STREAM_CONTROLLER];\n\n useEffect(() => {\n source.reconcile(controller, stream.subagents, uiMessagesByParent);\n }, [controller, source, stream.subagents, uiMessagesByParent]);\n\n useReplaySafeEffect(() => () => source.dispose(), [source]);\n\n return useSyncExternalStore(\n source.subscribe,\n source.getSnapshot,\n source.getSnapshot,\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-langchain",
3
- "version": "0.0.32",
3
+ "version": "0.0.34",
4
4
  "description": "LangChain useStream adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "langchain",
@@ -40,18 +40,22 @@
40
40
  ],
41
41
  "sideEffects": false,
42
42
  "dependencies": {
43
- "@assistant-ui/core": "^0.3.20",
44
- "@assistant-ui/store": "^0.3.14",
43
+ "@assistant-ui/core": "^0.3.23",
44
+ "@assistant-ui/store": "^0.3.17",
45
45
  "assistant-cloud": "*",
46
- "assistant-stream": "^0.3.44"
46
+ "assistant-stream": "^0.3.47"
47
47
  },
48
48
  "peerDependencies": {
49
+ "@langchain/core": "^1.0.0",
49
50
  "@langchain/langgraph-sdk": "^1.9.20",
50
51
  "@langchain/react": "^1.0.20",
51
52
  "@types/react": "*",
52
53
  "react": "^18 || ^19"
53
54
  },
54
55
  "peerDependenciesMeta": {
56
+ "@langchain/core": {
57
+ "optional": true
58
+ },
55
59
  "@langchain/langgraph-sdk": {
56
60
  "optional": true
57
61
  },
@@ -63,15 +67,16 @@
63
67
  }
64
68
  },
65
69
  "devDependencies": {
66
- "@assistant-ui/x-buildutils": "0.0.28",
67
- "@langchain/core": "^1.2.11",
68
- "@langchain/langgraph-sdk": "^1.11.0",
69
- "@langchain/react": "^1.1.0",
70
+ "@assistant-ui/x-buildutils": "0.0.30",
71
+ "@assistant-ui/x-react18": "0.0.0",
72
+ "@langchain/core": "^1.2.13",
73
+ "@langchain/langgraph-sdk": "^1.12.0",
74
+ "@langchain/react": "^1.2.0",
70
75
  "@testing-library/react": "^16.3.3",
71
76
  "@types/react": "^19.3.0",
72
77
  "jsdom": "^30.0.1",
73
78
  "react": "^19.3.0",
74
- "vitest": "^5.0.1"
79
+ "vitest": "^5.0.3"
75
80
  },
76
81
  "publishConfig": {
77
82
  "access": "public",
@@ -90,6 +95,8 @@
90
95
  "build": "aui-build",
91
96
  "test": "vitest run",
92
97
  "typecheck": "tsc --noEmit",
98
+ "test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
99
+ "test:types:peer-react18": "aui-react18-types",
93
100
  "test:watch": "vitest"
94
101
  }
95
102
  }
@@ -27,6 +27,27 @@ const contentOf = (result: ReturnType<typeof convertLangChainBaseMessage>) => {
27
27
  return result.content;
28
28
  };
29
29
 
30
+ describe("convertLangChainBaseMessage modality", () => {
31
+ it("lifts voice modality onto human and ai messages and ignores unknown values", () => {
32
+ for (const message of [humanMessage("Question"), aiMessage("Answer")]) {
33
+ const spoken = convertLangChainBaseMessage({
34
+ ...message,
35
+ additional_kwargs: { modality: "voice" },
36
+ });
37
+ const unknown = convertLangChainBaseMessage({
38
+ ...message,
39
+ additional_kwargs: { modality: "video" },
40
+ });
41
+
42
+ expect(spoken).toHaveProperty("metadata", {
43
+ custom: {},
44
+ modality: "voice",
45
+ });
46
+ expect(unknown).toHaveProperty("metadata", { custom: {} });
47
+ }
48
+ });
49
+ });
50
+
30
51
  describe("convertLangChainBaseMessage file content parts", () => {
31
52
  it("converts a base64 file block", () => {
32
53
  const result = convertLangChainBaseMessage(
@@ -1113,6 +1134,118 @@ describe("convertLangChainBaseMessage malformed messages", () => {
1113
1134
  ]);
1114
1135
  });
1115
1136
 
1137
+ it("normalizes missing, string, and array tool-call args to an object", () => {
1138
+ for (const args of [undefined, "not-json", ["x"], null]) {
1139
+ const result = convertLangChainBaseMessage(
1140
+ {
1141
+ ...aiMessage([]),
1142
+ tool_calls: [{ id: "call-1", name: "lookup", args }],
1143
+ } as unknown as LangChainBaseMessage,
1144
+ {},
1145
+ );
1146
+ expect(contentOf(result)).toEqual([
1147
+ {
1148
+ type: "tool-call",
1149
+ toolCallId: "call-1",
1150
+ toolName: "lookup",
1151
+ args: {},
1152
+ argsText: "{}",
1153
+ },
1154
+ ]);
1155
+ }
1156
+ });
1157
+
1158
+ it("normalizes unsafe object tool-call args to an empty object", () => {
1159
+ const cyclic: Record<string, unknown> = {};
1160
+ cyclic.self = cyclic;
1161
+ class CustomArgs {
1162
+ query = "x";
1163
+ }
1164
+
1165
+ for (const args of [new Date(0), new Map(), new CustomArgs(), cyclic]) {
1166
+ const result = convertLangChainBaseMessage(
1167
+ {
1168
+ ...aiMessage([]),
1169
+ tool_calls: [{ id: "call-1", name: "lookup", args }],
1170
+ } as unknown as LangChainBaseMessage,
1171
+ {},
1172
+ );
1173
+ expect(contentOf(result)).toEqual([
1174
+ {
1175
+ type: "tool-call",
1176
+ toolCallId: "call-1",
1177
+ toolName: "lookup",
1178
+ args: {},
1179
+ argsText: "{}",
1180
+ },
1181
+ ]);
1182
+ }
1183
+ });
1184
+
1185
+ it("skips a null tool call entry and keeps the rest", () => {
1186
+ const result = convertLangChainBaseMessage(
1187
+ {
1188
+ ...aiMessage([]),
1189
+ tool_calls: [null, { id: "call-1", name: "lookup", args: {} }],
1190
+ } as unknown as LangChainBaseMessage,
1191
+ {},
1192
+ );
1193
+
1194
+ expect(contentOf(result)).toEqual([
1195
+ {
1196
+ type: "tool-call",
1197
+ toolCallId: "call-1",
1198
+ toolName: "lookup",
1199
+ args: {},
1200
+ argsText: "{}",
1201
+ },
1202
+ ]);
1203
+ });
1204
+
1205
+ it("skips a tool call without a name so its named result does not throw", () => {
1206
+ const messages = convertExternalMessages(
1207
+ [
1208
+ {
1209
+ ...aiMessage([]),
1210
+ tool_calls: [{ id: "call-1", args: {} }],
1211
+ } as unknown as LangChainBaseMessage,
1212
+ {
1213
+ _getType: () => "tool",
1214
+ id: "msg-tool",
1215
+ name: "search",
1216
+ tool_call_id: "call-1",
1217
+ content: "3 results",
1218
+ } as LangChainBaseMessage,
1219
+ ],
1220
+ (message) => convertLangChainBaseMessage(message, {}),
1221
+ false,
1222
+ {},
1223
+ );
1224
+
1225
+ expect(messages).toHaveLength(1);
1226
+ expect(messages[0]!.content).toEqual([]);
1227
+ });
1228
+
1229
+ it("keeps a tool call whose name is empty", () => {
1230
+ const result = convertLangChainBaseMessage(
1231
+ {
1232
+ ...aiMessage([]),
1233
+ tool_calls: [{ id: "call-1", name: "", args: {} }],
1234
+ },
1235
+ {},
1236
+ );
1237
+
1238
+ expect(contentOf(result)).toEqual([
1239
+ {
1240
+ type: "tool-call",
1241
+ toolCallId: "call-1",
1242
+ toolName: "",
1243
+ args: {},
1244
+ argsText: "{}",
1245
+ },
1246
+ ]);
1247
+ });
1248
+
1116
1249
  it("converts a system message with null content to empty text", () => {
1117
1250
  const result = convertLangChainBaseMessage(
1118
1251
  { _getType: () => "system", id: "msg-4", content: null },
@@ -1217,6 +1350,26 @@ describe("convertLangChainBaseMessage malformed messages", () => {
1217
1350
  }
1218
1351
  });
1219
1352
 
1353
+ it("warns once in development about a skipped tool call without a name", () => {
1354
+ vi.stubEnv("NODE_ENV", "development");
1355
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
1356
+ try {
1357
+ const message = {
1358
+ ...aiMessage([]),
1359
+ tool_calls: [null, { id: "call-1", args: {} }],
1360
+ } as unknown as LangChainBaseMessage;
1361
+ convertLangChainBaseMessage(message, {});
1362
+ convertLangChainBaseMessage(message, {});
1363
+ expect(warn).toHaveBeenCalledTimes(1);
1364
+ expect(warn).toHaveBeenCalledWith(
1365
+ "Skipping a tool call without a name; its result is not shown either",
1366
+ );
1367
+ } finally {
1368
+ warn.mockRestore();
1369
+ vi.unstubAllEnvs();
1370
+ }
1371
+ });
1372
+
1220
1373
  it("warns once in development about non-array content", () => {
1221
1374
  vi.stubEnv("NODE_ENV", "development");
1222
1375
  const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
@@ -6,6 +6,7 @@ import type { ReadonlyJSONObject } from "assistant-stream/utils";
6
6
  import {
7
7
  convertLangChainContentBlock,
8
8
  getCustomMetadata,
9
+ getMessageModality,
9
10
  uiMessageToDataPart,
10
11
  withAudioTranscript,
11
12
  } from "./converter";
@@ -55,6 +56,35 @@ const contentBlocks = (content: unknown): readonly LangChainContentBlock[] => {
55
56
  return [];
56
57
  };
57
58
 
59
+ const normalizeToolCallArgs = (args: unknown): ReadonlyJSONObject => {
60
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
61
+ return {};
62
+ }
63
+
64
+ try {
65
+ const prototype = Object.getPrototypeOf(args);
66
+ return prototype === Object.prototype || prototype === null
67
+ ? (args as ReadonlyJSONObject)
68
+ : {};
69
+ } catch {
70
+ return {};
71
+ }
72
+ };
73
+
74
+ const toolCallArgs = (
75
+ value: unknown,
76
+ ): { args: ReadonlyJSONObject; argsText: string } => {
77
+ const args = normalizeToolCallArgs(value);
78
+ try {
79
+ const argsText = JSON.stringify(args);
80
+ return typeof argsText === "string"
81
+ ? { args, argsText }
82
+ : { args: {}, argsText: "{}" };
83
+ } catch {
84
+ return { args: {}, argsText: "{}" };
85
+ }
86
+ };
87
+
58
88
  const contentToParts = (content: unknown) => {
59
89
  if (typeof content === "string")
60
90
  return [{ type: "text" as const, text: content }];
@@ -89,25 +119,39 @@ export const convertLangChainBaseMessage = (
89
119
  },
90
120
  };
91
121
 
92
- case "human":
122
+ case "human": {
123
+ const modality = getMessageModality(message.additional_kwargs);
93
124
  return {
94
125
  role: "user",
95
126
  id: message.id,
96
127
  content: contentToParts(message.content),
97
128
  metadata: {
98
129
  custom: getCustomMetadata(message.additional_kwargs),
130
+ ...(modality && { modality }),
99
131
  },
100
132
  };
133
+ }
101
134
 
102
135
  case "ai": {
103
136
  const toolCallParts =
104
- message.tool_calls?.map((tc) => ({
105
- type: "tool-call" as const,
106
- toolCallId: tc.id,
107
- toolName: tc.name,
108
- args: tc.args as ReadonlyJSONObject,
109
- argsText: JSON.stringify(tc.args),
110
- })) ?? [];
137
+ message.tool_calls
138
+ ?.filter((tc) => {
139
+ if (typeof tc?.name === "string") return true;
140
+ warnOnceInDevelopment(
141
+ "Skipping a tool call without a name; its result is not shown either",
142
+ );
143
+ return false;
144
+ })
145
+ .map((tc) => {
146
+ const { args, argsText } = toolCallArgs(tc.args);
147
+ return {
148
+ type: "tool-call" as const,
149
+ toolCallId: tc.id,
150
+ toolName: tc.name,
151
+ args,
152
+ argsText,
153
+ };
154
+ }) ?? [];
111
155
 
112
156
  const assistantStatus =
113
157
  typeof message.status === "object" ? message.status : undefined;
@@ -120,6 +164,7 @@ export const convertLangChainBaseMessage = (
120
164
  : undefined) ?? [];
121
165
 
122
166
  const timing = metadata.messageTiming?.[message.id ?? ""];
167
+ const modality = getMessageModality(message.additional_kwargs);
123
168
 
124
169
  return {
125
170
  role: "assistant",
@@ -135,6 +180,7 @@ export const convertLangChainBaseMessage = (
135
180
  metadata: {
136
181
  custom: getCustomMetadata(message.additional_kwargs),
137
182
  ...(timing && { timing }),
183
+ ...(modality && { modality }),
138
184
  },
139
185
  ...(assistantStatus && { status: assistantStatus }),
140
186
  };
package/src/converter.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AppendMessage,
3
3
  DataMessagePart,
4
+ MessageModality,
4
5
  ThreadAssistantMessage,
5
6
  ThreadUserMessage,
6
7
  } from "@assistant-ui/core";
@@ -146,6 +147,11 @@ export const getCustomMetadata = (
146
147
  ): Record<string, unknown> =>
147
148
  (additionalKwargs?.metadata as Record<string, unknown>) ?? {};
148
149
 
150
+ export const getMessageModality = (
151
+ additionalKwargs: Record<string, unknown> | undefined,
152
+ ): MessageModality | undefined =>
153
+ additionalKwargs?.modality === "voice" ? "voice" : undefined;
154
+
149
155
  export const uiMessageToDataPart = <
150
156
  TUIMessage extends { name: string; props: Record<string, unknown> },
151
157
  >(
@@ -0,0 +1,175 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { BaseMessage } from "@langchain/core/messages";
3
+ import {
4
+ matchesSubscription,
5
+ SubscriptionHandle,
6
+ } from "@langchain/langgraph-sdk/client";
7
+ import {
8
+ type Event,
9
+ messagesProjection,
10
+ type ProjectionSpec,
11
+ StreamStore,
12
+ } from "@langchain/langgraph-sdk/stream";
13
+ import { subagentMessagesProjection } from "./subagentMessagesProjection";
14
+
15
+ const PARENT = ["tools:parent"];
16
+ const CHILD = ["tools:parent", "tools:child"];
17
+
18
+ const human = (id: string, content: string) => ({
19
+ id,
20
+ type: "human",
21
+ content,
22
+ });
23
+ const ai = (id: string, content: string, tool_calls: unknown[] = []) => ({
24
+ id,
25
+ type: "ai",
26
+ content,
27
+ tool_calls,
28
+ });
29
+ const tool = (id: string, content: string, tool_call_id: string) => ({
30
+ id,
31
+ type: "tool",
32
+ content,
33
+ tool_call_id,
34
+ });
35
+
36
+ const values = (namespace: string[], messages: unknown[]) =>
37
+ ({
38
+ method: "values",
39
+ params: { namespace, data: { messages } },
40
+ }) as unknown as Event;
41
+
42
+ const messagesEvent = (namespace: string[], data: Record<string, unknown>) =>
43
+ ({
44
+ method: "messages",
45
+ params: { namespace, node: "model", data },
46
+ }) as unknown as Event;
47
+
48
+ /**
49
+ * Feeds the projection through the per-subscription matcher the SDK client
50
+ * applies before it hands an event to a subscription.
51
+ */
52
+ const openProjection = (spec: ProjectionSpec<BaseMessage[]>) => {
53
+ let handle: SubscriptionHandle<Event> | undefined;
54
+ const unsubscribe = vi.fn(async () => {});
55
+ const subscribe = vi.fn(
56
+ async (params: SubscriptionHandle<Event>["params"]) => {
57
+ handle = new SubscriptionHandle<Event>(
58
+ "subscription",
59
+ params,
60
+ unsubscribe,
61
+ );
62
+ return handle;
63
+ },
64
+ );
65
+ const store = new StreamStore<BaseMessage[]>(spec.initial);
66
+ const runtime = spec.open({
67
+ thread: { subscribe } as never,
68
+ store,
69
+ rootBus: {
70
+ channels: [
71
+ "values",
72
+ "checkpoints",
73
+ "lifecycle",
74
+ "input",
75
+ "messages",
76
+ "tools",
77
+ ],
78
+ subscribe: () => () => {},
79
+ },
80
+ });
81
+ const push = (event: Event) => {
82
+ if (matchesSubscription(event, handle!.params)) handle!.push(event);
83
+ };
84
+ const ids = () => store.getSnapshot().map((message) => message.id);
85
+ return { subscribe, unsubscribe, runtime, push, ids };
86
+ };
87
+
88
+ const parentTurn = [
89
+ human("parent-human", "research"),
90
+ ai("parent-ai", "", [
91
+ { id: "call-child", name: "task", args: { subagent_type: "worker" } },
92
+ ]),
93
+ ];
94
+
95
+ const childRun = [
96
+ values(CHILD, [human("child-human", "sub task")]),
97
+ messagesEvent(CHILD, { event: "message-start", id: "child-ai", role: "ai" }),
98
+ messagesEvent(CHILD, {
99
+ event: "content-block-start",
100
+ index: 0,
101
+ content: { type: "text", text: "hello from the child" },
102
+ }),
103
+ messagesEvent(CHILD, { event: "message-finish" }),
104
+ values(CHILD, [
105
+ human("child-human", "sub task"),
106
+ ai("child-ai", "hello from the child"),
107
+ ]),
108
+ ];
109
+
110
+ describe("subagentMessagesProjection", () => {
111
+ it("keeps the SDK projection's identity and pins its subscription to depth 0", async () => {
112
+ const spec = subagentMessagesProjection(PARENT);
113
+ expect(spec.namespace).toEqual(PARENT);
114
+ expect(spec.initial).toEqual([]);
115
+ expect(spec.key).not.toBe(messagesProjection(PARENT).key);
116
+
117
+ const { subscribe, runtime } = openProjection(spec);
118
+ await vi.waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
119
+ expect(subscribe).toHaveBeenCalledWith({
120
+ channels: ["messages", "values"],
121
+ namespaces: [PARENT],
122
+ depth: 0,
123
+ });
124
+ await runtime.dispose();
125
+ });
126
+
127
+ it("ignores a nested subagent's events while the parent's tool call runs", async () => {
128
+ const { subscribe, runtime, push, ids } = openProjection(
129
+ subagentMessagesProjection(PARENT),
130
+ );
131
+ await vi.waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
132
+
133
+ push(values(PARENT, parentTurn));
134
+ await vi.waitFor(() =>
135
+ expect(ids()).toEqual(["parent-human", "parent-ai"]),
136
+ );
137
+
138
+ for (const event of childRun) push(event);
139
+ push(
140
+ messagesEvent(PARENT, {
141
+ event: "message-start",
142
+ id: "parent-sentinel",
143
+ role: "ai",
144
+ }),
145
+ );
146
+ push(messagesEvent(PARENT, { event: "message-finish" }));
147
+ await vi.waitFor(() => expect(ids()).toContain("parent-sentinel"));
148
+ expect(ids()).toEqual(["parent-human", "parent-ai", "parent-sentinel"]);
149
+
150
+ push(
151
+ values(PARENT, [
152
+ ...parentTurn,
153
+ tool("parent-tool", "done", "call-child"),
154
+ ]),
155
+ );
156
+ await vi.waitFor(() =>
157
+ expect(ids()).toEqual([
158
+ "parent-human",
159
+ "parent-ai",
160
+ "parent-tool",
161
+ "parent-sentinel",
162
+ ]),
163
+ );
164
+ await runtime.dispose();
165
+ });
166
+
167
+ it("unsubscribes the underlying subscription on dispose", async () => {
168
+ const { subscribe, unsubscribe, runtime } = openProjection(
169
+ subagentMessagesProjection(PARENT),
170
+ );
171
+ await vi.waitFor(() => expect(subscribe).toHaveBeenCalledOnce());
172
+ await runtime.dispose();
173
+ await vi.waitFor(() => expect(unsubscribe).toHaveBeenCalledOnce());
174
+ });
175
+ });