agents-can-communicate 0.1.18 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +78 -70
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-hook.mjs +94 -12
  4. package/bin/acc-mcp.mjs +6 -2
  5. package/bin/acc.mjs +6 -1
  6. package/docs/ADAPTER_AUTHORING.md +172 -0
  7. package/docs/ARCHITECTURE.md +131 -0
  8. package/docs/CAPABILITIES.md +102 -214
  9. package/docs/CLI.md +157 -0
  10. package/docs/CONCEPTS.md +134 -0
  11. package/docs/CONFIGURATION.md +143 -0
  12. package/docs/DESIGN_DECISIONS.md +89 -0
  13. package/docs/GETTING_STARTED.md +145 -0
  14. package/docs/GLOSSARY.md +26 -0
  15. package/docs/MCP.md +94 -0
  16. package/docs/PROTOCOL.md +200 -0
  17. package/docs/RELEASING.md +109 -0
  18. package/docs/SECURITY_MODEL.md +131 -0
  19. package/docs/TROUBLESHOOTING.md +102 -0
  20. package/docs/WHY_ACC.md +61 -0
  21. package/docs/index.md +42 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +20 -22
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +12 -4
  33. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
  34. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  36. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  37. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  38. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
  41. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +20 -22
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +18 -11
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +20 -22
  47. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
  50. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
  51. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +7 -3
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  56. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  57. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  58. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +20 -22
  59. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +9 -9
  60. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  61. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  64. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  65. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  67. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  68. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +20 -22
  69. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  70. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  73. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
  77. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/cli/src/args.mjs +11 -30
  79. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
  80. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  81. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +9 -2
  82. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  83. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  85. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  86. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
  87. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  88. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  89. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  90. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  91. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  92. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  93. package/node_modules/@agents-can-communicate/core/src/service.mjs +11 -10
  94. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  95. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  96. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  97. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  98. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  99. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
  100. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +115 -63
  103. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  104. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
  105. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
  106. package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
  108. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
  109. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  110. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  111. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  112. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  113. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  114. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  115. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  116. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  117. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  118. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
  119. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  120. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  129. package/package.json +19 -1
  130. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  131. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  132. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  133. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  134. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.1.18",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,79 @@
1
+ class InputValidationError extends Error {}
2
+
3
+ const fail = (path, message) => {
4
+ throw new InputValidationError(`${path} ${message}`);
5
+ };
6
+
7
+ const isObject = value => value !== null && typeof value === "object"
8
+ && !Array.isArray(value);
9
+
10
+ function matches(schema, value, path) {
11
+ try {
12
+ validateSchema(schema, value, path);
13
+ return true;
14
+ } catch (error) {
15
+ if (!(error instanceof InputValidationError)) throw error;
16
+ return false;
17
+ }
18
+ }
19
+
20
+ function validateSchema(schema, value, path) {
21
+ if (schema.const !== undefined && !Object.is(value, schema.const)) {
22
+ fail(path, `must equal ${JSON.stringify(schema.const)}`);
23
+ }
24
+ if (schema.enum !== undefined && !schema.enum.some(item => Object.is(item, value))) {
25
+ fail(path, `must be one of ${schema.enum.map(item => JSON.stringify(item)).join(", ")}`);
26
+ }
27
+
28
+ const objectKeywords = schema.type === "object" || schema.properties !== undefined
29
+ || schema.required !== undefined || schema.additionalProperties !== undefined;
30
+ if (objectKeywords) {
31
+ if (!isObject(value)) fail(path, "must be an object");
32
+ const properties = schema.properties ?? {};
33
+ for (const key of schema.required ?? []) {
34
+ if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
35
+ }
36
+ if (schema.additionalProperties === false) {
37
+ for (const key of Object.keys(value)) {
38
+ if (!Object.hasOwn(properties, key)) fail(`${path}.${key}`, "is not a known field");
39
+ }
40
+ }
41
+ for (const [key, child] of Object.entries(properties)) {
42
+ if (Object.hasOwn(value, key)) validateSchema(child, value[key], `${path}.${key}`);
43
+ }
44
+ } else if (schema.type === "array") {
45
+ if (!Array.isArray(value)) fail(path, "must be an array");
46
+ if (schema.items !== undefined) {
47
+ value.forEach((item, index) => validateSchema(schema.items, item, `${path}[${index}]`));
48
+ }
49
+ } else if (schema.type === "string" && typeof value !== "string") {
50
+ fail(path, "must be a string");
51
+ } else if (schema.type === "boolean" && typeof value !== "boolean") {
52
+ fail(path, "must be a boolean");
53
+ } else if (schema.type === "integer" && !Number.isInteger(value)) {
54
+ fail(path, "must be an integer");
55
+ }
56
+
57
+ if (schema.minimum !== undefined && value < schema.minimum) {
58
+ fail(path, `must be at least ${schema.minimum}`);
59
+ }
60
+ if (schema.maximum !== undefined && value > schema.maximum) {
61
+ fail(path, `must be at most ${schema.maximum}`);
62
+ }
63
+ if (schema.anyOf !== undefined
64
+ && !schema.anyOf.some(branch => matches(branch, value, path))) {
65
+ fail(path, "must match an accepted shape");
66
+ }
67
+ if (schema.oneOf !== undefined
68
+ && schema.oneOf.filter(branch => matches(branch, value, path)).length !== 1) {
69
+ fail(path, "must match exactly one accepted shape");
70
+ }
71
+ if (schema.not !== undefined && matches(schema.not, value, path)) {
72
+ fail(path, "contains fields that cannot be combined");
73
+ }
74
+ return value;
75
+ }
76
+
77
+ export function validateToolInput(schema, value) {
78
+ return validateSchema(schema, value, "arguments");
79
+ }
@@ -15,42 +15,37 @@ function escapeText(value) {
15
15
  return result;
16
16
  }
