@clwnt/clawnet 0.7.7 → 0.7.8
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/service.ts +76 -34
- package/src/tools.ts +58 -2
package/package.json
CHANGED
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,50 @@ 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
|
+
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
|
+
api.logger.debug?.(`[clawnet] ${accountId}: marked ${emailIds.length} message(s) + ${taskIds.length} task(s) notified`);
|
|
189
|
+
} catch (err: any) {
|
|
190
|
+
api.logger.warn(`[clawnet] ${accountId}: mark-notified failed (non-fatal): ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Mark incoming A2A tasks as 'working' (protocol semantics, separate from notification tracking)
|
|
195
|
+
for (const msg of messages) {
|
|
196
|
+
if (msg.type === "task" && msg.content.startsWith("[A2A Task ")) {
|
|
197
|
+
try {
|
|
198
|
+
await fetch(`${auth.baseUrl}/a2a`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { Authorization: `Bearer ${auth.token}`, "Content-Type": "application/json" },
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
jsonrpc: "2.0",
|
|
203
|
+
id: `ack-${msg.id}`,
|
|
204
|
+
method: "tasks/respond",
|
|
205
|
+
params: { id: msg.id, state: "working" },
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
} catch {
|
|
209
|
+
// Non-fatal — task may get re-delivered next cycle
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
166
214
|
} catch (err: any) {
|
|
167
215
|
state.lastError = { message: err.message, at: new Date() };
|
|
168
216
|
state.counters.errors++;
|
|
@@ -286,13 +334,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
286
334
|
|
|
287
335
|
// --- Poll ---
|
|
288
336
|
|
|
289
|
-
async function pollAccount(account: ClawnetAccount): Promise<number> {
|
|
337
|
+
async function pollAccount(account: ClawnetAccount): Promise<{ a2aDmCount: number; sentTaskUpdates: number; notifyCount: number }> {
|
|
290
338
|
const resolvedToken = resolveToken(account.token);
|
|
291
339
|
if (!resolvedToken) {
|
|
292
340
|
api.logger.warn(`[clawnet] No token resolved for account "${account.id}", skipping`);
|
|
293
|
-
return 0;
|
|
341
|
+
return { a2aDmCount: 0, sentTaskUpdates: 0, notifyCount: 0 };
|
|
294
342
|
}
|
|
295
343
|
|
|
344
|
+
// Store auth context for deliverBatch to use for mark-notified calls
|
|
345
|
+
accountAuth.set(account.id, { token: resolvedToken, baseUrl: cfg.baseUrl });
|
|
346
|
+
|
|
296
347
|
const headers = {
|
|
297
348
|
Authorization: `Bearer ${resolvedToken}`,
|
|
298
349
|
"Content-Type": "application/json",
|
|
@@ -307,11 +358,14 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
307
358
|
count: number;
|
|
308
359
|
task_count?: number;
|
|
309
360
|
sent_task_updates?: number;
|
|
361
|
+
notify_count?: number;
|
|
310
362
|
plugin_config?: {
|
|
311
363
|
poll_seconds: number;
|
|
312
364
|
debounce_seconds: number;
|
|
313
365
|
max_batch_size: number;
|
|
314
366
|
deliver_channel: string;
|
|
367
|
+
notify_on_new?: boolean;
|
|
368
|
+
remind_after_hours?: number | null;
|
|
315
369
|
};
|
|
316
370
|
};
|
|
317
371
|
|
|
@@ -342,23 +396,29 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
342
396
|
|
|
343
397
|
const a2aDmCount = checkData.task_count ?? 0;
|
|
344
398
|
const sentTaskUpdates = checkData.sent_task_updates ?? 0;
|
|
399
|
+
const notifyCount = checkData.notify_count ?? (checkData.count + a2aDmCount + sentTaskUpdates);
|
|
345
400
|
|
|
346
401
|
if (checkData.count === 0) {
|
|
347
402
|
// Email inbox clear — release any delivery lock (agent finished processing)
|
|
348
403
|
deliveryLock.delete(account.id);
|
|
349
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
404
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// If nothing needs notification, skip fetch (but don't release lock — inbox still has items)
|
|
408
|
+
if (notifyCount === 0) {
|
|
409
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
350
410
|
}
|
|
351
411
|
|
|
352
|
-
// Skip if a recent
|
|
412
|
+
// Skip if a recent delivery is still being processed.
|
|
353
413
|
// TTL-based lock: after successful POST, lock for 10 min to let the agent work.
|
|
354
414
|
const lockUntil = deliveryLock.get(account.id);
|
|
355
415
|
if (lockUntil && new Date() < lockUntil) {
|
|
356
416
|
api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
|
|
357
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
417
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
358
418
|
}
|
|
359
419
|
|
|
360
420
|
state.lastInboxNonEmptyAt = new Date();
|
|
361
|
-
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting`);
|
|
421
|
+
api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (${notifyCount} to notify)`);
|
|
362
422
|
|
|
363
423
|
// Fetch full messages
|
|
364
424
|
const inboxRes = await fetch(`${cfg.baseUrl}/inbox`, { headers });
|
|
@@ -367,7 +427,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
367
427
|
}
|
|
368
428
|
const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
|
|
369
429
|
|
|
370
|
-
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
|
|
430
|
+
if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
371
431
|
|
|
372
432
|
// Normalize API field names: API returns "from", plugin uses "from_agent"
|
|
373
433
|
const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
|
|
@@ -376,6 +436,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
376
436
|
content: m.content,
|
|
377
437
|
subject: m.email?.subject ?? m.subject,
|
|
378
438
|
created_at: m.created_at,
|
|
439
|
+
type: "email" as const,
|
|
379
440
|
}));
|
|
380
441
|
|
|
381
442
|
state.counters.messagesSeen += normalized.length;
|
|
@@ -385,7 +446,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
385
446
|
pendingMessages.set(account.id, [...existing, ...normalized]);
|
|
386
447
|
scheduleFlush(account.id, account.agentId);
|
|
387
448
|
|
|
388
|
-
return { a2aDmCount, sentTaskUpdates };
|
|
449
|
+
return { a2aDmCount, sentTaskUpdates, notifyCount };
|
|
389
450
|
}
|
|
390
451
|
|
|
391
452
|
async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
|
|
@@ -427,7 +488,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
427
488
|
|
|
428
489
|
api.logger.info(`[clawnet] ${account.id}: ${tasks.length} A2A task(s) to deliver`);
|
|
429
490
|
|
|
430
|
-
// Convert A2A tasks to the message format
|
|
491
|
+
// Convert A2A tasks to the message format for delivery
|
|
492
|
+
// Working transition + mark-notified happen post-delivery in deliverBatch
|
|
431
493
|
const messages: InboxMessage[] = tasks.map((task) => {
|
|
432
494
|
const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
|
|
433
495
|
const lastMsg = history[history.length - 1];
|
|
@@ -438,6 +500,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
438
500
|
from_agent: task.from,
|
|
439
501
|
content: `[A2A Task ${task.id}]${contactInfo}\n${text}`,
|
|
440
502
|
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
503
|
+
type: "task" as const,
|
|
441
504
|
};
|
|
442
505
|
});
|
|
443
506
|
|
|
@@ -445,28 +508,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
445
508
|
const existing = pendingMessages.get(account.id) ?? [];
|
|
446
509
|
pendingMessages.set(account.id, [...existing, ...messages]);
|
|
447
510
|
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
511
|
}
|
|
471
512
|
|
|
472
513
|
async function pollSentTaskUpdates(account: ClawnetAccount) {
|
|
@@ -477,12 +518,12 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
477
518
|
const lockUntil = deliveryLock.get(account.id);
|
|
478
519
|
if (lockUntil && new Date() < lockUntil) return;
|
|
479
520
|
|
|
480
|
-
// Fetch tasks I sent that need attention
|
|
521
|
+
// Fetch tasks I sent that need my attention or have finished
|
|
481
522
|
const body = {
|
|
482
523
|
jsonrpc: "2.0",
|
|
483
524
|
id: `sent-poll-${Date.now()}`,
|
|
484
525
|
method: "tasks/list",
|
|
485
|
-
params: { role: "sender", status: "input-required", limit: 50 },
|
|
526
|
+
params: { role: "sender", status: "input-required,completed,failed", limit: 50 },
|
|
486
527
|
};
|
|
487
528
|
const res = await fetch(`${cfg.baseUrl}/a2a`, {
|
|
488
529
|
method: "POST",
|
|
@@ -512,6 +553,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
|
|
|
512
553
|
from_agent: task.to, // the agent that responded
|
|
513
554
|
content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
|
|
514
555
|
created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
|
|
556
|
+
type: "task" as const,
|
|
515
557
|
};
|
|
516
558
|
});
|
|
517
559
|
|
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[] = [
|
|
@@ -268,6 +270,30 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
|
|
|
268
270
|
{ operation: "account.rate_limits", method: "GET", path: "/me/rate-limits", description: "Check your current rate limits" },
|
|
269
271
|
// Docs
|
|
270
272
|
{ operation: "docs.help", method: "GET", path: "/docs/skill", description: "Get the full ClawNet documentation — features, usage examples, safety rules, setup, troubleshooting, and rate limits" },
|
|
273
|
+
// A2A (JSON-RPC via /a2a)
|
|
274
|
+
{ operation: "a2a.card.update", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "card/update", description: "Update your A2A Agent Card skills", params: {
|
|
275
|
+
skills: { type: "array", description: "Array of {id, name, description} skill objects", required: true },
|
|
276
|
+
}},
|
|
277
|
+
{ operation: "a2a.tasks.list", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/list", description: "List your A2A tasks", params: {
|
|
278
|
+
status: { type: "string", description: "Filter by status (e.g. 'submitted', 'working', 'completed', 'failed', comma-separated for multiple)" },
|
|
279
|
+
role: { type: "string", description: "'sender' or 'recipient'" },
|
|
280
|
+
limit: { type: "number", description: "Max tasks to return" },
|
|
281
|
+
}},
|
|
282
|
+
{ operation: "a2a.tasks.get", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/get", description: "Get a specific A2A task by ID", params: {
|
|
283
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
284
|
+
}},
|
|
285
|
+
{ operation: "a2a.tasks.respond", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/respond", description: "Respond to an A2A task", params: {
|
|
286
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
287
|
+
state: { type: "string", description: "New state: completed, input-required, working, or failed", required: true },
|
|
288
|
+
message: { type: "string", description: "Response message text" },
|
|
289
|
+
artifacts: { type: "array", description: "Array of artifact objects (for completed tasks)" },
|
|
290
|
+
}},
|
|
291
|
+
{ operation: "a2a.tasks.cancel", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/cancel", description: "Cancel an A2A task", params: {
|
|
292
|
+
id: { type: "string", description: "Task ID", required: true },
|
|
293
|
+
}},
|
|
294
|
+
{ operation: "a2a.tasks.count", jsonrpc: true, method: "POST", path: "/a2a", rpc_method: "tasks/count", description: "Count A2A tasks by status", params: {
|
|
295
|
+
status: { type: "string", description: "Filter by status" },
|
|
296
|
+
}},
|
|
271
297
|
];
|
|
272
298
|
|
|
273
299
|
// --- Dynamic capabilities ---
|
|
@@ -537,11 +563,11 @@ export function registerTools(api: any) {
|
|
|
537
563
|
|
|
538
564
|
api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
|
|
539
565
|
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."),
|
|
566
|
+
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
567
|
parameters: {
|
|
542
568
|
type: "object",
|
|
543
569
|
properties: {
|
|
544
|
-
to: { type: "string", description: "Recipient agent name" },
|
|
570
|
+
to: { type: "string", description: "Recipient: agent name (e.g. 'Tom') or external A2A URL (e.g. 'https://example.com/a2a/agent')" },
|
|
545
571
|
message: { type: "string", description: "Message content" },
|
|
546
572
|
task_id: { type: "string", description: "If following up on a task (after agent asked for input), provide the task ID" },
|
|
547
573
|
},
|
|
@@ -549,6 +575,26 @@ export function registerTools(api: any) {
|
|
|
549
575
|
},
|
|
550
576
|
async execute(_id: string, params: { to: string; message: string; task_id?: string }) {
|
|
551
577
|
const cfg = loadFreshConfig(api);
|
|
578
|
+
const isExternalUrl = params.to.startsWith("https://") || params.to.startsWith("http://");
|
|
579
|
+
|
|
580
|
+
if (params.to.startsWith("http://")) {
|
|
581
|
+
return textResult({ error: "Only HTTPS URLs are supported for external A2A endpoints. Use https:// instead of http://." });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (isExternalUrl) {
|
|
585
|
+
// External A2A: route through tasks/send-external on internal endpoint
|
|
586
|
+
const a2aParams: Record<string, unknown> = {
|
|
587
|
+
url: params.to,
|
|
588
|
+
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
589
|
+
};
|
|
590
|
+
if (params.task_id) {
|
|
591
|
+
a2aParams.taskId = params.task_id;
|
|
592
|
+
}
|
|
593
|
+
const result = await a2aCall(cfg, "/a2a", "tasks/send-external", a2aParams, ctx?.agentId, ctx?.sessionKey);
|
|
594
|
+
return textResult(result.data);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Internal ClawNet agent: existing path
|
|
552
598
|
const a2aParams: Record<string, unknown> = {
|
|
553
599
|
message: { role: "user", parts: [{ kind: "text", text: params.message }] },
|
|
554
600
|
};
|
|
@@ -720,6 +766,16 @@ export function registerTools(api: any) {
|
|
|
720
766
|
if (query) path += (path.includes('?') ? '&' : '?') + query;
|
|
721
767
|
}
|
|
722
768
|
|
|
769
|
+
// JSON-RPC operations: dispatch via a2aCall()
|
|
770
|
+
if (op.jsonrpc && op.rpc_method) {
|
|
771
|
+
const rpcParams: Record<string, unknown> = {};
|
|
772
|
+
for (const [key, val] of Object.entries(params)) {
|
|
773
|
+
if (val !== undefined) rpcParams[key] = val;
|
|
774
|
+
}
|
|
775
|
+
const result = await a2aCall(cfg, op.path, op.rpc_method, Object.keys(rpcParams).length > 0 ? rpcParams : undefined, ctx?.agentId, ctx?.sessionKey);
|
|
776
|
+
return textResult(result.data);
|
|
777
|
+
}
|
|
778
|
+
|
|
723
779
|
// Build body for non-GET requests
|
|
724
780
|
let body: Record<string, unknown> | undefined;
|
|
725
781
|
let rawBody: string | undefined;
|