@aman_asmuei/aman-agent 0.31.0-next.0 → 0.31.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/README.md CHANGED
@@ -94,40 +94,47 @@
94
94
 
95
95
  ---
96
96
 
97
- ## What's New in v0.30.0
97
+ ## What's New in v0.31.0
98
98
 
99
- > **Agent hardening trust, durability, insight.**<br/>
100
- > The agent now asks before delegating, persists background tasks across crashes, and ships a real analytics dashboard in `/eval report`.
99
+ > **Multi-agent (A2A) via MCP server mode.**<br/>
100
+ > Multiple `aman-agent` instances on the same machine can now discover each other via a local registry and delegate tasks to each other over the MCP protocol. No new wire format, no broker, no new runtime daemon.
101
101
 
102
102
  <table>
103
103
  <tr>
104
104
  <td width="33%" valign="top">
105
105
 
106
- **Delegation confirmation**
106
+ **`aman-agent serve`**
107
107
 
108
- The agent now prompts you before executing `delegate_task` or `team_run`. No more silent autonomous sub-agents you stay in the loop on every hand-off.
108
+ Run any profile as a local MCP server. Registers in `~/.aman-agent/registry.json` (mode `0600`), exposes `agent.info`, `agent.delegate`, and `agent.send` tools over localhost HTTP with bearer auth.
109
109
 
110
110
  </td>
111
111
  <td width="33%" valign="top">
112
112
 
113
- **Persistent background tasks**
113
+ **`/delegate @coder <task>`**
114
114
 
115
- Background task state is written to `~/.aman-agent/bg-tasks.json` on every transition survives crashes, terminal closures, and reboots. Visible in `/eval report`.
115
+ From any other `aman-agent`, delegate to a running serve instance by handle. The `@`-prefix routes through `delegateRemote` which dials via `StreamableHTTPClientTransport` using the bearer from the registry.
116
116
 
117
117
  </td>
118
118
  <td width="33%" valign="top">
119
119
 
120
- **Rich `/eval report`**
120
+ **`/agents list|info|ping`**
121
121
 
122
- Trust score, sentiment trend, energy distribution, burnout risk, frustration correlations, and background-task stats all in one dashboard.
122
+ Discover, inspect, and latency-check every agent currently running on this machine. `/agents list` merges local registry entries with remotes (local wins on name collision).
123
123
 
124
124
  </td>
125
125
  </tr>
126
126
  </table>
127
127
 
128
+ See the [Multi-agent (A2A)](#multi-agent-a2a) section below for the full walkthrough.
129
+
128
130
  <details>
129
131
  <summary><strong>Highlights from earlier releases</strong></summary>
130
132
 
133
+ **v0.30 — Agent hardening**
134
+ - Delegation confirmation prompts (no more silent sub-agents)
135
+ - Persistent background task state surviving crashes
136
+ - Rich `/eval report` with trust, sentiment, energy, burnout risk
137
+
131
138
  **v0.29 — Ecosystem parity**
132
139
  - Auto-relate memories after extraction (knowledge graph edges)
133
140
  - Stale reference cleanup
@@ -128,6 +128,20 @@ async function delegateRemote(task, agentName, options = {}) {
128
128
  const transport = new StreamableHTTPClientTransport(url, {
129
129
  requestInit: {
130
130
  headers: { Authorization: `Bearer ${entry.token}` }
131
+ },
132
+ // Disable SSE reconnection scheduling. On close(), the SDK aborts
133
+ // the controller; without this override, the SSE stream's error
134
+ // handler races to schedule a new _reconnectionTimeout AFTER close()
135
+ // cleared the old one, and the timer (plus its referenced socket)
136
+ // pins Node's event loop until the undici keepalive times out. A
137
+ // delegateRemote caller then can't exit cleanly. maxRetries: 0
138
+ // drops the schedule-on-error path entirely; we're doing a single
139
+ // RPC, not a persistent stream, so reconnection has no value here.
140
+ reconnectionOptions: {
141
+ maxRetries: 0,
142
+ initialReconnectionDelay: 1,
143
+ maxReconnectionDelay: 1,
144
+ reconnectionDelayGrowFactor: 1
131
145
  }
132
146
  });
133
147
  const client = new Client({ name: "aman-agent-a2a-caller", version: "0.1.0" });
@@ -141,13 +155,19 @@ async function delegateRemote(task, agentName, options = {}) {
141
155
  ...options.context ? { context: options.context } : {}
142
156
  }
143
157
  });