17
17
 
18
+ function escapePeerValue(value) {
19
+ if (typeof value === "string") return escapeText(value);
20
+ if (Array.isArray(value)) return value.map(escapePeerValue);
21
+ if (value !== null && typeof value === "object") {
22
+ return Object.fromEntries(Object.entries(value)
23
+ .map(([key, child]) => [key, escapePeerValue(child)]));
24
+ }
25
+ return value;
26
+ }
27
+
18
28
  const attributedMessage = message => ({
19
- messageId: message.messageId,
20
- from: message.fromSessionId,
21
- type: message.type,
22
- priority: message.priority,
23
- requiresAck: message.requiresAck,
24
- sentAt: message.sentAt,
29
+ ...escapePeerValue(message),
25
30
  trust: "untrusted peer content",
26
- subject: escapeText(message.subject),
27
- body: escapeText(message.body),
28
31
  });
29
32
 
30
- export async function readResource(uri, { service, participantId, workspaceId }) {
31
- const snapshot = await service.store.snapshot(workspaceId);
33
+ export async function readResource(uri, { service, participantId, workspaceId, session }) {
32
34
  switch (uri) {
33
- case "acc://snapshot":
34
- return { ...snapshot,
35
- messages: snapshot.messages.map(attributedMessage) };
35
+ case "acc://snapshot": {
36
+ const { snapshot } = await service.sync({ workspaceId, scope: "full" });
37
+ return { ...snapshot, messages: snapshot.messages.map(attributedMessage) };
38
+ }
36
39
  case "acc://roster":
37
40
  return (await service.sync({ workspaceId })).roster;
38
- case "acc://workstreams":
39
- return snapshot.workstreams;
40
- case "acc://tasks":
41
- return snapshot.tasks;
42
41
  case "acc://inbox": {
43
- const mine = new Set(snapshot.receipts
44
- .filter(receipt => receipt.recipientParticipantId === participantId)
45
- .map(receipt => receipt.messageId));
46
- // A participant sees what was addressed to it, plus what it sent, so a
47
- // fresh reader can follow its own thread.
48
- return snapshot.messages
49
- .filter(message => mine.has(message.messageId)
50
- || message.toParticipantIds.includes(participantId)
51
- || snapshot.sessions.some(session => session.sessionId === message.fromSessionId
52
- && session.participantId === participantId))
53
- .map(attributedMessage);
42
+ if (session === undefined) {
43
+ throw new AccError(EXIT.DATA, "the inbox resource requires a resolved session",
44
+ { participantId });
45
+ }
46
+ const inbox = await service.readInbox({ workspaceId, sessionId: session.sessionId,
47
+ generation: session.generation });
48
+ return inbox.map(item => attributedMessage(item.message));
54
49
  }
55
50
  default:
56
51
  throw new AccError(EXIT.DATA, `unknown resource: ${uri}`, { uri });
@@ -1,14 +1,18 @@
1
- import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
- import { noteNudge } from "@agents-can-communicate/core";
1
+ import { createRequire } from "node:module";
2
+
3
+ import { AccError, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS }
4
+ from "@agents-can-communicate/protocol";
3
5
  import { clearSessionBinding, loadSessionBinding, storeSessionBinding }
4
6
  from "@agents-can-communicate/adapter-sdk";
5
7
 
6
8
  import { readResource } from "./resources.mjs";
9
+ import { validateToolInput } from "./input-validator.mjs";
7
10
  import { MCP_CAPABILITIES, PUBLIC_TOOLS, RESOURCES } from "./tools.mjs";
8
11
 
9
12
  export const PROTOCOL_VERSION = "2026-07-28";
10
13
  export const SUPPORTED_VERSIONS = Object.freeze([PROTOCOL_VERSION]);
11
- const SERVER_INFO = Object.freeze({ name: "agents-can-communicate", version: "0.0.0" });
14
+ const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
15
+ const SERVER_INFO = Object.freeze({ name: "agents-can-communicate", version: PACKAGE_VERSION });
12
16
 
13
17
  const META = "io.modelcontextprotocol";
14
18
  const HEARTBEAT_CADENCE_MS = 60_000;
@@ -72,20 +76,40 @@ async function resolveSession(context) {
72
76
  return session;
73
77
  }
74
78
 
75
- // Kept for clients released before acc_inbox existed. New clients should use
76
- // the narrow inbox tool, but removing mail from acc_sync would strand deployed
77
- // clients that only know this response field. Returning a body is delivery, so
78
- // only those returned messages advance to injected.
79
- async function syncWithMail(service, owner, context, args) {
80
- const sync = await service.sync({ ...owner, cursor: args.cursor ?? null,
81
- scope: args.scope, limit: args.limit });
82
- const messages = await service.pendingMessages({ workspaceId: context.workspaceId,
83
- participantId: context.participantId, exceptSessionId: owner.sessionId });
84
- for (const message of messages) {
85
- await service.markDelivery({ ...owner, messageId: message.messageId,
86
- state: "injected" }).catch(() => null);
79
+ export async function recordAndOffer({ record, router, selectMessage = value => value }) {
80
+ const recorded = await record();
81
+ const message = selectMessage(recorded);
82
+ if (router === null || router === undefined
83
+ || !Array.isArray(message?.toParticipantIds) || message.toParticipantIds.length === 0) {
84
+ return { recorded, delivery: [] };
85
+ }
86
+ try {
87
+ return { recorded, delivery: await router.offer(message) };
88
+ } catch {
89
+ return { recorded, delivery: message.toParticipantIds.map(recipientParticipantId => ({
90
+ recipientParticipantId, outcome: "queued", transport: "durable",
91
+ errorCode: "transport_error",
92
+ })) };
93
+ }
94
+ }
95
+
96
+ const clientMessageId = (args, service) =>
97
+ args.clientMessageId ?? service.ids.next("client");
98
+
99
+ function obligationFor(kind, explicit, addressed) {
100
+ if (!GENERIC_MESSAGE_KINDS.includes(kind)) {
101
+ const command = kind === "answer"
102
+ ? "acc_reply" : kind === "handoff" ? "acc_finish" : null;
103
+ throw new AccError(EXIT.USAGE, command === null
104
+ ? `unknown message kind: ${kind}` : `${kind} messages require ${command}`);
105
+ }
106
+ const obligation = explicit ?? VALID_OBLIGATIONS[kind][0];
107
+ if (!VALID_OBLIGATIONS[kind].includes(obligation)
108
+ || (!addressed && obligation !== "none")) {
109
+ throw new AccError(EXIT.USAGE,
110
+ `message obligation ${obligation} is invalid for ${kind}`);
87
111
  }
88
- return messages.length === 0 ? sync : { ...sync, messages };
112
+ return obligation;
89
113
  }
90
114
 
91
115
  /**
@@ -93,7 +117,7 @@ async function syncWithMail(service, owner, context, args) {
93
117
  *
94
118
  * The hook runtime hands a session its pending messages when it builds a turn,
95
119
  * and marks them delivered. An MCP client has no turn and no hook, and no tool
96
- * ever handed it anything: it saw a `direct_request` line carrying a subject and
120
+ * ever handed it anything: it saw a `reply_required` line carrying a subject and
97
121
  * an id, and to read what a peer had actually said it had to ask for the whole
98
122
  * snapshot and search every message in the workspace for its own name.
99
123
  *
@@ -102,7 +126,7 @@ async function syncWithMail(service, owner, context, args) {
102
126
  * that had answered it.
103
127
  *
104
128
  * Returning them here is delivery, in the same sense and with the same honesty
105
- * as the turn: what is handed over is marked `injected`, and nothing else is.
129
+ * as the turn: what is handed over is marked `retrieved`, and nothing else is.
106
130
  * Acknowledgement stays a separate act, because being shown something is not
107
131
  * agreeing to it.
108
132
  */
@@ -113,72 +137,66 @@ async function callTool(name, args, context) {
113
137
  const service = context.service;
114
138
 
115
139
  switch (name) {
140
+ case "acc_status":
141
+ return service.collectStatus({});
116
142
  case "acc_sync":
117
- return syncWithMail(service, owner, context, args);
143
+ return service.sync({ ...owner, cursor: args.cursor ?? null,
144
+ scope: args.scope, limit: args.limit });
118
145
  case "acc_work":
119
- if (args.clear === true) return service.clearIntent({ ...owner });
146
+ if (args.clear === true) {
147
+ await service.clearIntent({ ...owner });
148
+ return { cleared: true };
149
+ }
120
150
  return service.setIntent({ ...owner, summary: args.summary, mode: args.mode,
121
- state: args.state, workstreamId: args.workstreamId ?? null,
122
- resourceHints: args.resourceHints ?? [] });
151
+ state: args.state, resourceHints: args.resourceHints ?? [] });
123
152
  case "acc_claim":
124
- if (args.action === "release") return service.releaseClaim({ ...owner,
125
- claimId: args.claimId }) ?? { released: args.claimId };
126
153
  if (args.action === "renew") return service.renewClaim({ ...owner,
127
154
  claimId: args.claimId, leaseSeconds: args.leaseSeconds });
128
155
  return service.acquireClaim({ ...owner, resource: args.resource,
129
156
  mode: args.mode ?? "exclusive", enforcement: "advisory",
130
157
  reason: args.reason ?? "unspecified", leaseSeconds: args.leaseSeconds });
158
+ case "acc_release":
159
+ await service.releaseClaim({ ...owner, claimId: args.claimId });
160
+ return { released: args.claimId };
131
161
  case "acc_message": {
132
- const message = await service.sendMessage({ ...owner, toParticipantIds: args.to ?? [],
133
- subject: args.subject, body: args.body, type: args.type ?? "note",
134
- priority: args.priority, requiresAck: args.requiresAck === true,
135
- workstreamId: args.workstreamId ?? null });
136
- // A note that reads like it wants a reply was sent fire-and-forget; hand
137
- // the nudge back with the message so the model that sent it can reconsider.
138
- const advice = noteNudge(message);
139
- return advice ? { ...message, advice } : message;
162
+ const kind = args.kind ?? "note";
163
+ const toParticipantIds = args.to ?? [];
164
+ const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
165
+ service.sendMessage({ ...owner, clientMessageId: clientMessageId(args, service),
166
+ toParticipantIds, subject: args.subject, body: args.body, kind,
167
+ obligation: obligationFor(kind, args.obligation, toParticipantIds.length > 0) }) });
168
+ const message = routed.recorded;
169
+ return { message, delivery: routed.delivery };
140
170
  }
141
171
  case "acc_inbox":
142
172
  return service.readInbox({ ...owner, messageId: args.messageId });
143
- case "acc_reply":
144
- return service.replyToMessage({ ...owner, messageId: args.messageId,
145
- body: args.body, subject: args.subject, type: args.type, priority: args.priority });
146
- case "acc_task":
147
- if (args.action === "claim") return service.claimTask({ ...owner,
148
- taskId: args.taskId, force: args.force === true });
149
- if (args.action === "decline") return service.declineTask({ ...owner,
150
- taskId: args.taskId, reason: args.reason });
151
- if (args.action === "transition") return service.transitionTask({ ...owner,
152
- taskId: args.taskId, state: args.state });
153
- return service.createTask({ ...owner, workstreamId: args.workstreamId,
154
- title: args.title, detail: args.detail, taskId: args.taskId,
155
- assigneeParticipantId: args.assigneeParticipantId,
156
- dependsOn: args.dependsOn ?? [] });
157
- case "acc_request":
158
- return service.requestWork({ ...owner, toParticipantId: args.toParticipantId,
159
- title: args.title, detail: args.detail, workstreamId: args.workstreamId,
160
- priority: args.priority, dependsOn: args.dependsOn ?? [] });
173
+ case "acc_reply": {
174
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
175
+ selectMessage: value => value.reply,
176
+ record: () => service.replyToMessage({ ...owner, messageId: args.messageId,
177
+ body: args.body, subject: args.subject,
178
+ clientMessageId: clientMessageId(args, service) }) });
179
+ return { message: routed.recorded.reply, delivery: routed.delivery };
180
+ }
181
+ case "acc_request": {
182
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
183
+ record: () => service.sendMessage({ ...owner,
184
+ clientMessageId: clientMessageId(args, service),
185
+ toParticipantIds: [args.toParticipantId], kind: "request", obligation: "reply",
186
+ subject: args.title, body: args.detail ?? args.title }) });
187
+ return { message: routed.recorded, delivery: routed.delivery };
188
+ }
161
189
  case "acc_ack":
