@clwnt/clawnet 0.7.7 → 0.7.9
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/package.json +1 -1
- package/src/cli.ts +4 -0
- package/src/config.ts +6 -0
- package/src/service.ts +75 -38
- package/src/tools.ts +64 -2
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -154,6 +154,10 @@ export function buildStatusText(api: any): string {
|
|
|
154
154
|
lines.push("Polling: **PAUSED** (run /clawnet resume to restart)");
|
|
155
155
|
}
|
|
156
156
|
lines.push(`Poll interval: ${pluginCfg.pollEverySeconds ?? "?"}s`);
|
|
157
|
+
const notifyOn = pluginCfg.notifyOnNew ?? true;
|
|
158
|
+
const remindH = pluginCfg.remindAfterHours ?? null;
|
|
159
|
+
lines.push(`Notify on new: ${notifyOn ? "on" : "off"}`);
|
|
160
|
+
lines.push(`Reminder interval: ${remindH ? `${remindH}h` : "never"}`);
|
|
157
161
|
|
|
158
162
|
const accounts: any[] = pluginCfg.accounts ?? [];
|
|
159
163
|
const agentList: any[] = currentConfig?.agents?.list ?? [];
|
package/src/config.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ClawnetConfig {
|
|
|
19
19
|
maxSnippetChars: number;
|
|
20
20
|
setupVersion: number;
|
|
21
21
|
paused: boolean;
|
|
22
|
+
notifyOnNew: boolean;
|
|
23
|
+
remindAfterHours: number | null;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
const DEFAULTS: ClawnetConfig = {
|
|
@@ -32,6 +34,8 @@ const DEFAULTS: ClawnetConfig = {
|
|
|
32
34
|
maxSnippetChars: 500,
|
|
33
35
|
setupVersion: 0,
|
|
34
36
|
paused: false,
|
|
37
|
+
notifyOnNew: true,
|
|
38
|
+
remindAfterHours: 4,
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
|
|
@@ -67,6 +71,8 @@ export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
|
|
|
67
71
|
deliveryMethod:
|
|
68
72
|
raw.deliveryMethod === "agent" ? "agent" : DEFAULTS.deliveryMethod,
|
|
69
73
|
paused: raw.paused === true,
|
|
74
|
+
notifyOnNew: raw.notifyOnNew !== false,
|
|
75
|
+
remindAfterHours: typeof raw.remindAfterHours === "number" ? raw.remindAfterHours : null,
|
|
70
76
|
};
|
|
71
77
|
}
|
|
72
78
|
|
package/src/service.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface InboxMessage {
|
|
|
11
11
|
content: string;
|
|
12
12
|
subject?: string;
|
|
13
13
|
created_at: string;
|
|
14
|
+
type: "email" | "task";
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface ServiceState {
|
|
@@ -72,7 +73,7 @@ async function reloadOnboardingMessage(): Promise<void> {
|
|
|
72
73
|
|
|
73
74
|
const SKILL_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
74
75
|
const SKILL_FILES = ["skill.json", "api-reference.md", "inbox-handler.md", "capabilities.json", "hook-template.txt", "tool-descriptions.json", "onboarding-message.txt", "inbox-protocol.md"];
|
|
75
|
-
export const PLUGIN_VERSION = "0.7.
|
|
76
|
+
export const PLUGIN_VERSION = "0.7.8"; // Reported to server via PATCH /me every 6h
|
|
76
77
|
|
|
77
78
|
function loadFreshConfig(api: any): ClawnetConfig {
|
|
78
79
|
const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
|
|
@@ -134,6 +135,9 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
134
135
|
|
|
135
136
|
// --- Batch delivery ---
|
|
136
137
|
|
|
138
|
+
// Per-account auth context for mark-notified calls from deliverBatch
|
|
139
|
+
const accountAuth = new Map<string, { token: string; baseUrl: string }>();
|
|
140
|
+
|
|
137
141
|
async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
|
|
138
142
|
if (messages.length === 0) return;
|
|
139
143
|
|
|
@@ -163,6 +167,37 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
163
167
|
api.logger.info(
|
|
164
168
|
`[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
|
|
165
169
|
);
|
|
170
|
+
|
|
171
|
+
// Post-delivery: mark items as notified + mark A2A tasks as working
|
|
172
|
+
const auth = accountAuth.get(accountId);
|
|
173
|
+
if (auth) {
|
|
174
|
+
const emailIds = messages.filter((m) => m.type === "email").map((m) => m.id);
|
|
175
|
+
const taskIds = messages.filter((m) => m.type === "task").map((m) => m.id);
|
|
176
|
+
|
|
177
|
+
// Mark notified (non-fatal)
|
|
178
|
+
if (emailIds.length > 0 || taskIds.length > 0) {
|
|
179
|
+
try {
|
|
180
|
+
const markRes = await fetch(`${auth.baseUrl}/inbox/mark-notified`, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
...(emailIds.length > 0 ? { message_ids: emailIds } : {}),
|
|
185
|
+
...(taskIds.length > 0 ? { task_ids: taskIds } : {}),
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
if (markRes.ok) {
|
|
189
|
+
const markData = await markRes.json().catch(() => ({})) as Record<string, unknown>;
|
|
190
|
+
api.logger.info(`[clawnet] ${accountId}: marked notified (${markData.marked_messages ?? 0} msgs, ${markData.marked_tasks ?? 0} tasks)`);
|
|
191
|
+
} else {
|
|
192
|
+
const errText = await markRes.text().catch(() => "");
|
|
193
|
+
api.logger.warn(`[clawnet] ${accountId}: mark-notified returned ${markRes.status}: ${errText}`);
|
|
194
|
+
}
|
|
195
|
+
} catch (err: any) {
|
|
196
|
+
api.logger.warn(`[clawnet] ${accountId}: mark-notified failed (non-fatal): ${err.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
}
|
|
166
201
|
} catch (err: any) {
|
|
167
202
|
state.lastError = { message: err.message, at: new Date() };
|
|
168
203
|
state.counters.errors++;
|
|
@@ -286,13 +321,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
286
321
|
|
|
287
322
|
// --- Poll ---
|
|
288
323
|
|
|
289
|
-
async function pollAccount(account: ClawnetAccount): Promise<number> {
|
|
324
|
+
async function pollAccount(account: ClawnetAccount): Promise<{ a2aDmCount: number; sentTaskUpdates: number; notifyCount: number }> {
|
|
290
325
|
const resolvedToken = resolveToken(account.token);
|
|
291
326
|
if (!resolvedToken) {
|
|
292
327
|
api.logger.warn(`[clawnet] No token resolved for account "${account.id}", skipping`);
|
|
293
|
-
return 0;
|
|
328
|
+
return { a2aDmCount: 0, sentTaskUpdates: 0, notifyCount: 0 };
|
|
294
329
|
}
|
|
295
330
|
|
|
331
|
+
// Store auth context for deliverBatch to use for mark-notified calls
|
|
332
|
+
accountAuth.set(account.id, { token: resolvedToken, baseUrl: cfg.baseUrl });
|
|
333
|
+
|
|
296
334
|
const headers = {
|
|
297
335
|
Authorization: `Bearer ${resolvedToken}`,
|
|
298
336
|
"Content-Type": "application/json",
|
|
@@ -307,11 +345,14 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
307
345
|
count: number;
|
|
308
346
|
task_count?: number;
|
|
309
347
|
sent_task_updates?: number;
|
|
348
|
+
notify_count?: number;
|
|
310
349
|
plugin_config?: {
|
|
311
350
|
poll_seconds: number;
|
|
312
351
|
debounce_seconds: number;
|
|
313
352
|
max_batch_size: number;
|
|
314
353
|
deliver_channel: string;
|
|
354
|
+
notify_on_new?: boolean;
|
|
355
|
+
remind_after_hours?: number | null;
|
|
315
356
|
};
|
|
316
357
|
};
|
|
317
358
|
|
|
@@ -335,30 +376,44 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
335
376
|
cfg.deliver.channel = pc.deliver_channel;
|
|
336
377
|
changed = true;
|
|
337
378
|
}
|
|
379
|
+
if (pc.notify_on_new !== undefined && pc.notify_on_new !== cfg.notifyOnNew) {
|
|
380
|
+
cfg.notifyOnNew = pc.notify_on_new;
|
|
381
|
+
changed = true;
|
|
382
|
+
}
|
|
383
|
+
if (pc.remind_after_hours !== undefined && pc.remind_after_hours !== cfg.remindAfterHours) {
|
|
384
|
+
cfg.remindAfterHours = pc.remind_after_hours;
|
|
385
|
+
changed = true;
|
|
386
|
+
}
|
|
338
387
|
if (changed) {
|
|
339
|
-
api.logger.info(`[clawnet] Config updated from server: poll=${cfg.pollEverySeconds}s debounce=${cfg.debounceSeconds}s batch=${cfg.maxBatchSize}`);
|
|
388
|
+
api.logger.info(`[clawnet] Config updated from server: poll=${cfg.pollEverySeconds}s debounce=${cfg.debounceSeconds}s batch=${cfg.maxBatchSize} notify=${cfg.notifyOnNew} remind=${cfg.remindAfterHours ?? "never"}`);
|
|
340
389
|
}
|
|
341
390
|
}
|
|
342
391
|
|
|
343
392
|
const a2aDmCount = checkData.task_count ?? 0;
|
|
344
393
|
const sentTaskUpdates = checkData.sent_task_updates ?? 0;
|
|
394
|
+
const notifyCount = checkData.notify_count ?? (checkData.count + a2aDmCount + sentTaskUpdates);
|
|
345
395
|
|
|
346
396
|
if (checkData.count === 0) {
|
|
347
397
|
// Email inbox clear — release any delivery lock (agent finished processing)
|
|
348
398
|
deliveryLock.delete(account.id);
|
|
349
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
399
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// If nothing needs notification, skip fetch (but don't release lock — inbox still has items)
|
|
403
|
+
if (notifyCount === 0) {
|
|
404
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
350
405
|
}
|
|
351
406
|
|
|
352
|
-
// Skip if a recent
|
|
407
|
+
// Skip if a recent delivery is still being processed.
|
|
353
408
|
// TTL-based lock: after successful POST, lock for 10 min to let the agent work.
|
|
354
409
|
const lockUntil = deliveryLock.get(account.id);
|
|
355
410
|
if (lockUntil && new Date() < lockUntil) {
|
|
356
411
|
api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
|
|
357
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
412
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
358
413
|
}
|
|
359
414
|
|
|
360
415
|
state.lastInboxNonEmptyAt = new Date();
|
|
361
|
-
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting`);
|
|
416
|
+
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (${notifyCount} to notify)`);
|
|
362
417
|
|
|
363
418
|
// Fetch full messages
|
|
364
419
|
const inboxRes = await fetch(`${cfg.baseUrl}/inbox`, { headers });
|
|
@@ -367,7 +422,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
367
422
|
}
|
|
368
423
|
const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
|
|
369
424
|
|
|
370
|
-
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
|
|
425
|
+
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
371
426
|
|
|
372
427
|
// Normalize API field names: API returns "from", plugin uses "from_agent"
|
|
373
428
|
const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
|
|
@@ -376,6 +431,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
376
431
|
content: m.content,
|
|
377
432
|
subject: m.email?.subject ?? m.subject,
|
|
378
433
|
created_at: m.created_at,
|
|
434
|
+
type: "email" as const,
|
|
379
435
|
}));
|
|
380
436
|
|
|
381
437
|
state.counters.messagesSeen += normalized.length;
|
|
@@ -385,7 +441,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
385
441
|
pendingMessages.set(account.id, [...existing, ...normalized]);
|
|
386
442
|
scheduleFlush(account.id, account.agentId);
|
|
387
443
|
|
|
388
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
444
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
389
445
|
}
|
|
390
446
|
|
|
391
447
|
async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
|
|
@@ -427,7 +483,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
427
483
|
|
|
428
484
|
api.logger.info(`[clawnet] ${account.id}: ${tasks.length} A2A task(s) to deliver`);
|
|
429
485
|
|
|
430
|
-
// Convert A2A tasks to the message format
|
|
486
|
+
// Convert A2A tasks to the message format for delivery
|
|
487
|
+
// mark-notified happens post-delivery in deliverBatch
|
|
431
488
|
const messages: InboxMessage[] = tasks.map((task) => {
|
|
432
489
|
const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
|
|
433
490
|
const lastMsg = history[history.length - 1];
|
|
@@ -438,6 +495,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
438
495
|
from_agent: task.from,
|
|
439
496
|
content: `[A2A Task ${task.id}]${contactInfo}\n${text}`,
|
|
440
497
|
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
498
|
+
type: "task" as const,
|
|
441
499
|
};
|
|
442
500
|
});
|
|
443
501
|
|
|
@@ -445,28 +503,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
445
503
|
const existing = pendingMessages.get(account.id) ?? [];
|
|
446
504
|
pendingMessages.set(account.id, [...existing, ...messages]);
|
|
447
505
|
scheduleFlush(account.id, account.agentId);
|
|
448
|
-
|
|
449
|
-
// Mark delivered tasks as 'working' so they don't get re-delivered on next poll.
|
|
450
|
-
// This is the equivalent of marking emails 'read' — acknowledges receipt.
|
|
451
|
-
for (const task of tasks) {
|
|
452
|
-
try {
|
|
453
|
-
await fetch(`${cfg.baseUrl}/a2a`, {
|
|
454
|
-
method: "POST",
|
|
455
|
-
headers: {
|
|
456
|
-
Authorization: `Bearer ${resolvedToken}`,
|
|
457
|
-
"Content-Type": "application/json",
|
|
458
|
-
},
|
|
459
|
-
body: JSON.stringify({
|
|
460
|
-
jsonrpc: "2.0",
|
|
461
|
-
id: `ack-${task.id}`,
|
|
462
|
-
method: "tasks/respond",
|
|
463
|
-
params: { id: task.id, state: "working" },
|
|
464
|
-
}),
|
|
465
|
-
});
|
|
466
|
-
} catch {
|
|
467
|
-
// Non-fatal — task may get re-delivered next cycle
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
506
|
}
|
|
471
507
|
|
|
472
508
|
async function pollSentTaskUpdates(account: ClawnetAccount) {
|
|
@@ -477,12 +513,12 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
477
513
|
const lockUntil = deliveryLock.get(account.id);
|
|
478
514
|
if (lockUntil && new Date() < lockUntil) return;
|
|
479
515
|
|
|
480
|
-
// Fetch tasks I sent that need attention
|
|
516
|
+
// Fetch tasks I sent that need my attention or have finished
|
|
481
517
|
const body = {
|
|
482
518
|
jsonrpc: "2.0",
|
|
483
519
|
id: `sent-poll-${Date.now()}`,
|
|
484
520
|
method: "tasks/list",
|
|
485
|
-
params: { role: "sender", status: "input-required", limit: 50 },
|
|
521
|
+
params: { role: "sender", status: "input-required,completed,failed", limit: 50 },
|
|
486
522
|
};
|
|
487
523
|
const res = await fetch(`${cfg.baseUrl}/a2a`, {
|
|
488
524
|
method: "POST",
|
|
@@ -512,6 +548,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
512
548
|
from_agent: task.to, // the agent that responded
|
|
513
549
|
content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
|
|
514
550
|
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
551
|
+
type: "task" as const,
|
|
515
552
|
};
|
|
516
553
|
});
|
|
517
554
|
|
|
@@ -565,10 +602,10 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
565
602
|
let hadError = false;
|
|
566
603
|
for (const account of enabledAccounts) {
|
|
567
604
|
try {
|
|
568
|
-
const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
|
|
605
|
+
const { a2aDmCount, sentTaskUpdates, notifyCount } = await pollAccount(account);
|
|
569
606
|
|
|
570
607
|
// Also poll for A2A DMs if any pending
|
|
571
|
-
if (a2aDmCount > 0) {
|
|
608
|
+
if (a2aDmCount > 0 && notifyCount > 0) {
|
|
572
609
|
try {
|
|
573
610
|
await pollAccountA2A(account, a2aDmCount);
|
|
574
611
|
} catch (a2aErr: any) {
|
|
@@ -577,7 +614,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
577
614
|
}
|
|
578
615
|
|
|
579
616
|
// Poll for sent task updates (tasks I sent that got a response)
|
|
580
|
-
if (sentTaskUpdates > 0) {
|
|
617
|
+
if (sentTaskUpdates > 0 && notifyCount > 0) {
|
|
581
618
|
try {
|
|
582
619
|
await pollSentTaskUpdates(account);
|
|
583
620
|
} catch (err: any) {
|
package/src/tools.ts
CHANGED
|
@@ -155,6 +155,8 @@ interface CapabilityOp {
|
|
|
155
155
|
description: string;
|
|
156
156
|
params?: Record<string, { type: string; description: string; required?: boolean }>;
|
|
157
157
|
rawBodyParam?: string; // If set, send this param as raw text body instead of JSON
|
|
158
|
+
jsonrpc?: boolean; // If true, dispatch via a2aCall() instead of REST
|
|
159
|
+
rpc_method?: string; // JSON-RPC method name (required when jsonrpc is true)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
@@ -201,6 +203,12 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
201
203
|
{ operation: "profile.capabilities", method: "PATCH", path: "/me/capabilities", description: "Set agent capabilities list", params: {
|
|
202
204
|
capabilities: { type: "array", description: "List of capability strings (replaces all)", required: true },
|
|
203
205
|
}},
|
|
206
|
+
// Notification settings
|
|
207
|
+
{ operation: "notifications.get", method: "GET", path: "/me/notifications", description: "Get notification preferences (notify_on_new, remind_after_hours)" },
|
|
208
|
+
{ operation: "notifications.update", method: "PATCH", path: "/me/notifications", description: "Update notification preferences", params: {
|
|
209
|
+
notify_on_new: { type: "boolean", description: "Notify when new messages/tasks arrive (default: true)" },
|
|
210
|
+
remind_after_hours: { type: "number", description: "Re-notify about unresolved items every N hours (1-168), or null to disable reminders" },
|
|
211
|
+
}},
|
|
204
212
|
// Contacts
|
|
205
213
|
{ operation: "contacts.list", method: "GET", path: "/contacts", description: "List your contacts", params: {
|
|
206
214
|
type: { type: "string", description: "'email' or 'agent'" },
|
|
@@ -268,6 +276,30 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
268
276
|
{ operation: "account.rate_limits", method: "GET", path: "/me/rate-limits", description: "Check your current rate limits" },
|
|
269
277
|
// Docs
|
|
270
278
|
{ operation: "docs.help", method: "GET", path: "/docs/skill", description: "Get the full ClawNet documentation — features, usage examples, safety rules, setup, troubleshooting, and rate limits" },
|
|
279
|
+
// A2A (JSON-RPC via /a2a)
|
|
280
|
+
{ operation: "a2a.card.update", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "card/update", description: "Update your A2A Agent Card skills", params: {
|
|
281
|
+
skills: { type: "array", description: "Array of {id, name, description} skill objects", required: true },
|
|
282
|
+
}},
|
|
283
|
+
{ operation: "a2a.tasks.list", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/list", description: "List your A2A tasks", params: {
|
|
284
|
+
status: { type: "string", description: "Filter by status (e.g. 'submitted', 'working', 'completed', 'failed', comma-separated for multiple)" },
|
|
285
|
+
role: { type: "string", description: "'sender' or 'recipient'" },
|
|
286
|
+
limit: { type: "number", description: "Max tasks to return" },
|
|
287
|
+
}},
|
|
288
|
+
{ operation: "a2a.tasks.get", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/get", description: "Get a specific A2A task by ID", params: {
|
|
289
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
290
|
+
}},
|
|
291
|
+
{ operation: "a2a.tasks.respond", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/respond", description: "Respond to an A2A task", params: {
|
|
292
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
293
|
+
state: { type: "string", description: "New state: completed, input-required, working, or failed", required: true },
|
|
294
|
+
message: { type: "string", description: "Response message text" },
|
|
295
|
+
artifacts: { type: "array", description: "Array of artifact objects (for completed tasks)" },
|
|
296
|
+
}},
|
|
297
|
+
{ operation: "a2a.tasks.cancel", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/cancel", description: "Cancel an A2A task", params: {
|
|
298
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
299
|
+
}},
|
|
300
|
+
{ operation: "a2a.tasks.count", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/count", description: "Count A2A tasks by status", params: {
|
|
301
|
+
status: { type: "string", description: "Filter by status" },
|
|
302
|
+
}},
|
|
271
303
|
];
|
|
272
304
|
|
|
273
305
|
// --- Dynamic capabilities ---
|
|
@@ -537,11 +569,11 @@ export function registerTools(api: any) {
|
|
|
537
569
|
|
|
538
570
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
539
571
|
name: "clawnet_task_send",
|
|
540
|
-
description: toolDesc("clawnet_task_send", "Send a task to another ClawNet agent. Use this when you need something from another agent — a question answered, an action performed, information looked up. Returns a task ID to check for their response later. For fire-and-forget notifications, use email instead."),
|
|
572
|
+
description: toolDesc("clawnet_task_send", "Send a task to another agent. Works with ClawNet agent names (e.g. 'Tom') or external A2A endpoint URLs (e.g. 'https://example.com/a2a/agent'). Use this when you need something from another agent — a question answered, an action performed, information looked up. Returns a task ID to check for their response later. For fire-and-forget notifications, use email instead."),
|
|
541
573
|
parameters: {
|
|
542
574
|
type: "object",
|
|
543
575
|
properties: {
|
|
544
|
-
to: { type: "string", description: "Recipient agent name" },
|
|
576
|
+
to: { type: "string", description: "Recipient: agent name (e.g. 'Tom') or external A2A URL (e.g. 'https://example.com/a2a/agent')" },
|
|
545
577
|
message: { type: "string", description: "Message content" },
|
|
546
578
|
task_id: { type: "string", description: "If following up on a task (after agent asked for input), provide the task ID" },
|
|
547
579
|
},
|
|
@@ -549,6 +581,26 @@ export function registerTools(api: any) {
|
|
|
549
581
|
},
|
|
550
582
|
async execute(_id: string, params: { to: string; message: string; task_id?: string }) {
|
|
551
583
|
const cfg = loadFreshConfig(api);
|
|
584
|
+
const isExternalUrl = params.to.startsWith("https://") || params.to.startsWith("http://");
|
|
585
|
+
|
|
586
|
+
if (params.to.startsWith("http://")) {
|
|
587
|
+
return textResult({ error: "Only HTTPS URLs are supported for external A2A endpoints. Use https:// instead of http://." });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
if (isExternalUrl) {
|
|
591
|
+
// External A2A: route through tasks/send-external on internal endpoint
|
|
592
|
+
const a2aParams: Record<string, unknown> = {
|
|
593
|
+
url: params.to,
|
|
594
|
+
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
595
|
+
};
|
|
596
|
+
if (params.task_id) {
|
|
597
|
+
a2aParams.taskId = params.task_id;
|
|
598
|
+
}
|
|
599
|
+
const result = await a2aCall(cfg, "/a2a", "tasks/send-external", a2aParams, ctx?.agentId, ctx?.sessionKey);
|
|
600
|
+
return textResult(result.data);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Internal ClawNet agent: existing path
|
|
552
604
|
const a2aParams: Record<string, unknown> = {
|
|
553
605
|
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
554
606
|
};
|
|
@@ -720,6 +772,16 @@ export function registerTools(api: any) {
|
|
|
720
772
|
if (query) path += (path.includes('?') ? '&' : '?') + query;
|
|
721
773
|
}
|
|
722
774
|
|
|
775
|
+
// JSON-RPC operations: dispatch via a2aCall()
|
|
776
|
+
if (op.jsonrpc && op.rpc_method) {
|
|
777
|
+
const rpcParams: Record<string, unknown> = {};
|
|
778
|
+
for (const [key, val] of Object.entries(params)) {
|
|
779
|
+
if (val !== undefined) rpcParams[key] = val;
|
|
780
|
+
}
|
|
781
|
+
const result = await a2aCall(cfg, op.path, op.rpc_method, Object.keys(rpcParams).length > 0 ? rpcParams : undefined, ctx?.agentId, ctx?.sessionKey);
|
|
782
|
+
return textResult(result.data);
|
|
783
|
+
}
|
|
784
|
+
|
|
723
785
|
// Build body for non-GET requests
|
|
724
786
|
let body: Record<string, unknown> | undefined;
|
|
725
787
|
let rawBody: string | undefined;
|