@nelsonlongxiang/dsh-open-a2a-net 0.7.0 → 0.8.1-preview.1
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/LICENSE +21 -0
- package/README.md +109 -94
- package/README_zh.md +64 -66
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/nexusDist/assets/index-BuidhKVr.css +1 -0
- package/assets/nexusDist/assets/index-YtYrkDJi.js +9 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +349 -20
- package/cordis.patch.yml +167 -0
- package/docs/licenses/dsh-superagent-MIT.txt +21 -0
- package/lib/a2a-client.js +12 -4
- package/lib/card.js +2 -0
- package/lib/cli-deposit.js +75 -4
- package/lib/config.js +1 -2
- package/lib/direct-forward.js +23 -1
- package/lib/faces/admin.js +31 -48
- package/lib/faces/wire.js +110 -71
- package/lib/followup-admission.js +47 -0
- package/lib/followup-delivery.js +19 -2
- package/lib/index.js +618 -389
- package/lib/node-self.js +1 -1
- package/lib/outbound-scope.js +10 -0
- package/lib/peer-directory.js +3 -1
- package/lib/queue-guard.js +1 -1
- package/lib/receipt.js +24 -2
- package/lib/skill/SKILL.md +1 -0
- package/lib/skill/references/cli.md +193 -21
- package/lib/skill/references/collaboration.md +3 -2
- package/lib/skill/references/superagent.md +106 -0
- package/lib/superagent/a2a-watch.js +587 -0
- package/lib/superagent/contract.js +1 -0
- package/lib/superagent/host/a2a-source.js +251 -0
- package/lib/superagent/host/executor.js +520 -0
- package/lib/superagent/host/index.js +1107 -0
- package/lib/superagent/host/policy.js +308 -0
- package/lib/superagent/host/pr-follow.js +158 -0
- package/lib/superagent/host/review-requests.js +77 -0
- package/lib/superagent/host/role-tools.js +137 -0
- package/lib/superagent/host/store.js +136 -0
- package/lib/superagent/host/tools.js +297 -0
- package/lib/superagent/source-types.js +22 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/assignments.js +160 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/engine.js +244 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/errors.js +63 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/evals.js +381 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/ids.js +5 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/index.js +9 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/intake.js +68 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/memory.js +51 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/model.js +79 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/part.js +71 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/ports.js +1 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/reasons.js +35 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/replies.js +94 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/stage.js +27 -0
- package/lib/superagent/upstream/dsh-superagent/src/core/tickets.js +487 -0
- package/lib/superagent/upstream/snapshot-8c32a9d.js +62 -0
- package/lib/task-auth.js +21 -0
- package/lib/task-ledger-rows.js +4 -0
- package/lib/task-ledger.js +69 -3
- package/lib/tools/info.js +13 -7
- package/lib/types/a2a-client.d.ts +0 -7
- package/lib/types/cli-deposit.d.ts +65 -1
- package/lib/types/config.d.ts +6 -20
- package/lib/types/direct-forward.d.ts +14 -0
- package/lib/types/faces/admin.d.ts +5 -0
- package/lib/types/faces/wire.d.ts +20 -57
- package/lib/types/followup-admission.d.ts +13 -0
- package/lib/types/followup-delivery.d.ts +1 -1
- package/lib/types/index.d.ts +0 -2
- package/lib/types/outbound-scope.d.ts +1 -0
- package/lib/types/queue-guard.d.ts +2 -0
- package/lib/types/receipt.d.ts +21 -1
- package/lib/types/superagent/a2a-watch.d.ts +227 -0
- package/lib/types/superagent/contract.d.ts +36 -0
- package/lib/types/superagent/host/a2a-source.d.ts +90 -0
- package/lib/types/superagent/host/executor.d.ts +132 -0
- package/lib/types/superagent/host/index.d.ts +340 -0
- package/lib/types/superagent/host/policy.d.ts +128 -0
- package/lib/types/superagent/host/pr-follow.d.ts +51 -0
- package/lib/types/superagent/host/review-requests.d.ts +36 -0
- package/lib/types/superagent/host/role-tools.d.ts +85 -0
- package/lib/types/superagent/host/store.d.ts +61 -0
- package/lib/types/superagent/host/tools.d.ts +32 -0
- package/lib/types/superagent/source-types.d.ts +218 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/assignments.d.ts +53 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/engine.d.ts +191 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/errors.d.ts +58 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/evals.d.ts +99 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/ids.d.ts +1 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/index.d.ts +9 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/intake.d.ts +15 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/memory.d.ts +40 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/model.d.ts +432 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/part.d.ts +49 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/ports.d.ts +217 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/reasons.d.ts +36 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/replies.d.ts +55 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/stage.d.ts +18 -0
- package/lib/types/superagent/upstream/dsh-superagent/src/core/tickets.d.ts +151 -0
- package/lib/types/superagent/upstream/snapshot-8c32a9d.d.ts +317 -0
- package/lib/types/task-auth.d.ts +9 -0
- package/lib/types/task-ledger-rows.d.ts +21 -4
- package/lib/types/task-ledger.d.ts +9 -0
- package/lib/types/types.d.ts +7 -4
- package/nexus-stage/scripts/mockhost.py +2 -2
- package/nexus-stage/src/main.ts +27 -0
- package/nexus-stage/src/planning-card-text.ts +1 -1
- package/nexus-stage/src/planning-team-roles.ts +80 -0
- package/nexus-stage/src/planning-types.ts +2 -0
- package/nexus-stage/src/planning-view.ts +1 -0
- package/nexus-stage/src/planning.css +29 -9
- package/nexus-stage/src/wb-superagent.ts +518 -0
- package/nexus-stage/src/workbench.css +32 -0
- package/nexus-stage/src/workbench.ts +25 -3
- package/nexus-stage/src/world.ts +1 -1
- package/package.json +8 -2
- package/assets/nexusDist/assets/index-C7HagaGy.css +0 -1
- package/assets/nexusDist/assets/index-CA_YZTWG.js +0 -9
- package/assets/stageDist/assets/index-Bj7S2pA1.css +0 -1
- package/assets/stageDist/assets/index-C-28k_o5.js +0 -974
- package/assets/stageDist/assets/interiors-CQGBjffT.png +0 -0
- package/assets/stageDist/assets/office-tileset-CHQT-uYL.png +0 -0
- package/assets/stageDist/index.html +0 -13
- package/lib/legacy/teams-bridge-inbound.js +0 -156
- package/lib/teams-bridge.js +0 -18
- package/lib/types/legacy/teams-bridge-inbound.d.ts +0 -94
- package/lib/types/teams-bridge.d.ts +0 -112
package/cli/a2a.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Runs on the SDK subpaths only — no @deepseek-ai/* imports.
|
|
24
24
|
*/
|
|
25
25
|
import { createHash, generateKeyPairSync, randomUUID, sign as edSign, createPrivateKey } from 'node:crypto'
|
|
26
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
26
|
+
import { appendFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs'
|
|
27
27
|
import { homedir } from 'node:os'
|
|
28
28
|
import { basename, join } from 'node:path'
|
|
29
29
|
|
|
@@ -36,6 +36,11 @@ const IDENTITY_DIR = process.env.A2A_CLI_HOME !== undefined && process.env.A2A_C
|
|
|
36
36
|
: join(homedir(), '.a2a')
|
|
37
37
|
const IDENTITY = join(IDENTITY_DIR, 'cli.json')
|
|
38
38
|
const INBOX_CURSOR = join(IDENTITY_DIR, 'inbox.cursor')
|
|
39
|
+
// superagent p1-context-read: this identity's side of its conversations (one JSON line per route the host took).
|
|
40
|
+
const SENT_LOG = join(IDENTITY_DIR, 'sent.jsonl')
|
|
41
|
+
const SENT_LOG_PREV = join(IDENTITY_DIR, 'sent.1.jsonl')
|
|
42
|
+
const SENT_LOG_CAP = 1024 * 1024
|
|
43
|
+
const SENT_TEXT_CAP = 4000
|
|
39
44
|
const DEFAULT_HOME = 'http://127.0.0.1:3080'
|
|
40
45
|
const DEFAULT_SKILL_ROOTS = [join(homedir(), '.agents', 'skills')]
|
|
41
46
|
|
|
@@ -43,11 +48,12 @@ const USAGE = `a2a-net CLI — aligned with the plugin's model tools ('a2a tools
|
|
|
43
48
|
|
|
44
49
|
usage: a2a <command> [args] [--home URL] [--key <apiKey>] [--json]
|
|
45
50
|
|
|
46
|
-
户口制: init <name> [--uuid 16hex] [--force] · publish · inbox [--new|--wait|--hook] · revoke
|
|
51
|
+
户口制: init <name> [--uuid 16hex] [--force] · publish [--owes] · inbox [--new|--wait|--hook] · thread <context_id> · receipt <task_id> <result…> · admission <caller> [--local] · revoke
|
|
47
52
|
对齐面: whoami · teams [query] · route <team> <msg…> [--sync] [--context ID] [--task ID] · status · tasks · probe · report <msg…> [--task ID]
|
|
48
53
|
CLI 专有: skills [name] [--json] [--root DIR] · tools (this alignment table) · help
|
|
49
54
|
独立节点: serve [--config node.json] [--host 127.0.0.1|0.0.0.0] [--port N] [--home DIR] (the network layer without a DSH instance)
|
|
50
55
|
文件通道: file send <path…> <team> [--note TEXT] · file fetch <ref> [--out DIR] (#91: bytes never ride a message)
|
|
56
|
+
superagent: superagent request <text…> [--thread ID] · superagent reevaluate <trigger id> · superagent state (owner credential: the team's workbench Source)
|
|
51
57
|
|
|
52
58
|
host: --home > $A2A_HOME > the home saved by 'a2a init --home URL' > ${DEFAULT_HOME}
|
|
53
59
|
key: --key > $A2A_KEY (prefer the env var: a --key value stays in shell history)
|
|
@@ -58,6 +64,10 @@ route: async by default (delivered + task id; the receipt comes to 'a2a inbox
|
|
|
58
64
|
every route carries a task id born here; a transport failure is retried up to 3 times with
|
|
59
65
|
the SAME id, and --task ID resends an earlier one — the host runs one task id at most once
|
|
60
66
|
--json: teams / tasks / probe / route / report / inbox print the raw rows instead of the table
|
|
67
|
+
route --json adds class: sent | gone | retry (structured fields only; gone = resending the same task can never succeed)
|
|
68
|
+
route/receipt --json --exit-class: the exit code carries the class as well — 0 sent (or the same receipt
|
|
69
|
+
replayed), 4 gone, 5 the task already carries a different receipt (receipt only), 1 retry or any other failure;
|
|
70
|
+
without --exit-class every exit code stays as it was
|
|
61
71
|
|
|
62
72
|
inbox: bare 'inbox' lists the whole mailbox (newest first, the host keeps 64; mail it pushed out is counted)
|
|
63
73
|
--new only mail newer than the local cursor (~/.a2a/inbox.cursor), then advance it
|
|
@@ -65,11 +75,37 @@ inbox: bare 'inbox' lists the whole mailbox (newest first, the host keeps 64;
|
|
|
65
75
|
--wait block until unread mail arrives, print it and exit 0; exit 3 on --timeout
|
|
66
76
|
(default 30m; --interval polls, default 15s; --renew re-publishes an expiring deposit)
|
|
67
77
|
--hook for a Claude Code hook: unread mail as hookSpecificOutput.additionalContext JSON,
|
|
68
|
-
nothing at all when there is none or the host is unreachable (always exit 0)
|
|
78
|
+
nothing at all when there is none or the host is unreachable (always exit 0)
|
|
79
|
+
--after C replayable read with a cursor the CALLER keeps ('start', then the 'next' it got back):
|
|
80
|
+
one JSON object {epoch, after, next, mail (oldest first), lost, epochChanged[, cursorAhead]};
|
|
81
|
+
never reads or moves ~/.a2a/inbox.cursor; lost is null when the host's store restarted
|
|
82
|
+
|
|
83
|
+
thread: thread <context_id> [--limit N] [--before C] one conversation, newest first, as JSON:
|
|
84
|
+
mailbox rows the host tagged with that context_id (dir "in") and this identity's routes the host
|
|
85
|
+
took in it (dir "out", from ~/.a2a/sent.jsonl, with the sync reply when there was one);
|
|
86
|
+
--limit 1-200 (default 20), --before takes the next_before a previous page returned;
|
|
87
|
+
read-only: never moves ~/.a2a/inbox.cursor; unscoped counts mailbox rows with no context_id
|
|
88
|
+
|
|
89
|
+
receipt: receipt <task_id> [--attention | --acknowledged | --outcome O] [--to TEAM] <one line…> the formal receipt for a task this identity got:
|
|
90
|
+
settles the caller's debt; the envelope carries disposition "result" (default) or "attention" (a question
|
|
91
|
+
for the caller — it settles the task but is not a completion; the answer comes back as a new task in the
|
|
92
|
+
same context). A result says how the work ended with --outcome: completed (default), failed, rejected
|
|
93
|
+
(declined, not taken up) or abandoned (stopped before it finished); an attention receipt takes no outcome.
|
|
94
|
+
The target is the mailbox row's sender for that task id unless --to names one; it rides
|
|
95
|
+
task id receipt-<task_id>, so a task gets one receipt (a second, different one is refused)
|
|
96
|
+
--acknowledged: the task is taken up and its result follows — the caller's row stays owed but leaves the
|
|
97
|
+
queue count and expires 7 days after the acknowledgement instead of 24 hours after dispatch; it rides
|
|
98
|
+
task id ack-<task_id>, so the later receipt is not refused
|
|
99
|
+
|
|
100
|
+
admission: admission <caller> [--local] [--json] how <caller> would be admitted delivering to THIS identity right
|
|
101
|
+
now (the home host recomputes it from its current links): {caller, admitted, rule}. --local asks for the
|
|
102
|
+
same-host face (a session on the home host). Signed with this identity's key; the host answers only the
|
|
103
|
+
identity itself, with the rule name and nothing more. Exit 0 once answered (admitted or not), 1 when the
|
|
104
|
+
host refuses or does not know the query`
|
|
69
105
|
|
|
70
106
|
/** Flags that take a value (both '--home URL' and '--home=URL'); the rest are switches. */
|
|
71
|
-
const VALUE_FLAGS = new Set(['--note', '--out', '--home', '--key', '--uuid', '--root', '--timeout', '--interval', '--context', '--task'])
|
|
72
|
-
const SWITCHES = new Set(['--json', '--force', '--help', '-h', '--new', '--peek', '--wait', '--hook', '--renew', '--sync'])
|
|
107
|
+
const VALUE_FLAGS = new Set(['--note', '--out', '--home', '--key', '--uuid', '--root', '--timeout', '--interval', '--context', '--task', '--after', '--limit', '--before', '--to', '--outcome'])
|
|
108
|
+
const SWITCHES = new Set(['--json', '--force', '--help', '-h', '--new', '--peek', '--wait', '--hook', '--renew', '--sync', '--attention', '--acknowledged', '--owes', '--local', '--exit-class'])
|
|
73
109
|
|
|
74
110
|
/** A hook must never break the session it runs in: in --hook mode every failure is silent. */
|
|
75
111
|
const HOOK_MODE = process.argv.includes('--hook')
|
|
@@ -135,11 +171,13 @@ function identity() {
|
|
|
135
171
|
return id
|
|
136
172
|
}
|
|
137
173
|
|
|
174
|
+
/** An environment variable set to the empty string counts as unset (#490). */
|
|
175
|
+
const envValue = name => { const value = process.env[name]; return value !== undefined && value !== '' ? value : undefined }
|
|
138
176
|
const home = () => {
|
|
139
177
|
const saved = storedIdentity()?.home
|
|
140
|
-
return String(flag('--home') ??
|
|
178
|
+
return String(flag('--home') ?? envValue('A2A_HOME') ?? saved ?? DEFAULT_HOME).replace(/\/$/, '')
|
|
141
179
|
}
|
|
142
|
-
const apiKey = () => flag('--key') ?? (
|
|
180
|
+
const apiKey = () => flag('--key') ?? envValue('A2A_KEY')
|
|
143
181
|
|
|
144
182
|
/** The bound form's domain tag (src/origin-claim.ts ORIGIN_CLAIM_BOUND_TAG). */
|
|
145
183
|
const ORIGIN_CLAIM_BOUND_TAG = 'a2a-origin-claim/2'
|
|
@@ -183,10 +221,7 @@ function originClaimFor(base) {
|
|
|
183
221
|
}
|
|
184
222
|
|
|
185
223
|
/** A host behind dsh-pocket also wants its session token, as the dsh_pocket_token cookie. */
|
|
186
|
-
const pocketToken = () =>
|
|
187
|
-
const token = process.env.A2A_POCKET_TOKEN ?? process.env.A2A_TOKEN
|
|
188
|
-
return token !== undefined && token !== '' ? token : undefined
|
|
189
|
-
}
|
|
224
|
+
const pocketToken = () => envValue('A2A_POCKET_TOKEN') ?? envValue('A2A_TOKEN')
|
|
190
225
|
|
|
191
226
|
const keyHeaders = () => {
|
|
192
227
|
const key = apiKey()
|
|
@@ -222,6 +257,15 @@ async function homeFetch(url, init) {
|
|
|
222
257
|
return res
|
|
223
258
|
}
|
|
224
259
|
|
|
260
|
+
/** The home host's zone, which names its process team (#503); `dsh` when the host does not say. */
|
|
261
|
+
async function homeZone() {
|
|
262
|
+
try {
|
|
263
|
+
const res = await homeFetch(`${home()}/__dsh_a2a/state`, { headers: keyHeaders() })
|
|
264
|
+
const zone = res.ok ? (await res.json())?.zone : undefined
|
|
265
|
+
return typeof zone === 'string' && zone !== '' ? zone : 'dsh'
|
|
266
|
+
} catch { return 'dsh' }
|
|
267
|
+
}
|
|
268
|
+
|
|
225
269
|
/**
|
|
226
270
|
* Read a JSON object reply. A non-JSON or non-object body (a proxy page, an
|
|
227
271
|
* empty 405 from a front proxy, an old host) becomes an error object, and an
|
|
@@ -430,6 +474,210 @@ function unreadOf(box, cursor) {
|
|
|
430
474
|
return { unread, lost: 0 }
|
|
431
475
|
}
|
|
432
476
|
|
|
477
|
+
/**
|
|
478
|
+
* superagent p1-context-read: note a route the host took (class "sent") under its
|
|
479
|
+
* context_id, so `thread` can show this identity's side. The host names the context
|
|
480
|
+
* on every taken reply that carries one; a -32005 reply does not, so only an explicit
|
|
481
|
+
* --context covers it. Best effort: a journal failure never turns a send into an error.
|
|
482
|
+
*/
|
|
483
|
+
function journalSent(body, out) {
|
|
484
|
+
const contextId = typeof out?.context_id === 'string' && out.context_id !== '' ? out.context_id : body.context_id
|
|
485
|
+
if (sendClass(out) !== 'sent' || typeof contextId !== 'string' || contextId === '') return
|
|
486
|
+
const cap = (text) => text.length > SENT_TEXT_CAP ? { text: text.slice(0, SENT_TEXT_CAP), truncated: true, chars: text.length } : { text }
|
|
487
|
+
const reply = typeof out.result?.text === 'string' ? cap(out.result.text) : undefined
|
|
488
|
+
const row = {
|
|
489
|
+
handle: body.caller_session, at: Date.now(), team: body.team, task_id: String(out.task_id ?? body.task_id), context_id: contextId,
|
|
490
|
+
...cap(body.message), ...(reply !== undefined ? { reply: reply.text, ...(reply.truncated ? { reply_truncated: true, reply_chars: reply.chars } : {}) } : {}),
|
|
491
|
+
}
|
|
492
|
+
try {
|
|
493
|
+
mkdirSync(IDENTITY_DIR, { recursive: true })
|
|
494
|
+
// One older generation is kept; a rename is atomic, so a reader sees either file whole.
|
|
495
|
+
if ((statSync(SENT_LOG, { throwIfNoEntry: false })?.size ?? 0) > SENT_LOG_CAP) {
|
|
496
|
+
// Another process may have rolled it between the stat and here: then there is nothing left to roll.
|
|
497
|
+
try { renameSync(SENT_LOG, SENT_LOG_PREV) } catch (error) { if (error?.code !== 'ENOENT') throw error }
|
|
498
|
+
}
|
|
499
|
+
appendFileSync(SENT_LOG, `${JSON.stringify(row)}\n`, { mode: 0o600 })
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error(`[route] sent journal not written (${String(error?.code ?? error)}) — the send itself stands; 'a2a thread' will miss it`)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** This handle's journaled routes in one context, older generation first; unreadable lines are skipped. */
|
|
506
|
+
function sentIn(handle, contextId) {
|
|
507
|
+
const rows = []
|
|
508
|
+
for (const file of [SENT_LOG_PREV, SENT_LOG]) {
|
|
509
|
+
let lines = []
|
|
510
|
+
try { lines = readFileSync(file, 'utf8').split('\n') } catch { continue }
|
|
511
|
+
for (const line of lines) {
|
|
512
|
+
if (line === '') continue
|
|
513
|
+
try {
|
|
514
|
+
const r = JSON.parse(line)
|
|
515
|
+
if (r?.handle === handle && r.context_id === contextId && typeof r.at === 'number' && typeof r.task_id === 'string') rows.push(r)
|
|
516
|
+
} catch { /* a torn last line from a crashed write */ }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return rows
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** `thread` order, newest first: by time, then mailbox before journal, then seq / task id. */
|
|
523
|
+
const threadKey = (e) => e.dir === 'in' ? `${String(e.at)}:in:${String(e.seq)}` : `${String(e.at)}:out:${e.task_id}`
|
|
524
|
+
function threadBefore(a, b) {
|
|
525
|
+
if (a.at !== b.at) return a.at < b.at
|
|
526
|
+
if (a.dir !== b.dir) return a.dir === 'in'
|
|
527
|
+
return a.dir === 'in' ? a.seq < b.seq : a.task_id < b.task_id
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function cmdThread() {
|
|
531
|
+
const [contextId, extra] = positionals()
|
|
532
|
+
if (contextId === undefined || contextId === '' || extra !== undefined) usage('usage: a2a thread <context_id> [--limit N] [--before CURSOR]')
|
|
533
|
+
const limitRaw = flag('--limit')
|
|
534
|
+
const limit = limitRaw === undefined ? 20 : Number(limitRaw)
|
|
535
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200 || (limitRaw !== undefined && !/^\d+$/.test(limitRaw))) usage(`--limit takes a whole number from 1 to 200 (got "${String(limitRaw)}")`)
|
|
536
|
+
const beforeRaw = flag('--before')
|
|
537
|
+
const m = beforeRaw === undefined ? null : /^(\d{1,15}):(in|out):(\S+)$/.exec(beforeRaw)
|
|
538
|
+
if (beforeRaw !== undefined && m === null) usage(`--before takes the next_before a previous 'a2a thread' page returned (got "${beforeRaw}")`)
|
|
539
|
+
const before = m === null ? undefined : { at: Number(m[1]), dir: m[2], ...(m[2] === 'in' ? { seq: Number(m[3]) } : { task_id: m[3] }) }
|
|
540
|
+
if (before?.dir === 'in' && !Number.isInteger(before.seq)) usage(`--before: an "in" cursor ends in a mailbox seq (got "${beforeRaw}")`)
|
|
541
|
+
const handle = identity().handle
|
|
542
|
+
const box = await fetchMail(handle)
|
|
543
|
+
const inbound = box.mail.filter(r => r?.context_id === contextId && typeof r.at === 'number').map(r => ({
|
|
544
|
+
dir: 'in', at: r.at, seq: Number.isInteger(r.seq) ? r.seq : 0, from: r.from, ...(typeof r.task_id === 'string' ? { task_id: r.task_id } : {}),
|
|
545
|
+
...(typeof r.proof === 'string' ? { proof: r.proof } : {}), ...(typeof r.receipt_for === 'string' ? { receipt_for: r.receipt_for } : {}), text: r.text,
|
|
546
|
+
...(r.truncated === true ? { truncated: true, chars: r.chars } : {}), ...(r.dup !== undefined ? { dup: r.dup } : {}),
|
|
547
|
+
}))
|
|
548
|
+
const outbound = sentIn(handle, contextId).map(({ handle: _h, context_id: _c, ...r }) => ({ dir: 'out', ...r }))
|
|
549
|
+
const all = [...inbound, ...outbound].filter(e => before === undefined || threadBefore(e, before)).sort((a, b) => threadBefore(a, b) ? 1 : threadBefore(b, a) ? -1 : 0)
|
|
550
|
+
const entries = all.slice(0, limit)
|
|
551
|
+
const more = all.length > limit
|
|
552
|
+
console.log(JSON.stringify({
|
|
553
|
+
handle, context_id: contextId, ...(typeof box.epoch === 'string' ? { epoch: box.epoch } : {}),
|
|
554
|
+
entries, more, next_before: more ? threadKey(entries.at(-1)) : null,
|
|
555
|
+
// Mailbox rows from before the host tagged context (or deliveries that named none) cannot be placed.
|
|
556
|
+
unscoped: box.mail.filter(r => typeof r?.context_id !== 'string').length,
|
|
557
|
+
// Mail the host's cap pushed out is gone from this read, whatever its context.
|
|
558
|
+
mail_lost: Number.isInteger(box.oldest_seq) ? Math.max(0, box.oldest_seq - 1) : null,
|
|
559
|
+
}, null, 2))
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** superagent p1-receipt-outcome (S26): how a result receipt says the work ended; the host-derived outcomes are not the sender's to claim. */
|
|
563
|
+
const SENDER_OUTCOMES = ['completed', 'failed', 'rejected', 'abandoned']
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* superagent p1-receipt-cmd: the one formal-receipt shape for a CLI identity —
|
|
567
|
+
* `[A2A receipt] task <id> <one line>` plus the v2 envelope line (src/receipt.ts
|
|
568
|
+
* formatReceipt key order). disposition says what comes back: the result, or a
|
|
569
|
+
* question needing the caller's attention; an attention receipt carries no
|
|
570
|
+
* outcome because it is not a completion. The receipt rides its own fixed task id
|
|
571
|
+
* `receipt-<id>`, so the host's idempotency holds a task to one receipt.
|
|
572
|
+
*/
|
|
573
|
+
async function cmdReceipt() {
|
|
574
|
+
const [taskId, ...words] = positionals()
|
|
575
|
+
const summary = words.join(' ').replace(/\s+/g, ' ').trim()
|
|
576
|
+
if (taskId === undefined || summary === '') usage('usage: a2a receipt <task_id> [--attention | --acknowledged | --outcome O] [--to TEAM] <one line…>')
|
|
577
|
+
const outcome = flag('--outcome')
|
|
578
|
+
if (outcome !== undefined && has('--attention')) usage('receipt: --attention takes no --outcome — an attention receipt is a question, not an ending')
|
|
579
|
+
// superagent v4: an acknowledgement — the task is taken up and its result follows; the caller's row stays owed.
|
|
580
|
+
const acknowledged = has('--acknowledged')
|
|
581
|
+
if (acknowledged && (outcome !== undefined || has('--attention'))) usage('receipt: --acknowledged takes neither --outcome nor --attention — it is not an ending')
|
|
582
|
+
if (outcome !== undefined && !SENDER_OUTCOMES.includes(outcome)) usage(`receipt: --outcome takes ${SENDER_OUTCOMES.join(', ')} (got "${outcome}")`)
|
|
583
|
+
const receiptTask = `${acknowledged ? 'ack' : 'receipt'}-${taskId}`
|
|
584
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(taskId)) usage(`receipt: task id "${taskId}" has characters a receipt's own task id cannot carry (letters, digits, . _ : -)`)
|
|
585
|
+
if (receiptTask.length > 80) usage(`receipt: task id "${taskId}" is too long — its receipt rides "${receiptTask}" (${String(receiptTask.length)} characters, at most 80); it is not cut short`)
|
|
586
|
+
const to = flag('--to')
|
|
587
|
+
if (to !== undefined && to === '') usage('--to takes the team the receipt goes to')
|
|
588
|
+
const handle = identity().handle
|
|
589
|
+
const box = await fetchMail(handle)
|
|
590
|
+
const rows = box.mail.filter(r => r?.task_id === taskId)
|
|
591
|
+
const senders = [...new Set(rows.map(r => r.from))]
|
|
592
|
+
if (to === undefined && senders.length !== 1) {
|
|
593
|
+
usage(senders.length === 0
|
|
594
|
+
? `receipt: no mailbox row for task ${taskId} names its sender — pass --to <team>`
|
|
595
|
+
: `receipt: task ${taskId} came from more than one sender (${senders.join(', ')}) — pass --to <team>`)
|
|
596
|
+
}
|
|
597
|
+
const contextId = rows.find(r => typeof r.context_id === 'string')?.context_id
|
|
598
|
+
const envelope = acknowledged
|
|
599
|
+
? { idempotencyKey: taskId, disposition: 'acknowledged' }
|
|
600
|
+
: has('--attention')
|
|
601
|
+
? { idempotencyKey: taskId, disposition: 'attention' }
|
|
602
|
+
: { outcome: outcome ?? 'completed', idempotencyKey: taskId, disposition: 'result' }
|
|
603
|
+
await sendDirect('receipt', {
|
|
604
|
+
team: to ?? senders[0], message: `[A2A receipt] task ${taskId} ${summary}\n${JSON.stringify(envelope)}`,
|
|
605
|
+
caller_session: handle, wait: false, task_id: receiptTask, ...(contextId !== undefined ? { context_id: contextId } : {}),
|
|
606
|
+
})
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* superagent p1-mail-admission: ask the home host how <caller> would be admitted delivering to this identity now.
|
|
611
|
+
* The query is signed with this identity's key over ['a2a-admission', handle, caller, ingress, ts] (src/cli-deposit.ts
|
|
612
|
+
* admissionQueryPayload), so the host answers this identity only, never another one's links.
|
|
613
|
+
*/
|
|
614
|
+
async function cmdAdmission() {
|
|
615
|
+
const [caller] = positionals()
|
|
616
|
+
if (caller === undefined || caller === '') usage('usage: a2a admission <caller> [--local] [--json]')
|
|
617
|
+
const id = identity()
|
|
618
|
+
const ingress = has('--local') ? 'local' : 'wire'
|
|
619
|
+
const ts = Date.now()
|
|
620
|
+
const sig = edSign(null, Buffer.from(JSON.stringify(['a2a-admission', id.handle, caller, ingress, ts])), createPrivateKey(id.privateKeyPem)).toString('base64')
|
|
621
|
+
const out = await control({ action: 'admission', handle: id.handle, caller, ingress, ts, sig })
|
|
622
|
+
if (out.ok !== true) die(`[admission] the host did not answer for ${id.handle} (refused, or a host before p1-mail-admission)`)
|
|
623
|
+
const row = { caller, admitted: out.admitted === true, ...(typeof out.rule === 'string' ? { rule: out.rule } : {}) }
|
|
624
|
+
if (has('--json')) { console.log(JSON.stringify(row)); return }
|
|
625
|
+
console.log(row.admitted ? `[admission] ${caller} → admitted (${String(row.rule)})` : `[admission] ${caller} → not admitted`)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* superagent design v4 (PR-B2a): the owner's terminal face of the team's workbench Source. It rides the home's
|
|
630
|
+
* OWNER control credential ($A2A_KEY / pocket cookie), never a CLI identity. A request made this way is an ordinary
|
|
631
|
+
* message, not a person's (owner 2026-11-08): the router judges it against the team's rules, as an agent's.
|
|
632
|
+
*/
|
|
633
|
+
async function cmdSuperagent() {
|
|
634
|
+
const [sub, ...words] = positionals()
|
|
635
|
+
const url = `${home()}/__dsh_a2a/superagent`
|
|
636
|
+
const answer = async (res) => {
|
|
637
|
+
if (res.status === 401 || res.status === 403) die(keyHint(res.status))
|
|
638
|
+
if (res.status === 404) die(`[superagent] ${home()} runs no superagent team (plugin config superagent: true), or predates it`)
|
|
639
|
+
return await replyOf(res)
|
|
640
|
+
}
|
|
641
|
+
if (sub === 'request') {
|
|
642
|
+
const text = words.join(' ')
|
|
643
|
+
if (text === '') usage('usage: a2a superagent request <text…> [--thread <workbench thread id>] [--json]')
|
|
644
|
+
const thread = flag('--thread')
|
|
645
|
+
const out = await answer(await homeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...keyHeaders() },
|
|
646
|
+
body: JSON.stringify({ action: 'request', text, via: 'cli', ...(thread !== undefined ? { thread } : {}) }) }))
|
|
647
|
+
if (has('--json')) { console.log(JSON.stringify(out)); if (out.ok !== true) process.exitCode = 1; return }
|
|
648
|
+
if (out.ok !== true) die(`[superagent] request refused: ${String(out.error)}`)
|
|
649
|
+
console.log(`[superagent] ${String(out.result)} · trigger ${String(out.triggerId)} · thread ${String(out.thread)}${out.result === 'filtered' ? ' (the workbench Source is off)' : ''}`)
|
|
650
|
+
return
|
|
651
|
+
}
|
|
652
|
+
if (sub === 'reevaluate') {
|
|
653
|
+
// Design superagent-reevaluate-note D3: the owner asks the router to judge a stored message again.
|
|
654
|
+
const [triggerId] = words
|
|
655
|
+
if (triggerId === undefined || triggerId === '') usage('usage: a2a superagent reevaluate <trigger id> [--json]')
|
|
656
|
+
const out = await answer(await homeFetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...keyHeaders() },
|
|
657
|
+
body: JSON.stringify({ action: 'reevaluate', trigger_id: triggerId }) }))
|
|
658
|
+
if (has('--json')) { console.log(JSON.stringify(out)); if (out.ok !== true) process.exitCode = 1; return }
|
|
659
|
+
if (out.ok !== true) die(`[superagent] reevaluate refused: ${String(out.error)}`)
|
|
660
|
+
console.log(out.queued === true ? `[superagent] ${triggerId} queued for a fresh judgement` : `[superagent] ${triggerId} is being judged right now; nothing queued`)
|
|
661
|
+
return
|
|
662
|
+
}
|
|
663
|
+
if (sub === 'state') {
|
|
664
|
+
const out = await answer(await homeFetch(url, { headers: keyHeaders() }))
|
|
665
|
+
if (has('--json')) { console.log(JSON.stringify(out)); return }
|
|
666
|
+
if (out.error !== undefined && !Array.isArray(out.tickets)) die(`[superagent] ${String(out.error)}`)
|
|
667
|
+
const settings = out.settings ?? {}
|
|
668
|
+
console.log(`[superagent] router ${settings.router ?? '(none)'} · accept mode ${String(settings.acceptMode)} · policy ${String(out.policyVersion)}`)
|
|
669
|
+
for (const t of out.tickets ?? []) {
|
|
670
|
+
const stage = t.ended ? `ended ${String(t.ended.outcome)}` : `${String(t.stage?.stage ?? '?')}${t.stage?.action ? ` / ${String(t.stage.action)}` : ''}`
|
|
671
|
+
console.log(` ${String(t.id)} ${stage.padEnd(22)} ${String(t.title)}`)
|
|
672
|
+
}
|
|
673
|
+
for (const th of out.threads ?? []) {
|
|
674
|
+
for (const r of th.replies ?? []) console.log(` ↳ ${String(th.ref?.id)} ${String(r.message?.kind)}: ${String(r.message?.text ?? '').slice(0, 120)}`)
|
|
675
|
+
}
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
usage('usage: a2a superagent request <text…> [--thread ID] [--json] · a2a superagent reevaluate <trigger id> [--json] · a2a superagent state [--json]')
|
|
679
|
+
}
|
|
680
|
+
|
|
433
681
|
/** One mailbox read: the rows, plus the gh#113 seq bounds when the host has them. */
|
|
434
682
|
async function fetchMail(handle) {
|
|
435
683
|
const out = await control({ action: 'inbox', handle })
|
|
@@ -437,6 +685,34 @@ async function fetchMail(handle) {
|
|
|
437
685
|
return out
|
|
438
686
|
}
|
|
439
687
|
|
|
688
|
+
/**
|
|
689
|
+
* superagent p1-inbox-after: a read whose cursor the caller owns. The cursor is
|
|
690
|
+
* `<epoch>:<seq>` (or 'start'); it only ever comes back as `next`. Same epoch:
|
|
691
|
+
* rows past seq, and how many the cap pushed out before them. A new epoch (the
|
|
692
|
+
* host's store restarted): every row, `lost` unknown (null), never a guessed 0.
|
|
693
|
+
*/
|
|
694
|
+
function afterRead(box, after) {
|
|
695
|
+
const start = after === 'start'
|
|
696
|
+
const m = start ? null : /^([^:\s]{1,128}):(\d{1,15})$/.exec(after)
|
|
697
|
+
if (!start && m === null) usage(`--after takes 'start' or the '<epoch>:<seq>' cursor a previous read returned (got "${after}")`)
|
|
698
|
+
const rows = [...box.mail].sort((a, b) => a.seq - b.seq)
|
|
699
|
+
const top = (fallback) => rows.length ? Math.max(fallback, rows.at(-1).seq) : fallback
|
|
700
|
+
if (start) {
|
|
701
|
+
const lost = Math.max(0, box.oldest_seq - 1)
|
|
702
|
+
return { mail: rows, lost, epochChanged: false, next: `${box.epoch}:${String(top(lost))}` }
|
|
703
|
+
}
|
|
704
|
+
if (m[1] !== box.epoch) return { mail: rows, lost: null, epochChanged: true, next: `${box.epoch}:${String(top(0))}` }
|
|
705
|
+
const seq = Number(m[2])
|
|
706
|
+
// A cursor past the newest seq this host ever issued was not handed out by it (forged or mixed up):
|
|
707
|
+
// flag it, keep it as given, never clamp it silently.
|
|
708
|
+
const issued = Number.isInteger(box.next_seq) ? box.next_seq - 1 : top(Math.max(0, box.oldest_seq - 1))
|
|
709
|
+
if (seq > issued) return { mail: [], lost: null, epochChanged: false, cursorAhead: true, next: after }
|
|
710
|
+
const mail = rows.filter(r => r.seq > seq)
|
|
711
|
+
const lost = Math.max(0, box.oldest_seq - seq - 1)
|
|
712
|
+
const floor = lost > 0 ? box.oldest_seq - 1 : seq
|
|
713
|
+
return { mail, lost, epochChanged: false, next: `${box.epoch}:${String(mail.length ? mail.at(-1).seq : floor)}` }
|
|
714
|
+
}
|
|
715
|
+
|
|
440
716
|
/** The loss line: mail the host's 64-row cap pushed out before it was read. */
|
|
441
717
|
const lostLine = (lost) => `${String(lost)} message${lost === 1 ? ' was' : 's were'} pushed out of the mailbox before ${lost === 1 ? 'it was' : 'they were'} read (the host keeps the newest 64)`
|
|
442
718
|
|
|
@@ -513,12 +789,25 @@ async function publishDeposit() {
|
|
|
513
789
|
const signCard = await loadSignCard()
|
|
514
790
|
const id = identity()
|
|
515
791
|
const privateKey = createPrivateKey(id.privateKeyPem)
|
|
516
|
-
|
|
792
|
+
// superagent p1-mailbox-owes (S33): --owes declares, in the signed card, that this identity owes a receipt for what
|
|
793
|
+
// its mailbox takes; every publish (and --renew) states it afresh, so a publish without it withdraws the declaration.
|
|
794
|
+
const capabilities = { route: true, async: true, ...(has('--owes') ? { owes: true } : {}) }
|
|
795
|
+
const card = signCard({ name: id.name, session: `cli-${id.name}`, team: id.handle, capabilities, expiresAt: Date.now() + 2 * 24 * 60 * 60 * 1000 }, privateKey)
|
|
517
796
|
return await control({ action: 'publish', card: JSON.stringify(card) })
|
|
518
797
|
}
|
|
519
798
|
|
|
520
799
|
async function cmdInbox() {
|
|
521
800
|
const handle = identity().handle
|
|
801
|
+
const after = flag('--after')
|
|
802
|
+
if (after !== undefined) {
|
|
803
|
+
if (['--new', '--peek', '--wait', '--hook'].some(has)) usage('--after is its own cursor: it does not combine with --new, --peek, --wait or --hook')
|
|
804
|
+
const box = await fetchMail(handle)
|
|
805
|
+
if (!seqCapable(box)) die('[inbox] --after needs a host that numbers its mailbox (gh#113); upgrade the home host — no time-based fallback')
|
|
806
|
+
const read = afterRead(box, after)
|
|
807
|
+
if (read.cursorAhead) console.error(`[inbox] cursor ${after} is ahead of this host (newest seq ${String(Number.isInteger(box.next_seq) ? box.next_seq - 1 : '?')}): it was not issued here — check the consumer's stored cursor`)
|
|
808
|
+
console.log(JSON.stringify({ handle, epoch: box.epoch, after, ...read }, null, 2))
|
|
809
|
+
return
|
|
810
|
+
}
|
|
522
811
|
if (has('--hook')) {
|
|
523
812
|
const input = await hookInput()
|
|
524
813
|
const cursor = readCursor(handle)
|
|
@@ -846,14 +1135,46 @@ async function cmdFileFetch() {
|
|
|
846
1135
|
console.log(`[file fetch] ${path} (${String(bytes.byteLength)} bytes, sha256 ${ref.sha256})`)
|
|
847
1136
|
}
|
|
848
1137
|
|
|
1138
|
+
/**
|
|
1139
|
+
* superagent p1-send-classes: one observation per --json route reply — 'sent',
|
|
1140
|
+
* 'gone' or 'retry' — from structured fields only (delivered, code,
|
|
1141
|
+
* routed/result, a replay's prior), never from error text. 'gone'
|
|
1142
|
+
* only where resending the same task can never succeed; anything unknown retries.
|
|
1143
|
+
*/
|
|
1144
|
+
/**
|
|
1145
|
+
* superagent p3 G3 (design P3-7): with --exit-class the exit code carries the class, because a caller that runs the CLI
|
|
1146
|
+
* through a command runner may keep the exit code and lose stdout. Opt-in only; without it no exit code changes.
|
|
1147
|
+
*/
|
|
1148
|
+
const CLASS_EXIT = { sent: 0, gone: 4, retry: 1 }
|
|
1149
|
+
const RECEIPT_CONFLICT_EXIT = 5
|
|
1150
|
+
const exitClass = () => has('--exit-class')
|
|
1151
|
+
|
|
1152
|
+
function sendClass(out, prior) {
|
|
1153
|
+
// A replay proves the host already holds this id: it landed, unless it already failed.
|
|
1154
|
+
if (prior !== undefined) return prior.status === 'failed' ? 'gone' : 'sent'
|
|
1155
|
+
if (out?.delivered === true || (out?.routed === true && out?.result !== undefined) || out?.code === -32005) return 'sent'
|
|
1156
|
+
if (out?.code === -32001 || out?.code === -32002) return 'gone'
|
|
1157
|
+
// acceptance:"unknown", 401/403, 5xx, -32003/-32007/-32008/-32009/-32011, generic -32000, anything unrecognised.
|
|
1158
|
+
return 'retry'
|
|
1159
|
+
}
|
|
1160
|
+
|
|
849
1161
|
async function sendDirect(verb, body) {
|
|
1162
|
+
if (exitClass() && !has('--json')) usage(`${verb}: --exit-class needs --json (the class it exits with is the one --json prints)`)
|
|
850
1163
|
const { res } = await postDirect(body)
|
|
851
1164
|
// Exhausted 5xx resends: the outcome is unknown, not "not delivered" (the hint above names the safe resend).
|
|
852
1165
|
if (res.status >= 500 && !has('--json')) die(`[${verb}] not confirmed (HTTP ${String(res.status)}) — the task may have landed; use the --task resend above`)
|
|
853
|
-
if (res.status !== 409)
|
|
1166
|
+
if (res.status !== 409) {
|
|
1167
|
+
const out = await replyOf(res)
|
|
1168
|
+
if (verb === 'route' || verb === 'receipt') journalSent(body, out)
|
|
1169
|
+
return printDelivery(verb, res, out)
|
|
1170
|
+
}
|
|
854
1171
|
const out = await replyOf(res)
|
|
855
1172
|
if (out.replay !== true) {
|
|
856
|
-
if (has('--json')) console.log(JSON.stringify(out, null, 2))
|
|
1173
|
+
if (has('--json')) console.log(JSON.stringify({ ...out, class: sendClass(out) }, null, 2))
|
|
1174
|
+
// A task id already used for another message: a receipt says so with 5; a route exits with the class --json printed.
|
|
1175
|
+
if (exitClass()) process.exit(verb === 'receipt' ? RECEIPT_CONFLICT_EXIT : CLASS_EXIT[sendClass(out)])
|
|
1176
|
+
// p1-receipt-cmd: one receipt per task — a follow-up is a new task in the same context, never a second receipt.
|
|
1177
|
+
if (verb === 'receipt') die(`[receipt] task ${String(body.task_id)} already carried a different receipt — a task gets one receipt; send any follow-up as a new task in the same context (a2a route <team> … --context <id>)`)
|
|
857
1178
|
die(`[${verb}] task ${String(body.task_id)} was already used for a different message (or with${body.wait === true ? 'out' : ''} --sync) — not sent; drop --task to send it as a new task`)
|
|
858
1179
|
}
|
|
859
1180
|
const prior = await queryOutcome(body)
|
|
@@ -862,7 +1183,8 @@ async function sendDirect(verb, body) {
|
|
|
862
1183
|
// async delivery waits for its receipt; a host before the gh#112 fix also
|
|
863
1184
|
// leaves refused async rounds pending).
|
|
864
1185
|
if (has('--json')) {
|
|
865
|
-
console.log(JSON.stringify({ ...out, received: true, ...(prior.status === 'completed' ? { delivered: true } : prior.status === 'failed' ? { delivered: false } : {}), prior }, null, 2))
|
|
1186
|
+
console.log(JSON.stringify({ ...out, received: true, ...(prior.status === 'completed' ? { delivered: true } : prior.status === 'failed' ? { delivered: false } : {}), prior, class: sendClass(out, prior) }, null, 2))
|
|
1187
|
+
if (exitClass()) { process.exitCode = CLASS_EXIT[sendClass(out, prior)]; return }
|
|
866
1188
|
if (prior.status === 'failed' || (body.wait === true && prior.status !== 'completed')) process.exitCode = 1
|
|
867
1189
|
return
|
|
868
1190
|
}
|
|
@@ -882,15 +1204,18 @@ async function sendDirect(verb, body) {
|
|
|
882
1204
|
}
|
|
883
1205
|
|
|
884
1206
|
/** One readable line for a /a2a/direct reply; the raw reply stays one --json away. */
|
|
885
|
-
|
|
886
|
-
const out = await replyOf(res)
|
|
1207
|
+
function printDelivery(verb, res, out) {
|
|
887
1208
|
if (has('--json')) {
|
|
888
|
-
console.log(JSON.stringify(out, null, 2))
|
|
1209
|
+
console.log(JSON.stringify({ ...out, class: sendClass(out) }, null, 2))
|
|
1210
|
+
if (exitClass()) { process.exitCode = CLASS_EXIT[sendClass(out)]; return }
|
|
889
1211
|
if (out.delivered !== true) process.exitCode = 1
|
|
890
1212
|
return
|
|
891
1213
|
}
|
|
892
1214
|
if (res.status === 401 || res.status === 403) die(`[${verb}] ${keyHint(res.status)}${out.error !== undefined ? ` (${String(out.error)})` : ''}`)
|
|
1215
|
+
if(out.acceptance==='unknown')die(`[${verb}] 接纳状态未知,请用原 task id 对账,不要换 id 重派 · task ${String(out.task_id)}`)
|
|
1216
|
+
if(out.acceptance==='inserted'){console.log(`[${verb}] 已送达、后续处理未确认,请按原 task id 对账,不要重派 · task ${String(out.task_id)}`);return}
|
|
893
1217
|
if(out.delivered===true&&out.receiverBookkeeping==='failed'){console.log(`[${verb}] 已送达、接收方记账失败,请对账,不要重派 · task ${String(out.task_id)} — delivered; receiver bookkeeping failed; reconcile, do not redispatch`);return}
|
|
1218
|
+
if(has('--sync')&&out.task_status==='TASK_STATE_DELIVERED'&&out.reply_final!==true&&out.bridge===undefined){console.log(`[${verb}] 已送达、未得到最终答复,请按 task id 对账 · ${String(out.task_id)} — no final reply`);console.error(`[${verb}] status TASK_STATE_DELIVERED — delivered, not failed; reconcile the original task`);if(out.result?.text)console.log(String(out.result.text));return}
|
|
894
1219
|
// A sync round answers with the target's reply instead of a delivery marker.
|
|
895
1220
|
if (out.routed === true && typeof out.result?.text === 'string') {
|
|
896
1221
|
console.log(`[${verb}] reply from ${String(out.team)} · task ${String(out.task_id)}${typeof out.context_id === 'string' && out.context_id !== '' ? ` · context ${out.context_id}` : ''}`)
|
|
@@ -968,6 +1293,7 @@ async function main() {
|
|
|
968
1293
|
return
|
|
969
1294
|
}
|
|
970
1295
|
if (cmd === 'inbox') { await cmdInbox(); return }
|
|
1296
|
+
if (cmd === 'thread') { await cmdThread(); return }
|
|
971
1297
|
if (cmd === 'file') {
|
|
972
1298
|
const sub = positionals()[0]
|
|
973
1299
|
if (sub === 'send') { await cmdFileSend(); return }
|
|
@@ -1025,7 +1351,7 @@ async function main() {
|
|
|
1025
1351
|
console.log(`dead (${String(dead.length)}):`)
|
|
1026
1352
|
listCapped(dead, 20, t => ` ${String(t.taskId)} → ${String(t.team)} (${String(t.reason ?? '?')})${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
|
|
1027
1353
|
console.log(`archived (${String(archived.length)}):`)
|
|
1028
|
-
listCapped(archived, 20, t => ` ${String(t.taskId)} ${String(t.outcome ?? '?')}${t.endedBy !== undefined && t.endedBy !== 'receipt' ? ` (${String(t.endedBy)})` : ''}${t.withdrawalResult?` withdrawal=${String(t.withdrawalResult)}`:''}${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
|
|
1354
|
+
listCapped(archived, 20, t => ` ${String(t.taskId)} ${String(t.outcome ?? '?')}${t.disposition !== undefined ? ` disposition=${String(t.disposition)}` : ''}${t.endedBy !== undefined && t.endedBy !== 'receipt' ? ` (${String(t.endedBy)})` : ''}${t.withdrawalResult?` withdrawal=${String(t.withdrawalResult)}`:''}${senderTail(t)}${t.reminderAttempts!==undefined?` reminders=${t.reminderDelivered??0}/${t.reminderAttempts} last=${t.reminderLastAt??'?'}${t.repeatedlyUnreconciled?' 多次提醒未对账':''}`:''}`)
|
|
1029
1355
|
if (s.conservation != null) console.log(`conservation: conserved=${String(s.conservation.conserved)} delta=${String(s.conservation.delta)}`)
|
|
1030
1356
|
if (s.settlement != null) console.log(`settlement: mode=${String(s.settlement.mode)} refusalsTotal=${String(s.settlement.refusalsTotal)} offChain=${String(s.settlement.offChain)}`)
|
|
1031
1357
|
if (s.settlement?.enforceRefused) console.log(`enforce refused: ${String(s.settlement.enforceRefused.reason)}; blockers=${JSON.stringify(s.settlement.enforceRefused.blockers)}`)
|
|
@@ -1069,10 +1395,13 @@ async function main() {
|
|
|
1069
1395
|
console.log(`deposit ${depositLine(list, identity().handle, 'OFFLINE')}`)
|
|
1070
1396
|
return
|
|
1071
1397
|
}
|
|
1398
|
+
if (cmd === 'receipt') { await cmdReceipt(); return }
|
|
1399
|
+
if (cmd === 'admission') { await cmdAdmission(); return }
|
|
1400
|
+
if (cmd === 'superagent') { await cmdSuperagent(); return }
|
|
1072
1401
|
if (cmd === 'report') {
|
|
1073
1402
|
const text = positionals().join(' ')
|
|
1074
1403
|
if (text === '') usage('usage: a2a report <message…> — one-way report into the host process-team mailbox')
|
|
1075
|
-
await sendDirect('report', { team:
|
|
1404
|
+
await sendDirect('report', { team: await homeZone(), message: `[a2a-net cli report] ${text}`, caller_session: identity().handle, wait: false, task_id: taskFlag() ?? newTaskId() })
|
|
1076
1405
|
return
|
|
1077
1406
|
}
|
|
1078
1407
|
if (cmd === 'revoke') {
|