162
- return service.markDelivery({ ...owner, messageId: args.messageId,
163
- state: args.state ?? "acknowledged" });
164
- case "acc_decide":
165
- return service.recordDecision({ ...owner, title: args.title, outcome: args.outcome,
166
- authority: args.authority ?? "workstream", workstreamId: args.workstreamId ?? null,
167
- decidedBy: args.decidedBy, supersedes: args.supersedes ?? null,
168
- humanConfirmed: args.humanConfirmed === true });
169
- case "acc_workstream":
170
- if (args.action === "coordinate") {
171
- return service.acquireCoordinator({ ...owner, workstreamId: args.workstreamId });
172
- }
173
- if (args.action === "release") {
174
- return service.releaseCoordinator({ ...owner, workstreamId: args.workstreamId });
175
- }
176
- return service.createWorkstream({ ...owner, title: args.title,
177
- objective: args.objective });
178
- case "acc_finish":
179
- return service.finishSession({ ...owner, goal: args.goal, status: args.status,
190
+ return service.acknowledgeMessage({ ...owner, messageId: args.messageId });
191
+ case "acc_finish": {
192
+ const routed = await recordAndOffer({ router: context.deliveryRouter,
193
+ selectMessage: value => value.message,
194
+ record: () => service.finishSession({ ...owner,
195
+ clientMessageId: clientMessageId(args, service), goal: args.goal, status: args.status,
180
196
  completed: args.completed ?? [], remaining: args.remaining ?? [],
181
- blockers: args.blockers ?? [], toParticipantId: args.toParticipantId ?? null });
197
+ blockers: args.blockers ?? [], toParticipantId: args.toParticipantId }) });
198
+ return { message: routed.recorded.message, delivery: routed.delivery };
199
+ }
182
200
  default:
