n2n-nexus 0.7.0 → 0.8.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.
@@ -1,18 +1,19 @@
1
1
  import http from "node:http";
2
2
  import net from "node:net";
3
+ import { EventEmitter } from "node:events";
3
4
  import { URL } from "node:url";
4
5
  import { StorageManager } from "../storage/index.js";
5
6
  import { SqliteMeetingStore } from "../storage/sqlite-meeting.js";
6
7
  import { GlobalMessageStore } from "../storage/global-messages.js";
7
- import { SessionStore } from "../storage/sessions.js";
8
- import { DirectMessageStore } from "../storage/direct-messages.js";
9
8
  import { EventStore } from "../storage/events.js";
10
9
  import { runMigrations } from "../storage/migrations.js";
11
10
  import { createTask, updateTask, getTask, listTasks, cancelTask } from "../storage/tasks.js";
11
+ import { AgentStore, messagePreview } from "../storage/agents.js";
12
12
  import { MAX_BODY_BYTES } from "../constants.js";
13
13
  import { ADDRESS_LIMIT_FACTOR, RateLimiter, rateLimitFromEnv } from "./rate-limit.js";
14
14
  import { ResultContractError, finalizeResult } from "./tool-outputs.js";
15
- import { TOOL_DEFINITIONS, isToolName, parseToolArgs, taskUpdateSchema, sessionRegistrationSchema, sessionDisconnectSchema, ToolValidationError } from "./tools.js";
15
+ import { TOOL_DEFINITIONS, isToolName, parseToolArgs, taskUpdateSchema, callerSchema, hookRequestSchema, ToolValidationError } from "./tools.js";
16
+ import { LISTEN_WAIT_MS, MESSAGE_TRUST_NOTE, NO_AGENT_SESSION_ERROR, NOTICE_PREFIX, NOTIFY_CLOSING, NOTIFY_LIST_LIMIT, STOP_CLOSING, agentSessionId } from "./hook-contract.js";
16
17
  import { z } from "zod";
17
18
  // ---------------------------------------------------------------------------
18
19
  // Storage bootstrap
@@ -79,20 +80,7 @@ async function toolGetProject(args) {
79
80
  ? { manifest, internalDocs: await StorageManager.getProjectDocs(args.projectId) }
80
81
  : { manifest };
81
82
  }
