@hanphone/dsh-a2a 0.1.0 → 0.2.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.
Files changed (44) hide show
  1. package/README.md +98 -47
  2. package/README.zh.md +43 -42
  3. package/lib/client.js +49 -1
  4. package/lib/index.js +272 -17
  5. package/lib/tsconfig.client.tsbuildinfo +1 -1
  6. package/lib/tsconfig.tsbuildinfo +1 -1
  7. package/lib/types/api.d.ts +13 -0
  8. package/lib/types/api.d.ts.map +1 -1
  9. package/lib/types/api.js +5 -0
  10. package/lib/types/api.js.map +1 -1
  11. package/lib/types/client/index.d.ts +5 -0
  12. package/lib/types/client/index.d.ts.map +1 -1
  13. package/lib/types/index.d.ts.map +1 -1
  14. package/lib/types/index.js +74 -10
  15. package/lib/types/index.js.map +1 -1
  16. package/lib/types/server/a2a-server.d.ts +3 -1
  17. package/lib/types/server/a2a-server.d.ts.map +1 -1
  18. package/lib/types/server/a2a-server.js +5 -1
  19. package/lib/types/server/a2a-server.js.map +1 -1
  20. package/lib/types/server/identity.d.ts +38 -0
  21. package/lib/types/server/identity.d.ts.map +1 -0
  22. package/lib/types/server/identity.js +89 -0
  23. package/lib/types/server/identity.js.map +1 -0
  24. package/lib/types/server/inbound-registry.d.ts +60 -0
  25. package/lib/types/server/inbound-registry.d.ts.map +1 -0
  26. package/lib/types/server/inbound-registry.js +86 -0
  27. package/lib/types/server/inbound-registry.js.map +1 -0
  28. package/lib/types/server/store.d.ts +2 -0
  29. package/lib/types/server/store.d.ts.map +1 -1
  30. package/lib/types/server/store.js +4 -0
  31. package/lib/types/server/store.js.map +1 -1
  32. package/lib/types/service.d.ts +19 -0
  33. package/lib/types/service.d.ts.map +1 -1
  34. package/lib/types/service.js +12 -0
  35. package/lib/types/service.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/api.ts +27 -2
  38. package/src/client/index.ts +99 -2
  39. package/src/index.ts +76 -10
  40. package/src/server/a2a-server.ts +7 -2
  41. package/src/server/identity.ts +109 -0
  42. package/src/server/inbound-registry.ts +129 -0
  43. package/src/server/store.ts +5 -0
  44. package/src/service.ts +23 -0
package/lib/index.js CHANGED
@@ -175,7 +175,8 @@ const a2aDomainSpec = defineDomain({
175
175
  tables: {
176
176
  tasks: domainTable(json$1),
177
177
  contexts: domainTable(json$1),
178
- agents: domainTable(json$1)
178
+ agents: domainTable(json$1),
179
+ identity: domainTable(json$1)
179
180
  }
180
181
  });
