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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aixyz",
3
- "version": "0.10.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.10.0",
27
- "@aixyz/config": "0.10.0",
28
- "@aixyz/erc-8004": "0.10.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
+ });
@@ -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
- // TODO(@fuxingloh): supporting streaming later
28
- const result = await this.agent.generate({ prompt });
29
- const responseMessage: Message = {
30
- kind: "message",
31
- messageId: randomUUID(),
32
- role: "agent",
33
- parts: [{ kind: "text", text: result.text }],
34
- contextId: requestContext.contextId,
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
- eventBus.publish(responseMessage);
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: requestContext.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: false,
117
+ streaming: true,
75
118
  pushNotifications: false,
76
119
  },
77
120
  defaultInputModes: ["text/plain"],