aixyz 0.10.0 → 0.11.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/package.json +4 -4
- package/server/adapters/a2a.test.ts +161 -0
- package/server/adapters/a2a.ts +56 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aixyz",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Payment-native SDK for AI Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@a2a-js/sdk": "^0.3.10",
|
|
26
|
-
"@aixyz/cli": "0.
|
|
27
|
-
"@aixyz/config": "0.
|
|
28
|
-
"@aixyz/erc-8004": "0.
|
|
26
|
+
"@aixyz/cli": "0.11.0",
|
|
27
|
+
"@aixyz/config": "0.11.0",
|
|
28
|
+
"@aixyz/erc-8004": "0.11.0",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
30
30
|
"@next/env": "^16.1.6",
|
|
31
31
|
"@x402/core": "^2.3.1",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
import { DefaultExecutionEventBus } from "@a2a-js/sdk/server";
|
|
3
|
+
import type { Message, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent } from "@a2a-js/sdk";
|
|
4
|
+
import type { AgentExecutionEvent } from "@a2a-js/sdk/server";
|
|
5
|
+
import { ToolLoopAgentExecutor } from "./a2a";
|
|
6
|
+
import type { ToolLoopAgent } from "ai";
|
|
7
|
+
import type { RequestContext } from "@a2a-js/sdk/server";
|
|
8
|
+
|
|
9
|
+
function makeRequestContext(overrides?: Partial<RequestContext>): RequestContext {
|
|
10
|
+
return {
|
|
11
|
+
taskId: "task-1",
|
|
12
|
+
contextId: "ctx-1",
|
|
13
|
+
userMessage: {
|
|
14
|
+
kind: "message",
|
|
15
|
+
messageId: "msg-1",
|
|
16
|
+
role: "user",
|
|
17
|
+
parts: [{ kind: "text", text: "Hello!" }],
|
|
18
|
+
},
|
|
19
|
+
task: undefined,
|
|
20
|
+
referenceTasks: undefined,
|
|
21
|
+
context: undefined,
|
|
22
|
+
...overrides,
|
|
23
|
+
} as unknown as RequestContext;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function makeAsyncIterable(chunks: string[]): AsyncIterable<string> {
|
|
27
|
+
return {
|
|
28
|
+
[Symbol.asyncIterator]: async function* () {
|
|
29
|
+
for (const chunk of chunks) {
|
|
30
|
+
yield chunk;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("ToolLoopAgentExecutor streaming", () => {
|
|
37
|
+
test("publishes initial task, working status, artifact chunks, and completed status when streaming", async () => {
|
|
38
|
+
const chunks = ["Hello", ", ", "world", "!"];
|
|
39
|
+
const mockAgent = {
|
|
40
|
+
stream: mock(async () => ({
|
|
41
|
+
textStream: makeAsyncIterable(chunks),
|
|
42
|
+
})),
|
|
43
|
+
} as unknown as ToolLoopAgent<never>;
|
|
44
|
+
|
|
45
|
+
const executor = new ToolLoopAgentExecutor(mockAgent);
|
|
46
|
+
const eventBus = new DefaultExecutionEventBus();
|
|
47
|
+
|
|
48
|
+
const events: AgentExecutionEvent[] = [];
|
|
49
|
+
eventBus.on("event", (event) => events.push(event));
|
|
50
|
+
|
|
51
|
+
const ctx = makeRequestContext();
|
|
52
|
+
await executor.execute(ctx, eventBus);
|
|
53
|
+
|
|
54
|
+
// First event: initial Task object
|
|
55
|
+
const initialTask = events[0] as Task;
|
|
56
|
+
expect(initialTask.kind).toBe("task");
|
|
57
|
+
expect(initialTask.id).toBe("task-1");
|
|
58
|
+
expect(initialTask.contextId).toBe("ctx-1");
|
|
59
|
+
expect(initialTask.status.state).toBe("submitted");
|
|
60
|
+
|
|
61
|
+
// Second event: working status
|
|
62
|
+
const workingUpdate = events[1] as TaskStatusUpdateEvent;
|
|
63
|
+
expect(workingUpdate.kind).toBe("status-update");
|
|
64
|
+
expect(workingUpdate.status.state).toBe("working");
|
|
65
|
+
expect(workingUpdate.final).toBe(false);
|
|
66
|
+
expect(workingUpdate.taskId).toBe("task-1");
|
|
67
|
+
expect(workingUpdate.contextId).toBe("ctx-1");
|
|
68
|
+
|
|
69
|
+
// Artifact update events (one per non-empty chunk)
|
|
70
|
+
const artifactUpdates = events.slice(2, events.length - 1) as TaskArtifactUpdateEvent[];
|
|
71
|
+
expect(artifactUpdates.length).toBe(chunks.length);
|
|
72
|
+
expect(artifactUpdates[0].kind).toBe("artifact-update");
|
|
73
|
+
expect(artifactUpdates[0].append).toBe(false);
|
|
74
|
+
expect((artifactUpdates[0].artifact.parts[0] as { kind: string; text: string }).text).toBe("Hello");
|
|
75
|
+
|
|
76
|
+
// Subsequent chunks should have append: true
|
|
77
|
+
expect(artifactUpdates[1].append).toBe(true);
|
|
78
|
+
expect((artifactUpdates[1].artifact.parts[0] as { kind: string; text: string }).text).toBe(", ");
|
|
79
|
+
|
|
80
|
+
// All artifact updates share the same artifactId
|
|
81
|
+
const artifactId = artifactUpdates[0].artifact.artifactId;
|
|
82
|
+
expect(artifactUpdates.every((e) => e.artifact.artifactId === artifactId)).toBe(true);
|
|
83
|
+
|
|
84
|
+
// Last event: completed status
|
|
85
|
+
const completedUpdate = events[events.length - 1] as TaskStatusUpdateEvent;
|
|
86
|
+
expect(completedUpdate.kind).toBe("status-update");
|
|
87
|
+
expect(completedUpdate.status.state).toBe("completed");
|
|
88
|
+
expect(completedUpdate.final).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("does not publish initial task when task context already exists", async () => {
|
|
92
|
+
const mockAgent = {
|
|
93
|
+
stream: mock(async () => ({
|
|
94
|
+
textStream: makeAsyncIterable(["hi"]),
|
|
95
|
+
})),
|
|
96
|
+
} as unknown as ToolLoopAgent<never>;
|
|
97
|
+
|
|
98
|
+
const executor = new ToolLoopAgentExecutor(mockAgent);
|
|
99
|
+
const eventBus = new DefaultExecutionEventBus();
|
|
100
|
+
|
|
101
|
+
const events: AgentExecutionEvent[] = [];
|
|
102
|
+
eventBus.on("event", (event) => events.push(event));
|
|
103
|
+
|
|
104
|
+
const existingTask: Task = {
|
|
105
|
+
kind: "task",
|
|
106
|
+
id: "task-1",
|
|
107
|
+
contextId: "ctx-1",
|
|
108
|
+
status: { state: "working", timestamp: new Date().toISOString() },
|
|
109
|
+
};
|
|
110
|
+
await executor.execute(makeRequestContext({ task: existingTask }), eventBus);
|
|
111
|
+
|
|
112
|
+
// No initial task published; first event is working status
|
|
113
|
+
const first = events[0] as TaskStatusUpdateEvent;
|
|
114
|
+
expect(first.kind).toBe("status-update");
|
|
115
|
+
expect(first.status.state).toBe("working");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("skips empty chunks", async () => {
|
|
119
|
+
const mockAgent = {
|
|
120
|
+
stream: mock(async () => ({
|
|
121
|
+
textStream: makeAsyncIterable(["", "content", ""]),
|
|
122
|
+
})),
|
|
123
|
+
} as unknown as ToolLoopAgent<never>;
|
|
124
|
+
|
|
125
|
+
const executor = new ToolLoopAgentExecutor(mockAgent);
|
|
126
|
+
const eventBus = new DefaultExecutionEventBus();
|
|
127
|
+
|
|
128
|
+
const events: AgentExecutionEvent[] = [];
|
|
129
|
+
eventBus.on("event", (event) => events.push(event));
|
|
130
|
+
|
|
131
|
+
await executor.execute(makeRequestContext(), eventBus);
|
|
132
|
+
|
|
133
|
+
// initial task + working + 1 artifact ("content") + completed
|
|
134
|
+
expect(events.length).toBe(4);
|
|
135
|
+
const artifact = events[2] as TaskArtifactUpdateEvent;
|
|
136
|
+
expect(artifact.kind).toBe("artifact-update");
|
|
137
|
+
expect((artifact.artifact.parts[0] as { kind: string; text: string }).text).toBe("content");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("publishes error message and finishes when streaming throws", async () => {
|
|
141
|
+
const mockAgent = {
|
|
142
|
+
stream: mock(async () => {
|
|
143
|
+
throw new Error("stream failed");
|
|
144
|
+
}),
|
|
145
|
+
} as unknown as ToolLoopAgent<never>;
|
|
146
|
+
|
|
147
|
+
const executor = new ToolLoopAgentExecutor(mockAgent);
|
|
148
|
+
const eventBus = new DefaultExecutionEventBus();
|
|
149
|
+
|
|
150
|
+
const events: AgentExecutionEvent[] = [];
|
|
151
|
+
eventBus.on("event", (event) => events.push(event));
|
|
152
|
+
|
|
153
|
+
await executor.execute(makeRequestContext(), eventBus);
|
|
154
|
+
|
|
155
|
+
// initial task + working status + error message
|
|
156
|
+
expect(events.length).toBe(3);
|
|
157
|
+
const errorMsg = events[2] as Message;
|
|
158
|
+
expect(errorMsg.kind).toBe("message");
|
|
159
|
+
expect((errorMsg.parts[0] as { kind: string; text: string }).text).toContain("stream failed");
|
|
160
|
+
});
|
|
161
|
+
});
|
package/server/adapters/a2a.ts
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
RequestContext,
|
|
8
8
|
TaskStore,
|
|
9
9
|
} from "@a2a-js/sdk/server";
|
|
10
|
-
import { AgentCard, Message, TextPart } from "@a2a-js/sdk";
|
|
10
|
+
import { AgentCard, Message, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, TextPart } from "@a2a-js/sdk";
|
|
11
11
|
import type { ToolLoopAgent, ToolSet } from "ai";
|
|
12
12
|
import { getAixyzConfigRuntime } from "../../config";
|
|
13
13
|
import { AixyzServer } from "../index";
|
|
@@ -18,23 +18,66 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
|
|
|
18
18
|
constructor(private agent: ToolLoopAgent<never, TOOLS>) {}
|
|
19
19
|
|
|
20
20
|
async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
|
|
21
|
+
const { taskId, contextId, userMessage, task } = requestContext;
|
|
21
22
|
try {
|
|
22
23
|
// Extract the user's message text
|
|
23
|
-
const userMessage = requestContext.userMessage;
|
|
24
24
|
const textParts = userMessage.parts.filter((part): part is TextPart => part.kind === "text");
|
|
25
25
|
const prompt = textParts.map((part) => part.text).join("\n");
|
|
26
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
// Publish the initial Task object if one does not exist yet — required by ResultManager
|
|
28
|
+
// before any TaskArtifactUpdateEvent can be processed.
|
|
29
|
+
if (!task) {
|
|
30
|
+
const initialTask: Task = {
|
|
31
|
+
kind: "task",
|
|
32
|
+
id: taskId,
|
|
33
|
+
contextId,
|
|
34
|
+
status: { state: "submitted", timestamp: new Date().toISOString() },
|
|
35
|
+
history: [userMessage],
|
|
36
|
+
};
|
|
37
|
+
eventBus.publish(initialTask);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Signal that the agent is working
|
|
41
|
+
const workingUpdate: TaskStatusUpdateEvent = {
|
|
42
|
+
kind: "status-update",
|
|
43
|
+
taskId,
|
|
44
|
+
contextId,
|
|
45
|
+
status: { state: "working", timestamp: new Date().toISOString() },
|
|
46
|
+
final: false,
|
|
35
47
|
};
|
|
48
|
+
eventBus.publish(workingUpdate);
|
|
36
49
|
|
|
37
|
-
|
|
50
|
+
// Stream the response and publish artifact chunks as they arrive
|
|
51
|
+
const result = await this.agent.stream({ prompt });
|
|
52
|
+
const artifactId = randomUUID();
|
|
53
|
+
let firstChunk = true;
|
|
54
|
+
|
|
55
|
+
for await (const chunk of result.textStream) {
|
|
56
|
+
if (chunk) {
|
|
57
|
+
const artifactUpdate: TaskArtifactUpdateEvent = {
|
|
58
|
+
kind: "artifact-update",
|
|
59
|
+
taskId,
|
|
60
|
+
contextId,
|
|
61
|
+
artifact: {
|
|
62
|
+
artifactId,
|
|
63
|
+
parts: [{ kind: "text", text: chunk }],
|
|
64
|
+
},
|
|
65
|
+
append: !firstChunk,
|
|
66
|
+
};
|
|
67
|
+
eventBus.publish(artifactUpdate);
|
|
68
|
+
firstChunk = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Publish the final completed status
|
|
73
|
+
const completedUpdate: TaskStatusUpdateEvent = {
|
|
74
|
+
kind: "status-update",
|
|
75
|
+
taskId,
|
|
76
|
+
contextId,
|
|
77
|
+
status: { state: "completed", timestamp: new Date().toISOString() },
|
|
78
|
+
final: true,
|
|
79
|
+
};
|
|
80
|
+
eventBus.publish(completedUpdate);
|
|
38
81
|
eventBus.finished();
|
|
39
82
|
} catch (error) {
|
|
40
83
|
// Handle errors by publishing an error message
|
|
@@ -48,7 +91,7 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
|
|
|
48
91
|
text: `Error: ${error instanceof Error ? error.message : "An unknown error occurred"}`,
|
|
49
92
|
},
|
|
50
93
|
],
|
|
51
|
-
contextId
|
|
94
|
+
contextId,
|
|
52
95
|
};
|
|
53
96
|
|
|
54
97
|
eventBus.publish(errorMessage);
|
|
@@ -71,7 +114,7 @@ export function getAgentCard(): AgentCard {
|
|
|
71
114
|
version: config.version,
|
|
72
115
|
url: new URL("/agent", config.url).toString(),
|
|
73
116
|
capabilities: {
|
|
74
|
-
streaming:
|
|
117
|
+
streaming: true,
|
|
75
118
|
pushNotifications: false,
|
|
76
119
|
},
|
|
77
120
|
defaultInputModes: ["text/plain"],
|