pi-a2a-adaptor 1.0.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/src/client.ts ADDED
@@ -0,0 +1,365 @@
1
+ import * as http from "node:http";
2
+ import * as https from "node:https";
3
+ import type {
4
+ A2AClientOptions,
5
+ A2ATask,
6
+ AgentCard,
7
+ ClientConfig,
8
+ JSONRPCRequest,
9
+ JSONRPCResponse,
10
+ ListTasksParams,
11
+ ListTasksResult,
12
+ Message,
13
+ PushNotificationConfig,
14
+ RemoteAgent,
15
+ SecurityConfig,
16
+ StreamResult,
17
+ TaskOptions,
18
+ TaskUpdateCallback,
19
+ } from "./types.js";
20
+ import { A2AError } from "./errors.js";
21
+ import { JSONRPCErrorCode } from "./types.js";
22
+
23
+ const METHODS = {
24
+ SEND_MESSAGE: "message/send",
25
+ STREAM_MESSAGE: "message/stream",
26
+ GET_TASK: "tasks/get",
27
+ CANCEL_TASK: "tasks/cancel",
28
+ RESUBSCRIBE: "tasks/resubscribe",
29
+ LIST_TASKS: "tasks/list",
30
+ PUSH_SET: "tasks/pushNotification/set",
31
+ PUSH_GET: "tasks/pushNotification/get",
32
+ PUSH_LIST: "tasks/pushNotificationConfig/list",
33
+ PUSH_DELETE: "tasks/pushNotificationConfig/delete",
34
+ } as const;
35
+
36
+ const ENDPOINTS = {
37
+ AGENT_CARD: "/.well-known/agent-card.json",
38
+ DISPATCH: "/",
39
+ } as const;
40
+
41
+ const TERMINAL_STATES = new Set(["completed", "failed", "canceled", "rejected"]);
42
+
43
+ export class A2AClient {
44
+ private config: ClientConfig;
45
+ private security: SecurityConfig;
46
+ private pendingStreams = new Map<string, AbortController>();
47
+ private requestIdCounter = 0;
48
+
49
+ constructor(config: ClientConfig, security: SecurityConfig) {
50
+ this.config = config;
51
+ this.security = security;
52
+ }
53
+
54
+ // ─── Agent Discovery ───
55
+
56
+ async discoverAgent(url: string): Promise<RemoteAgent> {
57
+ const agentUrl = new URL(url);
58
+ const cardUrl = `${agentUrl.origin}${ENDPOINTS.AGENT_CARD}`;
59
+ const card = (await this.httpGet(cardUrl)) as AgentCard;
60
+ return { ...card, url: card.url || url, discoveredAt: Date.now() } as RemoteAgent;
61
+ }
62
+
63
+ // ─── Core Methods ───
64
+
65
+ async sendMessage(agent: RemoteAgent, message: Message, options: TaskOptions = {}): Promise<A2ATask | Message> {
66
+ if (!message.contextId) message.contextId = this.generateId();
67
+ message.kind = "message";
68
+
69
+ const request = this.createRequest(METHODS.SEND_MESSAGE, {
70
+ message,
71
+ configuration: this.buildSendConfig(options),
72
+ metadata: options.metadata,
73
+ });
74
+
75
+ const response = await this.httpPostJSON(agent, request, options);
76
+ if (response.error) throw A2AError.fromResponse(response);
77
+
78
+ const raw = response.result as unknown;
79
+
80
+ // Shape 1: { task: A2ATask }
81
+ if (raw && typeof raw === "object" && "task" in raw) {
82
+ const wrapped = raw as { task?: A2ATask; message?: Message };
83
+ if (wrapped.task) {
84
+ if (!TERMINAL_STATES.has(wrapped.task.status.state) && options.polling) {
85
+ return this.waitForTask(agent, wrapped.task.id, options.polling);
86
+ }
87
+ return wrapped.task;
88
+ }
89
+ if (wrapped.message) return wrapped.message;
90
+ }
91
+
92
+ // Shape 2: direct A2ATask (status + id present)
93
+ if (raw && typeof raw === "object" && "status" in raw && "id" in raw) {
94
+ const task = raw as A2ATask;
95
+ if (!TERMINAL_STATES.has(task.status.state) && options.polling) {
96
+ return this.waitForTask(agent, task.id, options.polling);
97
+ }
98
+ return task;
99
+ }
100
+
101
+ // Shape 3: direct Message (role + parts present)
102
+ if (raw && typeof raw === "object" && "role" in raw && "parts" in raw) {
103
+ return raw as Message;
104
+ }
105
+
106
+ throw new A2AError(JSONRPCErrorCode.InvalidAgentResponse, "Invalid response: no task or message\nRaw: " + JSON.stringify(raw).slice(0, 500));
107
+ }
108
+
109
+ async sendStreamingMessage(
110
+ agent: RemoteAgent,
111
+ message: Message,
112
+ onUpdate: TaskUpdateCallback,
113
+ options: TaskOptions = {}
114
+ ): Promise<A2ATask> {
115
+ if (!message.contextId) message.contextId = this.generateId();
116
+ message.kind = "message";
117
+
118
+ const request = this.createRequest(METHODS.STREAM_MESSAGE, {
119
+ message,
120
+ configuration: this.buildSendConfig(options),
121
+ metadata: options.metadata,
122
+ });
123
+
124
+ return this.sseRequest(agent, request, onUpdate);
125
+ }
126
+
127
+ async getTask(agent: RemoteAgent, taskId: string, historyLength?: number): Promise<A2ATask> {
128
+ const params: Record<string, unknown> = { id: taskId };
129
+ if (historyLength !== undefined) params.historyLength = historyLength;
130
+
131
+ const request = this.createRequest(METHODS.GET_TASK, params);
132
+ const response = await this.httpPostJSON(agent, request);
133
+ if (response.error) throw A2AError.fromResponse(response);
134
+ return response.result as unknown as A2ATask;
135
+ }
136
+
137
+ async cancelTask(agent: RemoteAgent, taskId: string): Promise<A2ATask> {
138
+ const request = this.createRequest(METHODS.CANCEL_TASK, { id: taskId });
139
+ const response = await this.httpPostJSON(agent, request);
140
+ if (response.error) throw A2AError.fromResponse(response);
141
+ return response.result as unknown as A2ATask;
142
+ }
143
+
144
+ async listTasks(agent: RemoteAgent, params: ListTasksParams = {}): Promise<ListTasksResult> {
145
+ const request = this.createRequest(METHODS.LIST_TASKS, params as unknown as Record<string, unknown>);
146
+ const response = await this.httpPostJSON(agent, request);
147
+ if (response.error) throw A2AError.fromResponse(response);
148
+ return response.result as unknown as ListTasksResult;
149
+ }
150
+
151
+ async resubscribeToTask(agent: RemoteAgent, taskId: string, onUpdate: TaskUpdateCallback, signal?: AbortSignal): Promise<void> {
152
+ const request = this.createRequest(METHODS.RESUBSCRIBE, { id: taskId });
153
+
154
+ return new Promise((resolve, reject) => {
155
+ const abortController = new AbortController();
156
+ if (signal) signal.addEventListener("abort", () => { abortController.abort(); resolve(); });
157
+
158
+ this.sendStreamingRequest(agent, request, abortController.signal, (rawData) => {
159
+ const response = rawData as JSONRPCResponse;
160
+ if (response.error) { reject(A2AError.fromResponse(response)); return; }
161
+ const result = response.result as StreamResult | undefined;
162
+ if (!result) return;
163
+
164
+ if (result.kind === "task") {
165
+ onUpdate({ id: result.task.id, contextId: result.task.contextId, status: result.task.status });
166
+ if (TERMINAL_STATES.has(result.task.status.state)) resolve();
167
+ }
168
+ if (result.kind === "status-update") {
169
+ onUpdate({ id: result.taskId, contextId: result.contextId, status: result.status });
170
+ if (TERMINAL_STATES.has(result.status.state)) resolve();
171
+ }
172
+ if (result.kind === "artifact-update") {
173
+ onUpdate({ id: result.taskId, contextId: result.contextId, artifacts: [result.artifact] });
174
+ }
175
+ }).catch(reject);
176
+ });
177
+ }
178
+
179
+ async setPushNotification(agent: RemoteAgent, taskId: string, config: PushNotificationConfig): Promise<PushNotificationConfig> {
180
+ const request = this.createRequest(METHODS.PUSH_SET, { ...config, taskId } as unknown as Record<string, unknown>);
181
+ const response = await this.httpPostJSON(agent, request);
182
+ if (response.error) throw A2AError.fromResponse(response);
183
+ return response.result as unknown as PushNotificationConfig;
184
+ }
185
+
186
+ async getPushNotification(agent: RemoteAgent, taskId: string): Promise<PushNotificationConfig> {
187
+ const request = this.createRequest(METHODS.PUSH_GET, { taskId } as unknown as Record<string, unknown>);
188
+ const response = await this.httpPostJSON(agent, request);
189
+ if (response.error) throw A2AError.fromResponse(response);
190
+ return response.result as unknown as PushNotificationConfig;
191
+ }
192
+
193
+ async listPushNotificationConfigs(agent: RemoteAgent, taskId: string): Promise<PushNotificationConfig[]> {
194
+ const request = this.createRequest(METHODS.PUSH_LIST, { taskId } as unknown as Record<string, unknown>);
195
+ const response = await this.httpPostJSON(agent, request);
196
+ if (response.error) throw A2AError.fromResponse(response);
197
+ return (response.result as unknown as { configs: PushNotificationConfig[] }).configs;
198
+ }
199
+
200
+ async deletePushNotificationConfig(agent: RemoteAgent, taskId: string, configId: string): Promise<void> {
201
+ const request = this.createRequest(METHODS.PUSH_DELETE, { taskId, id: configId } as unknown as Record<string, unknown>);
202
+ const response = await this.httpPostJSON(agent, request);
203
+ if (response.error) throw A2AError.fromResponse(response);
204
+ }
205
+
206
+ cancelAll(): void {
207
+ for (const [, ctrl] of this.pendingStreams) ctrl.abort();
208
+ this.pendingStreams.clear();
209
+ }
210
+
211
+ // ─── Private Helpers ───
212
+
213
+ private readonly TERMINAL_STATES = TERMINAL_STATES;
214
+
215
+ private async waitForTask(agent: RemoteAgent, taskId: string, options: NonNullable<TaskOptions["polling"]>): Promise<A2ATask> {
216
+ const { intervalMs = 2000, maxAttempts = 60, timeoutMs = 120000 } = options;
217
+ const deadline = Date.now() + timeoutMs;
218
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
219
+ if (Date.now() >= deadline) throw new A2AError(JSONRPCErrorCode.TaskTimeout, `Task ${taskId} timed out after ${timeoutMs}ms`);
220
+ await this.delay(intervalMs);
221
+ const task = await this.getTask(agent, taskId);
222
+ if (this.TERMINAL_STATES.has(task.status.state)) return task;
223
+ }
224
+ throw new A2AError(JSONRPCErrorCode.TaskTimeout, `Task ${taskId} exceeded max attempts (${maxAttempts})`);
225
+ }
226
+
227
+ private async sseRequest(agent: RemoteAgent, request: JSONRPCRequest, onUpdate: TaskUpdateCallback): Promise<A2ATask> {
228
+ return new Promise((resolve, reject) => {
229
+ const abortController = new AbortController();
230
+ const requestId = String(request.id);
231
+ this.pendingStreams.set(requestId, abortController);
232
+ let latestTask: A2ATask | null = null;
233
+
234
+ this.sendStreamingRequest(agent, request, abortController.signal, (rawData) => {
235
+ const response = rawData as JSONRPCResponse;
236
+ if (response.error) { this.pendingStreams.delete(requestId); reject(A2AError.fromResponse(response)); return; }
237
+ const result = response.result as StreamResult | undefined;
238
+ if (!result) return;
239
+ switch (result.kind) {
240
+ case "task":
241
+ latestTask = result.task;
242
+ // Only resolve on terminal task state (final event)
243
+ if (this.TERMINAL_STATES.has(result.task.status.state)) {
244
+ this.pendingStreams.delete(requestId);
245
+ resolve(result.task);
246
+ }
247
+ break;
248
+ case "status-update":
249
+ onUpdate({ id: result.taskId, contextId: result.contextId, status: result.status });
250
+ if (result.final && latestTask) {
251
+ this.pendingStreams.delete(requestId);
252
+ resolve(latestTask);
253
+ }
254
+ break;
255
+ case "artifact-update":
256
+ onUpdate({ id: result.taskId, contextId: result.contextId, artifacts: [result.artifact] });
257
+ if (latestTask) {
258
+ latestTask = { ...latestTask, artifacts: [...(latestTask.artifacts || []), result.artifact] };
259
+ }
260
+ break;
261
+ }
262
+ }).catch((err) => { this.pendingStreams.delete(requestId); reject(err); });
263
+ });
264
+ }
265
+
266
+ private createRequest(method: string, params?: Record<string, unknown>): JSONRPCRequest {
267
+ return { jsonrpc: "2.0", id: this.nextRequestId(), method, params: params ?? {} };
268
+ }
269
+
270
+ private buildSendConfig(options: TaskOptions) {
271
+ return {
272
+ acceptedOutputModes: options.acceptedOutputModes ?? ["text/plain", "application/json"],
273
+ blocking: options.blocking ?? false,
274
+ historyLength: options.historyLength,
275
+ pushNotificationConfig: options.pushNotificationConfig,
276
+ };
277
+ }
278
+
279
+ private nextRequestId(): string { this.requestIdCounter++; return `${Date.now()}-${this.requestIdCounter.toString(36)}`; }
280
+ private generateId(): string { return `ctx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; }
281
+ private delay(ms: number): Promise<void> { return new Promise((r) => setTimeout(r, ms)); }
282
+ private getDispatchUrl(agent: RemoteAgent): string { const origin = new URL(agent.url).origin; return `${origin}${ENDPOINTS.DISPATCH}`; }
283
+
284
+ // ─── HTTP Transport ───
285
+
286
+ private httpMod(url: string): typeof http | typeof https {
287
+ return new URL(url).protocol === "https:" ? https : http;
288
+ }
289
+
290
+ private async httpPostJSON(agent: RemoteAgent, request: JSONRPCRequest, options: TaskOptions = {}): Promise<JSONRPCResponse> {
291
+ const url = this.getDispatchUrl(agent);
292
+ const body = JSON.stringify(request);
293
+ const timeout = options.timeout ?? this.config.timeout;
294
+ return new Promise((resolve, reject) => {
295
+ const parsed = new URL(url);
296
+ const req = this.httpMod(url).request({
297
+ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "POST",
298
+ headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), ...this.buildAuthHeaders() }, timeout,
299
+ }, (res: http.IncomingMessage) => {
300
+ let data = "";
301
+ res.on("data", (chunk: string) => (data += chunk));
302
+ res.on("end", () => { try { resolve(JSON.parse(data)); } catch { reject(new Error(`Invalid JSON: ${data.slice(0, 200)}`)); } });
303
+ });
304
+ req.on("error", reject);
305
+ req.on("timeout", () => { req.destroy(); reject(new Error(`Request timed out after ${timeout}ms`)); });
306
+ req.write(body); req.end();
307
+ });
308
+ }
309
+
310
+ private async httpGet(url: string): Promise<unknown> {
311
+ return new Promise((resolve, reject) => {
312
+ const parsed = new URL(url);
313
+ const req = this.httpMod(url).request({
314
+ hostname: parsed.hostname, port: parsed.port, path: parsed.pathname, method: "GET",
315
+ headers: { ...this.buildAuthHeaders() }, timeout: this.config.timeout,
316
+ }, (res: http.IncomingMessage) => {
317
+ let data = "";
318
+ res.on("data", (chunk: string) => (data += chunk));
319
+ res.on("end", () => {
320
+ if (res.statusCode && res.statusCode >= 400) { reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`)); return; }
321
+ try { resolve(JSON.parse(data)); } catch { reject(new Error(`Invalid JSON: ${data.slice(0, 200)}`)); }
322
+ });
323
+ });
324
+ req.on("error", reject); req.end();
325
+ });
326
+ }
327
+
328
+ private sendStreamingRequest(agent: RemoteAgent, request: JSONRPCRequest, signal: AbortSignal, callback: (data: unknown) => void): Promise<void> {
329
+ return new Promise((resolve, reject) => {
330
+ const url = this.getDispatchUrl(agent);
331
+ const body = JSON.stringify(request);
332
+ const req = this.httpMod(url).request({
333
+ hostname: new URL(url).hostname, port: new URL(url).port, path: new URL(url).pathname, method: "POST",
334
+ headers: { "Content-Type": "application/json", Accept: "text/event-stream", "Content-Length": Buffer.byteLength(body), ...this.buildAuthHeaders() },
335
+ timeout: this.config.timeout,
336
+ }, (res: http.IncomingMessage) => {
337
+ let buf = "";
338
+ res.on("data", (chunk: string) => {
339
+ buf += chunk;
340
+ const lines = buf.split("\n");
341
+ buf = lines.pop() ?? "";
342
+ for (const line of lines) {
343
+ if (line.startsWith("data: ")) {
344
+ const ds = line.slice(6).trim();
345
+ if (ds === "[DONE]") { resolve(); return; }
346
+ try { callback(JSON.parse(ds)); } catch { /* skip */ }
347
+ }
348
+ }
349
+ });
350
+ res.on("end", () => resolve());
351
+ res.on("error", reject);
352
+ });
353
+ req.on("error", reject);
354
+ signal.addEventListener("abort", () => { req.destroy(); resolve(); });
355
+ req.write(body); req.end();
356
+ });
357
+ }
358
+
359
+ private buildAuthHeaders(): Record<string, string> {
360
+ const h: Record<string, string> = {};
361
+ if (this.security.defaultScheme === "bearer" && this.security.bearerToken) h["Authorization"] = `Bearer ${this.security.bearerToken}`;
362
+ else if (this.security.defaultScheme === "apiKey" && this.security.apiKey) h["Authorization"] = `ApiKey ${this.security.apiKey}`;
363
+ return h;
364
+ }
365
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,38 @@
1
+ import type { JSONRPCResponse } from "./types.js";
2
+ import { JSONRPCErrorCode } from "./types.js";
3
+
4
+ export class A2AError extends Error {
5
+ constructor(
6
+ public code: number,
7
+ message: string,
8
+ public data?: unknown
9
+ ) {
10
+ super(message);
11
+ this.name = "A2AError";
12
+ }
13
+
14
+ static fromResponse(response: JSONRPCResponse): A2AError {
15
+ const err = response.error!;
16
+ return new A2AError(err.code, err.message, err.data);
17
+ }
18
+
19
+ isTaskNotFound(): boolean { return this.code === JSONRPCErrorCode.TaskNotFound; }
20
+ isTaskNotCancelable(): boolean { return this.code === JSONRPCErrorCode.TaskNotCancelable; }
21
+ isPushNotSupported(): boolean { return this.code === JSONRPCErrorCode.PushNotificationNotSupported; }
22
+ isUnsupportedOperation(): boolean { return this.code === JSONRPCErrorCode.UnsupportedOperation; }
23
+ isTimeout(): boolean { return this.code === JSONRPCErrorCode.TaskTimeout; }
24
+ }
25
+
26
+ export class AgentDiscoveryError extends Error {
27
+ constructor(message: string, public url: string) {
28
+ super(`Failed to discover agent at ${url}: ${message}`);
29
+ this.name = "AgentDiscoveryError";
30
+ }
31
+ }
32
+
33
+ export class TransportError extends Error {
34
+ constructor(message: string, public status?: number, public url?: string) {
35
+ super(message);
36
+ this.name = "TransportError";
37
+ }
38
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { A2AClient } from "./client.js";
2
+ export { AgentRegistry } from "./registry.js";
3
+ export { TaskManager } from "./task-manager.js";
4
+ export { A2AError, AgentDiscoveryError, TransportError } from "./errors.js";
5
+ export * from "./types.js";
@@ -0,0 +1,37 @@
1
+ import type { A2AClient } from "./client.js";
2
+ import type { RemoteAgent } from "./types.js";
3
+
4
+ interface CachedAgent {
5
+ agent: RemoteAgent;
6
+ cachedAt: number;
7
+ }
8
+
9
+ export class AgentRegistry {
10
+ private registry = new Map<string, CachedAgent>();
11
+ private cacheTtl: number;
12
+
13
+ constructor(cacheTtl = 300000) {
14
+ this.cacheTtl = cacheTtl;
15
+ }
16
+
17
+ async discover(client: A2AClient, url: string, force = false): Promise<RemoteAgent> {
18
+ const cached = this.registry.get(url);
19
+ if (cached && !force && Date.now() - cached.cachedAt < this.cacheTtl) return cached.agent;
20
+ const card = await client.discoverAgent(url);
21
+ this.registry.set(url, { agent: card, cachedAt: Date.now() });
22
+ return card;
23
+ }
24
+
25
+ lookup(ref: string): RemoteAgent | null {
26
+ const cached = this.registry.get(ref);
27
+ if (cached) return cached.agent;
28
+ for (const [, entry] of this.registry) {
29
+ if (entry.agent.name.toLowerCase() === ref.toLowerCase()) return entry.agent;
30
+ }
31
+ return null;
32
+ }
33
+
34
+ list(): RemoteAgent[] { return Array.from(this.registry.values()).map((e) => e.agent); }
35
+ remove(url: string): boolean { return this.registry.delete(url); }
36
+ clear(): void { this.registry.clear(); }
37
+ }
@@ -0,0 +1,101 @@
1
+ import type { A2AClient } from "./client.js";
2
+ import type { RemoteAgent, TaskOptions, A2ATask, TaskUpdateCallback } from "./types.js";
3
+ import type { AgentRegistry } from "./registry.js";
4
+
5
+ export class TaskManager {
6
+ constructor(
7
+ private client: A2AClient,
8
+ private registry: AgentRegistry
9
+ ) {}
10
+
11
+ async sendTask(
12
+ agent: RemoteAgent,
13
+ message: string,
14
+ options?: TaskOptions,
15
+ onUpdate?: TaskUpdateCallback
16
+ ): Promise<A2ATask> {
17
+ if (onUpdate) {
18
+ return this.client.sendStreamingMessage(
19
+ agent,
20
+ { role: "user", parts: [{ kind: "text", text: message }], messageId: this.genId() },
21
+ onUpdate,
22
+ options
23
+ );
24
+ }
25
+ const result = await this.client.sendMessage(
26
+ agent,
27
+ { role: "user", parts: [{ kind: "text", text: message }], messageId: this.genId() },
28
+ options
29
+ );
30
+ return this.asTask(result);
31
+ }
32
+
33
+ async sendParallelTasks(
34
+ steps: Array<{ agent: RemoteAgent; message: string; options?: TaskOptions }>,
35
+ onUpdate?: (update: Partial<A2ATask>, index: number) => void
36
+ ): Promise<A2ATask[]> {
37
+ return Promise.all(
38
+ steps.map((step, i) =>
39
+ this.sendTask(
40
+ step.agent,
41
+ step.message,
42
+ step.options,
43
+ onUpdate ? (u) => onUpdate(u, i) : undefined
44
+ )
45
+ )
46
+ );
47
+ }
48
+
49
+ async sendChainTasks(
50
+ steps: Array<{ agent: RemoteAgent; message: string; options?: TaskOptions }>,
51
+ continueOnError = false
52
+ ): Promise<A2ATask> {
53
+ let previousOutput = "";
54
+
55
+ for (let i = 0; i < steps.length; i++) {
56
+ const { agent, message, options } = steps[i];
57
+ const taskText = message.replace(/\{previous\}/g, previousOutput);
58
+
59
+ let result: A2ATask;
60
+ try {
61
+ result = await this.sendTask(agent, taskText, options);
62
+ } catch (err) {
63
+ if (!continueOnError) throw err;
64
+ // On error, pass error message as previous output
65
+ previousOutput = `[Error in step ${i + 1}: ${err}]`;
66
+ continue;
67
+ }
68
+
69
+ previousOutput = this.extractText(result);
70
+ }
71
+
72
+ // Return the last successful task, or throw if all failed
73
+ const lastStep = steps[steps.length - 1];
74
+ return await this.sendTask(lastStep.agent, previousOutput, lastStep.options);
75
+ }
76
+
77
+ private asTask(result: A2ATask | any): A2ATask {
78
+ if ((result as A2ATask).status) return result as A2ATask;
79
+ throw new Error(`Expected a task, got: ${JSON.stringify(result).slice(0, 200)}`);
80
+ }
81
+
82
+ private extractText(task: A2ATask): string {
83
+ if (task.artifacts && task.artifacts.length > 0) {
84
+ return task.artifacts[0].parts
85
+ .filter((p) => p.kind === "text" && p.text)
86
+ .map((p) => (p as any).text)
87
+ .join("\n");
88
+ }
89
+ if (task.status?.message?.parts) {
90
+ return task.status.message.parts
91
+ .filter((p) => p.kind === "text" && p.text)
92
+ .map((p) => (p as any).text)
93
+ .join("\n");
94
+ }
95
+ return "";
96
+ }
97
+
98
+ private genId(): string {
99
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
100
+ }
101
+ }