aixyz 0.20.0 → 0.22.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
@@ -6,6 +6,7 @@ export type Accepts = AcceptsX402 | AcceptsFree;
6
6
  export type AcceptsX402 = {
7
7
  scheme: "exact";
8
8
  price: string;
9
+ // TODO(kevin): update type to Network (`string:string`)
9
10
  network?: string;
10
11
  payTo?: string;
11
12
  };
@@ -0,0 +1,129 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { AixyzApp } from "../index";
3
+
4
+ /** Minimal request shape compatible with both Node's IncomingMessage and Express's Request. */
5
+ interface NodeRequest {
6
+ headers: IncomingMessage["headers"];
7
+ url?: string;
8
+ method?: string;
9
+ }
10
+
11
+ /** Minimal response shape compatible with both Node's ServerResponse and Express's Response. */
12
+ interface NodeResponse {
13
+ writeHead(statusCode: number, headers?: Record<string, string>): this;
14
+ write(chunk: unknown): boolean;
15
+ end(): this;
16
+ }
17
+
18
+ /**
19
+ * Converts an {@link AixyzApp} into Express-compatible middleware.
20
+ *
21
+ * This adapter bridges the web-standard `Request`/`Response` API used by
22
+ * `AixyzApp` with Express's Node-style `(req, res, next)` middleware signature.
23
+ * Incoming Express requests are converted to web-standard `Request` objects,
24
+ * routed through `app.fetch()`, and the resulting `Response` is streamed back
25
+ * to the Express response. If the `AixyzApp` does not match the request
26
+ * (404), control is passed to the next Express middleware via `next()`.
27
+ *
28
+ * The returned middleware handles all AixyzApp concerns — A2A, MCP, x402
29
+ * payment verification, and any registered plugins — so you can mount it
30
+ * alongside your own Express routes without conflict.
31
+ *
32
+ * **Important:** Do not apply `express.json()` or other body-parsing middleware
33
+ * before this middleware, as it needs access to the raw request body stream.
34
+ *
35
+ * @param app - A fully initialized {@link AixyzApp} instance (call
36
+ * `app.initialize()` before passing it here).
37
+ * @returns An async Express middleware function `(req, res, next) => void`.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import express from "express";
42
+ * import { AixyzApp } from "aixyz/app";
43
+ * import { toExpressMiddleware } from "aixyz/app/adapters/express";
44
+ * import { A2APlugin } from "aixyz/app/plugins/a2a";
45
+ * import { MCPPlugin } from "aixyz/app/plugins/mcp";
46
+ * import * as agent from "./agent";
47
+ * import * as myTool from "./tools/my-tool";
48
+ *
49
+ * const app = new AixyzApp();
50
+ * await app.withPlugin(new A2APlugin(agent));
51
+ * await app.withPlugin(new MCPPlugin([{ name: "myTool", exports: myTool }]));
52
+ * await app.initialize();
53
+ *
54
+ * const server = express();
55
+ *
56
+ * // Your own Express routes
57
+ * server.get("/health", (_req, res) => res.json({ status: "ok" }));
58
+ *
59
+ * // Mount AixyzApp (handles /agent, /mcp, /.well-known/agent-card.json, etc.)
60
+ * server.use(toExpressMiddleware(app));
61
+ *
62
+ * server.listen(3000);
63
+ * ```
64
+ */
65
+ export function toExpressMiddleware(app: AixyzApp) {
66
+ return async (req: NodeRequest, res: NodeResponse, next: (err?: unknown) => void) => {
67
+ try {
68
+ const request = toWebRequest(req);
69
+ const response = await app.fetch(request);
70
+
71
+ // Let Express handle unmatched routes
72
+ if (response.status === 404) {
73
+ return next();
74
+ }
75
+
76
+ await writeResponse(response, res);
77
+ } catch (err) {
78
+ next(err);
79
+ }
80
+ };
81
+ }
82
+
83
+ function toWebRequest(req: NodeRequest): Request {
84
+ const protocol = (req.headers["x-forwarded-proto"] as string) || "http";
85
+ const host = req.headers.host || "localhost";
86
+ const url = `${protocol}://${host}${req.url || "/"}`;
87
+
88
+ const headers = new Headers();
89
+ for (const [key, value] of Object.entries(req.headers)) {
90
+ if (value === undefined) continue;
91
+ if (Array.isArray(value)) {
92
+ for (const v of value) headers.append(key, v);
93
+ } else {
94
+ headers.set(key, value);
95
+ }
96
+ }
97
+
98
+ const method = (req.method || "GET").toUpperCase();
99
+ const hasBody = method !== "GET" && method !== "HEAD";
100
+
101
+ return new Request(url, {
102
+ method,
103
+ headers,
104
+ body: hasBody ? (req as unknown as ReadableStream) : undefined,
105
+ // @ts-expect-error -- Node 18+ supports duplex on RequestInit
106
+ duplex: hasBody ? "half" : undefined,
107
+ });
108
+ }
109
+
110
+ async function writeResponse(response: Response, res: NodeResponse): Promise<void> {
111
+ res.writeHead(response.status, Object.fromEntries(response.headers));
112
+
113
+ if (!response.body) {
114
+ res.end();
115
+ return;
116
+ }
117
+
118
+ const reader = response.body.getReader();
119
+ try {
120
+ while (true) {
121
+ const { done, value } = await reader.read();
122
+ if (done) break;
123
+ res.write(value);
124
+ }
125
+ } finally {
126
+ reader.releaseLock();
127
+ res.end();
128
+ }
129
+ }
package/app/index.ts ADDED
@@ -0,0 +1,138 @@
1
+ import type { AcceptsX402 } from "../accepts";
2
+ import type { FacilitatorClient } from "@x402/core/server";
3
+ import { type HttpMethod, type RouteHandler, type Middleware, type RouteEntry } from "./types";
4
+ import { PaymentGateway } from "./payment/payment";
5
+ import { Network } from "@x402/core/types";
6
+ import { getAixyzConfig } from "@aixyz/config";
7
+ import { BasePlugin } from "./plugin";
8
+
9
+ export { BasePlugin };
10
+ export type { HttpMethod, RouteHandler, Middleware, RouteEntry };
11
+
12
+ export interface AixyzAppOptions {
13
+ facilitators?: FacilitatorClient | FacilitatorClient[];
14
+ }
15
+
16
+ /**
17
+ * Framework-agnostic route and middleware registry with optional x402 payment gating.
18
+ * Call `fetch()` to dispatch a web-standard Request through payment verification, middleware, and route handler.
19
+ */
20
+ export class AixyzApp {
21
+ readonly routes = new Map<string, RouteEntry>();
22
+ readonly payment?: PaymentGateway;
23
+ private middlewares: Middleware[] = [];
24
+ private plugins: BasePlugin[] = [];
25
+ private readonly poweredByHeader: boolean;
26
+
27
+ constructor(options?: AixyzAppOptions) {
28
+ // TODO(future): getAiXyzConfig will be materialized.
29
+ // this is internal, we control it so it's fine for us to use—but we changing it in the future.
30
+ const config = getAixyzConfig();
31
+ this.poweredByHeader = config.build.poweredByHeader;
32
+
33
+ if (options?.facilitators) {
34
+ this.payment = new PaymentGateway(options.facilitators, config);
35
+ this.payment.register((config.x402.network as Network) ?? "eip155:8453");
36
+ }
37
+ }
38
+
39
+ /** Initialize payment gateway and plugins. Must be called after all routes are registered. */
40
+ async initialize(): Promise<void> {
41
+ if (this.payment) {
42
+ // Register payment routes with the gateway before initializing
43
+ for (const [, entry] of this.routes) {
44
+ if (entry.payment) {
45
+ this.payment.addRoute(entry.method, entry.path, entry.payment);
46
+ }
47
+ }
48
+ await this.payment.initialize();
49
+ }
50
+
51
+ for (const plugin of this.plugins) {
52
+ await plugin.initialize?.(this);
53
+ }
54
+ }
55
+
56
+ /** Register a plugin. Calls plugin.register(this) and returns this for chaining. */
57
+ async withPlugin<B extends BasePlugin>(plugin: B): Promise<this> {
58
+ this.plugins.push(plugin);
59
+ await plugin.register(this);
60
+ return this;
61
+ }
62
+
63
+ /** Returns the canonical lookup key for a route (e.g. "POST /agent"). */
64
+ getRouteKey(method: HttpMethod, path: string) {
65
+ return `${method} ${path}`;
66
+ }
67
+
68
+ /** Register a route with an optional x402 payment requirement. */
69
+ route(method: HttpMethod, path: string, handler: RouteHandler, options?: { payment?: AcceptsX402 }): void {
70
+ const key = this.getRouteKey(method, path);
71
+ this.routes.set(key, {
72
+ method,
73
+ path,
74
+ handler,
75
+ payment: options?.payment,
76
+ });
77
+ }
78
+
79
+ /** Append a middleware to the chain. Middlewares run in registration order before the route handler. */
80
+ use(middleware: Middleware): void {
81
+ this.middlewares.push(middleware);
82
+ }
83
+
84
+ /** Returns the registered middlewares (read-only access for adapters). */
85
+ getMiddlewares(): Middleware[] {
86
+ return this.middlewares;
87
+ }
88
+
89
+ /** Dispatch a web-standard Request through payment verification, middleware, and route handler. */
90
+ fetch = async (request: Request): Promise<Response> => {
91
+ const response = await this.dispatch(request);
92
+ if (this.poweredByHeader) {
93
+ response.headers.set("X-Powered-By", "aixyz");
94
+ }
95
+ return response;
96
+ };
97
+
98
+ private dispatch = async (request: Request): Promise<Response> => {
99
+ const url = new URL(request.url);
100
+ const key = this.getRouteKey(request.method as HttpMethod, url.pathname);
101
+ const entry = this.routes.get(key);
102
+
103
+ if (!entry) {
104
+ return new Response("Not Found", { status: 404 });
105
+ }
106
+
107
+ if (entry.payment && this.payment) {
108
+ const rejection = await this.payment.verify(request);
109
+ if (rejection) return rejection;
110
+ }
111
+
112
+ let index = 0;
113
+ const middlewares = this.middlewares;
114
+ const handler = entry.handler;
115
+
116
+ const next = async (): Promise<Response> => {
117
+ if (index < middlewares.length) {
118
+ const mw = middlewares[index++];
119
+ return mw(request, next);
120
+ }
121
+ return handler(request);
122
+ };
123
+
124
+ const response = await next();
125
+
126
+ if (entry.payment && this.payment) {
127
+ const settlementResult = await this.payment.settle(request);
128
+ if (settlementResult?.success) {
129
+ const paymentResultHeader = settlementResult.headers["PAYMENT-RESPONSE"];
130
+ const cloned = new Response(response.body, response);
131
+ cloned.headers.set("PAYMENT-RESPONSE", paymentResultHeader);
132
+ return cloned;
133
+ }
134
+ }
135
+
136
+ return response;
137
+ };
138
+ }
@@ -0,0 +1,157 @@
1
+ import { FacilitatorClient, ProcessSettleResultResponse, x402ResourceServer } from "@x402/core/server";
2
+ import {
3
+ x402HTTPResourceServer,
4
+ type HTTPAdapter,
5
+ type HTTPRequestContext,
6
+ type RoutesConfig,
7
+ type RouteConfig,
8
+ type HTTPResponseInstructions,
9
+ } from "@x402/core/http";
10
+ import { ExactEvmScheme } from "@x402/evm/exact/server";
11
+ import type { AcceptsX402 } from "../../accepts";
12
+ import { Network, PaymentPayload, PaymentRequirements } from "@x402/core/types";
13
+ import { AixyzConfig } from "@aixyz/config";
14
+
15
+ /**
16
+ * Converts a web-standard Request into the x402 HTTPAdapter interface.
17
+ */
18
+ function toHTTPAdapter(request: Request): HTTPAdapter {
19
+ const url = new URL(request.url);
20
+ return {
21
+ getHeader: (name) => request.headers.get(name) ?? undefined,
22
+ getMethod: () => request.method,
23
+ getPath: () => url.pathname,
24
+ getUrl: () => request.url,
25
+ getAcceptHeader: () => request.headers.get("accept") ?? "",
26
+ getUserAgent: () => request.headers.get("user-agent") ?? "",
27
+ getQueryParams: () => Object.fromEntries(url.searchParams),
28
+ getQueryParam: (name) => url.searchParams.get(name) ?? undefined,
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Converts x402 HTTPResponseInstructions into a web-standard Response.
34
+ */
35
+ function toResponse(instructions: HTTPResponseInstructions): Response {
36
+ const headers = new Headers(instructions.headers);
37
+ let body: string | undefined;
38
+ if (instructions.body != null) {
39
+ body = instructions.isHtml ? String(instructions.body) : JSON.stringify(instructions.body);
40
+ }
41
+ if (body && !headers.has("content-type")) {
42
+ headers.set("content-type", instructions.isHtml ? "text/html" : "application/json");
43
+ }
44
+ return new Response(body, { status: instructions.status, headers });
45
+ }
46
+
47
+ interface PaymentContext {
48
+ paymentPayload: PaymentPayload;
49
+ paymentRequirements: PaymentRequirements;
50
+ declaredExtensions?: Record<string, unknown>;
51
+ }
52
+
53
+ /**
54
+ * Thin wrapper around x402HTTPResourceServer
55
+ */
56
+ export class PaymentGateway {
57
+ readonly resourceServer: x402ResourceServer;
58
+ private httpServer?: x402HTTPResourceServer;
59
+ private readonly config: AixyzConfig;
60
+ private readonly pendingRoutes = new Map<string, RouteConfig>();
61
+ private readonly verifiedPayments = new WeakMap<Request, PaymentContext>();
62
+
63
+ constructor(facilitators: FacilitatorClient | FacilitatorClient[], config: AixyzConfig) {
64
+ this.resourceServer = new x402ResourceServer(facilitators);
65
+ this.config = config;
66
+ }
67
+
68
+ /** Register an EVM payment scheme for the given network (e.g. Base mainnet). */
69
+ register(network: Network) {
70
+ this.resourceServer.register(network, new ExactEvmScheme());
71
+ }
72
+
73
+ /** Returns the canonical lookup key for a payment route (e.g. "POST /agent"). */
74
+ getRouteKey(method: string, path: string) {
75
+ return `${method.toUpperCase()} ${path}`;
76
+ }
77
+
78
+ /**
79
+ * Add a payment-gated route. Must be called before initialize().
80
+ */
81
+ addRoute(method: string, path: string, accepts: AcceptsX402): void {
82
+ const pattern = this.getRouteKey(method, path);
83
+ this.pendingRoutes.set(pattern, {
84
+ accepts: {
85
+ scheme: accepts.scheme,
86
+ payTo: accepts.payTo ?? this.config.x402.payTo,
87
+ price: accepts.price,
88
+ network: (accepts.network as Network) ?? (this.config.x402.network as Network),
89
+ },
90
+ });
91
+ }
92
+
93
+ /**
94
+ * Initialize the payment gateway. Builds the x402HTTPResourceServer from registered routes.
95
+ * Must be called after all routes are added.
96
+ */
97
+ async initialize(): Promise<void> {
98
+ const routes: RoutesConfig =
99
+ this.pendingRoutes.size > 0 ? Object.fromEntries(this.pendingRoutes) : { "* /*": { accepts: [] } };
100
+ this.httpServer = new x402HTTPResourceServer(this.resourceServer, routes);
101
+ await this.httpServer.initialize();
102
+ }
103
+
104
+ /**
105
+ * Verify payment for a request. Returns a 402 Response if payment is required/invalid,
106
+ * or null if the request is authorized to proceed.
107
+ */
108
+ async verify(request: Request): Promise<Response | null> {
109
+ if (!this.httpServer) {
110
+ throw new Error("PaymentGateway not initialized. Call initialize() first.");
111
+ }
112
+
113
+ const adapter = toHTTPAdapter(request);
114
+ const context: HTTPRequestContext = {
115
+ adapter,
116
+ path: adapter.getPath(),
117
+ method: adapter.getMethod(),
118
+ paymentHeader: adapter.getHeader("payment-signature") || adapter.getHeader("PAYMENT-SIGNATURE"),
119
+ };
120
+
121
+ const result = await this.httpServer.processHTTPRequest(context);
122
+
123
+ switch (result.type) {
124
+ case "no-payment-required":
125
+ return null;
126
+
127
+ case "payment-verified":
128
+ this.verifiedPayments.set(request, {
129
+ paymentPayload: result.paymentPayload,
130
+ paymentRequirements: result.paymentRequirements,
131
+ declaredExtensions: result.declaredExtensions,
132
+ });
133
+ return null;
134
+
135
+ case "payment-error":
136
+ return toResponse(result.response);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Settle a previously verified payment. Call after the handler responds successfully.
142
+ * Returns settlement headers to merge into the response, or null if nothing to settle.
143
+ */
144
+ async settle(request: Request): Promise<ProcessSettleResultResponse | null> {
145
+ const ctx = this.verifiedPayments.get(request);
146
+ if (!ctx || !this.httpServer) return null;
147
+ this.verifiedPayments.delete(request);
148
+
149
+ const result = await this.httpServer.processSettlement(
150
+ ctx.paymentPayload,
151
+ ctx.paymentRequirements,
152
+ ctx.declaredExtensions,
153
+ );
154
+
155
+ return result;
156
+ }
157
+ }
package/app/plugin.ts ADDED
@@ -0,0 +1,7 @@
1
+ import type { AixyzApp } from "./index";
2
+
3
+ export abstract class BasePlugin {
4
+ abstract readonly name: string;
5
+ abstract register(app: AixyzApp): void | Promise<void>;
6
+ initialize?(app: AixyzApp): void | Promise<void>;
7
+ }
@@ -4,28 +4,30 @@ import {
4
4
  DefaultRequestHandler,
5
5
  ExecutionEventBus,
6
6
  InMemoryTaskStore,
7
+ JsonRpcTransportHandler,
7
8
  RequestContext,
8
9
  TaskStore,
9
10
  } from "@a2a-js/sdk/server";
10
11
  import { AgentCard, Message, Task, TaskArtifactUpdateEvent, TaskStatusUpdateEvent, TextPart } from "@a2a-js/sdk";
11
12
  import type { ToolLoopAgent, ToolSet } from "ai";
12
13
  import { getAixyzConfigRuntime } from "../../config";
13
- import { AixyzServer } from "../index";
14
- import { agentCardHandler, jsonRpcHandler, UserBuilder } from "@a2a-js/sdk/server/express";
14
+ import { BasePlugin } from "../plugin";
15
+ import type { AixyzApp } from "../index";
15
16
  import { Accepts, AcceptsScheme } from "../../accepts";
16
17
 
18
+ /**
19
+ * Wraps a Vercel AI SDK ToolLoopAgent into the A2A AgentExecutor interface.
20
+ * Streams text chunks as artifact updates and publishes task lifecycle events.
21
+ */
17
22
  export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements AgentExecutor {
18
23
  constructor(private agent: ToolLoopAgent<never, TOOLS>) {}
19
24
 
20
25
  async execute(requestContext: RequestContext, eventBus: ExecutionEventBus): Promise<void> {
21
26
  const { taskId, contextId, userMessage, task } = requestContext;
22
27
  try {
23
- // Extract the user's message text
24
28
  const textParts = userMessage.parts.filter((part): part is TextPart => part.kind === "text");
25
29
  const prompt = textParts.map((part) => part.text).join("\n");
26
30
 
27
- // Publish the initial Task object if one does not exist yet — required by ResultManager
28
- // before any TaskArtifactUpdateEvent can be processed.
29
31
  if (!task) {
30
32
  const initialTask: Task = {
31
33
  kind: "task",
@@ -37,7 +39,6 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
37
39
  eventBus.publish(initialTask);
38
40
  }
39
41
 
40
- // Signal that the agent is working
41
42
  const workingUpdate: TaskStatusUpdateEvent = {
42
43
  kind: "status-update",
43
44
  taskId,
@@ -47,7 +48,6 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
47
48
  };
48
49
  eventBus.publish(workingUpdate);
49
50
 
50
- // Stream the response and publish artifact chunks as they arrive
51
51
  const result = await this.agent.stream({ prompt });
52
52
  const artifactId = randomUUID();
53
53
  let firstChunk = true;
@@ -69,7 +69,6 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
69
69
  }
70
70
  }
71
71
 
72
- // Publish the final completed status
73
72
  const completedUpdate: TaskStatusUpdateEvent = {
74
73
  kind: "status-update",
75
74
  taskId,
@@ -80,7 +79,6 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
80
79
  eventBus.publish(completedUpdate);
81
80
  eventBus.finished();
82
81
  } catch (error) {
83
- // Handle errors by publishing an error message
84
82
  const errorMessage: Message = {
85
83
  kind: "message",
86
84
  messageId: randomUUID(),
@@ -93,18 +91,17 @@ export class ToolLoopAgentExecutor<TOOLS extends ToolSet = ToolSet> implements A
93
91
  ],
94
92
  contextId,
95
93
  };
96
-
97
94
  eventBus.publish(errorMessage);
98
95
  eventBus.finished();
99
96
  }
100
97
  }
101
98
 
102
99
  async cancelTask(_taskId: string, eventBus: ExecutionEventBus): Promise<void> {
103
- // TODO(@fuxingloh): The ToolLoopAgent doesn't support cancellation, so we just finish
104
100
  eventBus.finished();
105
101
  }
106
102
  }
107
103
 
104
+ /** Build an A2A AgentCard from the runtime config, pointing to the given agent endpoint path. */
108
105
  export function getAgentCard(agentPath = "/agent"): AgentCard {
109
106
  const config = getAixyzConfigRuntime();
110
107
  return {
@@ -113,58 +110,73 @@ export function getAgentCard(agentPath = "/agent"): AgentCard {
113
110
  protocolVersion: "0.3.0",
114
111
  version: config.version,
115
112
  url: new URL(agentPath, config.url).toString(),
116
- capabilities: {
117
- streaming: true,
118
- pushNotifications: false,
119
- },
113
+ capabilities: { streaming: true, pushNotifications: false },
120
114
  defaultInputModes: ["text/plain"],
121
115
  defaultOutputModes: ["text/plain"],
122
116
  skills: config.skills,
123
117
  };
124
118
  }
125
119
 
126
- export function useA2A<TOOLS extends ToolSet = ToolSet>(
127
- app: AixyzServer,
128
- exports: {
129
- default: ToolLoopAgent<never, TOOLS>;
130
- accepts?: Accepts;
131
- },
132
- prefix?: string,
133
- taskStore: TaskStore = new InMemoryTaskStore(),
134
- ): void {
135
- if (exports.accepts) {
136
- // TODO(@fuxingloh): validation should be done at build stage
137
- AcceptsScheme.parse(exports.accepts);
138
- } else {
139
- // TODO(@fuxingloh): right now we just don't register the agent if accepts is not provided,
140
- // but it might be a better idea to do it in aixyz-cli (aixyz-pack).
141
- return;
120
+ /**
121
+ * A2A protocol plugin. Registers the well-known agent card endpoint
122
+ * and a JSON-RPC endpoint that delegates to the given ToolLoopAgent.
123
+ * Routes are only registered if the agent exports a valid `accepts` payment config.
124
+ */
125
+ export class A2APlugin<TOOLS extends ToolSet = ToolSet> extends BasePlugin {
126
+ readonly name = "a2a";
127
+
128
+ constructor(
129
+ private exports: { default: ToolLoopAgent<never, TOOLS>; accepts?: Accepts },
130
+ private prefix?: string,
131
+ private taskStore: TaskStore = new InMemoryTaskStore(),
132
+ ) {
133
+ super();
142
134
  }
143
135
 
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
-
149
- const agentExecutor = new ToolLoopAgentExecutor(exports.default);
150
- const requestHandler = new DefaultRequestHandler(getAgentCard(agentPath), taskStore, agentExecutor);
136
+ register(app: AixyzApp): void {
137
+ if (this.exports.accepts) {
138
+ AcceptsScheme.parse(this.exports.accepts);
139
+ } else {
140
+ return;
141
+ }
151
142
 
152
- app.express.use(
153
- wellKnownPath,
154
- agentCardHandler({
155
- agentCardProvider: requestHandler,
156
- }),
157
- );
143
+ const agentPath: `/${string}` = this.prefix ? `/${this.prefix}/agent` : "/agent";
144
+ const wellKnownPath: `/${string}` = this.prefix
145
+ ? `/${this.prefix}/.well-known/agent-card.json`
146
+ : "/.well-known/agent-card.json";
147
+
148
+ const agentExecutor = new ToolLoopAgentExecutor(this.exports.default);
149
+ const requestHandler = new DefaultRequestHandler(getAgentCard(agentPath), this.taskStore, agentExecutor);
150
+ const jsonRpcTransport = new JsonRpcTransportHandler(requestHandler);
151
+
152
+ // Agent card — pure web-standard handler
153
+ app.route("GET", wellKnownPath, async () => {
154
+ const card = await requestHandler.getAgentCard();
155
+ return Response.json(card);
156
+ });
157
+
158
+ // JSON-RPC endpoint — pure web-standard handler using JsonRpcTransportHandler
159
+ app.route(
160
+ "POST",
161
+ agentPath,
162
+ async (request: Request) => {
163
+ const body = await request.json();
164
+ const result = await jsonRpcTransport.handle(body);
165
+
166
+ // If result is an AsyncGenerator (streaming), collect all chunks
167
+ if (Symbol.asyncIterator in Object(result)) {
168
+ const chunks: unknown[] = [];
169
+ for await (const chunk of result as AsyncGenerator) {
170
+ chunks.push(chunk);
171
+ }
172
+ return Response.json(chunks[chunks.length - 1]);
173
+ }
158
174
 
159
- if (exports.accepts.scheme === "exact") {
160
- app.withX402Exact(`POST ${agentPath}`, exports.accepts);
175
+ return Response.json(result);
176
+ },
177
+ {
178
+ payment: this.exports.accepts.scheme === "exact" ? this.exports.accepts : undefined,
179
+ },
180
+ );
161
181
  }
162
-
163
- app.express.use(
164
- agentPath,
165
- jsonRpcHandler({
166
- requestHandler,
167
- userBuilder: UserBuilder.noAuthentication,
168
- }),
169
- );
170
182
  }