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 +1 -1
- package/package.json +4 -4
- package/server/adapters/a2a.test.ts +161 -0
- package/server/adapters/a2a.ts +68 -19
- package/server/adapters/erc-8004.ts +4 -4
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.
|
|
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.
|
|
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.
|
|
27
|
-
"@aixyz/config": "0.
|
|
28
|
-
"@aixyz/erc-8004": "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
|
+
});
|
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);
|
|
@@ -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(
|
|
115
|
+
url: new URL(agentPath, config.url).toString(),
|
|
73
116
|
capabilities: {
|
|
74
|
-
streaming:
|
|
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
|
-
|
|
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(
|
|
160
|
+
app.withX402Exact(`POST ${agentPath}`, exports.accepts);
|
|
112
161
|
}
|
|
113
162
|
|
|
114
163
|
app.express.use(
|
|
115
|
-
|
|
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:
|
|
15
|
+
a2a: string[];
|
|
16
16
|
},
|
|
17
17
|
): StrictAgentRegistrationFile {
|
|
18
18
|
const config = getAixyzConfigRuntime();
|
|
19
19
|
const services: StrictAgentRegistrationFile["services"] = [];
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
for (const path of options.a2a) {
|
|
22
22
|
services.push({
|
|
23
23
|
name: "A2A",
|
|
24
|
-
endpoint: new URL(
|
|
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:
|
|
56
|
+
a2a: string[];
|
|
57
57
|
};
|
|
58
58
|
},
|
|
59
59
|
): void {
|