82
- function requireSession(sessionId, instanceId) {
83
- if (!sessionId)
84
- throw new Error("A registered sessionId is required");
85
- return SessionStore.require(sessionId, instanceId);
86
- }
87
- async function toolSendMessage(args, instanceId, sessionId) {
88
- if (args.to !== undefined) {
89
- const sender = requireSession(sessionId, instanceId);
90
- const saved = DirectMessageStore.send(sender.sessionId, { ...args, to: args.to });
91
- return {
92
- ok: true, sentTo: "session", messageId: saved.messageId, status: saved.status,
93
- to: saved.to, replyTo: saved.replyTo, message: `Direct message ${saved.status}.`
94
- };
95
- }
83
+ async function toolSendMessage(args, instanceId) {
96
84
  const meetingId = args.meetingId ?? SqliteMeetingStore.getActiveMeetingId();
97
85
  if (meetingId) {
98
86
  const msg = {
@@ -221,34 +209,231 @@ async function toolHostDeleteProject(args, instanceId) {
221
209
  return { ok: true, projectId: args.projectId, message: `Project '${args.projectId}' deleted.` };
222
210
  }
223
211
  // ---------------------------------------------------------------------------
224
- // Tool dispatcher
212
+ // Agent sessions: direct-message tools
225
213
  // ---------------------------------------------------------------------------
226
- async function dispatchTool(name, rawArgs, instanceId, sessionId) {
227
- switch (name) {
228
- case "list_sessions": {
229
- const args = parseToolArgs(name, rawArgs);
230
- const self = sessionId ? requireSession(sessionId, instanceId) : null;
231
- const sessions = SessionStore.list(args);
232
- return { self, sessions, count: sessions.length };
233
- }
234
- case "update_session": {
235
- const args = parseToolArgs(name, rawArgs);
236
- return { session: SessionStore.update(requireSession(sessionId, instanceId).sessionId, args) };
237
- }
238
- case "read_inbox": {
239
- const args = parseToolArgs(name, rawArgs);
240
- const self = requireSession(sessionId, instanceId);
241
- const messages = DirectMessageStore.readInbox(self.sessionId, args.count);
242
- return { sessionId: self.sessionId, messages, count: messages.length };
214
+ /** Wakes a Stop hook waiting in listen mode; the event name is the session id. */
215
+ const agentWake = new EventEmitter();
216
+ agentWake.setMaxListeners(0);
217
+ function wake(sessionIds) {
218
+ for (const id of sessionIds)
219
+ agentWake.emit(id);
220
+ }
221
+ /** Expire overdue messages and wake the senders that got a notice. */
222
+ function expireMessages(now) {
223
+ wake(AgentStore.expireMessages(now));
224
+ }
225
+ function profileOf(s) {
226
+ return { id: s.id, name: s.name, client: s.client, cwd: s.cwd, note: s.note, status: s.status, lastActiveAt: s.lastActiveAt };
227
+ }
228
+ /**
229
+ * The agent session behind a direct-message tool call: Claude through the
230
+ * toolUseId its PreToolUse hook bound, Codex through its session id.
231
+ */
232
+ function resolveAgent(caller) {
233
+ const now = Date.now();
234
+ let id = null;
235
+ if (caller?.client === "claude")
236
+ id = AgentStore.resolveToolUse(caller.toolUseId, now);
237
+ else if (caller?.client === "codex")
238
+ id = agentSessionId("codex", caller.sessionId);
239
+ if (!id || !AgentStore.get(id))
240
+ throw new Error(NO_AGENT_SESSION_ERROR);
241
+ AgentStore.refresh(id, now);
242
+ expireMessages(now);
243
+ return AgentStore.get(id);
244
+ }
245
+ function deliver(from, to, text, replyTo, notifyWhenIdle) {
246
+ if (to.id === from.id)
247
+ throw new Error("You cannot send a message to yourself.");
248
+ const messageId = AgentStore.addMessage(from.id, to.id, text, replyTo);
249
+ if (notifyWhenIdle)
250
+ AgentStore.subscribeIdle(from.id, to.id);
251
+ wake([to.id]);
252
+ return { messageId, to: { id: to.id, name: to.name, status: to.status } };
253
+ }
254
+ async function toolPeers(caller) {
255
+ const self = resolveAgent(caller);
256
+ return { self: profileOf(self), peers: AgentStore.listPeers(self.id).map(profileOf) };
257
+ }
258
+ async function toolSend(args, caller) {
259
+ const self = resolveAgent(caller);
260
+ const to = AgentStore.findRecipient(args.to);
261
+ if (!to)
262
+ throw new Error(`Unknown recipient '${args.to}'. Call peers for session ids and names.`);
263
+ return deliver(self, to, args.text, null, args.notifyWhenIdle);
264
+ }
265
+ async function toolReply(args, caller) {
266
+ const self = resolveAgent(caller);
267
+ const original = AgentStore.getMessage(args.messageId);
268
+ if (!original || original.toId !== self.id)
269
+ throw new Error(`Message '${args.messageId}' was not sent to you.`);
270
+ return deliver(self, AgentStore.get(original.fromId), args.text, original.id, false);
271
+ }
272
+ async function toolRead(args, caller) {
273
+ const self = resolveAgent(caller);
274
+ const { messages, remaining } = AgentStore.readUnread(self.id, args.count);
275
+ return {
276
+ messages: messages.map(m => ({
277
+ messageId: m.id, from: { id: m.fromId, name: m.fromName }, replyTo: m.replyTo, text: m.text, createdAt: m.createdAt
278
+ })),
279
+ remaining
280
+ };
281
+ }
282
+ async function toolListen(args, caller) {
283
+ const self = resolveAgent(caller);
284
+ AgentStore.setListening(self.id, args.on);
285
+ return { listening: args.on };
286
+ }
287
+ async function toolSetProfile(args, caller) {
288
+ const self = resolveAgent(caller);
289
+ return { profile: profileOf(AgentStore.updateProfile(self.id, args)) };
290
+ }
291
+ /**
292
+ * Notification text for messages and notices; marks them notified. `closing` ends
293
+ * the message part: NOTIFY_CLOSING, or STOP_CLOSING when it continues a turn that was
294
+ * about to end (a model left to itself there tended to stop after the preview).
295
+ */
296
+ function notify(messages, notices, label, closing) {
297
+ const lines = [];
298
+ if (messages.length > 0) {
299
+ const n = messages.length;
300
+ lines.push(`${NOTICE_PREFIX} ${n} ${label} direct message${n === 1 ? "" : "s"}:`);
301
+ for (const m of messages.slice(0, NOTIFY_LIST_LIMIT)) {
302
+ lines.push(`- From ${m.fromName} (${m.fromId}) at ${m.createdAt}: ${messagePreview(m.text)}`);
243
303
  }
244
- case "get_message": {
245
- const args = parseToolArgs(name, rawArgs);
246
- return { message: DirectMessageStore.get(args.messageId, requireSession(sessionId, instanceId).sessionId) };
304
+ if (n > NOTIFY_LIST_LIMIT)
305
+ lines.push(`- ...and ${n - NOTIFY_LIST_LIMIT} more`);
306
+ }
307
+ if (notices.length > 0) {
308
+ lines.push(messages.length > 0 ? "Notices:" : `${NOTICE_PREFIX} Notices:`);
309
+ for (const notice of notices)
310
+ lines.push(`- ${notice.text}`);
311
+ }
312
+ if (messages.length > 0)
313
+ lines.push(`${MESSAGE_TRUST_NOTE} ${closing}`);
314
+ AgentStore.markNotified(messages.map(m => m.id), notices.map(n => n.id));
315
+ return lines.join("\n");
316
+ }
317
+ /** Notification for every unread message and pending notice, or null when there are none. */
318
+ function notifyAll(sessionId, closing = NOTIFY_CLOSING) {
319
+ const messages = AgentStore.unread(sessionId);
320
+ const notices = AgentStore.pendingNotices(sessionId);
321
+ return messages.length || notices.length ? notify(messages, notices, "unread", closing) : null;
322
+ }
323
+ /** Notification for what was not notified yet, or null when nothing is new. */
324
+ function notifyFresh(sessionId, closing = NOTIFY_CLOSING) {
325
+ const messages = AgentStore.unread(sessionId, true);
326
+ const notices = AgentStore.pendingNotices(sessionId);
327
+ return messages.length || notices.length ? notify(messages, notices, "new", closing) : null;
328
+ }
329
+ const contextOutput = (hookEventName, text) => text === null ? null : { hookSpecificOutput: { hookEventName, additionalContext: text } };
330
+ const blockOutput = (reason) => ({ decision: "block", reason });
331
+ /**
332
+ * Listen mode: wait until something not notified yet arrives (then block the
333
+ * stop with it), the wait runs out, or the hook's HTTP client goes away.
334
+ */
335
+ function waitForMessages(sessionId, waitMs, signal) {
336
+ return new Promise((resolve) => {
337
+ let done = false;
338
+ const finish = (output) => {
339
+ if (done)
340
+ return;
341
+ done = true;
342
+ clearTimeout(timer);
343
+ agentWake.off(sessionId, check);
344
+ signal.removeEventListener("abort", stopWaiting);
345
+ resolve(output);
346
+ };
347
+ const stopWaiting = () => {
348
+ if (done)
349
+ return;
350
+ AgentStore.setStatusIf(sessionId, "listening", "idle");
351
+ finish(null);
352
+ };
353
+ function check() {
354
+ if (done)
355
+ return;
356
+ // SessionEnd (or listen off) during the wait: nobody will act on a block any more.
357
+ if (!AgentStore.get(sessionId)?.listening)
358
+ return finish(null);
359
+ const text = notifyFresh(sessionId, STOP_CLOSING);
360
+ if (text === null)
361
+ return;
362
+ AgentStore.setStatus(sessionId, "working");
363
+ finish(blockOutput(text));
247
364
  }
248
- case "acknowledge_message": {
249
- const args = parseToolArgs(name, rawArgs);
250
- return { message: DirectMessageStore.acknowledge(args.messageId, requireSession(sessionId, instanceId).sessionId, args.outcome) };
365
+ const timer = setTimeout(stopWaiting, waitMs);
366
+ agentWake.on(sessionId, check);
367
+ signal.addEventListener("abort", stopWaiting, { once: true });
368
+ if (signal.aborted)
369
+ stopWaiting();
370
+ else
371
+ check();
372
+ });
373
+ }
374
+ async function handleStop(sessionId, input, listenWaitMs, signal) {
375
+ if (input.stop_hook_active !== true) {
376
+ const text = notifyAll(sessionId, STOP_CLOSING);
377
+ if (text !== null) {
378
+ AgentStore.setStatus(sessionId, "working");
379
+ return blockOutput(text);
251
380
  }
381
+ }
382
+ if (AgentStore.get(sessionId).listening) {
383
+ AgentStore.setStatus(sessionId, "listening");
384
+ wake(AgentStore.fireIdle(sessionId, "idle"));
385
+ return waitForMessages(sessionId, listenWaitMs, signal);
386
+ }
387
+ AgentStore.setStatus(sessionId, "idle");
388
+ wake(AgentStore.fireIdle(sessionId, "idle"));
389
+ return null;
390
+ }
391
+ /** One client hook event (POST /api/hook). Returns what the hook prints, or null. */
392
+ async function handleHook(client, input, listenWaitMs, signal) {
393
+ if (typeof input.session_id !== "string" || !input.session_id)
394
+ return null;
395
+ const id = agentSessionId(client, input.session_id);
396
+ const now = Date.now();
397
+ AgentStore.touch(id, client, typeof input.cwd === "string" ? input.cwd : "", now);
398
+ expireMessages(now);
399
+ switch (input.hook_event_name) {
400
+ case "SessionStart":
401
+ AgentStore.setStatus(id, "idle");
402
+ return null;
403
+ case "UserPromptSubmit":
404
+ AgentStore.setStatus(id, "working");
405
+ return contextOutput("UserPromptSubmit", notifyAll(id));
406
+ case "PreToolUse":
407
+ if (typeof input.tool_use_id === "string" && input.tool_use_id)
408
+ AgentStore.bindToolUse(input.tool_use_id, id, now);
409
+ return null;
410
+ case "PostToolUse":
411
+ AgentStore.setStatus(id, "working");
412
+ return contextOutput("PostToolUse", notifyFresh(id));
413
+ case "Stop":
414
+ return handleStop(id, input, listenWaitMs, signal);
415
+ case "SessionEnd":
416
+ AgentStore.setStatus(id, "offline");
417
+ AgentStore.setListening(id, false);
418
+ wake([id, ...AgentStore.fireIdle(id, "offline")]);
419
+ return null;
420
+ default:
421
+ return null;
422
+ }
423
+ }
424
+ // ---------------------------------------------------------------------------
425
+ // Tool dispatcher
426
+ // ---------------------------------------------------------------------------
427
+ async function dispatchTool(name, rawArgs, instanceId, caller) {
428
+ switch (name) {
429
+ case "peers":
430
+ parseToolArgs(name, rawArgs);
431
+ return toolPeers(caller);
432
+ case "send": return toolSend(parseToolArgs(name, rawArgs), caller);
433
+ case "read": return toolRead(parseToolArgs(name, rawArgs), caller);
434
+ case "reply": return toolReply(parseToolArgs(name, rawArgs), caller);
435
+ case "listen": return toolListen(parseToolArgs(name, rawArgs), caller);
436
+ case "set_profile": return toolSetProfile(parseToolArgs(name, rawArgs), caller);
252
437
  case "sync_project_assets": return toolSyncProjectAssets(parseToolArgs(name, rawArgs), instanceId);
253
438
  case "upload_project_asset": return toolUploadAsset(parseToolArgs(name, rawArgs), instanceId);
254
439
  case "update_project": return toolUpdateProject(parseToolArgs(name, rawArgs), instanceId);
@@ -256,7 +441,7 @@ async function dispatchTool(name, rawArgs, instanceId, sessionId) {
256
441
  case "search_projects": return toolSearchProjects(parseToolArgs(name, rawArgs));
257
442
  case "get_global_topology": return toolGetGlobalTopology(parseToolArgs(name, rawArgs));
258
443
  case "get_project": return toolGetProject(parseToolArgs(name, rawArgs));
259
- case "send_message": return toolSendMessage(parseToolArgs(name, rawArgs), instanceId, sessionId);
444
+ case "send_message": return toolSendMessage(parseToolArgs(name, rawArgs), instanceId);
260
445
  case "read_messages": return toolReadMessages(parseToolArgs(name, rawArgs), instanceId);
261
446
  case "list_events": return toolListEvents(parseToolArgs(name, rawArgs));
262
447
  case "get_global_strategy":
@@ -310,9 +495,9 @@ function rateLimited(retryAfterSec, scope) {
310
495
  * Run a tool for an HTTP request. Validation failures and handler rejections
311
496
  * are the caller's fault and surface as 400, not as daemon errors.
312
497
  */
313
- async function callTool(name, args, instanceId, sessionId) {
498
+ async function callTool(name, args, instanceId, caller) {
314
499
  try {
315
- return finalizeResult(name, await dispatchTool(name, args, instanceId, sessionId));
500
+ return finalizeResult(name, await dispatchTool(name, args, instanceId, caller));
316
501
  }
317
502
  catch (e) {
318
503
  if (e instanceof HttpError)
@@ -330,8 +515,10 @@ const UNKNOWN_INSTANCE = "unknown";
330
515
  function actor(body) {
331
516
  return typeof body.instanceId === "string" && body.instanceId ? body.instanceId : UNKNOWN_INSTANCE;
332
517
  }
333
- function sessionActor(body) {
334
- return z.string().min(1).optional().parse(body.sessionId);
518
+ /** A malformed caller names no session: direct-message tools then refuse, every other tool ignores it. */
519
+ function callerOf(body) {
520
+ const parsed = callerSchema.safeParse(body.caller);
521
+ return parsed.success ? parsed.data : undefined;
335
522
  }
336
523
  /** Query-string parameters as tool arguments; `numeric` names are converted to numbers. */
337
524
  function queryArgs(url, numeric) {
@@ -435,6 +622,7 @@ export async function createDaemonServer(options) {
435
622
  await initializeStorage();
436
623
  const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
437
624
  const allowedHosts = options.allowedHosts ?? allowedHostsFromEnv();
625
+ const listenWaitMs = options.listenWaitMs ?? LISTEN_WAIT_MS;
438
626
  const limit = options.rateLimit ?? rateLimitFromEnv();
439
627
  const instanceLimiter = new RateLimiter(limit.perSecond, limit.burst);
440
628
  const addressLimiter = new RateLimiter(limit.perSecond * ADDRESS_LIMIT_FACTOR, limit.burst * ADDRESS_LIMIT_FACTOR);
@@ -443,13 +631,13 @@ export async function createDaemonServer(options) {
443
631
  * Requests that name no instance ("unknown") are only limited per address:
444
632
  * pooling them in one shared bucket let any client exhaust it for all others.
445
633
  */
446
- const call = (name, args, instanceId, sessionId) => {
634
+ const call = (name, args, instanceId, caller) => {
447
635
  if (instanceId !== UNKNOWN_INSTANCE) {
448
636
  const wait = instanceLimiter.take(`instance:${instanceId}`);
449
637
  if (wait)
450
638
  throw rateLimited(wait, `instance '${instanceId}'`);
451
639
  }
452
- return callTool(name, args, instanceId, sessionId);
640
+ return callTool(name, args, instanceId, caller);
453
641
  };
454
642
  const server = http.createServer(async (req, res) => {
455
643
  const method = req.method || "GET";
@@ -487,27 +675,6 @@ export async function createDaemonServer(options) {
487
675
  if (method === "GET" && path === "/api/tools") {
488
676
  return jsonResponse(res, 200, { tools: TOOL_DEFINITIONS });
489
677
  }
490
- if (method === "POST" && path === "/api/sessions/register") {
491
- const input = sessionRegistrationSchema.parse(await parseBody(req));
492
- let session;
493
- try {
494
- session = SessionStore.register(input);
495
- }
496
- catch (e) {
497
- throw new HttpError(400, e instanceof Error ? e.message : String(e));
498
- }
499
- return jsonResponse(res, 200, { ok: true, session });
500
- }
501
- if (method === "POST" && path === "/api/sessions/disconnect") {
502
- const input = sessionDisconnectSchema.parse(await parseBody(req));
503
- try {
504
- SessionStore.disconnect(input.sessionId, input.instanceId);
505
- }
506
- catch (e) {
507
- throw new HttpError(400, e instanceof Error ? e.message : String(e));
508
- }
509
- return jsonResponse(res, 200, { ok: true });
510
- }
511
678
  // Tool call dispatcher ← core endpoint
512
679
  if (method === "POST" && path === "/api/tools/call") {
513
680
  const body = await parseBody(req);
@@ -519,12 +686,27 @@ export async function createDaemonServer(options) {
519
686
  if (!isToolName(toolName)) {
520
687
  return jsonResponse(res, 404, { ok: false, error: `Unknown tool: '${toolName}'` });
521
688
  }
522
- return jsonResponse(res, 200, { ok: true, result: await call(toolName, args, instanceId, sessionActor(body)) });
689
+ return jsonResponse(res, 200, { ok: true, result: await call(toolName, args, instanceId, callerOf(body)) });
690
+ }
691
+ // Client hook events. Hooks name no instance, so only the per-address limit above applies.
692
+ if (method === "POST" && path === "/api/hook") {
693
+ const body = await parseBody(req);
694
+ const hook = hookRequestSchema.safeParse(body);
695
+ if (!hook.success)
696
+ return jsonResponse(res, 400, { ok: false, error: `Invalid hook request:\n${z.prettifyError(hook.error)}` });
697
+ // A listening Stop hook is held open; the client killing the hook or the daemon closing ends the wait.
698
+ const gone = new AbortController();
699
+ res.on("close", () => { if (!res.writableEnded)
700
+ gone.abort(); });
701
+ const output = await handleHook(hook.data.client, hook.data.input, listenWaitMs, AbortSignal.any([gone.signal, closing.signal]));
702
+ if (gone.signal.aborted)
703
+ return;
704
+ return jsonResponse(res, 200, { output }, closing.signal.aborted ? { Connection: "close" } : {});
523
705
  }
524
706
  // Messages
525
707
  if (method === "POST" && path === "/api/messages/send") {
526
708
  const body = await parseBody(req);
527
- return jsonResponse(res, 200, await call("send_message", body, actor(body), sessionActor(body)));
709
+ return jsonResponse(res, 200, await call("send_message", body, actor(body)));
528
710
  }
529
711
  if (method === "GET" && path === "/api/messages/unread") {
530
712
  const result = await call("read_messages", queryArgs(url, ["count"]), url.searchParams.get("instanceId") || UNKNOWN_INSTANCE);
@@ -664,5 +846,13 @@ export async function createDaemonServer(options) {
664
846
  jsonResponse(res, 500, { ok: false, error: message });
665
847
  }
666
848
  });
849
+ // server.close() waits for in-flight requests, and a listening Stop hook stays open
850
+ // for minutes: release those first so a shutdown is not held up by them.
851
+ const closing = new AbortController();
852
+ const close = server.close.bind(server);
853
+ server.close = (callback) => {
854
+ closing.abort();
855
+ return close(callback);
856
+ };
667
857
  return { server };
668
858
  }
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { MESSAGE_CATEGORIES } from "../types.js";
3
3
  import { TASK_STATUSES } from "../storage/tasks.js";
4
+ import { AGENT_STATUSES, HOOK_CLIENTS } from "./hook-contract.js";
4
5
  /**
5
6
  * Result contracts, one per tool. Every result passes through its contract on
6
7
  * the way out of the daemon (`finalizeResult`): undeclared fields are stripped
@@ -12,19 +13,14 @@ import { TASK_STATUSES } from "../storage/tasks.js";
12
13
  * 2025-era clients require.
13
14
  */
14
15
  const ack = { ok: z.literal(true), message: z.string() };
15
- const sessionSchema = z.object({
16
- sessionId: z.string(), instanceId: z.string(), name: z.string(),
17
- clientName: z.string(), workspace: z.string(), projectId: z.string().nullable(),
18
- inbound: z.enum(["accept", "refuse"]), reception: z.literal("pull"),
19
- online: z.boolean(), createdAt: z.string(), lastSeenAt: z.string()
16
+ const agentStatus = z.enum(AGENT_STATUSES);
17
+ const agentProfileSchema = z.object({
18
+ id: z.string(), name: z.string(), client: z.enum(HOOK_CLIENTS), cwd: z.string(),
19
+ note: z.string(), status: agentStatus, lastActiveAt: z.string()
20
20
  });
21
- const deliveryStatus = z.enum(["accepted", "fetched", "acknowledged", "replied", "refused", "expired"]);
22
- const directMessageSchema = z.object({
23
- messageId: z.string(), from: z.string(), to: z.string(), text: z.string(),
24
- category: z.enum(MESSAGE_CATEGORIES).optional(), replyTo: z.string().nullable(),
25
- status: deliveryStatus, createdAt: z.string(), expiresAt: z.string(),
26
- fetchedAt: z.string().nullable(), acknowledgedAt: z.string().nullable(),
27
- repliedAt: z.string().nullable(), reason: z.string().nullable()
21
+ const sentDirectSchema = z.object({
22
+ messageId: z.string(),
23
+ to: z.object({ id: z.string(), name: z.string(), status: agentStatus })
28
24
  });
29
25
  const messageSchema = z.object({
30
26
  id: z.number().int().optional(),
@@ -94,19 +90,26 @@ export const TOOL_OUTPUT_SCHEMAS = {
94
90
  manifest: storedManifest,
95
91
  internalDocs: z.string().optional()
96
92
  }),
97
- list_sessions: z.object({ self: sessionSchema.nullable(), sessions: z.array(sessionSchema), count: z.number().int() }),
98
- update_session: z.object({ session: sessionSchema }),
99
- read_inbox: z.object({ sessionId: z.string(), messages: z.array(directMessageSchema), count: z.number().int() }),
100
- get_message: z.object({ message: directMessageSchema }),
101
- acknowledge_message: z.object({ message: directMessageSchema }),
93
+ peers: z.object({ self: agentProfileSchema, peers: z.array(agentProfileSchema) }),
94
+ send: sentDirectSchema,
95
+ read: z.object({
96
+ messages: z.array(z.object({
97
+ messageId: z.string(),
98
+ from: z.object({ id: z.string(), name: z.string() }),
99
+ replyTo: z.string().nullable(),
100
+ text: z.string(),
101
+ createdAt: z.string()
102
+ })),
103
+ remaining: z.number().int().describe("Unread messages left after this batch")
104
+ }),
105
+ reply: sentDirectSchema,
106
+ listen: z.object({ listening: z.boolean() }),
107
+ set_profile: z.object({ profile: agentProfileSchema }),
102
108
  send_message: z.object({
103
109
  ...ack,
104
- sentTo: z.enum(["meeting", "global", "session"]),
110
+ sentTo: z.enum(["meeting", "global"]),
105
111
  meetingId: z.string().optional(),
106
- messageId: z.union([z.number().int(), z.string()]).optional(),
107
- status: deliveryStatus.optional(),
108
- to: z.string().optional(),
109
- replyTo: z.string().nullable().optional()
112
+ messageId: z.number().int().optional()
110
113
  }),
111
114
  read_messages: z.object({
112
115
  source: z.enum(["meeting", "global"]),