@nelsonlongxiang/dsh-open-a2a-net 0.5.14 → 0.6.24

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 (86) hide show
  1. package/README.md +243 -121
  2. package/README_zh.md +144 -84
  3. package/assets/nexusDist/assets/index-034h1cNc.js +3 -0
  4. package/assets/nexusDist/assets/index-PG9EieGX.css +1 -0
  5. package/assets/nexusDist/index.html +26 -0
  6. package/assets/stageDist/assets/index-Bj7S2pA1.css +1 -0
  7. package/assets/stageDist/assets/index-C-28k_o5.js +974 -0
  8. package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
  9. package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
  10. package/assets/stageDist/index.html +13 -0
  11. package/lib/a2a-client.js +190 -13
  12. package/lib/canvas-store.js +218 -0
  13. package/lib/card.js +25 -0
  14. package/lib/client/A2aControl.js +45 -137
  15. package/lib/client/locales.js +16 -40
  16. package/lib/client.js +164 -467
  17. package/lib/client.js.map +1 -1
  18. package/lib/group-store.js +6 -0
  19. package/lib/idempotency-store.js +291 -0
  20. package/lib/index.js +4217 -259
  21. package/lib/joined-store.js +11 -0
  22. package/lib/layout-store.js +155 -0
  23. package/lib/peer-store.js +98 -13
  24. package/lib/receipt-ladder.js +35 -0
  25. package/lib/receipt.js +102 -0
  26. package/lib/self-suppress.js +62 -0
  27. package/lib/session-transcript.js +150 -0
  28. package/lib/skill/SKILL.md +77 -0
  29. package/lib/skill/references/discovery.md +38 -0
  30. package/lib/skill/references/membership.md +50 -0
  31. package/lib/skill/references/pitfalls.md +42 -0
  32. package/lib/skill/references/routing.md +43 -0
  33. package/lib/skill-registration.js +52 -0
  34. package/lib/stage-mount.js +32 -0
  35. package/lib/task-ledger.js +540 -0
  36. package/lib/team-store.js +122 -0
  37. package/lib/teams-bridge.js +18 -0
  38. package/lib/transport-caps.js +31 -0
  39. package/lib/types/a2a-client.d.ts +72 -1
  40. package/lib/types/canvas-store.d.ts +102 -0
  41. package/lib/types/client/A2aControl.d.ts +18 -28
  42. package/lib/types/client/locales.d.ts +1 -1
  43. package/lib/types/group-store.d.ts +6 -0
  44. package/lib/types/idempotency-store.d.ts +164 -0
  45. package/lib/types/index.d.ts +210 -6
  46. package/lib/types/joined-store.d.ts +9 -0
  47. package/lib/types/layout-store.d.ts +63 -0
  48. package/lib/types/peer-store.d.ts +45 -2
  49. package/lib/types/receipt-ladder.d.ts +25 -0
  50. package/lib/types/receipt.d.ts +51 -0
  51. package/lib/types/self-suppress.d.ts +41 -0
  52. package/lib/types/session-transcript.d.ts +37 -0
  53. package/lib/types/skill-registration.d.ts +3 -0
  54. package/lib/types/stage-mount.d.ts +29 -0
  55. package/lib/types/task-ledger.d.ts +308 -0
  56. package/lib/types/team-store.d.ts +55 -0
  57. package/lib/types/teams-bridge.d.ts +112 -0
  58. package/lib/types/transport-caps.d.ts +29 -0
  59. package/lib/types/types.d.ts +43 -0
  60. package/nexus-stage/package-lock.json +2262 -0
  61. package/nexus-stage/package.json +21 -0
  62. package/nexus-stage/scripts/__pycache__/mockhost.cpython-312.pyc +0 -0
  63. package/nexus-stage/scripts/mockhost.py +253 -0
  64. package/nexus-stage/src/canvas-ops.ts +175 -0
  65. package/nexus-stage/src/canvas-wire.ts +156 -0
  66. package/nexus-stage/src/edges.ts +72 -0
  67. package/nexus-stage/src/fault.ts +56 -0
  68. package/nexus-stage/src/federation.ts +234 -0
  69. package/nexus-stage/src/layout-doc.ts +143 -0
  70. package/nexus-stage/src/layout-wire.ts +158 -0
  71. package/nexus-stage/src/main.ts +361 -0
  72. package/nexus-stage/src/planning-menus.ts +352 -0
  73. package/nexus-stage/src/planning-nodelist.ts +294 -0
  74. package/nexus-stage/src/planning-types.ts +111 -0
  75. package/nexus-stage/src/planning-view.ts +1313 -0
  76. package/nexus-stage/src/planning.css +280 -0
  77. package/nexus-stage/src/seat.ts +35 -0
  78. package/nexus-stage/src/stage-name.ts +13 -0
  79. package/nexus-stage/src/tokens.ts +32 -0
  80. package/nexus-stage/src/viewport.ts +83 -0
  81. package/nexus-stage/src/workbench.css +267 -0
  82. package/nexus-stage/src/workbench.ts +1530 -0
  83. package/nexus-stage/src/world.ts +509 -0
  84. package/nexus-stage/tsconfig.json +25 -0
  85. package/nexus-stage/vite.config.ts +16 -0
  86. package/package.json +83 -22
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: a2a-network
3
+ description: Use when a task needs cross-session or cross-node collaboration over the DSH A2A network — joining and teaming, routing work to remote teams and agents, reconciling receipts and the task ledger, and reading the A2A workbench (/__dsh_a2a_nexus/). Covers admission rules, the receipt contract, and the failure shapes that look like bugs but are contract semantics.
4
+ ---
5
+
6
+ # DSH A2A Network
7
+
8
+ Drive the decentralized A2A network: sessions publish agent cards, join a
9
+ membership layer, form canvas teams, and route work to each other through
10
+ `a2a_route`. One plugin (`@nelsonlongxiang/dsh-open-a2a-net`) owns the whole
11
+ surface on every node.
12
+
13
+ ## Objects
14
+
15
+ - **Node** — one DSH host process exposing `/.well-known/agent-card.json` and
16
+ the `__dsh_a2a/state` face. Peers discover each other through cards.
17
+ - **Session node** — a live DSH session on a node. Visibility follows
18
+ membership: an unjoined session is invisible to peer discovery AND refused
19
+ at the routing admission gate.
20
+ - **Team** — a routable identity. Two kinds: the node's process team
21
+ (`<zone>/<id8>` from the session id) and canvas teams
22
+ (`<zone>/canvas/<name>`, user-composed multi-member groups).
23
+ - **Task** — one routed round. Carries a task id, an idempotency fingerprint,
24
+ and a receipt contract.
25
+
26
+ ## Routing (a2a_route)
27
+
28
+ ```text
29
+ a2a_route { team: "<zone>/<id8 or canvas name>", message, context_id?, async? }
30
+ ```
31
+
32
+ - `async: true` delivers without waiting (`delivered:true` + task id); the
33
+ target routes a receipt — a message starting `[A2A receipt] task <id>` —
34
+ back to your team.
35
+ - Sync (default) waits for the round's final answer. If the 15s transport
36
+ budget fires first, the result is **-32005 DELIVERED-UNSETTLED**: the
37
+ delivery landed and the peer keeps executing. **Never re-dispatch blindly**
38
+ — reconcile via the task id (the receipt or `a2a_teams` coordinates) instead.
39
+ - `context_id` from a previous reply continues that conversation.
40
+
41
+ ## Membership rules
42
+
43
+ - An unjoined session has NO network: `a2a_route` and `a2a_teams` refuse it
44
+ (join gate), and discovery does not list it. The session that originates a
45
+ task is exempt from the gate.
46
+ - Join through the GUI sidebar (A2A 网络) or the join gate prompt; leave
47
+ through the same surface. Membership is visible; leaving removes you from
48
+ discovery too.
49
+ - Inbound deliveries from a caller with no `caller_session` are refused
50
+ (anonymous injection is a closed hole).
51
+
52
+ ## Reading the state
53
+
54
+ `__dsh_a2a/state` (per node) carries `sessions` (live/joined), `sessionTeams`,
55
+ `remote` (peer session rows with `origin` = node label + LAN IP and `via` =
56
+ the peer URL), `activity` (routing ring), `tasks` (the ledger: pending /
57
+ stale / archived). The A2A workbench (`/__dsh_a2a_nexus/`, merged responsive
58
+ shell since 0.6.2 — the 2D planning board is the stage, directory / ledger /
59
+ activity / plan ride a collapsible rail, narrow screens get a drawer)
60
+ renders the same data for humans.
61
+
62
+ ## Failure shapes that are contract semantics
63
+
64
+ - `-32000` with "declares no team membership" — the admission gate working
65
+ as designed; join (or route through a teamed member).
66
+ - `-32000` with "caller_session is required" — the delivery had no caller
67
+ identity; the protocol contract requires it.
68
+ - `-32005` — sync wait budget fired; delivery landed, reconcile the task id.
69
+ - "No live DSH session node accepts team X" — the team resolved but no
70
+ session node accepted the round; check whether the target session is live.
71
+
72
+ ## Details
73
+
74
+ - [routing.md](references/routing.md) — receipts, idempotency, ledger anatomy
75
+ - [discovery.md](references/discovery.md) — cards, peer boundary model, invisibility
76
+ - [membership.md](references/membership.md) — join/leave, allowlist, canvas teams
77
+ - [pitfalls.md](references/pitfalls.md) — operational pitfalls observed in production
@@ -0,0 +1,38 @@
1
+ # Discovery and the peer boundary model
2
+
3
+ ## Cards
4
+
5
+ Each node serves `/.well-known/agent-card.json` (unsigned serve-fresh fields:
6
+ `peers`, `sessionTeams`, `teamMemberships`, `version`, `lanIp`; signed core:
7
+ identity + capability flags). Peers poll cards and aggregate `sessionTeams`
8
+ into their `remote` rows — this is how the planning canvas shows other nodes.
9
+
10
+ ## Visibility follows membership
11
+
12
+ An unjoined session node is NOT advertised: it is absent from the served
13
+ card's `sessionTeams` and invisible to peer discovery. Join to appear; leave
14
+ to vanish. The node's own state face keeps every session (management surface
15
+ is local-only).
16
+
17
+ ## The peer boundary model (connection layer)
18
+
19
+ Referrals — peer URLs learned from other peers' cards — pass two gates:
20
+
21
+ 1. `referralLearning: false` freezes the tracked set to the configured seed
22
+ (`peers` config) for that config's lifetime: referrals are neither learned
23
+ nor offered.
24
+ 2. A non-empty `peerAllowlist` admits a referral only when its host matches
25
+ an entry — exact `host[:port]`, bare domain suffix (`.example.com`), or
26
+ the full URL. An empty list keeps the historical open-referral posture.
27
+
28
+ Seeds from the `peers` config are owner-configured and never filtered.
29
+
30
+ Self-referrals (a peer listing this node's own URL back) are dropped and
31
+ remembered as aliases so mirrored lists stop re-offering them.
32
+
33
+ ## Where to look
34
+
35
+ - Node state: `GET /__dsh_a2a/state`
36
+ - Card: `GET /.well-known/agent-card.json`
37
+ - Direct delivery: `POST /a2a/direct` (`{team, message, caller_session, wait}`)
38
+ - Task query: `POST /a2a/query` (`{task_id, fingerprint}`)
@@ -0,0 +1,50 @@
1
+ # Membership: join, leave, teaming
2
+
3
+ ## Joining
4
+
5
+ - GUI: sidebar → A2A 网络 → join toggle. The join writes the session into
6
+ `a2a/joined.json` (node home) — this is the discovery + admission
7
+ membership layer.
8
+ - API-created sessions do NOT materialize a consumable agent; open the
9
+ session in the GUI once (a queued prompt is then consumed) or create the
10
+ session through the GUI.
11
+
12
+ ## Teaming
13
+
14
+ - `a2a_team_join { team }` — declare a membership in `a2a/teams.json`
15
+ (roster store, restored at construction). Subject to the owner's
16
+ `teamJoinAllowlist` (empty list denies every join; entries are exact names
17
+ or trailing-`*` prefixes).
18
+ - `a2a_team_leave { team }` — retract one declaration.
19
+ - Canvas teams (`<zone>/canvas/<name>`) are user-composed groups created in
20
+ the planning view (建队) — any joined node can be a member; a node may sit
21
+ in multiple teams.
22
+
23
+ ## Admission (S3, default on)
24
+
25
+ - Outbound: a session with no declared team has no network
26
+ (`teamScopeRouting`, default true; explicit false restores the open seam).
27
+ - Inbound: `/a2a/direct` requires a SHARED team between caller and target —
28
+ the caller's declared teams (roster store) must intersect the target's
29
+ routable set. Refusal text names the missing membership.
30
+ - Anonymous deliveries (no `caller_session`) are refused outright.
31
+
32
+ ## Information-plane scoping (S5, default on)
33
+
34
+ - The read-only fleet tools answer from the joined-team scope only
35
+ (`teamScopeInfo`, default true; the union of this host's roster
36
+ declarations): `a2a_teams` lists `[this host]` rows plus peer rows
37
+ serving a joined team (own name or roster declaration — the `note`
38
+ field explains the scope), `a2a_status` narrows peers/routes/activity
39
+ to that scope (local traffic stays), `a2a_probe` sweeps serving peers
40
+ and refuses a single URL whose card cannot prove service.
41
+ - `a2a_tasks` (this host's own dispatch ledger) stays unscoped — it is
42
+ the receipt-reconciliation surface for this host's own dispatches.
43
+ - A host whose nodes declare no teams keeps host-internal facts only;
44
+ explicit `teamScopeInfo: false` restores the open plane.
45
+
46
+ ## Leaving
47
+
48
+ - `a2a_team_leave` per team; the network-level leave (退网) removes the
49
+ session from discovery and the join gate grants. Rejoining later is cheap —
50
+ there is no penalty for cycling membership around task boundaries.
@@ -0,0 +1,42 @@
1
+ # Operational pitfalls (production-observed)
2
+
3
+ ## Sessions
4
+
5
+ - **API-created sessions do not materialize.** `session/create` registers an
6
+ in-memory record; the sessions directory never appears and queued prompts
7
+ are never consumed. Create sessions through the GUI, or open the session in
8
+ the GUI once after creating via API.
9
+ - **adopt-style revive only works for previously materialized sessions** —
10
+ it is an idempotent revive, not a creation path.
11
+
12
+ ## Routing
13
+
14
+ - **Do not blindly re-dispatch after a sync timeout.** -32005 means the
15
+ delivery landed; the peer may still be executing. Reconcile the task id
16
+ first (receipt or query), or you will double-execute.
17
+ - **Unroutable caller labels** (e.g. CLI-initiated `cli-manager/...`) cannot
18
+ receive receipts — use the initiating session's own node team, or expect
19
+ the receipt to be dropped. Deliveries still land.
20
+ - A sync route's 15s transport budget is shorter than most model turns. For
21
+ anything that runs long, prefer `async: true` and the receipt contract.
22
+
23
+ ## Names and prefixes
24
+
25
+ - Python-tool registrations come in two families: shared `py__<name>` and
26
+ dispatcher-exclusive `py__dispatch__<name>`. A route resolving a member
27
+ tool against the wrong family fails with "cannot read properties of
28
+ undefined (reading 'find')" — check which wave registered the tool before
29
+ blaming the route.
30
+ - alpha.4 harness sessions have no `.events` accessor — integrations must
31
+ use `session.snapshotEvents()` (or guard), or they crash new-session turns
32
+ with "Cannot read properties of undefined (reading 'length')".
33
+
34
+ ## Configuration
35
+
36
+ - `teams-scan` roots are resolved against the process CWD when relative. A
37
+ bare `disabled: true` row hides whole team mounts; pointing `roots` at
38
+ per-team `.agents/teams` directories is the reliable form. Do not add a
39
+ second scan source alongside packaged team bundles — double registration
40
+ crashes alpha.4's strict NamedEntries loader.
41
+ - The "Set TEAMS_ROOT" hint in the dir-degraded error is misleading: there is
42
+ no such env var; the scan reads its `roots` from config.
@@ -0,0 +1,43 @@
1
+ # Routing deep-dive
2
+
3
+ ## The receipt contract
4
+
5
+ Every async route (`wait:false`) registers the task on the peer and returns
6
+ immediately. The peer's target session steers the message into its turn; when
7
+ the turn settles, the peer routes a receipt envelope back to the caller's
8
+ team:
9
+
10
+ ```text
11
+ [A2A receipt] task <task_id> <one-line outcome>
12
+ ```
13
+
14
+ Correlated receipts also carry `{"outcome":"completed","idempotencyKey":"..."}`
15
+ on a following line. Receipts are the ONLY completion signal for async work —
16
+ polling `a2a_teams` cannot see another host's ledger.
17
+
18
+ ## The ledger
19
+
20
+ `__dsh_a2a/state` → `tasks`:
21
+
22
+ - **pending** — routed, receipt still owed. An owed task with a delivered
23
+ marker is waiting on the target's turn, not lost.
24
+ - **stale/dead** — past the stale TTL, auto-dead-lettered. A dead task that
25
+ actually completed at the peer is a reconciliation candidate, not a rerun.
26
+ - **archived** — settled with a correlated receipt. The archive is bounded
27
+ (oldest-first eviction).
28
+
29
+ ## Idempotency
30
+
31
+ A route carries a fingerprint (caller + message + team + wait-mode). Re-
32
+ submitting the same fingerprint inside the idempotency window returns the
33
+ original verdict (-32002/-32003 shapes) instead of executing twice. Never
34
+ work around a duplicate verdict by mutating the message — reconcile instead.
35
+
36
+ ## -32005 (DELIVERED-UNSETTLED) procedure
37
+
38
+ 1. Note the task id from the result.
39
+ 2. Wait for the receipt envelope; or
40
+ 3. Re-check via the coordinating surface (`a2a_status` activity ring, the
41
+ peer's state `tasks` face) for the task id's settled outcome.
42
+ 4. Only re-dispatch when BOTH the receipt is absent AND the peer's ledger
43
+ shows the task unknown — otherwise you risk duplicate execution.
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Plugin-owned agent skill registration (obelisk pattern): the packaged
3
+ * `src/skill/SKILL.md` (+ references/) rides the standard DSH skill catalog.
4
+ * Install = apply registers the skill into ctx.skills; uninstall = the
5
+ * registration disposer unwinds with the fiber (no files copied, no residue).
6
+ */
7
+ import { existsSync, readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ const PACKAGED_SKILL_ROOT = new URL('./skill/', import.meta.url);
10
+ /** Locate and validate the packaged SKILL.md; throw loudly when missing. */
11
+ function readSkill() {
12
+ const path = fileURLToPath(new URL('SKILL.md', PACKAGED_SKILL_ROOT));
13
+ if (!existsSync(path)) {
14
+ throw new Error(`a2a skill bundle is missing; expected SKILL.md at ${path}`);
15
+ }
16
+ const raw = readFileSync(path, 'utf8');
17
+ const firstLineEnd = raw.indexOf('\n');
18
+ if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') {
19
+ throw new Error('a2a skill bundle is missing YAML frontmatter');
20
+ }
21
+ const closingStart = raw.indexOf('\n---', firstLineEnd);
22
+ if (closingStart < 0) {
23
+ throw new Error('a2a skill bundle has unterminated YAML frontmatter');
24
+ }
25
+ const frontmatter = raw.slice(firstLineEnd + 1, closingStart);
26
+ const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
27
+ const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
28
+ const skillName = nameMatch?.[1]?.trim() ?? '';
29
+ const description = descMatch?.[1]?.trim() ?? '';
30
+ if (skillName !== 'a2a-network' || description === '') {
31
+ throw new Error('a2a skill frontmatter must define the a2a-network name and a non-empty description');
32
+ }
33
+ const bodyStart = raw.indexOf('\n', closingStart + 1);
34
+ return { name: skillName, description, content: raw.slice(bodyStart + 1).trim() };
35
+ }
36
+ /** Register the a2a-network skill (runtime contribution). */
37
+ export function registerA2aNetworkSkill(ctx) {
38
+ const skills = ctx.skills;
39
+ if (skills === undefined)
40
+ return;
41
+ const skill = readSkill();
42
+ const registration = {
43
+ name: skill.name,
44
+ description: skill.description,
45
+ invocation: { modelInvocable: true, userInvocable: true },
46
+ provider: 'a2a',
47
+ source: 'runtime',
48
+ resourceBase: { kind: 'directory', path: fileURLToPath(PACKAGED_SKILL_ROOT) },
49
+ content: skill.content,
50
+ };
51
+ skills.register(registration);
52
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Stage-mount pathname resolution: both static stages (canvas office floor,
3
+ * Three.js nexus viewer) serve one built Vite tree behind a mount prefix,
4
+ * and both used to answer the bare `/mount` form directly with index.html.
5
+ * That shell then resolved its `./assets/…` script reference against `/`,
6
+ * fetched `/assets/index-*.js` from the web root — a 404 — and rendered a
7
+ * silent black shell (`main.ts` returns early when state fetches fail). The
8
+ * fix is the directory-classic one: a bare mount request redirects to
9
+ * `mount/`, every other relative resolution then lands inside the tree.
10
+ * @module @nelsonlongxiang/dsh-open-a2a-net/stage-mount
11
+ */
12
+ /**
13
+ * Resolve one request pathname against a stage mount prefix.
14
+ * @param pathname - `req.url` verbatim (query string allowed, split here).
15
+ * @param mount - the registered mount prefix, e.g. `/__dsh_a2a_nexus`.
16
+ * @returns the redirect target plus the within-tree relative path.
17
+ */
18
+ export function resolveStageMount(pathname, mount) {
19
+ const raw = pathname.split('?')[0] ?? '/';
20
+ if (raw === mount)
21
+ return { redirectTo: `${mount}/`, rel: '' };
22
+ if (raw === `${mount}/`)
23
+ return { rel: '/index.html' };
24
+ let rel;
25
+ if (raw.startsWith(`${mount}/`))
26
+ rel = raw.slice(mount.length);
27
+ else if (raw.startsWith('/'))
28
+ rel = raw;
29
+ else
30
+ rel = `/${raw}`;
31
+ return { rel };
32
+ }