181
182
  /**
@@ -214,6 +215,9 @@ var A2aDomain = class {
214
215
  get agents() {
215
216
  return this.handle.table("agents");
216
217
  }
218
+ get identity() {
219
+ return this.handle.table("identity");
220
+ }
217
221
  async close() {
218
222
  await this.handle.close();
219
223
  }
@@ -329,6 +333,172 @@ function withState(current, state, message, timestamp) {
329
333
  };
330
334
  }
331
335
  //#endregion
336
+ //#region lib/types/server/inbound-registry.js
337
+ /**
338
+ * Inbound connection registry: which remote peers are talking to this DSH's
339
+ * A2A server. Fed by the A2A server's `onInbound` hook (every JSON-RPC
340
+ * request / SSE open), surfaced through the dashboard API, and disconnectable
341
+ * (`closePeer` cancels the peer's active tasks and drops the record).
342
+ * @module dsh-a2a/server/inbound-registry
343
+ */
344
+ function newPeerId() {
345
+ return `peer-${crypto.randomUUID()}`;
346
+ }
347
+ /** In-memory inbound peer registry. */
348
+ var LiveInboundRegistry = class {
349
+ peers = /* @__PURE__ */ new Map();
350
+ findBySource(source) {
351
+ if (source === null) return void 0;
352
+ for (const peer of this.peers.values()) if (peer.source === source) return peer;
353
+ }
354
+ /** Observe one inbound event: method, source, task ids, streaming flag. */
355
+ note(input) {
356
+ const now = (/* @__PURE__ */ new Date()).toISOString();
357
+ const source = input.source ?? null;
358
+ let peer = this.findBySource(source);
359
+ if (peer === void 0) {
360
+ peer = {
361
+ id: newPeerId(),
362
+ label: source ?? "unknown",
363
+ source,
364
+ firstSeen: now,
365
+ lastSeen: now,
366
+ taskCount: 0,
367
+ active: /* @__PURE__ */ new Set(),
368
+ streamingCount: 0
369
+ };
370
+ this.peers.set(peer.id, peer);
371
+ }
372
+ peer.lastSeen = now;
373
+ peer.taskCount += input.taskIds.length;
374
+ for (const id of input.taskIds) peer.active.add(id);
375
+ if (input.streaming) peer.streamingCount += 1;
376
+ }
377
+ /** A task settled: drop it from every peer's active set. */
378
+ settle(taskId) {
379
+ for (const peer of this.peers.values()) if (peer.active.delete(taskId)) peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
380
+ }
381
+ /** Decrement streaming count when an SSE connection closes. */
382
+ endStream(source) {
383
+ const peer = this.findBySource(source ?? null);
384
+ if (peer !== void 0 && peer.streamingCount > 0) {
385
+ peer.streamingCount -= 1;
386
+ peer.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
387
+ }
388
+ }
389
+ list() {
390
+ return [...this.peers.values()].map((p) => ({
391
+ id: p.id,
392
+ label: p.label,
393
+ source: p.source,
394
+ firstSeen: p.firstSeen,
395
+ lastSeen: p.lastSeen,
396
+ taskCount: p.taskCount,
397
+ activeTaskIds: [...p.active],
398
+ streaming: p.streamingCount > 0
399
+ })).sort((a, b) => a.lastSeen < b.lastSeen ? 1 : -1);
400
+ }
401
+ closePeer(peerId) {
402
+ const peer = this.peers.get(peerId);
403
+ if (peer === void 0) return {
404
+ ok: false,
405
+ message: `inbound peer ${peerId} not found`
406
+ };
407
+ this.peers.delete(peerId);
408
+ return {
409
+ ok: true,
410
+ message: `inbound peer ${peer.label} closed`
411
+ };
412
+ }
413
+ activeTasksOf(peerId) {
414
+ return [...this.peers.get(peerId)?.active ?? []];
415
+ }
416
+ };
417
+ //#endregion
418
+ //#region lib/types/server/identity.js
419
+ /**
420
+ * Service identity: the inbound AgentCard's name/description/version,
421
+ * editable at runtime from the GUI dashboard and persisted in the `a2a`
422
+ * domain's `identity` table. Skills stay derived from the live tool registry
423
+ * (the "card derives from ctx.tools" design); identity editing builds a fresh
424
+ * card preserving the endpoint URL and swaps it onto the server, so routes
425
+ * and the facade see the new value immediately.
426
+ * @module dsh-a2a/server/identity
427
+ */
428
+ const IDENTITY_KEY = "service";
429
+ function decode(raw) {
430
+ if (raw === void 0) return void 0;
431
+ try {
432
+ const parsed = JSON.parse(raw);
433
+ if (typeof parsed.name !== "string" || typeof parsed.description !== "string" || typeof parsed.version !== "string") return;
434
+ return {
435
+ name: parsed.name,
436
+ description: parsed.description,
437
+ version: parsed.version
438
+ };
439
+ } catch {
440
+ return;
441
+ }
442
+ }
443
+ function encode(value) {
444
+ return JSON.stringify(value);
445
+ }
446
+ /** Read the persisted identity (undefined when never configured). */
447
+ function readIdentity(domain) {
448
+ return decode(domain.identity.get(IDENTITY_KEY));
449
+ }
450
+ /** Persist a new identity (fire-and-forget like the task store writes). */
451
+ function writeIdentity(domain, identity) {
452
+ domain.identity.put(IDENTITY_KEY, encode(identity)).catch((err) => {
453
+ throw new Error(`a2a: identity persistence failed: ${String(err)}`);
454
+ });
455
+ }
456
+ /**
457
+ * Build the AgentCard options for a given base URL/path and the override
458
+ * identity. `identity` (persisted) wins over the composition `defaults`; when
459
+ * no identity is stored, the composition defaults stand.
460
+ */
461
+ function cardOptionsFor(baseUrl, endpointPath, defaults, identity, skills, authToken) {
462
+ return {
463
+ baseUrl,
464
+ endpointPath,
465
+ name: identity?.name ?? defaults.name,
466
+ description: identity?.description ?? defaults.description,
467
+ version: identity?.version ?? defaults.version,
468
+ skills,
469
+ ...authToken !== void 0 ? { authToken } : {}
470
+ };
471
+ }
472
+ /**
473
+ * Build a fresh AgentCard from an existing one plus a new identity and skill
474
+ * list, preserving the endpoint URL (baseUrl/path) and security scheme.
475
+ */
476
+ function rebuildCardWithIdentity(card, identity, skills) {
477
+ return buildCard({
478
+ baseUrl: endpointBaseOf(card),
479
+ endpointPath: endpointPathOf$1(card),
480
+ name: identity.name,
481
+ description: identity.description,
482
+ version: identity.version,
483
+ skills,
484
+ ...card.securitySchemes !== void 0 ? { authToken: "preserved-scheme" } : {}
485
+ });
486
+ }
487
+ function endpointBaseOf(card) {
488
+ const url = card.supportedInterfaces?.[0]?.url;
489
+ if (!url) return "http://127.0.0.1";
490
+ return url.replace(/\/[^/]*$/, "");
491
+ }
492
+ function endpointPathOf$1(card) {
493
+ const url = card.supportedInterfaces?.[0]?.url;
494
+ if (!url) return "/a2a";
495
+ try {
496
+ return new URL(url).pathname;
497
+ } catch {
498
+ return "/a2a";
499
+ }
500
+ }
501
+ //#endregion
332
502
  //#region lib/types/server/executor.js
