aixyz 0.10.0 → 0.12.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/accepts.ts CHANGED
@@ -34,5 +34,5 @@ export { HTTPFacilitatorClient };
34
34
  * The default facilitator client provided by aixyz.
35
35
  */
36
36
  export const facilitator: FacilitatorClient = new HTTPFacilitatorClient({
37
- url: "https://x402.agently.to/facilitator",
37
+ url: "https://x402.use-agently.com/facilitator",
38
38
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aixyz",
3
- "version": "0.10.0",
3
+ "version": "0.12.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.12.0",
27
+ "@aixyz/config": "0.12.0",
28
+ "@aixyz/erc-8004": "0.12.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);
@@ -62,16 +105,16 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
62
105
  }
63
106
  }
64
107
 
65
- export function getAgentCard(): AgentCard {
108
+ export function getAgentCard(agentPath = "/agent"): AgentCard {
66
109
  const config = getAixyzConfigRuntime();
67
110
  return {
68
111
  name: config.name,
69
112
  description: config.description,
70
113
  protocolVersion: "0.3.0",
71
114
  version: config.version,
72
- url: new URL("/agent", config.url).toString(),
115
+ url: new URL(agentPath, config.url).toString(),
73
116
  capabilities: {
74
- streaming: false,
117
+ streaming: true,
75
118
  pushNotifications: false,
76
119
  },
77
120
  defaultInputModes: ["text/plain"],
@@ -86,6 +129,7 @@ export function useA2A<TOOLS extends ToolSet = ToolSet>(
86
129
  default: ToolLoopAgent<never, TOOLS>;
87
130
  accepts?: Accepts;
88
131
  },
132
+ prefix?: string,
89
133
  taskStore: TaskStore = new InMemoryTaskStore(),
90
134
  ): void {
91
135
  if (exports.accepts) {
@@ -97,22 +141,27 @@ export function useA2A<TOOLS extends ToolSet = ToolSet>(
97
141
  return;
98
142
  }
99
143
 
144
+ const agentPath: `/${string}` = prefix ? `/${prefix}/agent` : "/agent";
145
+ const wellKnownPath: `/${string}` = prefix
146
+ ? `/${prefix}/.well-known/agent-card.json`
147
+ : "/.well-known/agent-card.json";
148
+
100
149
  const agentExecutor = new ToolLoopAgentExecutor(exports.default);
101
- const requestHandler = new DefaultRequestHandler(getAgentCard(), taskStore, agentExecutor);
150
+ const requestHandler = new DefaultRequestHandler(getAgentCard(agentPath), taskStore, agentExecutor);
102
151
 
103
152
  app.express.use(
104
- "/.well-known/agent-card.json",
153
+ wellKnownPath,
105
154
  agentCardHandler({
106
155
  agentCardProvider: requestHandler,
107
156
  }),
108
157
  );
109
158
 
110
159
  if (exports.accepts.scheme === "exact") {
111
- app.withX402Exact("POST /agent", exports.accepts);
160
+ app.withX402Exact(`POST ${agentPath}`, exports.accepts);
112
161
  }
113
162
 
114
163
  app.express.use(
115
- "/agent",
164
+ agentPath,
116
165
  jsonRpcHandler({
117
166
  requestHandler,
118
167
  userBuilder: UserBuilder.noAuthentication,
@@ -12,16 +12,16 @@ export function getAgentRegistrationFile(
12
12
  data: unknown,
13
13
  options: {
14
14
  mcp: boolean;
15
- a2a: boolean;
15
+ a2a: string[];
16
16
  },
17
17
  ): StrictAgentRegistrationFile {
18
18
  const config = getAixyzConfigRuntime();
19
19
  const services: StrictAgentRegistrationFile["services"] = [];
20
20
 
21
- if (options.a2a) {
21
+ for (const path of options.a2a) {
22
22
  services.push({
23
23
  name: "A2A",
24
- endpoint: new URL("/.well-known/agent-card.json", config.url).toString(),
24
+ endpoint: new URL(path, config.url).toString(),
25
25
  version: "0.3.0",
26
26
  });
27
27
  }
@@ -53,7 +53,7 @@ export function useERC8004(
53
53
  default: unknown;
54
54
  options: {
55
55
  mcp: boolean;
56
- a2a: boolean;
56
+ a2a: string[];
57
57
  };
58
58
  },
59
59
  ): void {