@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.
- package/LICENSE +1 -1
- package/dist/attachSubagentTranscripts.d.ts +13 -14
- package/dist/attachSubagentTranscripts.d.ts.map +1 -1
- package/dist/convertMessages.d.ts +6 -9
- package/dist/convertMessages.d.ts.map +1 -1
- package/dist/convertMessages.js +57 -15
- package/dist/convertMessages.js.map +1 -1
- package/dist/converter.d.ts +128 -128
- package/dist/converter.d.ts.map +1 -1
- package/dist/converter.js +2 -1
- package/dist/converter.js.map +1 -1
- package/dist/findForkCheckpointInHistory.d.ts +15 -16
- package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
- package/dist/hooks.d.ts +5 -7
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -0
- package/dist/resolveForkCheckpoint.d.ts +4 -5
- package/dist/resolveForkCheckpoint.d.ts.map +1 -1
- package/dist/runtimeExtras.d.ts +1 -3
- package/dist/runtimeExtras.d.ts.map +1 -1
- package/dist/sdkIdentity.d.ts +1 -3
- package/dist/sdkIdentity.d.ts.map +1 -1
- package/dist/sdkIdentity.js +1 -1
- package/dist/streamingTiming.d.ts +2 -4
- package/dist/streamingTiming.d.ts.map +1 -1
- package/dist/subagentMessagesProjection.d.ts +17 -0
- package/dist/subagentMessagesProjection.d.ts.map +1 -0
- package/dist/subagentMessagesProjection.js +40 -0
- package/dist/subagentMessagesProjection.js.map +1 -0
- package/dist/types.d.ts +100 -103
- package/dist/types.d.ts.map +1 -1
- package/dist/uiMessages.d.ts +18 -6
- package/dist/uiMessages.d.ts.map +1 -1
- package/dist/uiMessages.js +33 -1
- package/dist/uiMessages.js.map +1 -1
- package/dist/useStreamRuntime.d.ts +7 -10
- package/dist/useStreamRuntime.d.ts.map +1 -1
- package/dist/useStreamRuntime.js +100 -26
- package/dist/useStreamRuntime.js.map +1 -1
- package/dist/useSubagentTranscripts.d.ts +3 -5
- package/dist/useSubagentTranscripts.d.ts.map +1 -1
- package/dist/useSubagentTranscripts.js +5 -3
- package/dist/useSubagentTranscripts.js.map +1 -1
- package/package.json +16 -9
- package/src/convertMessages.test.ts +153 -0
- package/src/convertMessages.ts +54 -8
- package/src/converter.ts +6 -0
- package/src/subagentMessagesProjection.test.ts +175 -0
- package/src/subagentMessagesProjection.ts +50 -0
- package/src/tests/settleOutsideAct.ts +15 -0
- package/src/uiMessages.test.ts +106 -0
- package/src/uiMessages.ts +43 -0
- package/src/useStreamRuntime.test.tsx +147 -7
- package/src/useStreamRuntime.ts +197 -45
- package/src/useStreamRuntime.voice.test.tsx +713 -0
- package/src/useSubagentTranscripts.fast-refresh.test.tsx +152 -0
- package/src/useSubagentTranscripts.test.tsx +2 -0
- 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.
|
|
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.
|
|
44
|
-
"@assistant-ui/store": "^0.3.
|
|
43
|
+
"@assistant-ui/core": "^0.3.23",
|
|
44
|
+
"@assistant-ui/store": "^0.3.17",
|
|
45
45
|
"assistant-cloud": "*",
|
|
46
|
-
"assistant-stream": "^0.3.
|
|
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.
|
|
67
|
-
"@
|
|
68
|
-
"@langchain/
|
|
69
|
-
"@langchain/
|
|
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.
|
|
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(() => {});
|
package/src/convertMessages.ts
CHANGED
|
@@ -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
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
});
|