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.
@@ -0,0 +1,523 @@
1
+ /**
2
+ * pi-a2a-adaptor pi-extension
3
+ *
4
+ * pi coding agent 扩展入口 — 注册 /a2a-* 命令和工具
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { A2AClient } from "../src/client.js";
9
+ import { A2AError } from "../src/errors.js";
10
+ import { AgentRegistry } from "../src/registry.js";
11
+ import { TaskManager } from "../src/task-manager.js";
12
+ import type { A2AConfig, RemoteAgent, TaskOptions } from "../src/types.js";
13
+
14
+ // ─── Global State ───
15
+
16
+ let a2aClient: A2AClient | null = null;
17
+ let registry: AgentRegistry | null = null;
18
+ let taskManager: TaskManager | null = null;
19
+ let config: A2AConfig | null = null;
20
+
21
+ // ─── Default Config ───
22
+
23
+ const DEFAULT_CONFIG: A2AConfig = {
24
+ client: {
25
+ timeout: 30000,
26
+ retryAttempts: 3,
27
+ retryDelay: 1000,
28
+ maxConcurrentTasks: 10,
29
+ streamingEnabled: true,
30
+ },
31
+ server: {
32
+ enabled: false,
33
+ port: 10000,
34
+ host: "0.0.0.0",
35
+ basePath: "/a2a",
36
+ },
37
+ discovery: {
38
+ cacheEnabled: true,
39
+ cacheTtl: 300000,
40
+ agentCardPath: "/.well-known/agent-card.json",
41
+ },
42
+ security: {
43
+ defaultScheme: "none",
44
+ verifySsl: true,
45
+ },
46
+ };
47
+
48
+ // ─── Helpers ───
49
+
50
+ function resolveAgent(ref: string): RemoteAgent {
51
+ // 1. Try by index (1-based from /a2a-agents output)
52
+ const idx = parseInt(ref, 10);
53
+ if (!isNaN(idx) && idx > 0) {
54
+ const agents = registry!.list();
55
+ if (idx <= agents.length) return agents[idx - 1];
56
+ }
57
+
58
+ // 2. Try by name
59
+ const found = registry!.lookup(ref);
60
+ if (found) return found;
61
+
62
+ // 3. Treat as URL
63
+ return {
64
+ name: ref,
65
+ description: "",
66
+ url: ref,
67
+ version: "1.0.0",
68
+ capabilities: {},
69
+ skills: [],
70
+ defaultInputModes: ["application/json"],
71
+ defaultOutputModes: ["application/json"],
72
+ discoveredAt: Date.now(),
73
+ };
74
+ }
75
+
76
+ function extractTextFromResult(task: any): string {
77
+ if (task.artifacts && task.artifacts.length > 0) {
78
+ return task.artifacts[0].parts
79
+ .filter((p: any) => p.kind === "text" && p.text)
80
+ .map((p: any) => p.text)
81
+ .join("\n");
82
+ }
83
+ if (task.status?.message?.parts) {
84
+ return task.status.message.parts
85
+ .filter((p: any) => p.kind === "text" && p.text)
86
+ .map((p: any) => p.text)
87
+ .join("\n");
88
+ }
89
+ return `Task ${task.id}: ${task.status?.state}`;
90
+ }
91
+
92
+ // ─── Extension Entry ───
93
+
94
+ export default function (pi: ExtensionAPI) {
95
+ // ─── Session Lifecycle ───
96
+
97
+ pi.on("session_start", async (_event, ctx) => {
98
+ config = DEFAULT_CONFIG;
99
+ a2aClient = new A2AClient(config.client, config.security);
100
+ registry = new AgentRegistry(config.discovery.cacheTtl);
101
+ taskManager = new TaskManager(a2aClient, registry);
102
+ ctx.ui?.notify?.("A2A adaptor initialized", "info");
103
+ });
104
+
105
+ pi.on("session_shutdown", async () => {
106
+ a2aClient?.cancelAll();
107
+ a2aClient = null;
108
+ registry = null;
109
+ taskManager = null;
110
+ config = null;
111
+ });
112
+
113
+ // ═══════════════════════════════════════════════════════════
114
+ // COMMANDS
115
+ // ═══════════════════════════════════════════════════════════
116
+
117
+ /**
118
+ * /a2a-discover <url>
119
+ */
120
+ pi.registerCommand("a2a-discover", {
121
+ description: "Discover an A2A agent at a URL",
122
+ handler: async (args, ctx) => {
123
+ const url = args.trim();
124
+ if (!url) {
125
+ ctx.ui?.notify?.("Usage: /a2a-discover <url>", "warning");
126
+ return;
127
+ }
128
+ try {
129
+ const agent = await registry!.discover(a2aClient!, url);
130
+ const info = [
131
+ `Name: ${agent.name}`,
132
+ `Description: ${agent.description}`,
133
+ `Version: ${agent.version}`,
134
+ `URL: ${agent.url}`,
135
+ `Skills: ${agent.skills.map((s) => s.name).join(", ")}`,
136
+ `Streaming: ${agent.capabilities.streaming ? "yes" : "no"}`,
137
+ `Push Notifications: ${agent.capabilities.pushNotifications ? "yes" : "no"}`,
138
+ ].join("\n");
139
+ ctx.ui?.notify?.(`Discovered: ${agent.name}\n${info}`, "success");
140
+ } catch (err: any) {
141
+ ctx.ui?.notify?.(`Discovery failed: ${err.message}`, "error");
142
+ }
143
+ },
144
+ });
145
+
146
+ /**
147
+ * /a2a-agents
148
+ */
149
+ pi.registerCommand("a2a-agents", {
150
+ description: "List all discovered A2A agents",
151
+ handler: async (_args, ctx) => {
152
+ const agents = registry!.list();
153
+ if (agents.length === 0) {
154
+ ctx.ui?.notify?.("No agents discovered. Use /a2a-discover <url>", "info");
155
+ return;
156
+ }
157
+ const list = agents.map((a, i) => `${i + 1}. ${a.name} (${a.url}) - ${a.skills.length} skills`).join("\n");
158
+ ctx.ui?.notify?.(`Discovered Agents:\n${list}\n\nUse number, name, or URL with /a2a-send`, "info");
159
+ },
160
+ });
161
+
162
+ /**
163
+ * /a2a-send <agent-ref> <message>
164
+ */
165
+ pi.registerCommand("a2a-send", {
166
+ description: "Send a task to an A2A agent",
167
+ handler: async (args, ctx) => {
168
+ const parts = args.trim().split(/\s+/);
169
+ if (parts.length < 2) {
170
+ ctx.ui?.notify?.("Usage: /a2a-send <agent-url-or-name> <message>", "warning");
171
+ return;
172
+ }
173
+ const agentRef = parts[0];
174
+ const message = parts.slice(1).join(" ");
175
+ try {
176
+ const agent = resolveAgent(agentRef);
177
+ ctx.ui?.notify?.(`Sending to ${agent.name}...`, "info");
178
+ const result = await taskManager!.sendTask(agent, message, {
179
+ polling: { intervalMs: 2000, maxAttempts: 60, timeoutMs: 120000 },
180
+ });
181
+ const text = extractTextFromResult(result);
182
+ ctx.ui?.notify?.(`Result:\n${text}`, "success");
183
+ } catch (err: any) {
184
+ ctx.ui?.notify?.(`Task failed: ${err.message}`, "error");
185
+ }
186
+ },
187
+ });
188
+
189
+ /**
190
+ * /a2a-broadcast <message> --agents <url1,url2,...>
191
+ */
192
+ pi.registerCommand("a2a-broadcast", {
193
+ description: "Broadcast a task to multiple agents in parallel",
194
+ handler: async (args, ctx) => {
195
+ const agentsMatch = args.match(/--agents\s+([^\s]+)/);
196
+ const message = args.replace(/--agents\s+[^\s]+/, "").trim();
197
+ if (!agentsMatch || !message) {
198
+ ctx.ui?.notify?.("Usage: /a2a-broadcast <message> --agents <url1,url2,...>", "warning");
199
+ return;
200
+ }
201
+ const urls = agentsMatch[1].split(",");
202
+ try {
203
+ ctx.ui?.notify?.(`Broadcasting to ${urls.length} agents...`, "info");
204
+ const results = await taskManager!.sendParallelTasks(
205
+ urls.map((url) => ({
206
+ agent: resolveAgent(url),
207
+ message,
208
+ options: { timeout: 60000 },
209
+ }))
210
+ );
211
+ const summary = results.map((r, i) => {
212
+ const status = r.status?.state || "unknown";
213
+ return `[${i + 1}] ${results[i] ? "✓" : "✗"} ${urls[i]}: ${status}`;
214
+ }).join("\n");
215
+ ctx.ui?.notify?.(`Results:\n${summary}`, "info");
216
+ } catch (err: any) {
217
+ ctx.ui?.notify?.(`Broadcast failed: ${err.message}`, "error");
218
+ }
219
+ },
220
+ });
221
+
222
+ /**
223
+ * /a2a-chain <agent1> <task1> | <agent2> <task2> | ...
224
+ */
225
+ pi.registerCommand("a2a-chain", {
226
+ description: "Chain tasks across multiple agents sequentially",
227
+ handler: async (args, ctx) => {
228
+ const steps = args.split("|").map((s) => s.trim()).filter(Boolean);
229
+ if (steps.length === 0) {
230
+ ctx.ui?.notify?.("Usage: /a2a-chain <agent1> <task1> | <agent2> <task2> | ...", "warning");
231
+ return;
232
+ }
233
+ const chainSteps: Array<{ agent: RemoteAgent; message: string; options?: TaskOptions }> = [];
234
+ try {
235
+ for (const step of steps) {
236
+ const parts = step.split(/\s+/);
237
+ if (parts.length < 2) {
238
+ ctx.ui?.notify?.(`Invalid step: ${step}`, "error");
239
+ return;
240
+ }
241
+ const agentRef = parts[0];
242
+ const task = parts.slice(1).join(" ");
243
+ const agent = resolveAgent(agentRef);
244
+ chainSteps.push({ agent, message: task, options: undefined });
245
+ }
246
+ ctx.ui?.notify?.(`Executing chain of ${chainSteps.length} steps...`, "info");
247
+ const result = await taskManager!.sendChainTasks(chainSteps);
248
+ const text = extractTextFromResult(result);
249
+ ctx.ui?.notify?.(`Chain completed:\n${text}`, "success");
250
+ } catch (err: any) {
251
+ ctx.ui?.notify?.(`Chain failed: ${err.message}`, "error");
252
+ }
253
+ },
254
+ });
255
+
256
+ /**
257
+ * /a2a-status <task-id> [agent-url]
258
+ */
259
+ pi.registerCommand("a2a-status", {
260
+ description: "Get status of an A2A task",
261
+ handler: async (args, ctx) => {
262
+ const parts = args.trim().split(/\s+/);
263
+ if (parts.length < 1) {
264
+ ctx.ui?.notify?.("Usage: /a2a-status <task-id> [agent-url]", "warning");
265
+ return;
266
+ }
267
+ const taskId = parts[0];
268
+ const agentUrl = parts[1];
269
+ try {
270
+ let agent: RemoteAgent;
271
+ if (agentUrl) {
272
+ agent = resolveAgent(agentUrl);
273
+ } else {
274
+ ctx.ui?.notify?.("Agent URL required. Usage: /a2a-status <task-id> <agent-url>", "error");
275
+ return;
276
+ }
277
+ const task = await a2aClient!.getTask(agent, taskId);
278
+ const info = [
279
+ `Task ID: ${task.id}`,
280
+ `State: ${task.status.state}`,
281
+ `Context ID: ${task.contextId}`,
282
+ `Artifacts: ${task.artifacts?.length || 0}`,
283
+ `History: ${task.history?.length || 0} messages`,
284
+ ].join("\n");
285
+ ctx.ui?.notify?.(info, "info");
286
+ } catch (err: any) {
287
+ ctx.ui?.notify?.(`Failed to get status: ${err.message}`, "error");
288
+ }
289
+ },
290
+ });
291
+
292
+ /**
293
+ * /a2a-cancel <task-id> [agent-url]
294
+ */
295
+ pi.registerCommand("a2a-cancel", {
296
+ description: "Cancel an A2A task",
297
+ handler: async (args, ctx) => {
298
+ const parts = args.trim().split(/\s+/);
299
+ if (parts.length < 2) {
300
+ ctx.ui?.notify?.("Usage: /a2a-cancel <task-id> <agent-url>", "warning");
301
+ return;
302
+ }
303
+ const taskId = parts[0];
304
+ const agent = resolveAgent(parts[1]);
305
+ try {
306
+ const task = await a2aClient!.cancelTask(agent, taskId);
307
+ ctx.ui?.notify?.(`Task ${taskId} canceled (state: ${task.status.state})`, "success");
308
+ } catch (err: any) {
309
+ ctx.ui?.notify?.(`Failed to cancel: ${err.message}`, "error");
310
+ }
311
+ },
312
+ });
313
+
314
+ /**
315
+ * /a2a-list [context-id]
316
+ */
317
+ pi.registerCommand("a2a-list", {
318
+ description: "List tasks on a remote agent",
319
+ handler: async (args, ctx) => {
320
+ const parts = args.trim().split(/\s+/);
321
+ if (parts.length < 2) {
322
+ ctx.ui?.notify?.("Usage: /a2a-list <agent-url> [context-id]", "warning");
323
+ return;
324
+ }
325
+ const agent = resolveAgent(parts[0]);
326
+ const contextId = parts[1] || undefined;
327
+ try {
328
+ const result = await a2aClient!.listTasks(agent, contextId ? { contextId } : {});
329
+ if (result.tasks.length === 0) {
330
+ ctx.ui?.notify?.("No tasks found", "info");
331
+ return;
332
+ }
333
+ const list = result.tasks.map((t) => `${t.id.slice(0, 8)}... ${t.status.state} ${t.artifacts?.length || 0} artifacts`).join("\n");
334
+ ctx.ui?.notify?.(`Tasks (${result.totalSize || result.tasks.length}):\n${list}`, "info");
335
+ } catch (err: any) {
336
+ ctx.ui?.notify?.(`Failed to list tasks: ${err.message}`, "error");
337
+ }
338
+ },
339
+ });
340
+
341
+ /**
342
+ * /a2a-resubscribe <task-id> <agent-url>
343
+ */
344
+ pi.registerCommand("a2a-resubscribe", {
345
+ description: "Resubscribe to a task's event stream",
346
+ handler: async (args, ctx) => {
347
+ const parts = args.trim().split(/\s+/);
348
+ if (parts.length < 2) {
349
+ ctx.ui?.notify?.("Usage: /a2a-resubscribe <task-id> <agent-url>", "warning");
350
+ return;
351
+ }
352
+ const taskId = parts[0];
353
+ const agent = resolveAgent(parts[1]);
354
+ try {
355
+ ctx.ui?.notify?.(`Resubscribing to task ${taskId.slice(0, 8)}...`, "info");
356
+ const updates: string[] = [];
357
+ await a2aClient!.resubscribeToTask(agent, taskId, (u) => {
358
+ updates.push(`[${u.status?.state || "update"}] ${JSON.stringify(u).slice(0, 200)}`);
359
+ });
360
+ if (updates.length === 0) {
361
+ ctx.ui?.notify?.("No updates received", "info");
362
+ } else {
363
+ ctx.ui?.notify?.(`Updates:\n${updates.slice(-5).join("\n")}`, "info");
364
+ }
365
+ } catch (err: any) {
366
+ ctx.ui?.notify?.(`Resubscribe failed: ${err.message}`, "error");
367
+ }
368
+ },
369
+ });
370
+
371
+ /**
372
+ * /a2a-config <key> <value>
373
+ */
374
+ pi.registerCommand("a2a-config", {
375
+ description: "Configure A2A settings",
376
+ handler: async (args, ctx) => {
377
+ const parts = args.trim().split(/\s+/);
378
+ if (parts.length < 2) {
379
+ ctx.ui?.notify?.("Usage: /a2a-config <key> <value>\nKeys: timeout, retryAttempts, cacheTtl, verifySsl", "warning");
380
+ return;
381
+ }
382
+ const key = parts[0];
383
+ const value = parts.slice(1).join(" ");
384
+ try {
385
+ if (!config) throw new Error("A2A not initialized");
386
+ switch (key) {
387
+ case "timeout":
388
+ config.client.timeout = parseInt(value, 10);
389
+ break;
390
+ case "retryAttempts":
391
+ config.client.retryAttempts = parseInt(value, 10);
392
+ break;
393
+ case "cacheTtl":
394
+ config.discovery.cacheTtl = parseInt(value, 10);
395
+ break;
396
+ case "verifySsl":
397
+ config.security.verifySsl = value.toLowerCase() === "true";
398
+ break;
399
+ default:
400
+ ctx.ui?.notify?.(`Unknown key: ${key}`, "error");
401
+ return;
402
+ }
403
+ // Reinitialize client with new config
404
+ a2aClient = new A2AClient(config.client, config.security);
405
+ taskManager = new TaskManager(a2aClient, registry!);
406
+ ctx.ui?.notify?.(`Configuration updated: ${key} = ${value}`, "success");
407
+ } catch (err: any) {
408
+ ctx.ui?.notify?.(`Failed to set config: ${err.message}`, "error");
409
+ }
410
+ },
411
+ });
412
+
413
+ /**
414
+ * /a2a-help
415
+ */
416
+ pi.registerCommand("a2a-help", {
417
+ description: "Show A2A adaptor help",
418
+ handler: async (_args, ctx) => {
419
+ const help = `
420
+ A2A Adaptor Commands:
421
+
422
+ Discovery:
423
+ /a2a-discover <url> - Discover agent at URL
424
+ /a2a-agents - List discovered agents
425
+
426
+ Task Management:
427
+ /a2a-send <agent> <message> - Send task to agent
428
+ /a2a-broadcast <msg> --agents <urls> - Broadcast to multiple agents
429
+ /a2a-chain <agent1> <task1> | <agent2> <task2> | ... - Chain tasks
430
+ /a2a-status <task-id> <url> - Get task status
431
+ /a2a-cancel <task-id> <url> - Cancel a task
432
+ /a2a-list <url> [context-id] - List tasks on agent
433
+ /a2a-resubscribe <task-id> <url> - Resubscribe to task stream
434
+
435
+ Configuration:
436
+ /a2a-config <key> <value> - Configure settings
437
+ /a2a-help - Show this help
438
+
439
+ Examples:
440
+ /a2a-discover https://agent.example.com
441
+ /a2a-send https://agent.example.com "Analyze this code"
442
+ /a2a-broadcast "Check security" --agents https://agent1.com,https://agent2.com
443
+ /a2a-chain scout "find bugs" | worker "fix {previous}"
444
+ /a2a-config timeout 60000
445
+ `.trim();
446
+ ctx.ui?.notify?.(help, "info");
447
+ },
448
+ });
449
+
450
+ // ═══════════════════════════════════════════════════════════
451
+ // TOOLS
452
+ // ═══════════════════════════════════════════════════════════
453
+
454
+ /**
455
+ * a2a_call tool
456
+ */
457
+ pi.registerTool({
458
+ name: "a2a_call",
459
+ label: "A2A Agent Call",
460
+ description: "Call a remote A2A agent to perform a task",
461
+ parameters: {
462
+ type: "object",
463
+ properties: {
464
+ agent_url: { type: "string", description: "URL or name of the A2A agent" },
465
+ message: { type: "string", description: "Task message to send" },
466
+ timeout: { type: "number", description: "Timeout in milliseconds", default: 60000 },
467
+ },
468
+ required: ["agent_url", "message"],
469
+ },
470
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
471
+ if (!taskManager) {
472
+ return { content: [{ type: "text" as const, text: "A2A not initialized" }], isError: true };
473
+ }
474
+ try {
475
+ const agent = resolveAgent(params.agent_url as string);
476
+ const result = await taskManager.sendTask(agent, params.message as string, {
477
+ timeout: (params.timeout as number) ?? 60000,
478
+ polling: { intervalMs: 2000, maxAttempts: 30, timeoutMs: 120000 },
479
+ });
480
+ const text = extractTextFromResult(result);
481
+ return { content: [{ type: "text" as const, text }] };
482
+ } catch (err: any) {
483
+ return { content: [{ type: "text" as const, text: `Error: ${err.message}` }], isError: true };
484
+ }
485
+ },
486
+ });
487
+
488
+ /**
489
+ * a2a_parallel tool
490
+ */
491
+ pi.registerTool({
492
+ name: "a2a_parallel",
493
+ label: "A2A Parallel Call",
494
+ description: "Call multiple A2A agents in parallel with the same message",
495
+ parameters: {
496
+ type: "object",
497
+ properties: {
498
+ agent_urls: { type: "array", items: { type: "string" }, description: "Array of agent URLs or names" },
499
+ message: { type: "string", description: "Task message to send to all agents" },
500
+ timeout: { type: "number", description: "Timeout in milliseconds", default: 60000 },
501
+ },
502
+ required: ["agent_urls", "message"],
503
+ },
504
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
505
+ if (!taskManager) {
506
+ return { content: [{ type: "text" as const, text: "A2A not initialized" }], isError: true };
507
+ }
508
+ try {
509
+ const urls = params.agent_urls as string[];
510
+ const steps = urls.map((url) => ({
511
+ agent: resolveAgent(url),
512
+ message: params.message as string,
513
+ options: { timeout: (params.timeout as number) ?? 60000 },
514
+ }));
515
+ const results = await taskManager.sendParallelTasks(steps);
516
+ const summary = results.map((r, i) => `[${urls[i]}] ${r.status?.state || "unknown"}:\n${extractTextFromResult(r)}`).join("\n\n");
517
+ return { content: [{ type: "text" as const, text: summary }] };
518
+ } catch (err: any) {
519
+ return { content: [{ type: "text" as const, text: `Error: ${err.message}` }], isError: true };
520
+ }
521
+ },
522
+ });
523
+ }
@@ -0,0 +1,8 @@
1
+ declare module "@earendil-works/pi-coding-agent" {
2
+ export interface ExtensionAPI {
3
+ on(event: string, handler: (event?: unknown, ctx?: any) => void | Promise<void>): void;
4
+ registerCommand(name: string, spec: { description?: string; handler: (args: string, ctx: any) => void | Promise<void> }): void;
5
+ registerTool(spec: { name: string; label?: string; description?: string; parameters?: unknown; execute: (toolCallId: any, params: any, signal: any, onUpdate: any, ctx: any) => Promise<{ content: { type: string; text: string }[]; isError?: boolean }> }): void;
6
+ }
7
+ export interface ExtensionContext {}
8
+ }