183
201
  throw new AccError(EXIT.USAGE, `unknown tool: ${name}`, { name });
184
202
  }
@@ -198,15 +216,27 @@ async function handle(message, context) {
198
216
  case "resources/list":
199
217
  return complete({ resources: [...RESOURCES] });
200
218
  case "resources/read": {
201
- const value = await readResource(params.uri, context);
219
+ // Snapshot and roster are observation-only. Inbox is a delivery boundary:
220
+ // resolve this configured participant's durable session and let the core
221
+ // inbox service record that the returned bodies were retrieved.
222
+ const resourceContext = params.uri === "acc://inbox"
223
+ ? { ...context, session: await resolveSession(context) }
224
+ : context;
225
+ const value = await readResource(params.uri, resourceContext);
202
226
  return complete({ contents: [{ uri: params.uri, mimeType: "application/json",
203
227
  text: JSON.stringify(value, null, 2) }] });
204
228
  }
205
229
  case "tools/call": {
206
230
  try {
207
- const value = await callTool(params.name, params.arguments ?? {}, context);
231
+ const args = params.arguments === undefined ? {} : params.arguments;
232
+ const tool = PUBLIC_TOOLS.find(candidate => candidate.name === params.name);
233
+ if (tool === undefined) {
234
+ throw new AccError(EXIT.USAGE, `unknown tool: ${params.name}`, { name: params.name });
235
+ }
236
+ validateToolInput(tool.inputSchema, args);
237
+ const value = await callTool(params.name, args, context);
208
238
  return complete({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
209
- structuredContent: JSON.stringify(value) });
239
+ structuredContent: value });
210
240
  } catch (error) {
211
241
  // A failing operation is a tool result, not a transport failure: the
212
242
  // model must see it and be able to react.
@@ -1,6 +1,6 @@
1
- // The model-facing surface stays at six high-level operations. Granular
2
- // internal transitions remain available to adapters through the CLI and are
3
- // deliberately not advertised here.
1
+ import { GENERIC_MESSAGE_KINDS } from "@agents-can-communicate/protocol";
2
+
3
+ // The model-facing surface stays at a small set of high-level operations.
4
4
  //
5
5
  // Every description says that delivery is polled, because a tool description is
6
6
  // the only contract the model ever sees. MCP guarantees no lifecycle, no push,
@@ -20,12 +20,18 @@ const string = description => ({ type: "string", description });
20
20
  const stringList = description => ({ type: "array", items: { type: "string" }, description });
21
21
 
22
22
  export const PUBLIC_TOOLS = Object.freeze([
23
+ {
24
+ name: "acc_status",
25
+ description: `Read who is here, current intents and claims, and the workspace's real `
26
+ + `protection level. ${POLLED}`,
27
+ inputSchema: object({}),
28
+ },
23
29
  {
24
30
  name: "acc_sync",
25
31
  description: `Read coordination state for this workspace: roster, attention items, and `
26
32
  + `events since a cursor. Use scope "full" to answer questions about the whole `
27
- + `workspace, including other participants' collapsed child sessions. Pending mail is `
28
- + `also returned for compatibility; prefer acc_inbox for targeted reads. ${POLLED}`,
33
+ + `workspace, including other participants' collapsed child sessions. Use acc_inbox `
34
+ + `instead for addressed messages. ${POLLED}`,
29
35
  inputSchema: object({
30
36
  cursor: string("Resume from this cursor; omit to start from the beginning."),
31
37
  scope: { type: "string", enum: ["delta", "full"],
@@ -38,48 +44,62 @@ export const PUBLIC_TOOLS = Object.freeze([
38
44
  name: "acc_work",
39
45
  description: `Publish what this session is doing now as one concise Intent. Intent is `
40
46
  + `awareness, not authorisation: it never reserves a resource. ${POLLED}`,
41
- inputSchema: object({
47
+ inputSchema: { ...object({
42
48
  summary: string("One line describing the current work."),
43
49
  mode: { type: "string",
44
50
  enum: ["observe", "explore", "edit", "review", "coordinate", "wait"] },
45
51
  state: { type: "string", enum: ["active", "blocked", "waiting", "done"] },
46
- workstreamId: string("Optional workstream this work belongs to."),
47
52
  clear: { type: "boolean",
48
53
  description: "Say this session has stopped working on anything." },
49
54
  resourceHints: stringList("Advisory resource URIs, for example file:src/main.mjs."),
50
- }, ["summary", "mode"]),
55
+ }), oneOf: [
56
+ { required: ["clear"], properties: { clear: { const: true } },
57
+ not: { anyOf: ["summary", "mode", "state", "resourceHints"]
58
+ .map(field => ({ required: [field] })) } },
59
+ { required: ["summary", "mode"], not: { required: ["clear"] } },
60
+ ] },
51
61
  },
52
62
  {
53
63
  name: "acc_claim",
54
- description: `Acquire, renew, or release a claim on a resource URI. Claims are `
64
+ description: `Acquire or renew a claim on a resource URI. Claims are `
55
65
  + `workspace-wide and advisory here: this client has no write guard, so a claim `
56
66
  + `informs peers rather than preventing an edit. ${POLLED}`,
57
- inputSchema: object({
58
- resource: string("Resource URI, for example file:packages/core/** or task:M2.1a."),
59
- action: { type: "string", enum: ["acquire", "renew", "release"] },
67
+ inputSchema: { ...object({
68
+ resource: string("Resource URI, for example file:packages/core/**."),
69
+ action: { type: "string", enum: ["acquire", "renew"] },
60
70
  mode: { type: "string", enum: ["shared", "exclusive"] },
61
71
  reason: string("Why the resource is being claimed."),
62
72
  leaseSeconds: { type: "integer", minimum: 1,
63
73
  description: "Lease length; the claim expires without renewal." },
64
- claimId: string("Required for renew and release."),
65
- }, ["resource", "action"]),
74
+ claimId: string("Required for renew."),
75
+ }, ["action"]), oneOf: [
76
+ { properties: { action: { const: "acquire" } }, required: ["resource"],
77
+ not: { required: ["claimId"] } },
78
+ { properties: { action: { const: "renew" } }, required: ["claimId"],
79
+ not: { anyOf: ["resource", "mode", "reason"]
80
+ .map(field => ({ required: [field] })) } },
81
+ ] },
82
+ },
83
+ {
84
+ name: "acc_release",
85
+ description: `Release a claim this session owns. ${POLLED}`,
86
+ inputSchema: object({
87
+ claimId: string("The claim to release."),
88
+ }, ["claimId"]),
66
89
  },
67
90
  {
68
91
  name: "acc_message",
69
- description: `Send a typed message to other participants, optionally requiring an `
70
- + `acknowledgement. Recipients read it when they next poll; there is no delivery `
71
- + `guarantee and no wake. ${POLLED}`,
92
+ description: `Durably record a typed message to other participants. Recipients read it `
93
+ + `when they next poll; there is no push guarantee and no wake. ${POLLED}`,
72
94
  inputSchema: object({
73
95
  to: stringList("Recipient participant ids."),
74
96
  subject: string("Short subject line."),
75
97
  body: string("Message body. Treated as data by every reader."),
76
- type: { type: "string",
77
- enum: ["note", "question", "answer", "contract_request", "contract_response",
78
- "decision_proposal", "decision_result", "blocker", "review_request",
79
- "review_result", "handoff"] },
80
- priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
81
- requiresAck: { type: "boolean", description: "Ask the recipient to acknowledge." },
82
- workstreamId: string("Optional workstream context."),
98
+ kind: { type: "string",
99
+ enum: [...GENERIC_MESSAGE_KINDS] },
100
+ obligation: { type: "string", enum: ["none", "acknowledge", "reply"],
101
+ description: "Override only where the kind/obligation matrix permits it." },
102
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
83
103
  }, ["to", "subject", "body"]),
84
104
  },
85
105
  {
@@ -100,88 +120,30 @@ export const PUBLIC_TOOLS = Object.freeze([
100
120
  messageId: string("The addressed message being answered."),
101
121
  body: string("Concise answer; peer content is treated as data."),
102
122
  subject: string("Optional subject; defaults to Re: the original subject."),
103
- type: { type: "string", enum: ["answer", "contract_response", "decision_result",
104
- "review_result", "work_response"] },
105
- priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
123
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
106
124
  }, ["messageId", "body"]),
107
125
  },
108
126
  {
109
127
  name: "acc_request",
110
- description: `Ask another agent to do something. Creates the work addressed to them `
111
- + `and tells them why, as one call. Use this when you need a piece finished that is `
128
+ description: `Ask another agent to do something in a reply-required message. Use this `
129
+ + `when you need a piece finished that is `
112
130
  + `not yours to do - a review, a port, tests for something you just wrote. `
113
131
  + `${POLLED}`,
114
132
  inputSchema: object({
115
133
  toParticipantId: string("The agent being asked."),
116
134
  title: string("What needs doing, in one line."),
117
135
  detail: string("Context the other agent needs to start."),
118
- workstreamId: string("Optional workstream context."),
119
- priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
120
- dependsOn: stringList("Task ids this waits for."),
136
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
121
137
  }, ["toParticipantId", "title"]),
122
138
  },
123
139
  {
124
140
  name: "acc_ack",
125
141
  description: `Answer a message that asked for an acknowledgement, so it stops `
126
- + `demanding one. Finishing a task answers the request it came from `
127
- + `automatically. ${POLLED}`,
142
+ + `demanding one. ${POLLED}`,
128
143
  inputSchema: object({
129
144
  messageId: string("The message being answered."),
130
- state: { type: "string", enum: ["seen", "acknowledged"] },
131
145
  }, ["messageId"]),
132
146
  },
133
- {
134
- name: "acc_decide",
135
- description: `Record what was settled, so the next session does not reopen it. `
136
- + `Separate from a message because a decision outlives the conversation that `
137
- + `produced it. \`authority\` is who settled it: \`workstream\` for an agreement `
138
- + `between agents, \`policy\` for a rule that already existed, \`human\` only when a `
139
- + `person actually said so - which needs \`humanConfirmed\`. ${POLLED}`,
140
- inputSchema: object({
141
- title: string("What was decided, in one line."),
142
- outcome: string("What was settled, and enough of why to act on it."),
143
- authority: { type: "string", enum: ["workstream", "policy", "human"],
144
- description: "Default: workstream." },
145
- humanConfirmed: { type: "boolean",
146
- description: "A person said so. Required for human authority." },
147
- workstreamId: string("Optional workstream context."),
148
- supersedes: string("A decision this replaces."),
149
- decidedBy: stringList("Participants who settled it. Defaults to you."),
150
- }, ["title", "outcome"]),
151
- },
152
- {
153
- name: "acc_workstream",
154
- description: `Group related work so several agents can see it as one thing, or take `
155
- + `on steering one that exists. Optional: a single request needs no workstream. `
156
- + `An open workstream with no coordinator is reported to everyone until somebody `
157
- + `takes it. ${POLLED}`,
158
- inputSchema: object({
159
- action: { type: "string", enum: ["create", "coordinate", "release"],
160
- description: "Default: create." },
161
- title: string("Short name. Creating one."),
162
- objective: string("What finishing it would mean. Creating one."),
163
- workstreamId: string("The workstream to coordinate or hand back."),
164
- }, []),
165
- },
166
- {
167
- name: "acc_task",
168
- description: `Create or transition an optional task within a workstream. Tasks are for `
169
- + `work that needs assignment, dependencies, or acceptance tracking; ordinary work `
170
- + `needs only Intent. ${POLLED}`,
171
- inputSchema: object({
172
- action: { type: "string", enum: ["create", "claim", "transition", "decline"] },
173
- workstreamId: string("Workstream the task belongs to."),
174
- title: string("Task title, required when creating."),
175
- detail: string("Context for whoever picks it up."),
176
- assigneeParticipantId: string("Agent this is for. Only they can take it."),
177
- taskId: string("Required for claim and transition."),
178
- state: { type: "string", enum: ["pending", "in_progress", "review", "done", "blocked"] },
179
- dependsOn: stringList("Task ids this task waits for."),
180
- reason: string("Why, when declining."),
181
- force: { type: "boolean",
182
- description: "Take work held by a session that has gone quiet." },
183
- }, ["action"]),
184
- },
185
147
  {
186
148
  name: "acc_finish",
187
149
  description: `Record a handoff describing what was completed and what remains, and `
@@ -194,32 +156,27 @@ export const PUBLIC_TOOLS = Object.freeze([
194
156
  remaining: stringList("What is left."),
195
157
  blockers: stringList("What is in the way."),
196
158
  toParticipantId: string("Participant taking over, if any."),
159
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
197
160
  }, ["goal"]),
198
161
  },
199
162
  ]);
200
163
 
201
164
  export const RESOURCES = Object.freeze([
202
165
  { uri: "acc://snapshot", name: "Workspace snapshot", mimeType: "application/json",
203
- description: "The whole coordination state: participants, intents, claims, tasks." },
166
+ description: "The whole coordination state: participants, intents, claims, and messages." },
204
167
  { uri: "acc://roster", name: "Participant roster", mimeType: "application/json",
205
168
  description: "Sessions with their harness and presence, including collapsed children." },
206
- { uri: "acc://workstreams", name: "Workstreams", mimeType: "application/json",
207
- description: "Open workstreams and their coordinator lease, if any." },
208
- { uri: "acc://tasks", name: "Tasks", mimeType: "application/json",
209
- description: "Tasks with state, assignee, and dependencies." },
210
169
  { uri: "acc://inbox", name: "Inbox", mimeType: "application/json",
211
170
  description: "Messages addressed to this participant, rendered as attributed data." },
212
171
  ]);
213
172
 
214
- // Declared, not assumed. MCP is a polling transport with no lifecycle contract,
215
- // so everything except polling stays false.
173
+ // Declared, not assumed. Manual MCP tool polling is not next-turn injection,
174
+ // live push, or a native reply route, so every adapter delivery mode is false.
216
175
  export const MCP_CAPABILITIES = Object.freeze({
217
176
  lifecycle: Object.freeze({ sessionStart: false, sessionResume: false, sessionEnd: false,
218
177
  childSessions: false }),
219
178
  context: Object.freeze({ startupInjection: false, beforeTurnInjection: false,
220
179
  safePointInjection: false }),
221
180
  guards: Object.freeze({ beforeRead: false, beforeWrite: false, beforeShell: false }),
222
- delivery: Object.freeze({ polling: true, activeNotification: false,
223
- wakeDormantSession: false }),
224
- execution: Object.freeze({ launch: false, resume: false, terminate: false }),
181
+ delivery: Object.freeze({ nextTurn: false, livePush: false, replyRoute: false }),
225
182
  });