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/LICENSE +21 -0
- package/README.md +158 -0
- package/package.json +29 -0
- package/pi-extension/index.ts +523 -0
- package/pi-extension/pi-types.d.ts +8 -0
- package/src/client.ts +365 -0
- package/src/errors.ts +38 -0
- package/src/index.ts +5 -0
- package/src/registry.ts +37 -0
- package/src/task-manager.ts +101 -0
- package/src/types.ts +360 -0
|
@@ -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
|
+
}
|