333
503
  /**
334
504
  * Executor abstraction for inbound tasks: an executor turns one task into
@@ -621,6 +791,10 @@ var A2AServer = class {
621
791
  this.opts = opts;
622
792
  this.card = opts.card;
623
793
  }
794
+ /** Swap the served AgentCard (runtime identity edits). Routes re-read `server.card` on every request. */
795
+ setCard(next) {
796
+ this.card = next;
797
+ }
624
798
  /**
625
799
  * Abort a running task by control path (facade /a2a task cancel): abort the
626
800
  * executor's signal, settle the task CANCELED, and wake stream waiters.
@@ -790,7 +964,6 @@ var A2AServer = class {
790
964
  const gateOutcome = await this.opts.gate(gateInputFrom(message, null));
791
965
  if (!gateOutcome.ok) throw rpcFault(A2A_ERROR_CODES.INVALID_PARAMS, gateOutcome.reason);
792
966
  const record = this.ensureTask(message, null);
793
- this.noteInbound({ method }, method, [record.taskId], false);
794
967
  await this.runTask(record);
795
968
  return toTask(this.opts.store.get(record.taskId) ?? record);
796
969
  }
@@ -1198,10 +1371,12 @@ async function handleApiRequest(req, res, impl) {
1198
1371
  res.end("Method Not Allowed");
1199
1372
  }
1200
1373
  function snapshotOf(impl) {
1374
+ const status = impl.status();
1201
1375
  return {
1202
- server: impl.status().server,
1376
+ server: status.server,
1203
1377
  tasks: impl.listTasks(),
1204
- agents: impl.agents()
1378
+ agents: impl.agents(),
1379
+ inbounds: status.inbounds ?? []
1205
1380
  };
1206
1381
  }
1207
1382
  async function dispatch(payload, impl) {
@@ -1218,6 +1393,12 @@ async function dispatch(payload, impl) {
1218
1393
  case "agent.disable": return impl.setAgentEnabled(payload.id, false);
1219
1394
  case "agent.refresh": return impl.refreshAgentCard(payload.id);
1220
1395
  case "task.cancel": return impl.cancelTask(payload.id);
1396
+ case "identity.update": return impl.updateIdentity({
1397
+ ...payload.name !== void 0 ? { name: payload.name } : {},
1398
+ ...payload.description !== void 0 ? { description: payload.description } : {},
1399
+ ...payload.version !== void 0 ? { version: payload.version } : {}
1400
+ });
1401
+ case "inbound.close": return impl.closeInbound(payload.id);
1221
1402
  default: return {
1222
1403
  ok: false,
1223
1404
  message: `unknown action ${String(payload.action)}`
@@ -1708,6 +1889,18 @@ var A2AService = class extends Service {
1708
1889
  async refreshAgentCard(id) {
1709
1890
  return this.impl.refreshAgentCard(id);
1710
1891
  }
1892
+ identity() {
1893
+ return this.impl.identity();
1894
+ }
1895
+ async updateIdentity(patch) {
1896
+ return this.impl.updateIdentity(patch);
1897
+ }
1898
+ async closeInbound(peerId) {
1899
+ return this.impl.closeInbound(peerId);
1900
+ }
1901
+ inbounds() {
1902
+ return this.impl.inbounds();
1903
+ }
1711
1904
  };
1712
1905
  //#endregion
1713
1906
  //#region lib/types/commands.js
@@ -1860,11 +2053,17 @@ function apply(ctx, config) {
1860
2053
  domain,
1861
2054
  store,
1862
2055
  registry: void 0,
2056
+ inbound: void 0,
1863
2057
  server: void 0,
1864
2058
  routes: void 0,
1865
2059
  card: void 0,
1866
2060
  executors: void 0,
1867
- enabled: serverConfig.enabled
2061
+ enabled: serverConfig.enabled,
2062
+ identityDefaults: {
2063
+ name: serverConfig.name ?? "My DSH Agent",
2064
+ description: serverConfig.description ?? "A DeepSeek Harness agent exposed over A2A v1.0",
2065
+ version: serverConfig.version ?? "0.1.0"
2066
+ }
1868
2067
  };
1869
2068
  const webServer = probeService(ctx, "webServer", "register");
1870
2069
  if (webServer === void 0) logger.warn("a2a: webServer not mounted; inbound server idle");
@@ -1873,16 +2072,13 @@ function apply(ctx, config) {
1873
2072
  ids: serverConfig.skills.ids,
1874
2073
  exclude: serverConfig.skills.exclude
1875
2074
  });
1876
- const card = buildCard({
1877
- baseUrl: serverConfig.baseUrl ?? `http://127.0.0.1:${process.env["DSH_A2A_PORT"] ?? "3000"}`,
1878
- endpointPath: serverConfig.endpointPath ?? "/a2a",
1879
- name: serverConfig.name ?? "My DSH Agent",
1880
- description: serverConfig.description ?? "A DeepSeek Harness agent exposed over A2A v1.0",
1881
- version: serverConfig.version ?? "0.1.0",
1882
- skills,
1883
- ...authToken ? { authToken } : {}
1884
- });
2075
+ const baseUrl = serverConfig.baseUrl ?? `http://127.0.0.1:${process.env["DSH_A2A_PORT"] ?? "3000"}`;
2076
+ const endpointPath = serverConfig.endpointPath ?? "/a2a";
2077
+ const persistedIdentity = readIdentity(domain);
2078
+ const card = buildCard(cardOptionsFor(baseUrl, endpointPath, holder.identityDefaults, persistedIdentity, skills, authToken));
1885
2079
  holder.card = card;
2080
+ const inbound = new LiveInboundRegistry();
2081
+ holder.inbound = inbound;
1886
2082
  const agents = probeService(ctx, "agents", "create");
1887
2083
  const presets = probeService(ctx, "agentPresets", "resolve");
1888
2084
  const sessionPool = agents ? new ContextSessionPool(agents, {
@@ -1925,7 +2121,16 @@ function apply(ctx, config) {
1925
2121
  executors,
1926
2122
  ...authToken ? { authToken } : {},
1927
2123
  gate,
1928
- onTaskSettled: (taskId) => logger.info(`[a2a] task settled ${taskId}`)
2124
+ onInbound: (facts) => inbound.note({
2125
+ method: facts.method,
2126
+ ...facts.source !== void 0 ? { source: facts.source } : {},
2127
+ taskIds: facts.taskIds,
2128
+ streaming: facts.streaming
2129
+ }),
2130
+ onTaskSettled: (taskId) => {
2131
+ logger.info(`[a2a] task settled ${taskId}`);
2132
+ inbound.settle(taskId);
2133
+ }
1929
2134
  });
1930
2135
  holder.server = server;
1931
2136
  const routes = new A2aRoutes(webServer, server);
@@ -1978,10 +2183,15 @@ function makeFacade(holder, log) {
1978
2183
  enabled: serverEnabled(),
1979
2184
  cardUrl: holder.card?.supportedInterfaces?.[0]?.url,
1980
2185
  skills: holder.card?.skills?.map((s) => s.id) ?? [],
1981
- executors: holder.executors ? ["session", ...holder.executors["subagent"] !== void 0 ? ["subagent"] : []] : []
2186
+ executors: holder.executors ? ["session", ...holder.executors["subagent"] !== void 0 ? ["subagent"] : []] : [],
2187
+ name: holder.card?.name,
2188
+ description: holder.card?.description,
2189
+ version: holder.card?.version,
2190
+ configured: readIdentity(holder.domain) !== void 0
1982
2191
  },
1983
2192
  tasks: holder.store.list().length,
1984
- agents: holder.registry?.list() ?? []
2193
+ agents: holder.registry?.list() ?? [],
2194
+ inbounds: holder.inbound?.list() ?? []
1985
2195
  };
1986
2196
  },
1987
2197
  async enableServer(enable) {
@@ -2045,6 +2255,51 @@ function makeFacade(holder, log) {
2045
2255
  ok: false,
2046
2256
  message: "outbound client not mounted"
2047
2257
  };
2258
+ },
2259
+ identity() {
2260
+ const current = holder.card;
2261
+ return {
2262
+ ...readIdentity(holder.domain) ?? {},
2263
+ defaults: holder.identityDefaults,
2264
+ name: current?.name ?? holder.identityDefaults.name,
2265
+ description: current?.description ?? holder.identityDefaults.description,
2266
+ version: current?.version ?? holder.identityDefaults.version
2267
+ };
2268
+ },
2269
+ async updateIdentity(patch) {
2270
+ const card = holder.card;
2271
+ const server = holder.server;
2272
+ if (card === void 0 || server === void 0) return {
2273
+ ok: false,
2274
+ message: "inbound server not mounted (no card)"
2275
+ };
2276
+ const base = readIdentity(holder.domain) ?? holder.identityDefaults;
2277
+ const next = {
2278
+ name: patch.name?.trim() || base.name,
2279
+ description: patch.description?.trim() || base.description,
2280
+ version: patch.version?.trim() || base.version
2281
+ };
2282
+ writeIdentity(holder.domain, next);
2283
+ const rebuilt = rebuildCardWithIdentity(card, next, card.skills ?? []);
2284
+ server.setCard(rebuilt);
2285
+ holder.card = rebuilt;
2286
+ log(`[a2a] identity updated: ${next.name}`);
2287
+ return {
2288
+ ok: true,
2289
+ message: `service identity updated (${next.name})`
2290
+ };
2291
+ },
2292
+ async closeInbound(peerId) {
2293
+ const inbound = holder.inbound;
2294
+ if (inbound === void 0) return {
2295
+ ok: false,
2296
+ message: "inbound registry not mounted"
2297
+ };
2298
+ for (const taskId of inbound.activeTasksOf(peerId)) await holder.server?.abort?.(taskId);
2299
+ return inbound.closePeer(peerId);
2300
+ },
2301
+ inbounds() {
2302
+ return holder.inbound?.list() ?? [];
2048
2303
  }
2049
2304
  };
2050
2305
  }