144
- const timeout = new Promise(
145
- (_, rej) => setTimeout(
158
+ let timeoutId;
159
+ const timeout = new Promise((_, rej) => {
160
+ timeoutId = setTimeout(
146
161
  () => rej(new Error(`remote delegate timed out after ${timeoutMs}ms`)),
147
162
  timeoutMs
148
- )
149
- );
150
- const result = await Promise.race([call, timeout]);
163
+ );
164
+ });
165
+ let result;
166
+ try {
167
+ result = await Promise.race([call, timeout]);
168
+ } finally {
169
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
170
+ }
151
171
  const text = Array.isArray(result.content) ? result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("") : "";
152
172
  if (result.isError) {
153
173
  return {
@@ -196,11 +216,11 @@ async function delegateRemote(task, agentName, options = {}) {
196
216
  };
197
217
  } finally {
198
218
  try {
199
- await client.close();
219
+ await transport.terminateSession();
200
220
  } catch {
201
221
  }
202
222
  try {
203
- await transport.terminateSession();
223
+ await client.close();
204
224
  } catch {
205
225
  }
206
226
  try {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/delegate-remote.ts","../src/server/registry.ts","../src/logger.ts"],"sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { findAgent } from \"./server/registry.js\";\nimport type { DelegationResult } from \"./delegate.js\";\nimport { log } from \"./logger.js\";\n\nexport interface RemoteDelegateOptions {\n context?: string;\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\n/**\n * Dial another aman-agent running as an A2A server on the same machine\n * and run a task through its `agent.delegate` MCP tool. Returns a\n * DelegationResult matching the shape of the local `delegateTask` so\n * callers can treat local and remote delegation uniformly.\n *\n * Trust model: same user, same machine — bearer comes from the local\n * registry file (mode 0600). See plan docs for the broader discussion.\n */\nexport async function delegateRemote(\n task: string,\n agentName: string,\n options: RemoteDelegateOptions = {},\n): Promise<DelegationResult> {\n const entry = await findAgent(agentName);\n if (!entry) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: `agent not found: ${agentName}`,\n };\n }\n\n const url = new URL(`http://127.0.0.1:${entry.port}/mcp`);\n const transport = new StreamableHTTPClientTransport(url, {\n requestInit: {\n headers: { Authorization: `Bearer ${entry.token}` },\n },\n });\n const client = new Client({ name: \"aman-agent-a2a-caller\", version: \"0.1.0\" });\n\n try {\n await client.connect(transport);\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const call = client.callTool({\n name: \"agent.delegate\",\n arguments: {\n task,\n ...(options.context ? { context: options.context } : {}),\n },\n });\n const timeout = new Promise<never>((_, rej) =>\n setTimeout(\n () => rej(new Error(`remote delegate timed out after ${timeoutMs}ms`)),\n timeoutMs,\n ),\n );\n const result = await Promise.race([call, timeout]);\n\n const text = Array.isArray(result.content)\n ? (result.content as Array<{ type: string; text?: string }>)\n .filter((c) => c.type === \"text\")\n .map((c) => c.text ?? \"\")\n .join(\"\")\n : \"\";\n\n // MCP tool-level errors arrive as { isError: true, content: [{text: \"...\"}] }.\n // Surface them distinctly from JSON.parse failures and from empty responses.\n if ((result as { isError?: boolean }).isError) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: `remote tool error: ${text || \"(no details)\"}`,\n };\n }\n\n const parsed = text ? JSON.parse(text) : { ok: false, error: \"empty response\" };\n\n log.debug(\"delegate-remote\", `@${agentName} ok=${parsed.ok}`);\n\n if (!parsed.ok) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: parsed.error ?? \"unknown remote error\",\n };\n }\n\n return {\n profile: `@${agentName}`,\n task,\n response: parsed.text ?? \"\",\n toolsUsed: parsed.tools_used ?? [],\n turns: parsed.turns ?? 0,\n success: true,\n };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n const normalized =\n lower.includes(\"401\") || lower.includes(\"unauthor\")\n ? `unauthorized: ${msg}`\n : msg;\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: normalized,\n };\n } finally {\n // Tear down in order: client (releases SDK state) → transport session\n // (tells the server to drop the session) → transport (aborts any pending\n // fetch/SSE keepalive that would otherwise pin the event loop open).\n // All three are best-effort: any throw here is logged and swallowed so\n // a teardown failure never masks a real result from the caller.\n try { await client.close(); } catch { /* best effort */ }\n try { await transport.terminateSession(); } catch { /* best effort */ }\n try { await transport.close(); } catch { /* best effort */ }\n }\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { log } from \"../logger.js\";\n\nexport interface AgentEntry {\n name: string; // unique handle, used as @name\n profile: string; // aman-agent profile this server loaded\n pid: number; // process id for liveness check\n port: number; // 127.0.0.1 port\n token: string; // 32-byte hex bearer\n started_at: number; // epoch ms\n version: string; // package version\n}\n\nexport interface ListOptions {\n prune?: boolean; // write the pruned registry back\n isAlive?: (pid: number) => boolean; // injectable for tests\n}\n\nfunction amanAgentHome(): string {\n return process.env.AMAN_AGENT_HOME || path.join(os.homedir(), \".aman-agent\");\n}\n\nfunction registryPath(): string {\n return path.join(amanAgentHome(), \"registry.json\");\n}\n\nasync function ensureHome(): Promise<void> {\n await fs.mkdir(amanAgentHome(), { recursive: true });\n}\n\nasync function readRaw(): Promise<AgentEntry[]> {\n try {\n const buf = await fs.readFile(registryPath(), \"utf-8\");\n const parsed = JSON.parse(buf);\n return Array.isArray(parsed) ? parsed : [];\n } catch (err: unknown) {\n const code = (err as { code?: string }).code;\n if (code === \"ENOENT\") return [];\n const message = err instanceof Error ? err.message : String(err);\n log.warn(\"registry\", `failed to read registry: ${message}`);\n return [];\n }\n}\n\nasync function writeAtomic(entries: AgentEntry[]): Promise<void> {\n await ensureHome();\n const tmp = registryPath() + \".tmp\";\n await fs.writeFile(tmp, JSON.stringify(entries, null, 2), { mode: 0o600 });\n await fs.rename(tmp, registryPath());\n // Ensure mode even if file already existed (chmod is idempotent).\n try {\n await fs.chmod(registryPath(), 0o600);\n } catch {\n // best effort\n }\n}\n\nfunction defaultIsAlive(pid: number): boolean {\n try {\n // Signal 0 probes existence without sending anything.\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function registerAgent(entry: AgentEntry): Promise<void> {\n const current = await readRaw();\n const filtered = current.filter((e) => e.name !== entry.name);\n if (filtered.length !== current.length) {\n log.warn(\"registry\", `replacing existing entry for name=\"${entry.name}\"`);\n }\n filtered.push(entry);\n await writeAtomic(filtered);\n}\n\nexport async function unregisterAgent(name: string): Promise<void> {\n const current = await readRaw();\n const next = current.filter((e) => e.name !== name);\n if (next.length !== current.length) {\n await writeAtomic(next);\n }\n}\n\nexport async function listAgents(opts: ListOptions = {}): Promise<AgentEntry[]> {\n const isAlive = opts.isAlive ?? defaultIsAlive;\n const raw = await readRaw();\n const alive = raw.filter((e) => isAlive(e.pid));\n if (opts.prune && alive.length !== raw.length) {\n await writeAtomic(alive);\n }\n return alive;\n}\n\nexport async function findAgent(name: string): Promise<AgentEntry | null> {\n const all = await listAgents();\n return all.find((e) => e.name === name) ?? null;\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nconst LOG_DIR = path.join(os.homedir(), \".aman-agent\");\nexport const LOG_PATH = path.join(LOG_DIR, \"debug.log\");\nconst MAX_LOG_SIZE = 1_048_576; // 1MB\n\ninterface LogEntry {\n timestamp: string;\n level: \"debug\" | \"warn\" | \"error\";\n module: string;\n message: string;\n data?: string;\n}\n\nfunction ensureDir(): void {\n if (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n }\n}\n\nfunction maybeRotate(): void {\n try {\n if (!fs.existsSync(LOG_PATH)) return;\n const stat = fs.statSync(LOG_PATH);\n if (stat.size >= MAX_LOG_SIZE) {\n const backupPath = LOG_PATH + \".1\";\n if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);\n fs.renameSync(LOG_PATH, backupPath);\n }\n } catch {\n // Rotation failure is non-critical\n }\n}\n\nfunction write(level: LogEntry[\"level\"], module: string, message: string, data?: unknown): void {\n try {\n ensureDir();\n maybeRotate();\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n level,\n module,\n message,\n };\n if (data !== undefined) {\n entry.data = data instanceof Error ? data.message : String(data);\n }\n fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + \"\\n\");\n } catch {\n // Logger must never throw\n }\n}\n\nexport const log = {\n debug: (module: string, message: string, data?: unknown) => write(\"debug\", module, message, data),\n warn: (module: string, message: string, data?: unknown) => write(\"warn\", module, message, data),\n error: (module: string, message: string, data?: unknown) => write(\"error\", module, message, data),\n};\n"],"mappings":"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","names":["fs","path","os","path","os","fs"]}
1
+ {"version":3,"sources":["../src/delegate-remote.ts","../src/server/registry.ts","../src/logger.ts"],"sourcesContent":["import { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StreamableHTTPClientTransport } from \"@modelcontextprotocol/sdk/client/streamableHttp.js\";\nimport { findAgent } from \"./server/registry.js\";\nimport type { DelegationResult } from \"./delegate.js\";\nimport { log } from \"./logger.js\";\n\nexport interface RemoteDelegateOptions {\n context?: string;\n timeoutMs?: number;\n}\n\nconst DEFAULT_TIMEOUT_MS = 120_000;\n\n/**\n * Dial another aman-agent running as an A2A server on the same machine\n * and run a task through its `agent.delegate` MCP tool. Returns a\n * DelegationResult matching the shape of the local `delegateTask` so\n * callers can treat local and remote delegation uniformly.\n *\n * Trust model: same user, same machine — bearer comes from the local\n * registry file (mode 0600). See plan docs for the broader discussion.\n */\nexport async function delegateRemote(\n task: string,\n agentName: string,\n options: RemoteDelegateOptions = {},\n): Promise<DelegationResult> {\n const entry = await findAgent(agentName);\n if (!entry) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: `agent not found: ${agentName}`,\n };\n }\n\n const url = new URL(`http://127.0.0.1:${entry.port}/mcp`);\n const transport = new StreamableHTTPClientTransport(url, {\n requestInit: {\n headers: { Authorization: `Bearer ${entry.token}` },\n },\n // Disable SSE reconnection scheduling. On close(), the SDK aborts\n // the controller; without this override, the SSE stream's error\n // handler races to schedule a new _reconnectionTimeout AFTER close()\n // cleared the old one, and the timer (plus its referenced socket)\n // pins Node's event loop until the undici keepalive times out. A\n // delegateRemote caller then can't exit cleanly. maxRetries: 0\n // drops the schedule-on-error path entirely; we're doing a single\n // RPC, not a persistent stream, so reconnection has no value here.\n reconnectionOptions: {\n maxRetries: 0,\n initialReconnectionDelay: 1,\n maxReconnectionDelay: 1,\n reconnectionDelayGrowFactor: 1,\n },\n });\n const client = new Client({ name: \"aman-agent-a2a-caller\", version: \"0.1.0\" });\n\n try {\n await client.connect(transport);\n\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const call = client.callTool({\n name: \"agent.delegate\",\n arguments: {\n task,\n ...(options.context ? { context: options.context } : {}),\n },\n });\n\n // Promise.race picks a winner but does NOT cancel the losing promise's\n // resources. Capturing the timer id lets us clear it after the call\n // resolves — otherwise the setTimeout keeps a Timeout handle alive for\n // the full timeoutMs (120 s default) and pins Node's event loop long\n // after the caller thinks the RPC is done. Equivalent effect to using\n // AbortSignal.timeout() but keeps the existing error message.\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, rej) => {\n timeoutId = setTimeout(\n () => rej(new Error(`remote delegate timed out after ${timeoutMs}ms`)),\n timeoutMs,\n );\n });\n let result;\n try {\n result = await Promise.race([call, timeout]);\n } finally {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }\n\n const text = Array.isArray(result.content)\n ? (result.content as Array<{ type: string; text?: string }>)\n .filter((c) => c.type === \"text\")\n .map((c) => c.text ?? \"\")\n .join(\"\")\n : \"\";\n\n // MCP tool-level errors arrive as { isError: true, content: [{text: \"...\"}] }.\n // Surface them distinctly from JSON.parse failures and from empty responses.\n if ((result as { isError?: boolean }).isError) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: `remote tool error: ${text || \"(no details)\"}`,\n };\n }\n\n const parsed = text ? JSON.parse(text) : { ok: false, error: \"empty response\" };\n\n log.debug(\"delegate-remote\", `@${agentName} ok=${parsed.ok}`);\n\n if (!parsed.ok) {\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: parsed.error ?? \"unknown remote error\",\n };\n }\n\n return {\n profile: `@${agentName}`,\n task,\n response: parsed.text ?? \"\",\n toolsUsed: parsed.tools_used ?? [],\n turns: parsed.turns ?? 0,\n success: true,\n };\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const lower = msg.toLowerCase();\n const normalized =\n lower.includes(\"401\") || lower.includes(\"unauthor\")\n ? `unauthorized: ${msg}`\n : msg;\n return {\n profile: `@${agentName}`,\n task,\n response: \"\",\n toolsUsed: [],\n turns: 0,\n success: false,\n error: normalized,\n };\n } finally {\n // Teardown order matters:\n // 1. terminateSession() sends an MCP DELETE to drop the server-side\n // session. This needs the transport's abort controller to still\n // be alive, so it MUST run BEFORE client.close() (which aborts\n // the controller). Earlier order threw DOMException[AbortError].\n // 2. client.close() then releases SDK-side state and aborts the\n // transport. Combined with reconnectionOptions: { maxRetries: 0 }\n // on construction, this leaves zero handles pinning the event\n // loop — verified via process.getActiveResourcesInfo() === [].\n // 3. transport.close() is a no-op after client.close() (which\n // transitively closes the transport) but kept for symmetry.\n // All three are best-effort: any throw here is swallowed so a\n // teardown failure never masks a real result from the caller.\n try { await transport.terminateSession(); } catch { /* best effort */ }\n try { await client.close(); } catch { /* best effort */ }\n try { await transport.close(); } catch { /* best effort */ }\n }\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport os from \"node:os\";\nimport { log } from \"../logger.js\";\n\nexport interface AgentEntry {\n name: string; // unique handle, used as @name\n profile: string; // aman-agent profile this server loaded\n pid: number; // process id for liveness check\n port: number; // 127.0.0.1 port\n token: string; // 32-byte hex bearer\n started_at: number; // epoch ms\n version: string; // package version\n}\n\nexport interface ListOptions {\n prune?: boolean; // write the pruned registry back\n isAlive?: (pid: number) => boolean; // injectable for tests\n}\n\nfunction amanAgentHome(): string {\n return process.env.AMAN_AGENT_HOME || path.join(os.homedir(), \".aman-agent\");\n}\n\nfunction registryPath(): string {\n return path.join(amanAgentHome(), \"registry.json\");\n}\n\nasync function ensureHome(): Promise<void> {\n await fs.mkdir(amanAgentHome(), { recursive: true });\n}\n\nasync function readRaw(): Promise<AgentEntry[]> {\n try {\n const buf = await fs.readFile(registryPath(), \"utf-8\");\n const parsed = JSON.parse(buf);\n return Array.isArray(parsed) ? parsed : [];\n } catch (err: unknown) {\n const code = (err as { code?: string }).code;\n if (code === \"ENOENT\") return [];\n const message = err instanceof Error ? err.message : String(err);\n log.warn(\"registry\", `failed to read registry: ${message}`);\n return [];\n }\n}\n\nasync function writeAtomic(entries: AgentEntry[]): Promise<void> {\n await ensureHome();\n const tmp = registryPath() + \".tmp\";\n await fs.writeFile(tmp, JSON.stringify(entries, null, 2), { mode: 0o600 });\n await fs.rename(tmp, registryPath());\n // Ensure mode even if file already existed (chmod is idempotent).\n try {\n await fs.chmod(registryPath(), 0o600);\n } catch {\n // best effort\n }\n}\n\nfunction defaultIsAlive(pid: number): boolean {\n try {\n // Signal 0 probes existence without sending anything.\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function registerAgent(entry: AgentEntry): Promise<void> {\n const current = await readRaw();\n const filtered = current.filter((e) => e.name !== entry.name);\n if (filtered.length !== current.length) {\n log.warn(\"registry\", `replacing existing entry for name=\"${entry.name}\"`);\n }\n filtered.push(entry);\n await writeAtomic(filtered);\n}\n\nexport async function unregisterAgent(name: string): Promise<void> {\n const current = await readRaw();\n const next = current.filter((e) => e.name !== name);\n if (next.length !== current.length) {\n await writeAtomic(next);\n }\n}\n\nexport async function listAgents(opts: ListOptions = {}): Promise<AgentEntry[]> {\n const isAlive = opts.isAlive ?? defaultIsAlive;\n const raw = await readRaw();\n const alive = raw.filter((e) => isAlive(e.pid));\n if (opts.prune && alive.length !== raw.length) {\n await writeAtomic(alive);\n }\n return alive;\n}\n\nexport async function findAgent(name: string): Promise<AgentEntry | null> {\n const all = await listAgents();\n return all.find((e) => e.name === name) ?? null;\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\nimport os from \"node:os\";\n\nconst LOG_DIR = path.join(os.homedir(), \".aman-agent\");\nexport const LOG_PATH = path.join(LOG_DIR, \"debug.log\");\nconst MAX_LOG_SIZE = 1_048_576; // 1MB\n\ninterface LogEntry {\n timestamp: string;\n level: \"debug\" | \"warn\" | \"error\";\n module: string;\n message: string;\n data?: string;\n}\n\nfunction ensureDir(): void {\n if (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n }\n}\n\nfunction maybeRotate(): void {\n try {\n if (!fs.existsSync(LOG_PATH)) return;\n const stat = fs.statSync(LOG_PATH);\n if (stat.size >= MAX_LOG_SIZE) {\n const backupPath = LOG_PATH + \".1\";\n if (fs.existsSync(backupPath)) fs.unlinkSync(backupPath);\n fs.renameSync(LOG_PATH, backupPath);\n }\n } catch {\n // Rotation failure is non-critical\n }\n}\n\nfunction write(level: LogEntry[\"level\"], module: string, message: string, data?: unknown): void {\n try {\n ensureDir();\n maybeRotate();\n const entry: LogEntry = {\n timestamp: new Date().toISOString(),\n level,\n module,\n message,\n };\n if (data !== undefined) {\n entry.data = data instanceof Error ? data.message : String(data);\n }\n fs.appendFileSync(LOG_PATH, JSON.stringify(entry) + \"\\n\");\n } catch {\n // Logger must never throw\n }\n}\n\nexport const log = {\n debug: (module: string, message: string, data?: unknown) => write(\"debug\", module, message, data),\n warn: (module: string, message: string, data?: unknown) => write(\"warn\", module, message, data),\n error: (module: string, message: string, data?: unknown) => write(\"error\", module, message, data),\n};\n"],"mappings":"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","names":["fs","path","os","path","os","fs"]}
package/dist/delegate.js CHANGED
@@ -149,6 +149,20 @@ async function delegateRemote(task, agentName, options = {}) {
149
149
  const transport = new StreamableHTTPClientTransport(url, {
150
150
  requestInit: {
151
151
  headers: { Authorization: `Bearer ${entry.token}` }
152
+ },
153
+ // Disable SSE reconnection scheduling. On close(), the SDK aborts
154
+ // the controller; without this override, the SSE stream's error
155
+ // handler races to schedule a new _reconnectionTimeout AFTER close()
156
+ // cleared the old one, and the timer (plus its referenced socket)
157
+ // pins Node's event loop until the undici keepalive times out. A
158
+ // delegateRemote caller then can't exit cleanly. maxRetries: 0
159
+ // drops the schedule-on-error path entirely; we're doing a single
160
+ // RPC, not a persistent stream, so reconnection has no value here.
161
+ reconnectionOptions: {
162
+ maxRetries: 0,
163
+ initialReconnectionDelay: 1,
164
+ maxReconnectionDelay: 1,
165
+ reconnectionDelayGrowFactor: 1
152
166
  }
153
167
  });
154
168
  const client = new Client({ name: "aman-agent-a2a-caller", version: "0.1.0" });
@@ -162,13 +176,19 @@ async function delegateRemote(task, agentName, options = {}) {
162
176
  ...options.context ? { context: options.context } : {}
163
177
  }
164
178
  });
165
- const timeout = new Promise(
166
- (_, rej) => setTimeout(
179
+ let timeoutId;
180
+ const timeout = new Promise((_, rej) => {
181
+ timeoutId = setTimeout(
167
182
  () => rej(new Error(`remote delegate timed out after ${timeoutMs}ms`)),
168
183
  timeoutMs
169
- )
170
- );
171
- const result = await Promise.race([call, timeout]);
184
+ );
185
+ });
186
+ let result;
187
+ try {
188
+ result = await Promise.race([call, timeout]);
189
+ } finally {
190
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
191
+ }
172
192
  const text = Array.isArray(result.content) ? result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("") : "";
173
193
  if (result.isError) {
174
194
  return {
@@ -217,11 +237,11 @@ async function delegateRemote(task, agentName, options = {}) {
217
237
  };
218
238
  } finally {
219
239
  try {
220
- await client.close();
240
+ await transport.terminateSession();
221
241
  } catch {
222
242
  }
223
243
  try {
224
- await transport.terminateSession();
244
+ await client.close();
225
245
  } catch {
226
246
  }
227
247
  try {