@assistant-ui/react-a2a 0.2.40 → 0.2.42
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/dist/A2AClient.d.ts +6 -0
- package/dist/A2AClient.d.ts.map +1 -1
- package/dist/A2AClient.js +9 -38
- package/dist/A2AClient.js.map +1 -1
- package/dist/A2AThreadRuntimeCore.d.ts +14 -4
- package/dist/A2AThreadRuntimeCore.d.ts.map +1 -1
- package/dist/A2AThreadRuntimeCore.js +54 -38
- package/dist/A2AThreadRuntimeCore.js.map +1 -1
- package/dist/conversions.d.ts.map +1 -1
- package/dist/conversions.js +2 -2
- package/dist/conversions.js.map +1 -1
- package/dist/useA2ARuntime.d.ts.map +1 -1
- package/dist/useA2ARuntime.js +40 -28
- package/dist/useA2ARuntime.js.map +1 -1
- package/package.json +7 -6
- package/src/A2AClient.test.ts +69 -0
- package/src/A2AClient.ts +13 -45
- package/src/A2AThreadRuntimeCore.test.ts +130 -18
- package/src/A2AThreadRuntimeCore.ts +80 -49
- package/src/conversions.test.ts +21 -0
- package/src/conversions.ts +2 -1
- package/src/useA2ARuntime.test.tsx +104 -0
- package/src/useA2ARuntime.ts +62 -35
package/dist/useA2ARuntime.js
CHANGED
|
@@ -3,7 +3,7 @@ import { A2AClient } from "./A2AClient.js";
|
|
|
3
3
|
import { A2AThreadRuntimeCore } from "./A2AThreadRuntimeCore.js";
|
|
4
4
|
import { a2aExtras } from "./a2aExtras.js";
|
|
5
5
|
import { useExternalStoreRuntime, useExternalStoreSharedOptions, useRuntimeAdapters } from "@assistant-ui/core/react";
|
|
6
|
-
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
7
7
|
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
8
8
|
//#region src/useA2ARuntime.ts
|
|
9
9
|
const serializeManagedClientOptions = (options) => {
|
|
@@ -14,8 +14,6 @@ const serializeManagedClientOptions = (options) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
function useA2ARuntime(options) {
|
|
17
|
-
const [_version, setVersion] = useState(0);
|
|
18
|
-
const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);
|
|
19
17
|
const runtimeAdapters = useRuntimeAdapters();
|
|
20
18
|
const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;
|
|
21
19
|
const threadListAdapter = options.adapters?.threadList;
|
|
@@ -70,8 +68,7 @@ function useA2ARuntime(options) {
|
|
|
70
68
|
coreOptionsRef.current = coreOptions;
|
|
71
69
|
const createCore = () => new A2AThreadRuntimeCore({
|
|
72
70
|
...coreOptionsRef.current,
|
|
73
|
-
client
|
|
74
|
-
notifyUpdate
|
|
71
|
+
client
|
|
75
72
|
});
|
|
76
73
|
const [pinnedCore, setPinnedCore] = useState(() => ({
|
|
77
74
|
client,
|
|
@@ -86,33 +83,48 @@ function useA2ARuntime(options) {
|
|
|
86
83
|
setPinnedCore(currentCore);
|
|
87
84
|
}
|
|
88
85
|
const core = currentCore.core;
|
|
86
|
+
const snapshot = useSyncExternalStore(core.subscribe, core.getSnapshot, core.getSnapshot);
|
|
89
87
|
useEffect(() => {
|
|
90
88
|
core.updateOptions(coreOptions);
|
|
91
89
|
});
|
|
92
90
|
const threadSwitchGenerationRef = useRef(0);
|
|
91
|
+
const switchingGenerationRef = useRef(null);
|
|
93
92
|
const threadList = useMemo(() => {
|
|
94
93
|
if (!threadListAdapter) return void 0;
|
|
95
94
|
const { onSwitchToNewThread, onSwitchToThread } = threadListAdapter;
|
|
95
|
+
const releaseSwitch = (generation) => {
|
|
96
|
+
if (switchingGenerationRef.current === generation) switchingGenerationRef.current = null;
|
|
97
|
+
};
|
|
96
98
|
return {
|
|
97
99
|
threadId: threadListAdapter.threadId,
|
|
98
100
|
onSwitchToNewThread: onSwitchToNewThread ? async () => {
|
|
99
101
|
const generation = ++threadSwitchGenerationRef.current;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
switchingGenerationRef.current = generation;
|
|
103
|
+
try {
|
|
104
|
+
core.applyExternalMessages([]);
|
|
105
|
+
core.resetContext();
|
|
106
|
+
await onSwitchToNewThread();
|
|
107
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
108
|
+
core.applyExternalMessages([]);
|
|
109
|
+
core.resetContext();
|
|
110
|
+
} finally {
|
|
111
|
+
releaseSwitch(generation);
|
|
112
|
+
}
|
|
106
113
|
} : void 0,
|
|
107
114
|
onSwitchToThread: onSwitchToThread ? async (threadId) => {
|
|
108
115
|
const generation = ++threadSwitchGenerationRef.current;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
switchingGenerationRef.current = generation;
|
|
117
|
+
try {
|
|
118
|
+
core.applyExternalMessages([]);
|
|
119
|
+
core.resetContext();
|
|
120
|
+
const result = await onSwitchToThread(threadId);
|
|
121
|
+
if (generation !== threadSwitchGenerationRef.current) return;
|
|
122
|
+
core.applyExternalMessages([]);
|
|
123
|
+
core.applyExternalMessages(result.messages);
|
|
124
|
+
core.resetContext();
|
|
125
|
+
} finally {
|
|
126
|
+
releaseSwitch(generation);
|
|
127
|
+
}
|
|
116
128
|
} : void 0
|
|
117
129
|
};
|
|
118
130
|
}, [threadListAdapter, core]);
|
|
@@ -133,19 +145,19 @@ function useA2ARuntime(options) {
|
|
|
133
145
|
const store = useMemo(() => {
|
|
134
146
|
return {
|
|
135
147
|
...shared,
|
|
136
|
-
isLoading:
|
|
137
|
-
messageRepository:
|
|
138
|
-
isRunning:
|
|
148
|
+
isLoading: snapshot.isLoading,
|
|
149
|
+
messageRepository: snapshot.messageRepository,
|
|
150
|
+
isRunning: snapshot.isRunning,
|
|
139
151
|
unstable_persistsHistory: true,
|
|
140
152
|
extras: a2aExtras.provide({
|
|
141
|
-
task:
|
|
142
|
-
artifacts:
|
|
143
|
-
agentCard:
|
|
153
|
+
task: snapshot.task,
|
|
154
|
+
artifacts: snapshot.artifacts,
|
|
155
|
+
agentCard: snapshot.agentCard
|
|
144
156
|
}),
|
|
145
|
-
onNew: (message) => core.append(message),
|
|
157
|
+
onNew: (message) => switchingGenerationRef.current === null ? core.append(message) : Promise.resolve(),
|
|
146
158
|
onVoiceTranscript: (message) => core.appendVoiceTranscript(message),
|
|
147
|
-
onEdit: (message) => core.edit(message),
|
|
148
|
-
onReload: (parentId) => core.reload(parentId),
|
|
159
|
+
onEdit: (message) => switchingGenerationRef.current === null ? core.edit(message) : Promise.resolve(),
|
|
160
|
+
onReload: (parentId) => switchingGenerationRef.current === null ? core.reload(parentId) : Promise.resolve(),
|
|
149
161
|
onCancel: () => core.cancel(),
|
|
150
162
|
unstable_onRecordToolInteraction: (options) => core.recordToolInteraction(options),
|
|
151
163
|
setMessages: (messages) => core.applyExternalMessages(messages),
|
|
@@ -155,7 +167,7 @@ function useA2ARuntime(options) {
|
|
|
155
167
|
}, [
|
|
156
168
|
adapterAdapters,
|
|
157
169
|
core,
|
|
158
|
-
|
|
170
|
+
snapshot,
|
|
159
171
|
shared
|
|
160
172
|
]);
|
|
161
173
|
const runtime = useExternalStoreRuntime(store);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useA2ARuntime.js","names":[],"sources":["../src/useA2ARuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport { A2AClient, type A2AClientOptions } from \"./A2AClient\";\nimport { A2AThreadRuntimeCore } from \"./A2AThreadRuntimeCore\";\nimport { a2aExtras } from \"./a2aExtras\";\nimport type { UseA2ARuntimeOptions } from \"./types\";\n\ntype ManagedA2AClientOptions = Omit<A2AClientOptions, \"headers\">;\n\nconst serializeManagedClientOptions = (\n options: ManagedA2AClientOptions,\n): string => {\n const fetchOptions = Object.fromEntries(\n Object.entries(options.fetchOptions ?? {}).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n\n return JSON.stringify({\n ...options,\n fetchOptions,\n });\n};\n\nexport function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {\n const [_version, setVersion] = useState(0);\n const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);\n const runtimeAdapters = useRuntimeAdapters();\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n const headersRef = useRef(options.headers);\n useInsertionEffect(() => {\n headersRef.current = options.headers;\n });\n const resolveHeaders = useCallback(() => {\n const headers = headersRef.current;\n return typeof headers === \"function\" ? headers() : (headers ?? {});\n }, []);\n\n const managedClientOptionsKey = options.client\n ? null\n : options.baseUrl\n ? serializeManagedClientOptions({\n baseUrl: options.baseUrl,\n basePath: options.basePath,\n tenant: options.tenant,\n extensions: options.extensions,\n fetchOptions: options.fetchOptions,\n })\n : null;\n\n const createClient = () => {\n if (options.client) return options.client;\n if (!managedClientOptionsKey) {\n throw new Error(\"useA2ARuntime requires either `client` or `baseUrl`\");\n }\n\n return new A2AClient({\n ...(JSON.parse(managedClientOptionsKey) as ManagedA2AClientOptions),\n headers: resolveHeaders,\n });\n };\n const [pinnedClient, setPinnedClient] = useState(() => ({\n key: managedClientOptionsKey,\n provided: options.client,\n client: createClient(),\n }));\n let currentClient = pinnedClient;\n if (\n pinnedClient.key !== managedClientOptionsKey ||\n pinnedClient.provided !== options.client\n ) {\n currentClient = {\n key: managedClientOptionsKey,\n provided: options.client,\n client: createClient(),\n };\n setPinnedClient(currentClient);\n }\n const client = currentClient.client;\n\n const coreOptions = {\n client,\n contextId: options.contextId,\n configuration: options.configuration,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(options.onArtifactComplete && {\n onArtifactComplete: options.onArtifactComplete,\n }),\n ...(historyAdapter && { history: historyAdapter }),\n };\n const coreOptionsRef = useRef(coreOptions);\n coreOptionsRef.current = coreOptions;\n\n const createCore = () =>\n new A2AThreadRuntimeCore({\n ...coreOptionsRef.current,\n client,\n notifyUpdate,\n });\n const [pinnedCore, setPinnedCore] = useState(() => ({\n client,\n core: createCore(),\n }));\n let currentCore = pinnedCore;\n if (pinnedCore.client !== client) {\n currentCore = { client, core: createCore() };\n setPinnedCore(currentCore);\n }\n const core = currentCore.core;\n\n useEffect(() => {\n core.updateOptions(coreOptions);\n });\n\n // Thread list\n const threadSwitchGenerationRef = useRef(0);\n const threadList = useMemo(() => {\n if (!threadListAdapter) return undefined;\n\n const { onSwitchToNewThread, onSwitchToThread } = threadListAdapter;\n\n return {\n threadId: threadListAdapter.threadId,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetContext();\n await onSwitchToNewThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n // Apply first so the abort inside resetContext finds an already\n // cleared repository and cannot persist the old thread's partial\n // assistant message.\n core.applyExternalMessages([]);\n core.resetContext();\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetContext();\n const result = await onSwitchToThread(threadId);\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.applyExternalMessages(result.messages);\n core.resetContext();\n }\n : undefined,\n };\n }, [threadListAdapter, core]);\n\n // Adapters\n const adapters = options.adapters;\n const adapterAdapters = useMemo(\n () => ({\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n threadList,\n }),\n [adapters, runtimeAdapters, threadList],\n );\n\n // Build store adapter\n const shared = useExternalStoreSharedOptions(options);\n const store = useMemo(() => {\n void _version;\n\n return {\n ...shared,\n isLoading: core.isLoading,\n messageRepository: core.getMessageRepository(),\n isRunning: core.isRunning(),\n unstable_persistsHistory: true,\n extras: a2aExtras.provide({\n task: core.getTask(),\n artifacts: core.getArtifacts(),\n agentCard: core.getAgentCard(),\n }),\n onNew: (message: AppendMessage) => core.append(message),\n onVoiceTranscript: (message: ThreadMessage) =>\n core.appendVoiceTranscript(message),\n onEdit: (message: AppendMessage) => core.edit(message),\n onReload: (parentId: string | null) => core.reload(parentId),\n onCancel: () => core.cancel(),\n unstable_onRecordToolInteraction: (options) =>\n core.recordToolInteraction(options),\n setMessages: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onImport: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n adapters: adapterAdapters,\n } satisfies ExternalStoreAdapter<ThreadMessage>;\n }, [adapterAdapters, core, _version, shared]);\n\n const runtime = useExternalStoreRuntime(store);\n\n useReplaySafeEffect(() => {\n core.attachRuntime(runtime);\n return () => {\n core.detachRuntime();\n };\n }, [core, runtime]);\n\n useEffect(() => {\n core.__internal_load();\n }, [core]);\n\n return runtime;\n}\n"],"mappings":";;;;;;;;AA6BA,MAAM,iCACJ,YACW;CACX,MAAM,eAAe,OAAO,YAC1B,OAAO,QAAQ,QAAQ,gBAAgB,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OACrD,EAAE,cAAc,CAAC,CACnB,CACF;CAEA,OAAO,KAAK,UAAU;EACpB,GAAG;EACH;CACF,CAAC;AACH;AAEA,SAAgB,cAAc,SAAiD;CAC7E,MAAM,CAAC,UAAU,cAAc,SAAS,CAAC;CACzC,MAAM,eAAe,kBAAkB,YAAY,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;CACnE,MAAM,kBAAkB,mBAAmB;CAC3C,MAAM,iBAAiB,QAAQ,UAAU,WAAW,iBAAiB;CACrE,MAAM,oBAAoB,QAAQ,UAAU;CAE5C,MAAM,aAAa,OAAO,QAAQ,OAAO;CACzC,yBAAyB;EACvB,WAAW,UAAU,QAAQ;CAC/B,CAAC;CACD,MAAM,iBAAiB,kBAAkB;EACvC,MAAM,UAAU,WAAW;EAC3B,OAAO,OAAO,YAAY,aAAa,QAAQ,IAAK,WAAW,CAAC;CAClE,GAAG,CAAC,CAAC;CAEL,MAAM,0BAA0B,QAAQ,SACpC,OACA,QAAQ,UACN,8BAA8B;EAC5B,SAAS,QAAQ;EACjB,UAAU,QAAQ;EAClB,QAAQ,QAAQ;EAChB,YAAY,QAAQ;EACpB,cAAc,QAAQ;CACxB,CAAC,IACD;CAEN,MAAM,qBAAqB;EACzB,IAAI,QAAQ,QAAQ,OAAO,QAAQ;EACnC,IAAI,CAAC,yBACH,MAAM,IAAI,MAAM,qDAAqD;EAGvE,OAAO,IAAI,UAAU;GACnB,GAAI,KAAK,MAAM,uBAAuB;GACtC,SAAS;EACX,CAAC;CACH;CACA,MAAM,CAAC,cAAc,mBAAmB,gBAAgB;EACtD,KAAK;EACL,UAAU,QAAQ;EAClB,QAAQ,aAAa;CACvB,EAAE;CACF,IAAI,gBAAgB;CACpB,IACE,aAAa,QAAQ,2BACrB,aAAa,aAAa,QAAQ,QAClC;EACA,gBAAgB;GACd,KAAK;GACL,UAAU,QAAQ;GAClB,QAAQ,aAAa;EACvB;EACA,gBAAgB,aAAa;CAC/B;CACA,MAAM,SAAS,cAAc;CAE7B,MAAM,cAAc;EAClB;EACA,WAAW,QAAQ;EACnB,eAAe,QAAQ;EACvB,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;EAClD,GAAI,QAAQ,YAAY,EAAE,UAAU,QAAQ,SAAS;EACrD,GAAI,QAAQ,sBAAsB,EAChC,oBAAoB,QAAQ,mBAC9B;EACA,GAAI,kBAAkB,EAAE,SAAS,eAAe;CAClD;CACA,MAAM,iBAAiB,OAAO,WAAW;CACzC,eAAe,UAAU;CAEzB,MAAM,mBACJ,IAAI,qBAAqB;EACvB,GAAG,eAAe;EAClB;EACA;CACF,CAAC;CACH,MAAM,CAAC,YAAY,iBAAiB,gBAAgB;EAClD;EACA,MAAM,WAAW;CACnB,EAAE;CACF,IAAI,cAAc;CAClB,IAAI,WAAW,WAAW,QAAQ;EAChC,cAAc;GAAE;GAAQ,MAAM,WAAW;EAAE;EAC3C,cAAc,WAAW;CAC3B;CACA,MAAM,OAAO,YAAY;CAEzB,gBAAgB;EACd,KAAK,cAAc,WAAW;CAChC,CAAC;CAGD,MAAM,4BAA4B,OAAO,CAAC;CAC1C,MAAM,aAAa,cAAc;EAC/B,IAAI,CAAC,mBAAmB,OAAO,KAAA;EAE/B,MAAM,EAAE,qBAAqB,qBAAqB;EAElD,OAAO;GACL,UAAU,kBAAkB;GAC5B,qBAAqB,sBACjB,YAAY;IACV,MAAM,aAAa,EAAE,0BAA0B;IAG/C,KAAK,sBAAsB,CAAC,CAAC;IAC7B,KAAK,aAAa;IAClB,MAAM,oBAAoB;IAC1B,IAAI,eAAe,0BAA0B,SAAS;IAItD,KAAK,sBAAsB,CAAC,CAAC;IAC7B,KAAK,aAAa;GACpB,IACA,KAAA;GACJ,kBAAkB,mBACd,OAAO,aAAqB;IAC1B,MAAM,aAAa,EAAE,0BAA0B;IAG/C,KAAK,sBAAsB,CAAC,CAAC;IAC7B,KAAK,aAAa;IAClB,MAAM,SAAS,MAAM,iBAAiB,QAAQ;IAC9C,IAAI,eAAe,0BAA0B,SAAS;IACtD,KAAK,sBAAsB,CAAC,CAAC;IAC7B,KAAK,sBAAsB,OAAO,QAAQ;IAC1C,KAAK,aAAa;GACpB,IACA,KAAA;EACN;CACF,GAAG,CAAC,mBAAmB,IAAI,CAAC;CAG5B,MAAM,WAAW,QAAQ;CACzB,MAAM,kBAAkB,eACf;EACL,aAAa,UAAU,eAAe,iBAAiB;EACvD,QAAQ,UAAU;EAClB,WAAW,UAAU;EACrB,OAAO,UAAU;EACjB,UAAU,UAAU;EACpB;CACF,IACA;EAAC;EAAU;EAAiB;CAAU,CACxC;CAGA,MAAM,SAAS,8BAA8B,OAAO;CACpD,MAAM,QAAQ,cAAc;EAG1B,OAAO;GACL,GAAG;GACH,WAAW,KAAK;GAChB,mBAAmB,KAAK,qBAAqB;GAC7C,WAAW,KAAK,UAAU;GAC1B,0BAA0B;GAC1B,QAAQ,UAAU,QAAQ;IACxB,MAAM,KAAK,QAAQ;IACnB,WAAW,KAAK,aAAa;IAC7B,WAAW,KAAK,aAAa;GAC/B,CAAC;GACD,QAAQ,YAA2B,KAAK,OAAO,OAAO;GACtD,oBAAoB,YAClB,KAAK,sBAAsB,OAAO;GACpC,SAAS,YAA2B,KAAK,KAAK,OAAO;GACrD,WAAW,aAA4B,KAAK,OAAO,QAAQ;GAC3D,gBAAgB,KAAK,OAAO;GAC5B,mCAAmC,YACjC,KAAK,sBAAsB,OAAO;GACpC,cAAc,aACZ,KAAK,sBAAsB,QAAQ;GACrC,WAAW,aACT,KAAK,sBAAsB,QAAQ;GACrC,UAAU;EACZ;CACF,GAAG;EAAC;EAAiB;EAAM;EAAU;CAAM,CAAC;CAE5C,MAAM,UAAU,wBAAwB,KAAK;CAE7C,0BAA0B;EACxB,KAAK,cAAc,OAAO;EAC1B,aAAa;GACX,KAAK,cAAc;EACrB;CACF,GAAG,CAAC,MAAM,OAAO,CAAC;CAElB,gBAAgB;EACd,KAAK,gBAAgB;CACvB,GAAG,CAAC,IAAI,CAAC;CAET,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"useA2ARuntime.js","names":[],"sources":["../src/useA2ARuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport { A2AClient, type A2AClientOptions } from \"./A2AClient\";\nimport { A2AThreadRuntimeCore } from \"./A2AThreadRuntimeCore\";\nimport { a2aExtras } from \"./a2aExtras\";\nimport type { UseA2ARuntimeOptions } from \"./types\";\n\ntype ManagedA2AClientOptions = Omit<A2AClientOptions, \"headers\">;\n\nconst serializeManagedClientOptions = (\n options: ManagedA2AClientOptions,\n): string => {\n const fetchOptions = Object.fromEntries(\n Object.entries(options.fetchOptions ?? {}).sort(([a], [b]) =>\n a.localeCompare(b),\n ),\n );\n\n return JSON.stringify({\n ...options,\n fetchOptions,\n });\n};\n\nexport function useA2ARuntime(options: UseA2ARuntimeOptions): AssistantRuntime {\n const runtimeAdapters = useRuntimeAdapters();\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n const headersRef = useRef(options.headers);\n useInsertionEffect(() => {\n headersRef.current = options.headers;\n });\n const resolveHeaders = useCallback(() => {\n const headers = headersRef.current;\n return typeof headers === \"function\" ? headers() : (headers ?? {});\n }, []);\n\n const managedClientOptionsKey = options.client\n ? null\n : options.baseUrl\n ? serializeManagedClientOptions({\n baseUrl: options.baseUrl,\n basePath: options.basePath,\n tenant: options.tenant,\n extensions: options.extensions,\n fetchOptions: options.fetchOptions,\n })\n : null;\n\n const createClient = () => {\n if (options.client) return options.client;\n if (!managedClientOptionsKey) {\n throw new Error(\"useA2ARuntime requires either `client` or `baseUrl`\");\n }\n\n return new A2AClient({\n ...(JSON.parse(managedClientOptionsKey) as ManagedA2AClientOptions),\n headers: resolveHeaders,\n });\n };\n const [pinnedClient, setPinnedClient] = useState(() => ({\n key: managedClientOptionsKey,\n provided: options.client,\n client: createClient(),\n }));\n let currentClient = pinnedClient;\n if (\n pinnedClient.key !== managedClientOptionsKey ||\n pinnedClient.provided !== options.client\n ) {\n currentClient = {\n key: managedClientOptionsKey,\n provided: options.client,\n client: createClient(),\n };\n setPinnedClient(currentClient);\n }\n const client = currentClient.client;\n\n const coreOptions = {\n client,\n contextId: options.contextId,\n configuration: options.configuration,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(options.onArtifactComplete && {\n onArtifactComplete: options.onArtifactComplete,\n }),\n ...(historyAdapter && { history: historyAdapter }),\n };\n const coreOptionsRef = useRef(coreOptions);\n coreOptionsRef.current = coreOptions;\n\n const createCore = () =>\n new A2AThreadRuntimeCore({\n ...coreOptionsRef.current,\n client,\n });\n const [pinnedCore, setPinnedCore] = useState(() => ({\n client,\n core: createCore(),\n }));\n let currentCore = pinnedCore;\n if (pinnedCore.client !== client) {\n currentCore = { client, core: createCore() };\n setPinnedCore(currentCore);\n }\n const core = currentCore.core;\n const snapshot = useSyncExternalStore(\n core.subscribe,\n core.getSnapshot,\n core.getSnapshot,\n );\n\n useEffect(() => {\n core.updateOptions(coreOptions);\n });\n\n // Thread list\n const threadSwitchGenerationRef = useRef(0);\n const switchingGenerationRef = useRef<number | null>(null);\n const threadList = useMemo(() => {\n if (!threadListAdapter) return undefined;\n\n const { onSwitchToNewThread, onSwitchToThread } = threadListAdapter;\n\n const releaseSwitch = (generation: number) => {\n if (switchingGenerationRef.current === generation) {\n switchingGenerationRef.current = null;\n }\n };\n\n return {\n threadId: threadListAdapter.threadId,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetContext();\n await onSwitchToNewThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n // Apply first so the abort inside resetContext finds an already\n // cleared repository and cannot persist the old thread's partial\n // assistant message.\n core.applyExternalMessages([]);\n core.resetContext();\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetContext();\n const result = await onSwitchToThread(threadId);\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.applyExternalMessages(result.messages);\n core.resetContext();\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n };\n }, [threadListAdapter, core]);\n\n // Adapters\n const adapters = options.adapters;\n const adapterAdapters = useMemo(\n () => ({\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n threadList,\n }),\n [adapters, runtimeAdapters, threadList],\n );\n\n // Build store adapter\n const shared = useExternalStoreSharedOptions(options);\n const store = useMemo(() => {\n return {\n ...shared,\n isLoading: snapshot.isLoading,\n messageRepository: snapshot.messageRepository,\n isRunning: snapshot.isRunning,\n unstable_persistsHistory: true,\n extras: a2aExtras.provide({\n task: snapshot.task,\n artifacts: snapshot.artifacts,\n agentCard: snapshot.agentCard,\n }),\n onNew: (message: AppendMessage) =>\n switchingGenerationRef.current === null\n ? core.append(message)\n : Promise.resolve(),\n onVoiceTranscript: (message: ThreadMessage) =>\n core.appendVoiceTranscript(message),\n onEdit: (message: AppendMessage) =>\n switchingGenerationRef.current === null\n ? core.edit(message)\n : Promise.resolve(),\n onReload: (parentId: string | null) =>\n switchingGenerationRef.current === null\n ? core.reload(parentId)\n : Promise.resolve(),\n onCancel: () => core.cancel(),\n unstable_onRecordToolInteraction: (options) =>\n core.recordToolInteraction(options),\n setMessages: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onImport: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n adapters: adapterAdapters,\n } satisfies ExternalStoreAdapter<ThreadMessage>;\n }, [adapterAdapters, core, snapshot, shared]);\n\n const runtime = useExternalStoreRuntime(store);\n\n useReplaySafeEffect(() => {\n core.attachRuntime(runtime);\n return () => {\n core.detachRuntime();\n };\n }, [core, runtime]);\n\n useEffect(() => {\n core.__internal_load();\n }, [core]);\n\n return runtime;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-a2a",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.42",
|
|
4
4
|
"description": "A2A (Agent-to-Agent) v1.0 protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"a2a",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@assistant-ui/core": "^0.3.
|
|
33
|
-
"@assistant-ui/
|
|
34
|
-
"@assistant-ui/store": "^0.3.
|
|
35
|
-
"assistant-stream": "^0.3.
|
|
32
|
+
"@assistant-ui/core": "^0.3.25",
|
|
33
|
+
"@assistant-ui/generative-ui": "^0.0.1",
|
|
34
|
+
"@assistant-ui/store": "^0.3.18",
|
|
35
|
+
"assistant-stream": "^0.3.49"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@types/react": "*",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@testing-library/dom": "^10.4.2",
|
|
50
50
|
"@testing-library/react": "^16.3.3",
|
|
51
51
|
"@types/react": "^19.3.0",
|
|
52
|
-
"jsdom": "^30.
|
|
52
|
+
"jsdom": "^30.1.2",
|
|
53
53
|
"react": "^19.3.0",
|
|
54
54
|
"react-dom": "^19.3.0",
|
|
55
55
|
"vitest": "^5.0.3"
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"scripts": {
|
|
71
71
|
"build": "aui-build",
|
|
72
72
|
"test": "vitest run",
|
|
73
|
+
"test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
|
|
73
74
|
"typecheck": "tsc --noEmit",
|
|
74
75
|
"test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
|
|
75
76
|
"test:types:peer-react18": "aui-react18-types",
|
package/src/A2AClient.test.ts
CHANGED
|
@@ -1149,6 +1149,75 @@ describe("A2AClient", () => {
|
|
|
1149
1149
|
expect(evt.event.status.message?.role).toBe("agent");
|
|
1150
1150
|
});
|
|
1151
1151
|
|
|
1152
|
+
it("forwards configurable SSE line and event limits", async () => {
|
|
1153
|
+
const sseData = JSON.stringify({
|
|
1154
|
+
status_update: {
|
|
1155
|
+
task_id: "t1",
|
|
1156
|
+
context_id: "ctx-1",
|
|
1157
|
+
status: {
|
|
1158
|
+
state: "TASK_STATE_WORKING",
|
|
1159
|
+
message: {
|
|
1160
|
+
message_id: "s1",
|
|
1161
|
+
role: "ROLE_AGENT",
|
|
1162
|
+
parts: [{ text: "x".repeat(64) }],
|
|
1163
|
+
},
|
|
1164
|
+
},
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
const consume = async (options: {
|
|
1168
|
+
maxStreamLineLength: number;
|
|
1169
|
+
maxStreamEventLength: number;
|
|
1170
|
+
}) => {
|
|
1171
|
+
fetchMock.mockResolvedValueOnce(
|
|
1172
|
+
mockSSEResponse([`data: ${sseData}`, "", ""]),
|
|
1173
|
+
);
|
|
1174
|
+
const limitedClient = new A2AClient({
|
|
1175
|
+
baseUrl: "https://agent.test",
|
|
1176
|
+
...options,
|
|
1177
|
+
});
|
|
1178
|
+
const events: A2AStreamEvent[] = [];
|
|
1179
|
+
for await (const event of limitedClient.streamMessage(userMessage)) {
|
|
1180
|
+
events.push(event);
|
|
1181
|
+
}
|
|
1182
|
+
return events;
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
await expect(
|
|
1186
|
+
consume({ maxStreamLineLength: 32, maxStreamEventLength: 1_024 }),
|
|
1187
|
+
).rejects.toThrow("SSE line exceeds maxLineLength");
|
|
1188
|
+
await expect(
|
|
1189
|
+
consume({ maxStreamLineLength: 1_024, maxStreamEventLength: 32 }),
|
|
1190
|
+
).rejects.toThrow("SSE event exceeds maxEventLength");
|
|
1191
|
+
await expect(
|
|
1192
|
+
consume({ maxStreamLineLength: 1_024, maxStreamEventLength: 1_024 }),
|
|
1193
|
+
).resolves.toHaveLength(1);
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
it("releases the response reader when decoder limits are invalid", async () => {
|
|
1197
|
+
const cancel = vi.fn();
|
|
1198
|
+
const body = new ReadableStream<Uint8Array>({ cancel });
|
|
1199
|
+
fetchMock.mockResolvedValueOnce(
|
|
1200
|
+
new Response(body, {
|
|
1201
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
1202
|
+
}),
|
|
1203
|
+
);
|
|
1204
|
+
const limitedClient = new A2AClient({
|
|
1205
|
+
baseUrl: "https://agent.test",
|
|
1206
|
+
maxStreamLineLength: 0,
|
|
1207
|
+
});
|
|
1208
|
+
const consume = async () => {
|
|
1209
|
+
for await (const _event of limitedClient.streamMessage(userMessage)) {
|
|
1210
|
+
void _event;
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
await expect(consume()).rejects.toThrow(
|
|
1215
|
+
"maxLineLength must be a positive safe integer",
|
|
1216
|
+
);
|
|
1217
|
+
expect(cancel).toHaveBeenCalledOnce();
|
|
1218
|
+
expect(body.locked).toBe(false);
|
|
1219
|
+
});
|
|
1220
|
+
|
|
1152
1221
|
it("drops a wrapped task or message whose ids are not strings", async () => {
|
|
1153
1222
|
const frames = [
|
|
1154
1223
|
{
|
package/src/A2AClient.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SSEEventDecoder, type SSEEvent } from "assistant-stream/utils";
|
|
2
|
-
import { isRecord } from "@assistant-ui/core/internal";
|
|
2
|
+
import { isRecord, raceWithAbortSignal } from "@assistant-ui/core/internal";
|
|
3
3
|
import type {
|
|
4
4
|
A2AAgentCard,
|
|
5
5
|
A2AErrorInfo,
|
|
@@ -33,6 +33,10 @@ export type A2AClientOptions = {
|
|
|
33
33
|
fetchOptions?:
|
|
34
34
|
| Omit<RequestInit, "headers" | "body" | "method" | "signal">
|
|
35
35
|
| undefined;
|
|
36
|
+
/** Maximum UTF-16 code units accepted in one SSE line. Defaults to 16 MiB. */
|
|
37
|
+
maxStreamLineLength?: number | undefined;
|
|
38
|
+
/** Maximum UTF-16 code units retained across one SSE event. Defaults to 16 MiB. */
|
|
39
|
+
maxStreamEventLength?: number | undefined;
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
export class A2AError extends Error {
|
|
@@ -680,49 +684,6 @@ function signalInit(signal?: AbortSignal): RequestInit {
|
|
|
680
684
|
return signal ? { signal } : {};
|
|
681
685
|
}
|
|
682
686
|
|
|
683
|
-
const getAbortReason = (signal: AbortSignal): unknown => {
|
|
684
|
-
if (signal.reason !== undefined) return signal.reason;
|
|
685
|
-
const error = new Error("The operation was aborted");
|
|
686
|
-
error.name = "AbortError";
|
|
687
|
-
return error;
|
|
688
|
-
};
|
|
689
|
-
|
|
690
|
-
const raceWithAbortSignal = <T>(
|
|
691
|
-
signal: AbortSignal | undefined,
|
|
692
|
-
operation: () => T | PromiseLike<T>,
|
|
693
|
-
): Promise<T> => {
|
|
694
|
-
if (!signal) return Promise.resolve().then(operation);
|
|
695
|
-
if (signal.aborted) return Promise.reject(getAbortReason(signal));
|
|
696
|
-
|
|
697
|
-
return new Promise<T>((resolve, reject) => {
|
|
698
|
-
let settled = false;
|
|
699
|
-
const cleanup = () => signal.removeEventListener("abort", handleAbort);
|
|
700
|
-
const resolveOnce = (value: T) => {
|
|
701
|
-
if (settled) return;
|
|
702
|
-
settled = true;
|
|
703
|
-
cleanup();
|
|
704
|
-
resolve(value);
|
|
705
|
-
};
|
|
706
|
-
const rejectOnce = (error: unknown) => {
|
|
707
|
-
if (settled) return;
|
|
708
|
-
settled = true;
|
|
709
|
-
cleanup();
|
|
710
|
-
reject(error);
|
|
711
|
-
};
|
|
712
|
-
const handleAbort = () => rejectOnce(getAbortReason(signal));
|
|
713
|
-
|
|
714
|
-
signal.addEventListener("abort", handleAbort, { once: true });
|
|
715
|
-
let result: T | PromiseLike<T>;
|
|
716
|
-
try {
|
|
717
|
-
result = operation();
|
|
718
|
-
} catch (error) {
|
|
719
|
-
rejectOnce(error);
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
Promise.resolve(result).then(resolveOnce, rejectOnce);
|
|
723
|
-
});
|
|
724
|
-
};
|
|
725
|
-
|
|
726
687
|
const SKIPPED_FRAME_SNIPPET_LENGTH = 120;
|
|
727
688
|
|
|
728
689
|
function describeSkippedFrame(data: string, reason: string): string {
|
|
@@ -739,6 +700,8 @@ export class A2AClient {
|
|
|
739
700
|
private basePath: string;
|
|
740
701
|
private tenant: string | undefined;
|
|
741
702
|
private extensionUris: string[] | undefined;
|
|
703
|
+
private maxStreamLineLength: number | undefined;
|
|
704
|
+
private maxStreamEventLength: number | undefined;
|
|
742
705
|
private fetchOptions: Omit<
|
|
743
706
|
RequestInit,
|
|
744
707
|
"headers" | "body" | "method" | "signal"
|
|
@@ -754,6 +717,8 @@ export class A2AClient {
|
|
|
754
717
|
: "";
|
|
755
718
|
this.tenant = options.tenant;
|
|
756
719
|
this.extensionUris = options.extensions;
|
|
720
|
+
this.maxStreamLineLength = options.maxStreamLineLength;
|
|
721
|
+
this.maxStreamEventLength = options.maxStreamEventLength;
|
|
757
722
|
const {
|
|
758
723
|
headers: _h,
|
|
759
724
|
body: _b,
|
|
@@ -1112,7 +1077,6 @@ export class A2AClient {
|
|
|
1112
1077
|
if (!reader) throw new Error("No response body");
|
|
1113
1078
|
|
|
1114
1079
|
const decoder = new TextDecoder();
|
|
1115
|
-
const sseDecoder = new SSEEventDecoder();
|
|
1116
1080
|
|
|
1117
1081
|
let firstSkipReason: string | undefined;
|
|
1118
1082
|
const noteSkip = (data: string, reason: string) => {
|
|
@@ -1148,6 +1112,10 @@ export class A2AClient {
|
|
|
1148
1112
|
|
|
1149
1113
|
let shouldCancel = true;
|
|
1150
1114
|
try {
|
|
1115
|
+
const sseDecoder = new SSEEventDecoder({
|
|
1116
|
+
maxLineLength: this.maxStreamLineLength,
|
|
1117
|
+
maxEventLength: this.maxStreamEventLength,
|
|
1118
|
+
});
|
|
1151
1119
|
while (true) {
|
|
1152
1120
|
let result: ReadableStreamReadResult<Uint8Array>;
|
|
1153
1121
|
try {
|
|
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
|
2
2
|
import {
|
|
3
3
|
applyA2uiOperations,
|
|
4
4
|
convertSurfaceToUISpec,
|
|
5
|
-
} from "@assistant-ui/
|
|
5
|
+
} from "@assistant-ui/generative-ui/a2ui";
|
|
6
6
|
import { A2AThreadRuntimeCore } from "./A2AThreadRuntimeCore";
|
|
7
7
|
import type { A2AClient } from "./A2AClient";
|
|
8
8
|
import type {
|
|
@@ -155,10 +155,10 @@ const toolInteraction = {
|
|
|
155
155
|
};
|
|
156
156
|
|
|
157
157
|
describe("A2AThreadRuntimeCore", () => {
|
|
158
|
-
let
|
|
158
|
+
let onUpdate = vi.fn<() => void>();
|
|
159
159
|
|
|
160
160
|
beforeEach(() => {
|
|
161
|
-
|
|
161
|
+
onUpdate = vi.fn<() => void>();
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
afterEach(() => {
|
|
@@ -169,11 +169,12 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
169
169
|
clientOverrides: Partial<A2AClient> = {},
|
|
170
170
|
coreOverrides: Record<string, unknown> = {},
|
|
171
171
|
) {
|
|
172
|
-
|
|
172
|
+
const core = new A2AThreadRuntimeCore({
|
|
173
173
|
client: createMockClient(clientOverrides),
|
|
174
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
175
174
|
...coreOverrides,
|
|
176
175
|
});
|
|
176
|
+
core.subscribe(onUpdate);
|
|
177
|
+
return core;
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
describe("late history loading", () => {
|
|
@@ -406,11 +407,11 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
406
407
|
expect(core.isLoading).toBe(true);
|
|
407
408
|
core.applyExternalMessages([]);
|
|
408
409
|
core.resetContext();
|
|
409
|
-
const updatesAfterReplacement =
|
|
410
|
+
const updatesAfterReplacement = onUpdate.mock.calls.length;
|
|
410
411
|
expect(core.isLoading).toBe(false);
|
|
411
412
|
resolve({ messages: [] });
|
|
412
413
|
await loading;
|
|
413
|
-
expect(
|
|
414
|
+
expect(onUpdate).toHaveBeenCalledTimes(updatesAfterReplacement);
|
|
414
415
|
});
|
|
415
416
|
|
|
416
417
|
it("does not import old history when agent-card discovery settles after a thread switch", async () => {
|
|
@@ -635,13 +636,10 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
635
636
|
const client = createMockClient({ streamMessage });
|
|
636
637
|
const core = new A2AThreadRuntimeCore({
|
|
637
638
|
client,
|
|
638
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
639
639
|
});
|
|
640
640
|
|
|
641
641
|
await core.append(createUserAppendMessage("First"));
|
|
642
642
|
|
|
643
|
-
// useA2ARuntime re-applies its options on every render, including the
|
|
644
|
-
// renders triggered by the stream's own notifyUpdate calls.
|
|
645
643
|
core.updateOptions({ client, contextId: undefined });
|
|
646
644
|
|
|
647
645
|
await core.append(createUserAppendMessage("Second"));
|
|
@@ -657,7 +655,6 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
657
655
|
const client = createMockClient({ streamMessage });
|
|
658
656
|
const core = new A2AThreadRuntimeCore({
|
|
659
657
|
client,
|
|
660
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
661
658
|
});
|
|
662
659
|
|
|
663
660
|
await core.append(createUserAppendMessage("First"));
|
|
@@ -711,7 +708,6 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
711
708
|
const client = createMockClient({ streamMessage });
|
|
712
709
|
const core = new A2AThreadRuntimeCore({
|
|
713
710
|
client,
|
|
714
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
715
711
|
});
|
|
716
712
|
|
|
717
713
|
await core.append(createUserAppendMessage("First"));
|
|
@@ -731,7 +727,6 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
731
727
|
const client = createMockClient({ streamMessage });
|
|
732
728
|
const core = new A2AThreadRuntimeCore({
|
|
733
729
|
client,
|
|
734
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
735
730
|
});
|
|
736
731
|
|
|
737
732
|
await core.append(createUserAppendMessage("First"));
|
|
@@ -1778,7 +1773,6 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
1778
1773
|
}),
|
|
1779
1774
|
}),
|
|
1780
1775
|
onArtifactComplete,
|
|
1781
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
1782
1776
|
});
|
|
1783
1777
|
|
|
1784
1778
|
await core.append(createUserAppendMessage("Go"));
|
|
@@ -2363,6 +2357,65 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
2363
2357
|
});
|
|
2364
2358
|
});
|
|
2365
2359
|
|
|
2360
|
+
// --- Runtime detachment ---
|
|
2361
|
+
|
|
2362
|
+
describe("runtime detachment", () => {
|
|
2363
|
+
it("keeps an onCancel replacement run abortable", async () => {
|
|
2364
|
+
const streamSignals: AbortSignal[] = [];
|
|
2365
|
+
const releaseStreams: Array<() => void> = [];
|
|
2366
|
+
const streamMessage = vi.fn().mockImplementation(async function* (
|
|
2367
|
+
_msg: any,
|
|
2368
|
+
_cfg: any,
|
|
2369
|
+
_meta: any,
|
|
2370
|
+
signal: AbortSignal,
|
|
2371
|
+
) {
|
|
2372
|
+
const streamIndex = streamSignals.push(signal) - 1;
|
|
2373
|
+
yield statusUpdateEvent("working");
|
|
2374
|
+
if (signal.aborted) return;
|
|
2375
|
+
await new Promise<void>((resolve) => {
|
|
2376
|
+
releaseStreams[streamIndex] = resolve;
|
|
2377
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2378
|
+
});
|
|
2379
|
+
});
|
|
2380
|
+
let core!: A2AThreadRuntimeCore;
|
|
2381
|
+
let replacementRun: Promise<void> | undefined;
|
|
2382
|
+
let startReplacement = true;
|
|
2383
|
+
core = createCore(
|
|
2384
|
+
{
|
|
2385
|
+
getAgentCard: vi.fn().mockResolvedValue({
|
|
2386
|
+
name: "Agent",
|
|
2387
|
+
url: "https://agent.example",
|
|
2388
|
+
}),
|
|
2389
|
+
streamMessage,
|
|
2390
|
+
},
|
|
2391
|
+
{
|
|
2392
|
+
onCancel: () => {
|
|
2393
|
+
if (!startReplacement) return;
|
|
2394
|
+
startReplacement = false;
|
|
2395
|
+
replacementRun = core.append(
|
|
2396
|
+
createUserAppendMessage("Replacement"),
|
|
2397
|
+
);
|
|
2398
|
+
},
|
|
2399
|
+
},
|
|
2400
|
+
);
|
|
2401
|
+
|
|
2402
|
+
const firstRun = core.append(createUserAppendMessage("First"));
|
|
2403
|
+
await vi.waitFor(() => expect(streamSignals).toHaveLength(1));
|
|
2404
|
+
|
|
2405
|
+
core.detachRuntime();
|
|
2406
|
+
await vi.waitFor(() => expect(streamSignals).toHaveLength(2));
|
|
2407
|
+
await vi.waitFor(() => expect(releaseStreams[1]).toBeTypeOf("function"));
|
|
2408
|
+
|
|
2409
|
+
await core.cancel();
|
|
2410
|
+
const replacementWasAborted = streamSignals[1]!.aborted;
|
|
2411
|
+
releaseStreams[1]!();
|
|
2412
|
+
await Promise.all([firstRun, replacementRun]);
|
|
2413
|
+
|
|
2414
|
+
expect(streamSignals[0]!.aborted).toBe(true);
|
|
2415
|
+
expect(replacementWasAborted).toBe(true);
|
|
2416
|
+
});
|
|
2417
|
+
});
|
|
2418
|
+
|
|
2366
2419
|
// --- Cancel ---
|
|
2367
2420
|
|
|
2368
2421
|
describe("cancel", () => {
|
|
@@ -2380,13 +2433,13 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
2380
2433
|
status: { state: "working" },
|
|
2381
2434
|
};
|
|
2382
2435
|
(core as any).abortController = new AbortController();
|
|
2383
|
-
|
|
2436
|
+
onUpdate.mockClear();
|
|
2384
2437
|
|
|
2385
2438
|
await core.cancel();
|
|
2386
2439
|
|
|
2387
2440
|
expect(cancelTask).toHaveBeenCalledWith("t1");
|
|
2388
2441
|
expect(core.getTask()!.status.state).toBe("canceled");
|
|
2389
|
-
expect(
|
|
2442
|
+
expect(onUpdate).toHaveBeenCalledOnce();
|
|
2390
2443
|
});
|
|
2391
2444
|
|
|
2392
2445
|
it("does nothing when no abort controller", async () => {
|
|
@@ -2487,6 +2540,67 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
2487
2540
|
expect(core.getTask()?.status.state).toBe("working");
|
|
2488
2541
|
});
|
|
2489
2542
|
|
|
2543
|
+
it("keeps the run onCancel starts when a send supersedes the active run", async () => {
|
|
2544
|
+
const signals: AbortSignal[] = [];
|
|
2545
|
+
const streamMessage = vi.fn().mockImplementation(async function* (
|
|
2546
|
+
_msg: any,
|
|
2547
|
+
_cfg: any,
|
|
2548
|
+
_meta: any,
|
|
2549
|
+
signal: AbortSignal,
|
|
2550
|
+
) {
|
|
2551
|
+
signals.push(signal);
|
|
2552
|
+
await new Promise<void>((resolve) => {
|
|
2553
|
+
if (signal.aborted) resolve();
|
|
2554
|
+
else
|
|
2555
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
2556
|
+
});
|
|
2557
|
+
});
|
|
2558
|
+
let restarted = false;
|
|
2559
|
+
let core!: A2AThreadRuntimeCore;
|
|
2560
|
+
core = createCore(
|
|
2561
|
+
{ streamMessage },
|
|
2562
|
+
{
|
|
2563
|
+
onCancel: () => {
|
|
2564
|
+
if (restarted) return;
|
|
2565
|
+
restarted = true;
|
|
2566
|
+
void core.append({
|
|
2567
|
+
...createUserAppendMessage("from onCancel"),
|
|
2568
|
+
parentId: core.getMessages().at(-1)!.id,
|
|
2569
|
+
});
|
|
2570
|
+
},
|
|
2571
|
+
},
|
|
2572
|
+
);
|
|
2573
|
+
|
|
2574
|
+
void core.append(createUserAppendMessage("first"));
|
|
2575
|
+
await vi.waitFor(() => expect(signals).toHaveLength(1));
|
|
2576
|
+
void core.append({
|
|
2577
|
+
...createUserAppendMessage("second"),
|
|
2578
|
+
parentId: core.getMessages().at(-1)!.id,
|
|
2579
|
+
});
|
|
2580
|
+
await vi.waitFor(() => expect(signals).toHaveLength(2));
|
|
2581
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2582
|
+
expect(signals).toHaveLength(2);
|
|
2583
|
+
expect(
|
|
2584
|
+
core.getMessages().map((message) => ({
|
|
2585
|
+
role: message.role,
|
|
2586
|
+
text: message.content.map((part) =>
|
|
2587
|
+
part.type === "text" ? part.text : "",
|
|
2588
|
+
),
|
|
2589
|
+
})),
|
|
2590
|
+
).toEqual([
|
|
2591
|
+
{ role: "user", text: ["first"] },
|
|
2592
|
+
{ role: "assistant", text: [] },
|
|
2593
|
+
{ role: "user", text: ["second"] },
|
|
2594
|
+
{ role: "user", text: ["from onCancel"] },
|
|
2595
|
+
{ role: "assistant", text: [] },
|
|
2596
|
+
]);
|
|
2597
|
+
|
|
2598
|
+
await core.cancel();
|
|
2599
|
+
|
|
2600
|
+
expect(signals.every((signal) => signal.aborted)).toBe(true);
|
|
2601
|
+
expect(core.isRunning()).toBe(false);
|
|
2602
|
+
});
|
|
2603
|
+
|
|
2490
2604
|
it("still cancels the server task when onCancel clears the thread", async () => {
|
|
2491
2605
|
let resolveCancel!: (task: A2ATask) => void;
|
|
2492
2606
|
const cancelTask = vi.fn().mockReturnValue(
|
|
@@ -2602,7 +2716,6 @@ describe("A2AThreadRuntimeCore", () => {
|
|
|
2602
2716
|
})),
|
|
2603
2717
|
}),
|
|
2604
2718
|
onError,
|
|
2605
|
-
notifyUpdate: notifyUpdate as unknown as () => void,
|
|
2606
2719
|
});
|
|
2607
2720
|
|
|
2608
2721
|
await expect(core.append(createUserAppendMessage("Go"))).rejects.toThrow(
|
|
@@ -2848,7 +2961,6 @@ describe("outbound message conversion", () => {
|
|
|
2848
2961
|
});
|
|
2849
2962
|
const core = new A2AThreadRuntimeCore({
|
|
2850
2963
|
client: createMockClient({ streamMessage }),
|
|
2851
|
-
notifyUpdate: vi.fn() as unknown as () => void,
|
|
2852
2964
|
});
|
|
2853
2965
|
return { core, streamMessage };
|
|
2854
2966
|
}
|