clawnexus 0.3.0 → 0.4.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.
@@ -26,4 +26,4 @@ export interface AgentCard {
26
26
  url?: string;
27
27
  };
28
28
  }
29
- export declare function buildAgentCard(instance: ClawInstance, daemonVersion: string): AgentCard;
29
+ export declare function buildAgentCard(instance: ClawInstance, daemonVersion: string, skills?: AgentSkill[]): AgentCard;
package/dist/a2a/card.js CHANGED
@@ -8,20 +8,25 @@ const DEFAULT_SKILL = {
8
8
  description: "General-purpose AI assistant",
9
9
  tags: ["general"],
10
10
  };
11
- function buildAgentCard(instance, daemonVersion) {
11
+ function buildAgentCard(instance, daemonVersion, skills) {
12
+ // Skill priority: remote_card.skills (remote) > skills param (local) > DEFAULT_SKILL
13
+ const resolvedSkills = instance.remote_card?.skills?.length
14
+ ? instance.remote_card.skills
15
+ : (skills && skills.length > 0 ? skills : [DEFAULT_SKILL]);
16
+ const remoteCapabilities = instance.remote_card?.capabilities;
12
17
  return {
13
18
  name: instance.alias ?? instance.auto_name,
14
19
  description: instance.display_name || instance.assistant_name,
15
20
  url: `http://${instance.address}:17890`,
16
21
  version: daemonVersion,
17
22
  capabilities: {
18
- streaming: false,
19
- pushNotifications: false,
20
- stateTransitionHistory: false,
23
+ streaming: remoteCapabilities?.streaming ?? false,
24
+ pushNotifications: remoteCapabilities?.pushNotifications ?? false,
25
+ stateTransitionHistory: remoteCapabilities?.stateTransitionHistory ?? false,
21
26
  },
22
- skills: [DEFAULT_SKILL],
23
- defaultInputModes: ["text/plain"],
24
- defaultOutputModes: ["text/plain"],
27
+ skills: resolvedSkills,
28
+ defaultInputModes: instance.remote_card?.input_modes ?? ["text/plain"],
29
+ defaultOutputModes: instance.remote_card?.output_modes ?? ["text/plain"],
25
30
  provider: {
26
31
  name: "ClawNexus",
27
32
  url: "https://github.com/SilverstreamsAI/ClawNexus",
@@ -0,0 +1,26 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type { RegistryStore } from "../registry/store.js";
3
+ import type { ClawInstance, RemoteCard } from "../types.js";
4
+ export interface CardFetcherOptions {
5
+ refreshIntervalMs?: number;
6
+ fetchTimeoutMs?: number;
7
+ staleMs?: number;
8
+ }
9
+ export declare class CardFetcher extends EventEmitter {
10
+ private readonly store;
11
+ private readonly refreshIntervalMs;
12
+ private readonly fetchTimeoutMs;
13
+ private readonly staleMs;
14
+ private refreshTimer;
15
+ private readonly pendingKeys;
16
+ private stopped;
17
+ constructor(store: RegistryStore, opts?: CardFetcherOptions);
18
+ start(): void;
19
+ stop(): void;
20
+ private onUpsert;
21
+ private shouldSkip;
22
+ private fetchAndApply;
23
+ fetchCard(instance: ClawInstance): Promise<RemoteCard | null>;
24
+ refreshAll(): Promise<void>;
25
+ determineCardUrl(instance: ClawInstance): string;
26
+ }
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ // CardFetcher — fetches remote Agent Cards from discovered instances
3
+ // Listens to store "upsert" events and populates remote_card on each instance.
4
+ // Self instances are skipped (they use local SkillsRegistry).
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.CardFetcher = void 0;
7
+ const node_events_1 = require("node:events");
8
+ const REFRESH_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
9
+ const FETCH_TIMEOUT_MS = 3000;
10
+ const STALE_MS = 5 * 60 * 1000; // 5 minutes
11
+ class CardFetcher extends node_events_1.EventEmitter {
12
+ store;
13
+ refreshIntervalMs;
14
+ fetchTimeoutMs;
15
+ staleMs;
16
+ refreshTimer = null;
17
+ pendingKeys = new Set();
18
+ stopped = false;
19
+ constructor(store, opts = {}) {
20
+ super();
21
+ this.store = store;
22
+ this.refreshIntervalMs = opts.refreshIntervalMs ?? REFRESH_INTERVAL_MS;
23
+ this.fetchTimeoutMs = opts.fetchTimeoutMs ?? FETCH_TIMEOUT_MS;
24
+ this.staleMs = opts.staleMs ?? STALE_MS;
25
+ }
26
+ start() {
27
+ if (this.refreshTimer !== null)
28
+ return; // already running
29
+ this.stopped = false;
30
+ this.store.on("upsert", this.onUpsert);
31
+ // Initial fetch for all existing instances
32
+ this.refreshAll().catch((err) => {
33
+ console.log(`[clawnexus] [CardFetcher] Initial refresh failed (non-fatal): ${err}`);
34
+ });
35
+ // Periodic refresh
36
+ this.refreshTimer = setInterval(() => {
37
+ this.refreshAll().catch((err) => {
38
+ console.log(`[clawnexus] [CardFetcher] Periodic refresh failed (non-fatal): ${err}`);
39
+ });
40
+ }, this.refreshIntervalMs);
41
+ }
42
+ stop() {
43
+ this.stopped = true;
44
+ this.store.off("upsert", this.onUpsert);
45
+ if (this.refreshTimer) {
46
+ clearInterval(this.refreshTimer);
47
+ this.refreshTimer = null;
48
+ }
49
+ this.pendingKeys.clear();
50
+ }
51
+ onUpsert = (instance) => {
52
+ if (this.stopped)
53
+ return;
54
+ if (this.shouldSkip(instance))
55
+ return;
56
+ const key = this.store.networkKey(instance.address, instance.gateway_port);
57
+ // Guard against infinite loop: if we're currently processing this key, skip
58
+ if (this.pendingKeys.has(key))
59
+ return;
60
+ this.pendingKeys.add(key);
61
+ this.fetchAndApply(instance, key).finally(() => {
62
+ this.pendingKeys.delete(key);
63
+ });
64
+ };
65
+ shouldSkip(instance) {
66
+ // Skip self instances — they use local SkillsRegistry
67
+ if (instance.is_self)
68
+ return true;
69
+ // Skip offline instances
70
+ if (instance.status === "offline")
71
+ return true;
72
+ // Skip if remote_card is fresh enough
73
+ if (instance.remote_card) {
74
+ const age = Date.now() - new Date(instance.remote_card.fetched_at).getTime();
75
+ if (age < this.staleMs)
76
+ return true;
77
+ }
78
+ return false;
79
+ }
80
+ async fetchAndApply(instance, key) {
81
+ try {
82
+ const card = await this.fetchCard(instance);
83
+ if (!card || this.stopped)
84
+ return;
85
+ // Re-fetch the instance from store (it may have been updated during fetch)
86
+ const current = this.store.getByNetworkKey(instance.address, instance.gateway_port);
87
+ if (!current)
88
+ return;
89
+ // Apply remote_card directly and re-upsert
90
+ current.remote_card = card;
91
+ // Use upsert so it persists (the pendingKeys guard prevents re-entry)
92
+ this.store.upsert(current);
93
+ this.emit("card_fetched", { key, skills_count: card.skills.length });
94
+ console.log(`[clawnexus] [CardFetcher] Fetched card for ${current.auto_name}: ${card.skills.length} skill(s)`);
95
+ }
96
+ catch (err) {
97
+ this.emit("card_error", { key, error: err });
98
+ }
99
+ }
100
+ async fetchCard(instance) {
101
+ const url = this.determineCardUrl(instance);
102
+ try {
103
+ const resp = await fetch(url, {
104
+ signal: AbortSignal.timeout(this.fetchTimeoutMs),
105
+ headers: { Accept: "application/json" },
106
+ });
107
+ if (!resp.ok)
108
+ return null;
109
+ const data = (await resp.json());
110
+ if (!data || !Array.isArray(data.skills))
111
+ return null;
112
+ return {
113
+ skills: data.skills,
114
+ capabilities: data.capabilities,
115
+ input_modes: data.defaultInputModes,
116
+ output_modes: data.defaultOutputModes,
117
+ card_url: url,
118
+ fetched_at: new Date().toISOString(),
119
+ };
120
+ }
121
+ catch {
122
+ // Timeout, network error, JSON parse error — all graceful
123
+ return null;
124
+ }
125
+ }
126
+ async refreshAll() {
127
+ if (this.stopped)
128
+ return;
129
+ const instances = this.store.getAll();
130
+ const promises = [];
131
+ for (const inst of instances) {
132
+ if (this.shouldSkip(inst))
133
+ continue;
134
+ const key = this.store.networkKey(inst.address, inst.gateway_port);
135
+ if (this.pendingKeys.has(key))
136
+ continue;
137
+ this.pendingKeys.add(key);
138
+ promises.push(this.fetchAndApply(inst, key).finally(() => {
139
+ this.pendingKeys.delete(key);
140
+ }));
141
+ }
142
+ await Promise.allSettled(promises);
143
+ }
144
+ determineCardUrl(instance) {
145
+ const proto = instance.tls ? "https" : "http";
146
+ // CDP-discovered instances have a ClawNexus daemon on port 17890
147
+ // Other discovery sources: try the daemon port too (if they have ClawNexus)
148
+ const port = 17890;
149
+ return `${proto}://${instance.address}:${port}/.well-known/agent-card.json`;
150
+ }
151
+ }
152
+ exports.CardFetcher = CardFetcher;
@@ -0,0 +1,30 @@
1
+ import type { A2ATask, A2AError } from "./types.js";
2
+ import type { A2ATaskStore } from "./store.js";
3
+ export interface A2AHandlerOptions {
4
+ gatewayUrl?: string;
5
+ timeoutMs?: number;
6
+ maxConcurrent?: number;
7
+ store?: A2ATaskStore;
8
+ }
9
+ export declare class A2AHandler {
10
+ private readonly gatewayUrl;
11
+ private readonly timeoutMs;
12
+ private readonly maxConcurrent;
13
+ private readonly store;
14
+ private conn;
15
+ private connecting;
16
+ private reconnectAttempt;
17
+ private readonly sessions;
18
+ private activeTasks;
19
+ constructor(opts?: A2AHandlerOptions);
20
+ handleTaskSend(params: unknown): Promise<A2ATask | A2AError>;
21
+ getTask(taskId: string): A2ATask | undefined;
22
+ /** Clean up connection and pending sessions. */
23
+ close(): void;
24
+ private getConnection;
25
+ private connect;
26
+ private scheduleReconnect;
27
+ private sendAndWait;
28
+ private extractResponse;
29
+ private persistTask;
30
+ }
@@ -0,0 +1,228 @@
1
+ "use strict";
2
+ // A2A Task Handler — manages tasks/send and tasks/get
3
+ // Uses a persistent Gateway connection (lazy init, auto-reconnect) shared across
4
+ // concurrent tasks. Each task is identified by a unique sessionKey for multiplexing.
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.A2AHandler = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const ws_1 = require("ws");
9
+ const gateway_js_1 = require("../agent/gateway.js");
10
+ const types_js_1 = require("./types.js");
11
+ const DEFAULT_TIMEOUT_MS = 60_000;
12
+ const DEFAULT_MAX_CONCURRENT = 5;
13
+ const RECONNECT_BASE_MS = 1_000;
14
+ const RECONNECT_MAX_MS = 30_000;
15
+ class A2AHandler {
16
+ gatewayUrl;
17
+ timeoutMs;
18
+ maxConcurrent;
19
+ store;
20
+ // Persistent Gateway connection
21
+ conn = null;
22
+ connecting = null;
23
+ reconnectAttempt = 0;
24
+ // Session-based dispatch for multiplexed tasks
25
+ sessions = new Map();
26
+ activeTasks = 0;
27
+ constructor(opts = {}) {
28
+ this.gatewayUrl = opts.gatewayUrl ?? "ws://127.0.0.1:18789";
29
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
30
+ this.maxConcurrent = opts.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
31
+ this.store = opts.store ?? null;
32
+ }
33
+ async handleTaskSend(params) {
34
+ const p = params;
35
+ if (!p?.message?.parts?.length) {
36
+ return { code: types_js_1.JSON_RPC_INVALID_PARAMS, message: "Missing message with parts" };
37
+ }
38
+ const textParts = p.message.parts.filter((part) => part.type === "text");
39
+ if (textParts.length === 0) {
40
+ return { code: types_js_1.JSON_RPC_INVALID_PARAMS, message: "No text parts in message" };
41
+ }
42
+ // Concurrency guard
43
+ if (this.activeTasks >= this.maxConcurrent) {
44
+ return {
45
+ code: types_js_1.JSON_RPC_TASK_LIMIT_EXCEEDED,
46
+ message: `Too many concurrent tasks (max: ${this.maxConcurrent})`,
47
+ };
48
+ }
49
+ const userText = textParts.map((part) => part.text).join("\n");
50
+ const taskId = (0, node_crypto_1.randomUUID)();
51
+ const sessionKey = `agent:main:main:dm:a2a-task-${taskId}`;
52
+ const task = {
53
+ id: taskId,
54
+ status: { state: "submitted" },
55
+ history: [p.message],
56
+ };
57
+ this.persistTask(task);
58
+ this.activeTasks++;
59
+ // Acquire shared connection
60
+ let conn;
61
+ try {
62
+ conn = await this.getConnection();
63
+ }
64
+ catch (err) {
65
+ this.activeTasks--;
66
+ task.status = {
67
+ state: "failed",
68
+ message: { role: "agent", parts: [{ type: "text", text: `Gateway connection failed: ${err.message}` }] },
69
+ };
70
+ this.persistTask(task);
71
+ return task;
72
+ }
73
+ task.status.state = "working";
74
+ this.persistTask(task);
75
+ try {
76
+ const result = await this.sendAndWait(conn.ws, sessionKey, userText);
77
+ const agentMsg = { role: "agent", parts: [{ type: "text", text: result }] };
78
+ task.status = { state: "completed", message: agentMsg };
79
+ task.artifacts = [{ parts: [{ type: "text", text: result }] }];
80
+ if (task.history)
81
+ task.history.push(agentMsg);
82
+ }
83
+ catch (err) {
84
+ task.status = {
85
+ state: "failed",
86
+ message: { role: "agent", parts: [{ type: "text", text: err.message }] },
87
+ };
88
+ }
89
+ finally {
90
+ this.activeTasks--;
91
+ this.persistTask(task);
92
+ }
93
+ return task;
94
+ }
95
+ getTask(taskId) {
96
+ return this.store?.get(taskId);
97
+ }
98
+ /** Clean up connection and pending sessions. */
99
+ close() {
100
+ for (const [, session] of this.sessions) {
101
+ clearTimeout(session.timer);
102
+ session.reject(new Error("Handler closed"));
103
+ }
104
+ this.sessions.clear();
105
+ if (this.conn) {
106
+ this.conn.close();
107
+ this.conn = null;
108
+ }
109
+ this.connecting = null;
110
+ }
111
+ // --- Connection management ---
112
+ async getConnection() {
113
+ if (this.conn && this.conn.ws.readyState === ws_1.WebSocket.OPEN) {
114
+ return this.conn;
115
+ }
116
+ // Avoid duplicate connect attempts
117
+ if (this.connecting)
118
+ return this.connecting;
119
+ this.connecting = this.connect();
120
+ try {
121
+ return await this.connecting;
122
+ }
123
+ finally {
124
+ this.connecting = null;
125
+ }
126
+ }
127
+ async connect() {
128
+ const conn = await (0, gateway_js_1.connectGateway)({ gatewayUrl: this.gatewayUrl });
129
+ this.conn = conn;
130
+ this.reconnectAttempt = 0;
131
+ // Shared event dispatch: route incoming events to the right session
132
+ conn.ws.on("message", (data) => {
133
+ let msg;
134
+ try {
135
+ msg = JSON.parse(data.toString());
136
+ }
137
+ catch {
138
+ return;
139
+ }
140
+ if (msg.type !== "event")
141
+ return;
142
+ const event = msg.event;
143
+ if (event !== "chat" && event !== "chat.update")
144
+ return;
145
+ const payload = msg.payload;
146
+ const sk = payload?.sessionKey ?? msg.sessionKey;
147
+ if (!sk)
148
+ return;
149
+ const session = this.sessions.get(sk);
150
+ if (!session)
151
+ return;
152
+ const state = payload?.state;
153
+ if (state === "final") {
154
+ this.sessions.delete(sk);
155
+ clearTimeout(session.timer);
156
+ session.resolve(this.extractResponse(payload));
157
+ }
158
+ else if (state === "error") {
159
+ this.sessions.delete(sk);
160
+ clearTimeout(session.timer);
161
+ session.reject(new Error(payload?.errorMessage ?? "OpenClaw chat error"));
162
+ }
163
+ });
164
+ conn.ws.on("close", () => {
165
+ this.conn = null;
166
+ // Reject all pending sessions — they'll fail their tasks gracefully
167
+ for (const [sk, session] of this.sessions) {
168
+ clearTimeout(session.timer);
169
+ session.reject(new Error("Gateway connection closed during task execution"));
170
+ this.sessions.delete(sk);
171
+ }
172
+ this.scheduleReconnect();
173
+ });
174
+ return conn;
175
+ }
176
+ scheduleReconnect() {
177
+ // Only reconnect if there could be future tasks (handler not closed)
178
+ const delay = Math.min(RECONNECT_BASE_MS * 2 ** this.reconnectAttempt, RECONNECT_MAX_MS);
179
+ this.reconnectAttempt++;
180
+ setTimeout(() => {
181
+ // Lazy: don't eagerly reconnect, just clear state so next getConnection() will connect fresh
182
+ this.connecting = null;
183
+ }, delay);
184
+ }
185
+ // --- Task execution ---
186
+ sendAndWait(ws, sessionKey, message) {
187
+ return new Promise((resolve, reject) => {
188
+ const requestId = (0, node_crypto_1.randomUUID)();
189
+ const timer = setTimeout(() => {
190
+ this.sessions.delete(sessionKey);
191
+ reject(new Error("Task execution timed out"));
192
+ }, this.timeoutMs);
193
+ this.sessions.set(sessionKey, { resolve, reject, timer });
194
+ ws.send(JSON.stringify({
195
+ type: "req",
196
+ id: requestId,
197
+ method: "chat.send",
198
+ params: { sessionKey, message, idempotencyKey: requestId },
199
+ }));
200
+ });
201
+ }
202
+ extractResponse(payload) {
203
+ if (!payload)
204
+ return "Task completed (no output)";
205
+ const messages = payload.messages;
206
+ if (messages && messages.length > 0) {
207
+ const lastAssistant = [...messages].reverse().find((m) => m.role === "assistant");
208
+ if (lastAssistant) {
209
+ const content = lastAssistant.content;
210
+ if (typeof content === "string")
211
+ return content;
212
+ if (Array.isArray(content)) {
213
+ return content
214
+ .filter((b) => b.type === "text")
215
+ .map((b) => b.text)
216
+ .join("\n");
217
+ }
218
+ }
219
+ }
220
+ return payload.content ?? "Task completed (no output)";
221
+ }
222
+ // --- Persistence ---
223
+ persistTask(task) {
224
+ if (this.store)
225
+ this.store.put(task);
226
+ }
227
+ }
228
+ exports.A2AHandler = A2AHandler;
@@ -0,0 +1,17 @@
1
+ import type { A2ATask } from "./types.js";
2
+ export declare class A2ATaskStore {
3
+ private readonly tasks;
4
+ private readonly filePath;
5
+ private flushTimer;
6
+ private flushInProgress;
7
+ private dirty;
8
+ constructor(configDir?: string);
9
+ init(): Promise<void>;
10
+ get(taskId: string): A2ATask | undefined;
11
+ put(task: A2ATask): void;
12
+ getAll(): A2ATask[];
13
+ close(): Promise<void>;
14
+ private evict;
15
+ private scheduleDirtyFlush;
16
+ private flushNow;
17
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ // A2A Task Store — persists task records to ~/.clawnexus/a2a-tasks.json
3
+ // FIFO eviction: keeps at most MAX_TASKS entries.
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.A2ATaskStore = void 0;
39
+ const fs = __importStar(require("node:fs"));
40
+ const path = __importStar(require("node:path"));
41
+ const os = __importStar(require("node:os"));
42
+ const CLAWNEXUS_DIR = path.join(os.homedir(), ".clawnexus");
43
+ const DEFAULT_FILE = "a2a-tasks.json";
44
+ const MAX_TASKS = 100;
45
+ const DEBOUNCE_MS = 500;
46
+ class A2ATaskStore {
47
+ tasks = new Map();
48
+ filePath;
49
+ flushTimer = null;
50
+ flushInProgress = null;
51
+ dirty = false;
52
+ constructor(configDir) {
53
+ const dir = configDir ?? CLAWNEXUS_DIR;
54
+ this.filePath = path.join(dir, DEFAULT_FILE);
55
+ }
56
+ async init() {
57
+ await fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
58
+ if (fs.existsSync(this.filePath)) {
59
+ try {
60
+ const raw = await fs.promises.readFile(this.filePath, "utf-8");
61
+ const data = JSON.parse(raw);
62
+ if (data.version === 1 && Array.isArray(data.tasks)) {
63
+ for (const t of data.tasks) {
64
+ this.tasks.set(t.id, t);
65
+ }
66
+ }
67
+ }
68
+ catch {
69
+ // Corrupted file — start fresh
70
+ }
71
+ }
72
+ }
73
+ get(taskId) {
74
+ return this.tasks.get(taskId);
75
+ }
76
+ put(task) {
77
+ this.tasks.set(task.id, task);
78
+ this.evict();
79
+ this.scheduleDirtyFlush();
80
+ }
81
+ getAll() {
82
+ return Array.from(this.tasks.values());
83
+ }
84
+ async close() {
85
+ if (this.flushTimer) {
86
+ clearTimeout(this.flushTimer);
87
+ this.flushTimer = null;
88
+ }
89
+ // Wait for any in-progress timer-based flush before deciding whether to flush again
90
+ if (this.flushInProgress) {
91
+ await this.flushInProgress;
92
+ }
93
+ if (this.dirty) {
94
+ await this.flushNow();
95
+ }
96
+ }
97
+ evict() {
98
+ if (this.tasks.size <= MAX_TASKS)
99
+ return;
100
+ // Map iteration order = insertion order; delete oldest entries
101
+ const excess = this.tasks.size - MAX_TASKS;
102
+ let removed = 0;
103
+ for (const key of this.tasks.keys()) {
104
+ if (removed >= excess)
105
+ break;
106
+ this.tasks.delete(key);
107
+ removed++;
108
+ }
109
+ }
110
+ scheduleDirtyFlush() {
111
+ this.dirty = true;
112
+ if (this.flushTimer)
113
+ return;
114
+ this.flushTimer = setTimeout(() => {
115
+ this.flushTimer = null;
116
+ this.flushInProgress = this.flushNow().finally(() => {
117
+ this.flushInProgress = null;
118
+ });
119
+ }, DEBOUNCE_MS);
120
+ }
121
+ async flushNow() {
122
+ const data = {
123
+ version: 1,
124
+ updated_at: new Date().toISOString(),
125
+ tasks: Array.from(this.tasks.values()),
126
+ };
127
+ const json = JSON.stringify(data, null, 2);
128
+ const tmpPath = this.filePath + ".tmp";
129
+ await fs.promises.writeFile(tmpPath, json, "utf-8");
130
+ await fs.promises.rename(tmpPath, this.filePath);
131
+ this.dirty = false;
132
+ }
133
+ }
134
+ exports.A2ATaskStore = A2ATaskStore;
@@ -0,0 +1,45 @@
1
+ export interface A2ARequest {
2
+ jsonrpc: "2.0";
3
+ method: string;
4
+ id: string | number;
5
+ params?: unknown;
6
+ }
7
+ export interface A2AResponse {
8
+ jsonrpc: "2.0";
9
+ id: string | number;
10
+ result?: unknown;
11
+ error?: A2AError;
12
+ }
13
+ export interface A2AError {
14
+ code: number;
15
+ message: string;
16
+ data?: unknown;
17
+ }
18
+ export interface A2ATask {
19
+ id: string;
20
+ status: A2ATaskStatus;
21
+ artifacts?: A2AArtifact[];
22
+ history?: A2AMessage[];
23
+ }
24
+ export interface A2ATaskStatus {
25
+ state: "submitted" | "working" | "completed" | "failed" | "canceled";
26
+ message?: A2AMessage;
27
+ }
28
+ export interface A2AMessage {
29
+ role: "user" | "agent";
30
+ parts: A2APart[];
31
+ }
32
+ export type A2APart = {
33
+ type: "text";
34
+ text: string;
35
+ };
36
+ export interface A2AArtifact {
37
+ name?: string;
38
+ parts: A2APart[];
39
+ }
40
+ export declare const JSON_RPC_PARSE_ERROR = -32700;
41
+ export declare const JSON_RPC_INVALID_REQUEST = -32600;
42
+ export declare const JSON_RPC_METHOD_NOT_FOUND = -32601;
43
+ export declare const JSON_RPC_INVALID_PARAMS = -32602;
44
+ export declare const JSON_RPC_INTERNAL_ERROR = -32603;
45
+ export declare const JSON_RPC_TASK_LIMIT_EXCEEDED = -32005;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ // A2A JSON-RPC 2.0 types and Task model (spec v0.2.1)
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.JSON_RPC_TASK_LIMIT_EXCEEDED = exports.JSON_RPC_INTERNAL_ERROR = exports.JSON_RPC_INVALID_PARAMS = exports.JSON_RPC_METHOD_NOT_FOUND = exports.JSON_RPC_INVALID_REQUEST = exports.JSON_RPC_PARSE_ERROR = void 0;
5
+ // --- JSON-RPC Error Codes ---
6
+ exports.JSON_RPC_PARSE_ERROR = -32700;
7
+ exports.JSON_RPC_INVALID_REQUEST = -32600;
8
+ exports.JSON_RPC_METHOD_NOT_FOUND = -32601;
9
+ exports.JSON_RPC_INVALID_PARAMS = -32602;
10
+ exports.JSON_RPC_INTERNAL_ERROR = -32603;
11
+ exports.JSON_RPC_TASK_LIMIT_EXCEEDED = -32005;