@synadia-ai/agent-fabric 0.0.0-stage → 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.
package/dist/cli.js ADDED
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env node
2
+ import { createInterface } from 'node:readline';
3
+ import { connect } from '@nats-io/transport-node';
4
+ import { assertValidSenderName, resolveNatsConnectionBundle, withAgentReconnectDefaults, Agents, NatsAgentError } from '@synadia-ai/agents';
5
+ import { AsyncLocalStorage } from 'node:async_hooks';
6
+ import { RequestRejectedError } from '@synadia-ai/agent-service';
7
+
8
+ var EDGE_RECORD_VERSION = 1;
9
+ var DEFAULT_EDGE_SUBJECT = "TRACE.edges";
10
+ var THREAD_ID_FIELD = "thread_id";
11
+ var ROOT_ID_FIELD = "root_id";
12
+ var SUBJECT_FORBIDDEN = /[\u0000-\u0020\u007f\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/;
13
+ function assertValidTraceOptions(options) {
14
+ if (options === void 0) return;
15
+ const subject = options.edgeSubject;
16
+ if (subject === void 0 || subject === null) return;
17
+ const reject = (reason) => {
18
+ throw new NatsAgentError(`trace.edgeSubject ${JSON.stringify(subject)}: ${reason}`);
19
+ };
20
+ if (typeof subject !== "string") reject("must be a string or null");
21
+ if (subject.length === 0) reject("must not be empty");
22
+ for (const token of subject.split(".")) {
23
+ if (token.length === 0) reject("must not contain an empty token");
24
+ if (SUBJECT_FORBIDDEN.test(token)) reject("must not contain whitespace or control characters");
25
+ if (token.includes("*") || token.includes(">")) reject("must not contain wildcards");
26
+ }
27
+ }
28
+ var COUNTERS_KEY = /* @__PURE__ */ Symbol.for("@synadia-ai/agent-fabric:trace-record-counts");
29
+ var counters = (() => {
30
+ const g = globalThis;
31
+ return g[COUNTERS_KEY] ??= { published: 0, dropped: 0 };
32
+ })();
33
+ function countTraceRecordPublished() {
34
+ counters.published += 1;
35
+ }
36
+ function countTraceRecordDropped() {
37
+ counters.dropped += 1;
38
+ }
39
+ function traceRecordCounts() {
40
+ return Object.freeze({ published: counters.published, dropped: counters.dropped });
41
+ }
42
+ var STORAGE_KEY = /* @__PURE__ */ Symbol.for("@synadia-ai/agent-fabric:trace-scope");
43
+ var storage = (() => {
44
+ const g = globalThis;
45
+ return g[STORAGE_KEY] ??= new AsyncLocalStorage();
46
+ })();
47
+ function bindActiveTrace(scope, fn) {
48
+ return storage.run(scope, fn);
49
+ }
50
+ function activeTrace() {
51
+ return storage.getStore();
52
+ }
53
+ var THREAD_ID_HEX_LEN = 32;
54
+ var THREAD_ID_RE = new RegExp(`^[0-9a-f]{${THREAD_ID_HEX_LEN}}$`);
55
+ function isThreadId(value) {
56
+ return THREAD_ID_RE.test(value);
57
+ }
58
+ function randomThreadId() {
59
+ const bytes = new Uint8Array(THREAD_ID_HEX_LEN / 2);
60
+ crypto.getRandomValues(bytes);
61
+ let out = "";
62
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
63
+ return out;
64
+ }
65
+ var TOOL_CALL_ID_MAX = 256;
66
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
67
+ function validToolCallId(toolCallId) {
68
+ if (LONE_SURROGATE.test(toolCallId)) return false;
69
+ const codePoints = Array.from(toolCallId).length;
70
+ return codePoints > 0 && codePoints <= TOOL_CALL_ID_MAX;
71
+ }
72
+ function buildEdgeRecord(agent, threadId, parentId, rootId, toolCallId, turnCountHint) {
73
+ const recordId = randomThreadId();
74
+ const record = {
75
+ version: EDGE_RECORD_VERSION,
76
+ kind: "edge",
77
+ record_id: recordId,
78
+ ts: Math.floor(Date.now() / 1e3),
79
+ agent,
80
+ thread_id: threadId,
81
+ parent_id: parentId,
82
+ root_id: rootId,
83
+ tool_call_id: toolCallId,
84
+ turn_count_hint: turnCountHint
85
+ };
86
+ return { recordId, payload: new TextEncoder().encode(JSON.stringify(record)) };
87
+ }
88
+ var TOOL_CALL_ID_CONTEXT_KEY = "toolCallId";
89
+ function fabricTracing(options = {}) {
90
+ assertValidTraceOptions(options);
91
+ const edgeSubject = options.edgeSubject === void 0 ? DEFAULT_EDGE_SUBJECT : options.edgeSubject;
92
+ return {
93
+ promptInterceptor: promptInterceptor(edgeSubject),
94
+ requestInterceptor,
95
+ heartbeatExtras: edgeSubject === null ? () => ({}) : () => {
96
+ const counts = traceRecordCounts();
97
+ return { records_published: counts.published, records_dropped: counts.dropped };
98
+ }
99
+ };
100
+ }
101
+ function promptInterceptor(edgeSubject) {
102
+ return {
103
+ beforePrompt(ctx) {
104
+ const raw = ctx.context[TOOL_CALL_ID_CONTEXT_KEY];
105
+ const toolCallId = typeof raw === "string" && validToolCallId(raw) ? raw : null;
106
+ const plan = { ...mintLineage(), toolCallId };
107
+ return {
108
+ fields: { [THREAD_ID_FIELD]: plan.threadId, [ROOT_ID_FIELD]: plan.rootId },
109
+ state: plan
110
+ };
111
+ },
112
+ async beforePublish(ctx, extras) {
113
+ const plan = extras?.state;
114
+ if (edgeSubject === null || plan === void 0) return;
115
+ await publishEdge(ctx, edgeSubject, plan);
116
+ }
117
+ };
118
+ }
119
+ function mintLineage() {
120
+ const threadId = randomThreadId();
121
+ const ambient = activeTrace();
122
+ if (ambient === void 0) {
123
+ return { threadId, parentId: null, rootId: threadId, turnCountHint: 0 };
124
+ }
125
+ return {
126
+ threadId,
127
+ parentId: ambient.threadId,
128
+ rootId: ambient.rootId,
129
+ turnCountHint: ambient.turnCountHint
130
+ };
131
+ }
132
+ async function publishEdge(ctx, subject, plan) {
133
+ if (!ctx.identity.canSign) {
134
+ warnEdgesUnsigned(ctx.connection);
135
+ countTraceRecordDropped();
136
+ return;
137
+ }
138
+ try {
139
+ const agent = await ctx.identity.selfId();
140
+ const record = buildEdgeRecord(
141
+ agent,
142
+ plan.threadId,
143
+ plan.parentId,
144
+ plan.rootId,
145
+ plan.toolCallId,
146
+ plan.turnCountHint
147
+ );
148
+ await ctx.identity.publishSigned(subject, record.payload, { nonce: record.recordId });
149
+ countTraceRecordPublished();
150
+ } catch (err) {
151
+ countTraceRecordDropped();
152
+ console.warn(`@synadia-ai/agent-fabric: failed to publish edge record on ${subject}`, err);
153
+ }
154
+ }
155
+ var warnedUnsigned = /* @__PURE__ */ new WeakSet();
156
+ function warnEdgesUnsigned(nc) {
157
+ if (warnedUnsigned.has(nc)) return;
158
+ warnedUnsigned.add(nc);
159
+ console.warn(
160
+ "@synadia-ai/agent-fabric: tracing is enabled but no identity signer is configured; edge records are not published (consumers ignore unsigned records). Pass identity: { signer } to sign them."
161
+ );
162
+ }
163
+ var requestInterceptor = {
164
+ async aroundRequest(ctx, next) {
165
+ await bindActiveTrace(scopeFor(ctx.envelope.extras ?? {}), next);
166
+ }
167
+ };
168
+ var HALF_LINEAGE_PAIR = "envelope `thread_id` and `root_id` must be given together";
169
+ function scopeFor(extras) {
170
+ const threadId = lineageField(extras, THREAD_ID_FIELD);
171
+ const rootId = lineageField(extras, ROOT_ID_FIELD);
172
+ if (threadId === void 0 !== (rootId === void 0)) {
173
+ throw new RequestRejectedError(400, HALF_LINEAGE_PAIR);
174
+ }
175
+ if (threadId !== void 0 && rootId !== void 0) {
176
+ return { threadId, rootId, turnCountHint: 0 };
177
+ }
178
+ const minted = randomThreadId();
179
+ return { threadId: minted, rootId: minted, turnCountHint: 0 };
180
+ }
181
+ function lineageField(extras, wireName) {
182
+ const value = Object.hasOwn(extras, wireName) ? extras[wireName] : void 0;
183
+ if (value === void 0 || value === null) return void 0;
184
+ if (typeof value !== "string") {
185
+ throw new RequestRejectedError(400, `envelope \`${wireName}\` must be a string`);
186
+ }
187
+ if (value === "") return void 0;
188
+ if (!isThreadId(value)) {
189
+ throw new RequestRejectedError(
190
+ 400,
191
+ `envelope \`${wireName}\` must be ${THREAD_ID_HEX_LEN} lowercase hex characters`
192
+ );
193
+ }
194
+ return value;
195
+ }
196
+
197
+ // src/caller.ts
198
+ var CALLER_INBOX_PREFIX = "_INBOX.agents";
199
+ var CANNOT_ANSWER = "The caller is a command line and cannot answer; continue with your best judgement.";
200
+ var DEFAULT_DISCOVER_TIMEOUT_MS = 1e3;
201
+ var NATS_URL_VAR = "NATS_URL";
202
+ var NATS_CREDS_VAR = "NATS_CREDS";
203
+ var FabricCallerError = class extends Error {
204
+ constructor(reason, message2) {
205
+ super(message2);
206
+ this.reason = reason;
207
+ this.name = "FabricCallerError";
208
+ }
209
+ reason;
210
+ };
211
+ var CallerStream = class {
212
+ #inner;
213
+ #sink;
214
+ constructor(inner, sink) {
215
+ this.#inner = inner;
216
+ this.#sink = sink;
217
+ }
218
+ /** The subject the prompt is published to. */
219
+ get subject() {
220
+ return this.#inner.subject;
221
+ }
222
+ /** Stop the stream, as `PromptStream.cancel()`. */
223
+ cancel() {
224
+ this.#sink.reject(notSent("it was cancelled"));
225
+ this.#inner.cancel();
226
+ }
227
+ async *[Symbol.asyncIterator]() {
228
+ try {
229
+ for await (const msg of this.#inner) yield msg;
230
+ } catch (err) {
231
+ this.#sink.reject(err);
232
+ throw err;
233
+ } finally {
234
+ this.#sink.reject(notSent("its stream ended first"));
235
+ }
236
+ }
237
+ };
238
+ function notSent(why) {
239
+ return new FabricCallerError("not-sent", `the prompt never went out, so it has no root: ${why}`);
240
+ }
241
+ var ROOT_SINK_KEY = "agentFabricCallerRootSink";
242
+ function rootDeferred() {
243
+ let settled = false;
244
+ let resolve;
245
+ let reject;
246
+ const rootId = new Promise((res, rej) => {
247
+ resolve = res;
248
+ reject = rej;
249
+ });
250
+ rootId.catch(() => {
251
+ });
252
+ const once = (fn) => (v) => {
253
+ if (settled) return;
254
+ settled = true;
255
+ fn(v);
256
+ };
257
+ return { rootId, sink: { resolve: once(resolve), reject: once(reject) } };
258
+ }
259
+ async function fabricCaller(options) {
260
+ const creds = options.creds;
261
+ if (typeof creds !== "string" || creds.length === 0) {
262
+ throw new FabricCallerError(
263
+ "config",
264
+ `no creds file: pass creds, or set ${NATS_CREDS_VAR} to the creds file the console gave you`
265
+ );
266
+ }
267
+ const url = options.url ?? process.env[NATS_URL_VAR];
268
+ if (url === void 0 || url.length === 0) {
269
+ throw new FabricCallerError(
270
+ "config",
271
+ `no server URL: pass url, or set ${NATS_URL_VAR} to the URL the console showed next to your creds file`
272
+ );
273
+ }
274
+ const tracing = fabricTracing(
275
+ options.edgeSubject !== void 0 ? { edgeSubject: options.edgeSubject } : {}
276
+ );
277
+ if (options.name !== void 0) {
278
+ try {
279
+ assertValidSenderName(options.name);
280
+ } catch (err) {
281
+ throw new FabricCallerError("config", `name: ${errorMessage(err)}`);
282
+ }
283
+ }
284
+ let bundle;
285
+ try {
286
+ bundle = await resolveNatsConnectionBundle({ url, creds }, { identity: "signed" });
287
+ } catch (err) {
288
+ throw new FabricCallerError("config", `creds file ${creds}: ${errorMessage(err)}`);
289
+ }
290
+ let nc;
291
+ try {
292
+ nc = await connect(
293
+ withAgentReconnectDefaults({
294
+ ...bundle.connectionOptions,
295
+ inboxPrefix: CALLER_INBOX_PREFIX,
296
+ ...options.name !== void 0 ? { name: options.name } : {},
297
+ // A caller is short-lived: a wrong URL fails now, naming the URL,
298
+ // instead of retrying forever before the first connection.
299
+ waitOnFirstConnect: false
300
+ })
301
+ );
302
+ } catch (err) {
303
+ bundle.wipe();
304
+ throw new FabricCallerError("connect", `could not connect to ${url}: ${errorMessage(err)}`);
305
+ }
306
+ let agents;
307
+ try {
308
+ agents = new Agents({
309
+ nc,
310
+ identity: {
311
+ signer: bundle.signer,
312
+ ...options.name !== void 0 ? { name: options.name } : {}
313
+ },
314
+ interceptors: [rootCapturing(tracing.promptInterceptor)]
315
+ });
316
+ } catch (err) {
317
+ await nc.close();
318
+ bundle.wipe();
319
+ throw err;
320
+ }
321
+ try {
322
+ await agents.selfId();
323
+ } catch (err) {
324
+ await agents.close();
325
+ await nc.close();
326
+ bundle.wipe();
327
+ throw new FabricCallerError(
328
+ "identity",
329
+ `the server at ${url} would not tell this creds file's identity, which signing needs: ${errorMessage(err)}`
330
+ );
331
+ }
332
+ const list = (opts = {}) => agents.discover({ timeoutMs: opts.timeoutMs ?? DEFAULT_DISCOVER_TIMEOUT_MS });
333
+ const find = async (who, opts = {}) => pickAgent(await list(opts), who);
334
+ const prompt2 = async (who, text, promptOptions = {}) => {
335
+ const target = typeof who === "string" ? await find(who) : who;
336
+ const { sink, rootId } = rootDeferred();
337
+ let inner;
338
+ try {
339
+ inner = await target.prompt(text, {
340
+ ...promptOptions,
341
+ context: { ...promptOptions.context ?? {}, [ROOT_SINK_KEY]: sink }
342
+ });
343
+ } catch (err) {
344
+ sink.reject(err);
345
+ throw err;
346
+ }
347
+ return { stream: new CallerStream(inner, sink), rootId };
348
+ };
349
+ const ask = async (who, text, askOptions = {}) => {
350
+ const { onQuery, ...promptOptions } = askOptions;
351
+ const { stream, rootId } = await prompt2(who, text, promptOptions);
352
+ let reply = "";
353
+ for await (const msg of stream) {
354
+ if (msg.type === "response") reply += msg.text;
355
+ else if (msg.type === "query") {
356
+ const answer = onQuery !== void 0 ? await onQuery(msg) : "";
357
+ await msg.reply(answer.trim() === "" ? CANNOT_ANSWER : answer);
358
+ }
359
+ }
360
+ return { text: reply, rootId: await rootId };
361
+ };
362
+ let closed = false;
363
+ const close = async () => {
364
+ if (closed) return;
365
+ closed = true;
366
+ try {
367
+ await agents.close();
368
+ await nc.close();
369
+ } finally {
370
+ bundle.wipe();
371
+ }
372
+ };
373
+ return { agents, url, list, find, prompt: prompt2, ask, close };
374
+ }
375
+ function pickAgent(found, who) {
376
+ const byAddress = found.find((a) => a.promptSubject === who);
377
+ if (byAddress !== void 0) return byAddress;
378
+ const ofKind = distinctAddresses(found.filter((a) => a.agent === who));
379
+ if (ofKind.length === 1) return ofKind[0];
380
+ if (ofKind.length > 1) {
381
+ throw new FabricCallerError(
382
+ "ambiguous",
383
+ `${ofKind.length} agents of kind "${who}": ${ofKind.map((a) => a.promptSubject).join(", ")}; name one by its address`
384
+ );
385
+ }
386
+ const there = distinctAddresses(found).map((a) => a.promptSubject).join(", ");
387
+ throw new FabricCallerError("not-found", `no agent "${who}"; there: ${there || "nobody"}`);
388
+ }
389
+ function distinctAddresses(agents) {
390
+ const seen = /* @__PURE__ */ new Map();
391
+ for (const a of agents) if (!seen.has(a.promptSubject)) seen.set(a.promptSubject, a);
392
+ return [...seen.values()];
393
+ }
394
+ function rootCapturing(inner) {
395
+ return {
396
+ async beforePrompt(ctx) {
397
+ const sink = ctx.context[ROOT_SINK_KEY];
398
+ let extras;
399
+ try {
400
+ extras = await inner.beforePrompt(ctx);
401
+ } catch (err) {
402
+ sink?.reject(err);
403
+ throw err;
404
+ }
405
+ const root = extras?.fields?.[ROOT_ID_FIELD];
406
+ if (typeof root === "string") sink?.resolve(root);
407
+ else sink?.reject(notSent("tracing minted no root"));
408
+ return extras;
409
+ },
410
+ async beforePublish(ctx, extras) {
411
+ await inner.beforePublish?.(ctx, extras);
412
+ }
413
+ };
414
+ }
415
+ function errorMessage(err) {
416
+ return err instanceof Error ? err.message : String(err);
417
+ }
418
+
419
+ // src/scratchpad.ts
420
+ var REFERENCE_RE = /sp-(?:ref|r1):[A-Za-z0-9_-]+/g;
421
+ function findReferences(text) {
422
+ return text.match(REFERENCE_RE) ?? [];
423
+ }
424
+
425
+ // src/command.ts
426
+ var EXIT_OK = 0;
427
+ var EXIT_FAILED = 1;
428
+ var EXIT_USAGE = 2;
429
+ var DEFAULT_TIMEOUT_S = 600;
430
+ var USAGE = `usage: agent-fabric agents [options]
431
+ agent-fabric prompt <address or kind> "<text>" [options]
432
+
433
+ List the agents on the Synadia Agent Fabric, or prompt one, with the creds
434
+ file the console gave you. The reply streams to stdout; the server's URL,
435
+ the agent, the task's root id and a ScratchPad reference go to stderr. A
436
+ question the agent asks is printed on stderr and answered from stdin.
437
+
438
+ options:
439
+ --creds <file> the creds file (default: $${NATS_CREDS_VAR})
440
+ --url <url> the URL the console showed next to your creds file
441
+ (default: $${NATS_URL_VAR})
442
+ --timeout <s> how long to wait for the whole reply, in seconds
443
+ (default: ${DEFAULT_TIMEOUT_S})
444
+ --json one JSON object on stdout instead of the stream
445
+ -h, --help this help
446
+ -- the rest is the text, even what starts with -
447
+
448
+ exit codes: 0 done, 1 no such agent or a refused prompt, 2 usage`;
449
+ function parseCommand(argv) {
450
+ const positional = [];
451
+ let creds;
452
+ let url;
453
+ let timeoutS = DEFAULT_TIMEOUT_S;
454
+ let json = false;
455
+ for (let i = 0; i < argv.length; i++) {
456
+ const arg = argv[i];
457
+ if (arg === "--") {
458
+ positional.push(...argv.slice(i + 1));
459
+ break;
460
+ }
461
+ const [flag, inline] = arg.startsWith("--") ? splitFlag(arg) : [arg, void 0];
462
+ const value = () => inline ?? argv[++i];
463
+ switch (flag) {
464
+ case "-h":
465
+ case "--help":
466
+ return { kind: "help" };
467
+ case "--json":
468
+ json = true;
469
+ break;
470
+ case "--creds":
471
+ case "--url":
472
+ case "--timeout": {
473
+ const v = value();
474
+ if (v === void 0 || v === "") return usage(`${flag} needs a value`);
475
+ if (flag === "--creds") creds = v;
476
+ else if (flag === "--url") url = v;
477
+ else {
478
+ const n = Number(v);
479
+ if (!Number.isFinite(n) || n <= 0) {
480
+ return usage(`--timeout must be a positive number of seconds, not "${v}"`);
481
+ }
482
+ timeoutS = n;
483
+ }
484
+ break;
485
+ }
486
+ default:
487
+ if (arg.startsWith("-") && arg !== "-") return usage(`unknown option ${arg}`);
488
+ positional.push(arg);
489
+ }
490
+ }
491
+ const [sub, ...rest] = positional;
492
+ const common = {
493
+ ...creds !== void 0 ? { creds } : {},
494
+ ...url !== void 0 ? { url } : {},
495
+ timeoutS,
496
+ json
497
+ };
498
+ if (sub === void 0) return usage("name a command: agents or prompt");
499
+ if (sub === "help") return { kind: "help" };
500
+ if (sub === "agents") {
501
+ if (rest.length > 0) return usage(`agents takes no arguments, not "${rest.join(" ")}"`);
502
+ return { kind: "agents", ...common };
503
+ }
504
+ if (sub === "prompt") {
505
+ const [who, ...words] = rest;
506
+ const text = words.join(" ");
507
+ if (who === void 0 || text.length === 0) {
508
+ return usage('prompt needs an agent and a text: prompt <address or kind> "<text>"');
509
+ }
510
+ return { kind: "prompt", who, text, ...common };
511
+ }
512
+ return usage(`unknown command "${sub}"`);
513
+ }
514
+ function splitFlag(arg) {
515
+ const eq = arg.indexOf("=");
516
+ return eq < 0 ? [arg, void 0] : [arg.slice(0, eq), arg.slice(eq + 1)];
517
+ }
518
+ function usage(message2) {
519
+ return { kind: "usage", message: message2 };
520
+ }
521
+ async function runCommand(argv, io) {
522
+ const parsed = parseCommand(argv);
523
+ if (parsed.kind === "help") {
524
+ io.out(`${USAGE}
525
+ `);
526
+ return EXIT_OK;
527
+ }
528
+ if (parsed.kind === "usage") {
529
+ io.err(`agent-fabric: ${parsed.message}
530
+
531
+ ${USAGE}
532
+ `);
533
+ return EXIT_USAGE;
534
+ }
535
+ const creds = parsed.creds ?? io.env[NATS_CREDS_VAR] ?? "";
536
+ const url = parsed.url ?? io.env[NATS_URL_VAR] ?? "";
537
+ if (creds === "") {
538
+ io.err(`agent-fabric: no creds file: pass --creds, or set ${NATS_CREDS_VAR}
539
+ `);
540
+ return EXIT_USAGE;
541
+ }
542
+ if (url === "") {
543
+ io.err(
544
+ `agent-fabric: no server URL: pass --url, or set ${NATS_URL_VAR} to the URL the console showed next to your creds file
545
+ `
546
+ );
547
+ return EXIT_USAGE;
548
+ }
549
+ io.err(`url ${url}
550
+ `);
551
+ let caller;
552
+ try {
553
+ caller = await fabricCaller({ creds, url, name: "agent-fabric" });
554
+ } catch (err) {
555
+ io.err(`agent-fabric: ${message(err)}
556
+ `);
557
+ return err instanceof FabricCallerError && err.reason === "config" ? EXIT_USAGE : EXIT_FAILED;
558
+ }
559
+ try {
560
+ if (parsed.kind === "agents") return await listAgents(caller, parsed.json, io);
561
+ return await prompt(caller, parsed.who, parsed.text, parsed, io);
562
+ } catch (err) {
563
+ if (err instanceof FabricCallerError) {
564
+ io.err(`agent-fabric: ${err.message}
565
+ `);
566
+ } else {
567
+ io.err(`agent-fabric: no reply: ${message(err)}
568
+ `);
569
+ }
570
+ return EXIT_FAILED;
571
+ } finally {
572
+ await caller.close();
573
+ }
574
+ }
575
+ function agentLine(a) {
576
+ return {
577
+ address: a.promptSubject,
578
+ kind: a.agent,
579
+ identity: a.identity ?? null,
580
+ verified: a.idSigVerified,
581
+ description: a.description
582
+ };
583
+ }
584
+ async function listAgents(caller, json, io) {
585
+ const seen = /* @__PURE__ */ new Set();
586
+ const lines = [];
587
+ for (const a of await caller.list()) {
588
+ if (seen.has(a.promptSubject)) continue;
589
+ seen.add(a.promptSubject);
590
+ lines.push(agentLine(a));
591
+ }
592
+ lines.sort((x, y) => x.address < y.address ? -1 : x.address > y.address ? 1 : 0);
593
+ if (json) {
594
+ io.out(`${JSON.stringify(lines)}
595
+ `);
596
+ return EXIT_OK;
597
+ }
598
+ if (lines.length === 0) io.err("no agents\n");
599
+ for (const l of lines) io.out(`${l.address} ${l.kind} ${describeIdentity(l)}
600
+ `);
601
+ return EXIT_OK;
602
+ }
603
+ function describeIdentity(l) {
604
+ if (l.identity === null) return "agent identity: none";
605
+ return l.verified ? `agent ${l.identity}, verified` : `agent ${l.identity}`;
606
+ }
607
+ async function prompt(caller, who, text, opts, io) {
608
+ const target = await caller.find(who);
609
+ io.err(`\u2192 ${target.promptSubject} (${describeIdentity(agentLine(target))})
610
+ `);
611
+ const { stream, rootId } = await caller.prompt(target, text, {
612
+ maxWaitMs: Math.round(opts.timeoutS * 1e3)
613
+ });
614
+ rootId.then(
615
+ (root2) => io.err(`root ${root2}
616
+ `),
617
+ () => {
618
+ }
619
+ );
620
+ let reply = "";
621
+ for await (const msg of stream) {
622
+ if (msg.type === "response") {
623
+ reply += msg.text;
624
+ if (!opts.json) io.out(msg.text);
625
+ } else if (msg.type === "query") {
626
+ io.err(`? ${msg.prompt}
627
+ `);
628
+ const answer = await io.readLine();
629
+ await msg.reply(answer === null || answer.trim() === "" ? CANNOT_ANSWER : answer);
630
+ }
631
+ }
632
+ const root = await rootId;
633
+ const references = findReferences(reply);
634
+ if (opts.json) {
635
+ io.out(`${JSON.stringify({ root, text: reply, references })}
636
+ `);
637
+ } else {
638
+ io.out("\n");
639
+ }
640
+ const last = references.at(-1);
641
+ if (last !== void 0) io.err(`reference: ${last}
642
+ `);
643
+ return EXIT_OK;
644
+ }
645
+ function message(err) {
646
+ return err instanceof Error ? err.message : String(err);
647
+ }
648
+
649
+ // src/cli.ts
650
+ function processIO() {
651
+ let lines;
652
+ return {
653
+ out: (text) => void process.stdout.write(text),
654
+ err: (text) => void process.stderr.write(text),
655
+ async readLine() {
656
+ lines ??= createInterface({ input: process.stdin, terminal: false })[Symbol.asyncIterator]();
657
+ const next = await lines.next();
658
+ return next.done === true ? null : next.value;
659
+ },
660
+ env: process.env
661
+ };
662
+ }
663
+ function main() {
664
+ return runCommand(process.argv.slice(2), processIO());
665
+ }
666
+ main().then(
667
+ (code) => {
668
+ process.exitCode = code;
669
+ process.stdin.destroy();
670
+ },
671
+ (err) => {
672
+ process.stderr.write(`agent-fabric: ${err instanceof Error ? err.message : String(err)}
673
+ `);
674
+ process.exitCode = 1;
675
+ process.stdin.destroy();
676
+ }
677
+ );
678
+ //# sourceMappingURL=cli.js.map
679
+ //# sourceMappingURL=cli.js.map