@assistant-ui/react-a2a 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/A2AMessageAccumulator.d.ts +16 -0
- package/dist/A2AMessageAccumulator.d.ts.map +1 -0
- package/dist/A2AMessageAccumulator.js +35 -0
- package/dist/A2AMessageAccumulator.js.map +1 -0
- package/dist/appendA2AChunk.d.ts +3 -0
- package/dist/appendA2AChunk.d.ts.map +1 -0
- package/dist/appendA2AChunk.js +90 -0
- package/dist/appendA2AChunk.js.map +1 -0
- package/dist/convertA2AMessages.d.ts +64 -0
- package/dist/convertA2AMessages.d.ts.map +1 -0
- package/dist/convertA2AMessages.js +93 -0
- package/dist/convertA2AMessages.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/testUtils.d.ts +4 -0
- package/dist/testUtils.d.ts.map +1 -0
- package/dist/testUtils.js +10 -0
- package/dist/testUtils.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +14 -0
- package/dist/types.js.map +1 -0
- package/dist/useA2AMessages.d.ts +25 -0
- package/dist/useA2AMessages.d.ts.map +1 -0
- package/dist/useA2AMessages.js +134 -0
- package/dist/useA2AMessages.js.map +1 -0
- package/dist/useA2ARuntime.d.ts +55 -0
- package/dist/useA2ARuntime.d.ts.map +1 -0
- package/dist/useA2ARuntime.js +215 -0
- package/dist/useA2ARuntime.js.map +1 -0
- package/package.json +68 -0
- package/src/A2AMessageAccumulator.ts +48 -0
- package/src/appendA2AChunk.ts +121 -0
- package/src/convertA2AMessages.ts +108 -0
- package/src/index.ts +6 -0
- package/src/testUtils.ts +11 -0
- package/src/types.ts +114 -0
- package/src/useA2AMessages.ts +180 -0
- package/src/useA2ARuntime.ts +331 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { A2AMessage } from "./types";
|
|
2
|
+
import { parsePartialJsonObject } from "assistant-stream/utils";
|
|
3
|
+
|
|
4
|
+
export const appendA2AChunk = (
|
|
5
|
+
prev: A2AMessage | undefined,
|
|
6
|
+
curr: A2AMessage,
|
|
7
|
+
): A2AMessage => {
|
|
8
|
+
// If no previous message or different message type, return current as-is
|
|
9
|
+
if (!prev || prev.role !== curr.role || prev.id !== curr.id) {
|
|
10
|
+
return curr;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// For assistant messages, we need to handle streaming content and tool calls
|
|
14
|
+
if (curr.role === "assistant") {
|
|
15
|
+
const newContent = Array.isArray(prev.content)
|
|
16
|
+
? [...prev.content]
|
|
17
|
+
: typeof prev.content === "string"
|
|
18
|
+
? [{ type: "text" as const, text: prev.content }]
|
|
19
|
+
: [];
|
|
20
|
+
|
|
21
|
+
// Append new content chunks
|
|
22
|
+
if (typeof curr.content === "string") {
|
|
23
|
+
const lastIndex = newContent.length - 1;
|
|
24
|
+
const lastPart = newContent[lastIndex];
|
|
25
|
+
|
|
26
|
+
if (lastPart?.type === "text") {
|
|
27
|
+
// Append to existing text part
|
|
28
|
+
(lastPart as { type: "text"; text: string }).text += curr.content;
|
|
29
|
+
} else {
|
|
30
|
+
// Create new text part
|
|
31
|
+
newContent.push({ type: "text", text: curr.content });
|
|
32
|
+
}
|
|
33
|
+
} else if (Array.isArray(curr.content)) {
|
|
34
|
+
for (const contentPart of curr.content) {
|
|
35
|
+
const lastIndex = newContent.length - 1;
|
|
36
|
+
const lastPart = newContent[lastIndex];
|
|
37
|
+
|
|
38
|
+
if (contentPart.type === "text" && lastPart?.type === "text") {
|
|
39
|
+
// Append to existing text part
|
|
40
|
+
(lastPart as { type: "text"; text: string }).text += contentPart.text;
|
|
41
|
+
} else {
|
|
42
|
+
// Add new content part
|
|
43
|
+
newContent.push(contentPart);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Merge tool calls - A2A typically sends complete tool calls rather than chunks
|
|
49
|
+
const newToolCalls = [...(prev.tool_calls ?? [])];
|
|
50
|
+
if (curr.tool_calls) {
|
|
51
|
+
for (const toolCall of curr.tool_calls) {
|
|
52
|
+
const existingIndex = newToolCalls.findIndex(
|
|
53
|
+
(tc) => tc.id === toolCall.id,
|
|
54
|
+
);
|
|
55
|
+
if (existingIndex >= 0) {
|
|
56
|
+
// Update existing tool call (merge args if needed)
|
|
57
|
+
const existing = newToolCalls[existingIndex]!;
|
|
58
|
+
newToolCalls[existingIndex] = {
|
|
59
|
+
...existing,
|
|
60
|
+
...toolCall,
|
|
61
|
+
// If argsText is provided in chunks, concatenate it
|
|
62
|
+
argsText: (existing.argsText || "") + (toolCall.argsText || ""),
|
|
63
|
+
// Try to parse merged args, fallback to existing or new args
|
|
64
|
+
args:
|
|
65
|
+
parsePartialJsonObject(
|
|
66
|
+
(existing.argsText || "") + (toolCall.argsText || ""),
|
|
67
|
+
) ||
|
|
68
|
+
toolCall.args ||
|
|
69
|
+
existing.args,
|
|
70
|
+
};
|
|
71
|
+
} else {
|
|
72
|
+
// Add new tool call
|
|
73
|
+
newToolCalls.push(toolCall);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Merge artifacts
|
|
79
|
+
const newArtifacts = [...(prev.artifacts ?? [])];
|
|
80
|
+
if (curr.artifacts) {
|
|
81
|
+
for (const artifact of curr.artifacts) {
|
|
82
|
+
const existingIndex = newArtifacts.findIndex(
|
|
83
|
+
(a) => a.name === artifact.name,
|
|
84
|
+
);
|
|
85
|
+
if (existingIndex >= 0) {
|
|
86
|
+
// Merge artifact parts
|
|
87
|
+
const existingArtifact = newArtifacts[existingIndex]!;
|
|
88
|
+
newArtifacts[existingIndex] = {
|
|
89
|
+
name: existingArtifact.name,
|
|
90
|
+
parts: [...existingArtifact.parts, ...artifact.parts],
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
// Add new artifact
|
|
94
|
+
newArtifacts.push(artifact);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result: A2AMessage = {
|
|
100
|
+
...prev,
|
|
101
|
+
content: newContent,
|
|
102
|
+
};
|
|
103
|
+
const newStatus = curr.status || prev.status;
|
|
104
|
+
if (newStatus) result.status = newStatus;
|
|
105
|
+
if (newToolCalls.length > 0) result.tool_calls = newToolCalls;
|
|
106
|
+
if (newArtifacts.length > 0) result.artifacts = newArtifacts;
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// For other message types (user, system, tool), just return the current message
|
|
111
|
+
// as they typically don't stream in chunks
|
|
112
|
+
const result: A2AMessage = {
|
|
113
|
+
...prev,
|
|
114
|
+
...curr,
|
|
115
|
+
};
|
|
116
|
+
// Preserve any existing artifacts and merge with new ones
|
|
117
|
+
if (curr.artifacts || prev.artifacts) {
|
|
118
|
+
result.artifacts = [...(prev.artifacts ?? []), ...(curr.artifacts ?? [])];
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useExternalMessageConverter } from "@assistant-ui/react";
|
|
4
|
+
import { A2AMessage } from "./types";
|
|
5
|
+
import { ToolCallMessagePart } from "@assistant-ui/react";
|
|
6
|
+
import { ThreadUserMessage } from "@assistant-ui/react";
|
|
7
|
+
|
|
8
|
+
const contentToParts = (content: A2AMessage["content"]) => {
|
|
9
|
+
if (typeof content === "string")
|
|
10
|
+
return [{ type: "text" as const, text: content }];
|
|
11
|
+
return content
|
|
12
|
+
.map((part): ThreadUserMessage["content"][number] | null => {
|
|
13
|
+
const type = part.type;
|
|
14
|
+
switch (type) {
|
|
15
|
+
case "text":
|
|
16
|
+
return { type: "text", text: part.text };
|
|
17
|
+
case "image_url":
|
|
18
|
+
if (typeof part.image_url === "string") {
|
|
19
|
+
return { type: "image", image: part.image_url };
|
|
20
|
+
} else {
|
|
21
|
+
return {
|
|
22
|
+
type: "image",
|
|
23
|
+
image: part.image_url.url,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
case "data":
|
|
27
|
+
// Convert data parts to text representation for display
|
|
28
|
+
return {
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `[Data: ${JSON.stringify(part.data)}]`,
|
|
31
|
+
};
|
|
32
|
+
default:
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.filter((part): part is NonNullable<typeof part> => part !== null);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const convertA2AMessage = (message: A2AMessage) => {
|
|
40
|
+
const role = message.role;
|
|
41
|
+
switch (role) {
|
|
42
|
+
case "system":
|
|
43
|
+
return {
|
|
44
|
+
id: message.id,
|
|
45
|
+
role: "system" as const,
|
|
46
|
+
content: [{ type: "text" as const, text: message.content as string }],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
case "user":
|
|
50
|
+
return {
|
|
51
|
+
id: message.id,
|
|
52
|
+
role: "user" as const,
|
|
53
|
+
content: contentToParts(message.content),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
case "assistant": {
|
|
57
|
+
const toolCallParts: ToolCallMessagePart[] =
|
|
58
|
+
message.tool_calls?.map((toolCall) => ({
|
|
59
|
+
type: "tool-call",
|
|
60
|
+
toolCallId: toolCall.id,
|
|
61
|
+
toolName: toolCall.name,
|
|
62
|
+
args: toolCall.args,
|
|
63
|
+
argsText: toolCall.argsText ?? JSON.stringify(toolCall.args),
|
|
64
|
+
})) ?? [];
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
id: message.id,
|
|
68
|
+
role: "assistant" as const,
|
|
69
|
+
content: [...contentToParts(message.content), ...toolCallParts],
|
|
70
|
+
status: message.status,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
case "tool":
|
|
75
|
+
return {
|
|
76
|
+
id: message.id,
|
|
77
|
+
role: "user" as const,
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "tool-call" as const,
|
|
81
|
+
toolCallId: message.tool_call_id!,
|
|
82
|
+
toolName: "", // A2A doesn't store tool name in tool messages
|
|
83
|
+
result: JSON.parse(message.content as string),
|
|
84
|
+
isError:
|
|
85
|
+
message.status?.type === "incomplete" &&
|
|
86
|
+
message.status?.reason === "error",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
default:
|
|
92
|
+
const _exhaustiveCheck: never = role;
|
|
93
|
+
throw new Error(`Unknown message role: ${_exhaustiveCheck}`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const convertA2AMessages = (messages: A2AMessage[]) =>
|
|
98
|
+
messages.map(convertA2AMessage);
|
|
99
|
+
|
|
100
|
+
export const useA2AMessageConverter = (
|
|
101
|
+
messages: A2AMessage[],
|
|
102
|
+
isRunning: boolean,
|
|
103
|
+
) =>
|
|
104
|
+
useExternalMessageConverter({
|
|
105
|
+
callback: convertA2AMessage,
|
|
106
|
+
messages,
|
|
107
|
+
isRunning,
|
|
108
|
+
});
|
package/src/index.ts
ADDED
package/src/testUtils.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { A2AMessage } from "./types";
|
|
2
|
+
import { A2AMessagesEvent } from "./useA2AMessages";
|
|
3
|
+
|
|
4
|
+
export const mockStreamCallbackFactory = (
|
|
5
|
+
events: Array<A2AMessagesEvent<A2AMessage>>,
|
|
6
|
+
) =>
|
|
7
|
+
async function* () {
|
|
8
|
+
for (const event of events) {
|
|
9
|
+
yield event;
|
|
10
|
+
}
|
|
11
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { MessageStatus } from "@assistant-ui/react";
|
|
2
|
+
import { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
3
|
+
|
|
4
|
+
// A2A Message Types
|
|
5
|
+
export type A2AMessage = {
|
|
6
|
+
id?: string;
|
|
7
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
8
|
+
content: string | A2AMessageContent[];
|
|
9
|
+
tool_calls?: A2AToolCall[];
|
|
10
|
+
tool_call_id?: string;
|
|
11
|
+
artifacts?: A2AArtifact[];
|
|
12
|
+
status?: MessageStatus;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type A2AMessageContent =
|
|
16
|
+
| { type: "text"; text: string }
|
|
17
|
+
| { type: "image_url"; image_url: string | { url: string } }
|
|
18
|
+
| { type: "data"; data: any };
|
|
19
|
+
|
|
20
|
+
export type A2AToolCall = {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
args: ReadonlyJSONObject;
|
|
24
|
+
argsText?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type A2AArtifact = {
|
|
28
|
+
name: string;
|
|
29
|
+
parts: A2AArtifactPart[];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type A2AArtifactPart = {
|
|
33
|
+
kind: "text" | "data" | "file";
|
|
34
|
+
data?: any;
|
|
35
|
+
text?: string;
|
|
36
|
+
metadata?: Record<string, any>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// A2A Events (similar to LangGraph events)
|
|
40
|
+
export type A2AEvent = {
|
|
41
|
+
event: A2AKnownEventTypes | string;
|
|
42
|
+
data: any;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export enum A2AKnownEventTypes {
|
|
46
|
+
TaskUpdate = "task-update",
|
|
47
|
+
TaskComplete = "task-complete",
|
|
48
|
+
TaskFailed = "task-failed",
|
|
49
|
+
Artifacts = "artifacts",
|
|
50
|
+
StateUpdate = "state-update",
|
|
51
|
+
Error = "error",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A2A Task State
|
|
55
|
+
export type A2ATaskState = {
|
|
56
|
+
id: string;
|
|
57
|
+
state: "pending" | "working" | "completed" | "failed";
|
|
58
|
+
progress?: number;
|
|
59
|
+
message?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// A2A Task Result
|
|
63
|
+
export type A2ATaskResult = {
|
|
64
|
+
id: string;
|
|
65
|
+
status: {
|
|
66
|
+
state: "pending" | "working" | "completed" | "failed";
|
|
67
|
+
message?: string;
|
|
68
|
+
};
|
|
69
|
+
artifacts?: A2AArtifact[];
|
|
70
|
+
history?: Array<{
|
|
71
|
+
messageId: string;
|
|
72
|
+
role: string;
|
|
73
|
+
parts?: Array<{ kind: string; text?: string; data?: any }>;
|
|
74
|
+
}>;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// A2A Configuration
|
|
78
|
+
export type A2AConfig = {
|
|
79
|
+
contextId?: string;
|
|
80
|
+
runConfig?: Record<string, any>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// A2A Send Message Configuration
|
|
84
|
+
export type A2ASendMessageConfig = A2AConfig & {
|
|
85
|
+
command?: A2ACommand;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// A2A Commands (for interrupts/resume)
|
|
89
|
+
export type A2ACommand = {
|
|
90
|
+
resume?: string;
|
|
91
|
+
interrupt?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// A2A Stream Callback
|
|
95
|
+
export type A2AStreamCallback<TMessage> = (
|
|
96
|
+
messages: TMessage[],
|
|
97
|
+
config: A2ASendMessageConfig & { abortSignal: AbortSignal },
|
|
98
|
+
) => Promise<AsyncGenerator<A2AEvent>> | AsyncGenerator<A2AEvent>;
|
|
99
|
+
|
|
100
|
+
// Event handler callback types
|
|
101
|
+
export type OnTaskUpdateEventCallback = (
|
|
102
|
+
data: A2ATaskState,
|
|
103
|
+
) => void | Promise<void>;
|
|
104
|
+
export type OnArtifactsEventCallback = (
|
|
105
|
+
artifacts: A2AArtifact[],
|
|
106
|
+
) => void | Promise<void>;
|
|
107
|
+
export type OnErrorEventCallback = (error: unknown) => void | Promise<void>;
|
|
108
|
+
export type OnStateUpdateEventCallback = (
|
|
109
|
+
state: unknown,
|
|
110
|
+
) => void | Promise<void>;
|
|
111
|
+
export type OnCustomEventCallback = (
|
|
112
|
+
type: string,
|
|
113
|
+
data: unknown,
|
|
114
|
+
) => void | Promise<void>;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useMemo } from "react";
|
|
2
|
+
import { v4 as uuidv4 } from "uuid";
|
|
3
|
+
import { A2AMessageAccumulator } from "./A2AMessageAccumulator";
|
|
4
|
+
import {
|
|
5
|
+
A2AEvent,
|
|
6
|
+
A2AKnownEventTypes,
|
|
7
|
+
A2ATaskState,
|
|
8
|
+
A2AArtifact,
|
|
9
|
+
A2ASendMessageConfig,
|
|
10
|
+
A2AStreamCallback,
|
|
11
|
+
OnTaskUpdateEventCallback,
|
|
12
|
+
OnArtifactsEventCallback,
|
|
13
|
+
OnErrorEventCallback,
|
|
14
|
+
OnStateUpdateEventCallback,
|
|
15
|
+
OnCustomEventCallback,
|
|
16
|
+
} from "./types";
|
|
17
|
+
|
|
18
|
+
export type A2AMessagesEvent<_TMessage> = A2AEvent;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_APPEND_MESSAGE = <TMessage>(
|
|
21
|
+
_: TMessage | undefined,
|
|
22
|
+
curr: TMessage,
|
|
23
|
+
) => curr;
|
|
24
|
+
|
|
25
|
+
export const useA2AMessages = <TMessage extends { id?: string }>({
|
|
26
|
+
stream,
|
|
27
|
+
appendMessage = DEFAULT_APPEND_MESSAGE,
|
|
28
|
+
eventHandlers,
|
|
29
|
+
}: {
|
|
30
|
+
stream: A2AStreamCallback<TMessage>;
|
|
31
|
+
appendMessage?: (prev: TMessage | undefined, curr: TMessage) => TMessage;
|
|
32
|
+
eventHandlers?: {
|
|
33
|
+
onTaskUpdate?: OnTaskUpdateEventCallback;
|
|
34
|
+
onArtifacts?: OnArtifactsEventCallback;
|
|
35
|
+
onError?: OnErrorEventCallback;
|
|
36
|
+
onStateUpdate?: OnStateUpdateEventCallback;
|
|
37
|
+
onCustomEvent?: OnCustomEventCallback;
|
|
38
|
+
};
|
|
39
|
+
}) => {
|
|
40
|
+
const [messages, setMessages] = useState<TMessage[]>([]);
|
|
41
|
+
const [taskState, setTaskState] = useState<A2ATaskState | undefined>();
|
|
42
|
+
const [artifacts, setArtifacts] = useState<A2AArtifact[]>([]);
|
|
43
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
44
|
+
|
|
45
|
+
const { onTaskUpdate, onArtifacts, onError, onStateUpdate, onCustomEvent } =
|
|
46
|
+
useMemo(() => eventHandlers ?? {}, [eventHandlers]);
|
|
47
|
+
|
|
48
|
+
const sendMessage = useCallback(
|
|
49
|
+
async (newMessages: TMessage[], config: A2ASendMessageConfig) => {
|
|
50
|
+
// ensure all messages have an ID
|
|
51
|
+
const newMessagesWithId = newMessages.map((m) =>
|
|
52
|
+
m.id ? m : { ...m, id: uuidv4() },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const accumulator = new A2AMessageAccumulator({
|
|
56
|
+
initialMessages: messages,
|
|
57
|
+
appendMessage,
|
|
58
|
+
});
|
|
59
|
+
setMessages(accumulator.addMessages(newMessagesWithId));
|
|
60
|
+
|
|
61
|
+
const abortController = new AbortController();
|
|
62
|
+
abortControllerRef.current = abortController;
|
|
63
|
+
const response = await stream(newMessagesWithId, {
|
|
64
|
+
...config,
|
|
65
|
+
abortSignal: abortController.signal,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
for await (const event of response) {
|
|
69
|
+
switch (event.event) {
|
|
70
|
+
case A2AKnownEventTypes.TaskUpdate:
|
|
71
|
+
const taskData = event.data as A2ATaskState;
|
|
72
|
+
setTaskState(taskData);
|
|
73
|
+
onTaskUpdate?.(taskData);
|
|
74
|
+
break;
|
|
75
|
+
|
|
76
|
+
case A2AKnownEventTypes.TaskComplete:
|
|
77
|
+
// Extract messages and artifacts from completed task
|
|
78
|
+
const { messages: taskMessages, artifacts: taskArtifacts } =
|
|
79
|
+
event.data;
|
|
80
|
+
if (taskMessages) {
|
|
81
|
+
setMessages(accumulator.addMessages(taskMessages));
|
|
82
|
+
}
|
|
83
|
+
if (taskArtifacts) {
|
|
84
|
+
setArtifacts(taskArtifacts);
|
|
85
|
+
onArtifacts?.(taskArtifacts);
|
|
86
|
+
}
|
|
87
|
+
// Clear task state on completion
|
|
88
|
+
setTaskState(undefined);
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case A2AKnownEventTypes.TaskFailed:
|
|
92
|
+
onError?.(event.data);
|
|
93
|
+
// Update task state to failed
|
|
94
|
+
if (taskState) {
|
|
95
|
+
setTaskState({
|
|
96
|
+
...taskState,
|
|
97
|
+
state: "failed",
|
|
98
|
+
message: event.data?.message,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case A2AKnownEventTypes.Artifacts:
|
|
104
|
+
const artifactData = event.data as A2AArtifact[];
|
|
105
|
+
setArtifacts(artifactData);
|
|
106
|
+
onArtifacts?.(artifactData);
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case A2AKnownEventTypes.StateUpdate:
|
|
110
|
+
onStateUpdate?.(event.data);
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case A2AKnownEventTypes.Error:
|
|
114
|
+
onError?.(event.data);
|
|
115
|
+
// Update the last assistant message with error status if available
|
|
116
|
+
const messages = accumulator.getMessages();
|
|
117
|
+
const lastAssistantMessage = messages.findLast(
|
|
118
|
+
(m): m is TMessage & { role: string; id: string } =>
|
|
119
|
+
m != null &&
|
|
120
|
+
"role" in m &&
|
|
121
|
+
m.role === "assistant" &&
|
|
122
|
+
m.id != null,
|
|
123
|
+
);
|
|
124
|
+
if (lastAssistantMessage) {
|
|
125
|
+
const errorMessage = {
|
|
126
|
+
...lastAssistantMessage,
|
|
127
|
+
status: {
|
|
128
|
+
type: "incomplete" as const,
|
|
129
|
+
reason: "error" as const,
|
|
130
|
+
error: event.data,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
setMessages(accumulator.addMessages([errorMessage]));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
default:
|
|
138
|
+
if (onCustomEvent) {
|
|
139
|
+
onCustomEvent(event.event, event.data);
|
|
140
|
+
} else {
|
|
141
|
+
console.warn(
|
|
142
|
+
"Unhandled A2A event received:",
|
|
143
|
+
event.event,
|
|
144
|
+
event.data,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
[
|
|
152
|
+
messages,
|
|
153
|
+
appendMessage,
|
|
154
|
+
stream,
|
|
155
|
+
onTaskUpdate,
|
|
156
|
+
onArtifacts,
|
|
157
|
+
onError,
|
|
158
|
+
onStateUpdate,
|
|
159
|
+
onCustomEvent,
|
|
160
|
+
taskState,
|
|
161
|
+
],
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const cancel = useCallback(() => {
|
|
165
|
+
if (abortControllerRef.current) {
|
|
166
|
+
abortControllerRef.current.abort();
|
|
167
|
+
}
|
|
168
|
+
}, [abortControllerRef]);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
messages,
|
|
172
|
+
artifacts,
|
|
173
|
+
taskState,
|
|
174
|
+
sendMessage,
|
|
175
|
+
cancel,
|
|
176
|
+
setMessages,
|
|
177
|
+
setArtifacts,
|
|
178
|
+
setTaskState,
|
|
179
|
+
};
|
|
180
|
+
};
|