agents-can-communicate 0.1.18 → 0.3.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 (155) hide show
  1. package/README.md +87 -69
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-bootstrap.mjs +56 -0
  4. package/bin/acc-claude-channel.mjs +177 -0
  5. package/bin/acc-hook.mjs +94 -12
  6. package/bin/acc-mcp.mjs +6 -2
  7. package/bin/acc.mjs +13 -3
  8. package/docs/ADAPTER_AUTHORING.md +204 -0
  9. package/docs/ARCHITECTURE.md +131 -0
  10. package/docs/CAPABILITIES.md +117 -214
  11. package/docs/CLI.md +164 -0
  12. package/docs/CONCEPTS.md +134 -0
  13. package/docs/CONFIGURATION.md +147 -0
  14. package/docs/DESIGN_DECISIONS.md +89 -0
  15. package/docs/GETTING_STARTED.md +145 -0
  16. package/docs/GLOSSARY.md +26 -0
  17. package/docs/HOW_IT_WORKS.md +277 -0
  18. package/docs/MCP.md +94 -0
  19. package/docs/PROTOCOL.md +200 -0
  20. package/docs/RELEASING.md +115 -0
  21. package/docs/SECURITY_MODEL.md +131 -0
  22. package/docs/TROUBLESHOOTING.md +108 -0
  23. package/docs/WHY_ACC.md +61 -0
  24. package/docs/index.md +44 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +269 -0
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
  33. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
  34. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
  35. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -2
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +22 -22
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
  42. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +150 -0
  43. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  44. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  45. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  46. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +199 -0
  48. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
  49. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
  50. package/node_modules/@agents-can-communicate/adapter-codex/package.json +11 -2
  51. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  52. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +22 -22
  53. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
  54. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
  55. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
  56. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
  57. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +68 -0
  58. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +22 -22
  60. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
  61. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
  64. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
  65. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -0
  66. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  67. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +31 -13
  68. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  69. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  70. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  71. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
  72. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
  73. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  74. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  75. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  76. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  77. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  78. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  79. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  80. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  81. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +22 -22
  82. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  83. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -18
  85. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  86. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  87. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
  88. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -2
  89. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
  90. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
  91. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
  92. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +28 -4
  93. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  94. package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
  95. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
  96. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  97. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
  98. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  99. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  101. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  102. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +131 -0
  103. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  104. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  105. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  106. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  107. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  108. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  109. package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
  110. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  111. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  112. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  113. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  114. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  115. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +131 -0
  116. package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
  117. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  118. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
  119. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +190 -105
  120. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
  122. package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
  123. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
  124. package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
  125. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
  126. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
  127. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
  128. package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
  129. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  130. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  131. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  132. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  133. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  134. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  135. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  136. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  137. package/node_modules/@agents-can-communicate/protocol/src/fields.mjs +17 -0
  138. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  139. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -90
  140. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  141. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  142. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  143. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  144. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  145. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  146. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  147. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  148. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  149. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  150. package/package.json +20 -1
  151. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  152. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  153. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  154. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  155. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -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
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.1.18",
3
+ "version": "0.3.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -12,7 +12,7 @@ export const CONFIG_FILENAME = "acc.workspace.json";
12
12
  // the wrong place for them, and a config carrying them is either a mistake or
13
13
  // an attempt to hand a peer state it would otherwise have to earn.
14
14
  export const RUNTIME_KEYS = Object.freeze(["sessions", "participants", "messages",
15
- "claims", "receipts", "intents", "events", "tokens", "credentials"]);
15
+ "claims", "receipts", "intents", "events", "deliveryBindings", "tokens", "credentials"]);
16
16
 
17
17
  const KNOWN_KEYS = Object.freeze(["schemaVersion", "workspaceId", "displayName",
18
18
  "roots", "policy", "requiredAdapters", "extensions"]);
@@ -0,0 +1,64 @@
1
+ import { AccError, EXIT } from "./errors.mjs";
2
+
3
+ export const VALID_OBLIGATIONS = Object.freeze({
4
+ note: Object.freeze(["none"]),
5
+ question: Object.freeze(["reply"]),
6
+ request: Object.freeze(["reply"]),
7
+ answer: Object.freeze(["none"]),
8
+ decision: Object.freeze(["none", "acknowledge"]),
9
+ handoff: Object.freeze(["none", "acknowledge"]),
10
+ });
11
+
12
+ export const MESSAGE_KINDS = Object.freeze(Object.keys(VALID_OBLIGATIONS));
13
+ // The generic send surface cannot construct reply links or structured handoff
14
+ // payloads. Those two complete shapes belong to reply and finish respectively.
15
+ export const GENERIC_MESSAGE_KINDS = Object.freeze(
16
+ ["note", "question", "request", "decision"]);
17
+ export const OBLIGATIONS = Object.freeze(["none", "acknowledge", "reply"]);
18
+
19
+ const data = (message, details) => {
20
+ throw new AccError(EXIT.DATA, message, details);
21
+ };
22
+
23
+ export function assertMessageSemantics(message) {
24
+ const room = message.toParticipantIds.length === 0;
25
+ if (room && !["note", "decision", "handoff"].includes(message.kind)) {
26
+ data(`a room message cannot have kind ${message.kind}`, { kind: message.kind });
27
+ }
28
+ if (room && message.obligation !== "none") {
29
+ data("a room message must have obligation none", { obligation: message.obligation });
30
+ }
31
+
32
+ const valid = VALID_OBLIGATIONS[message.kind] ?? [];
33
+ if (!valid.includes(message.obligation)) {
34
+ data(`message obligation ${message.obligation} is invalid for ${message.kind}`,
35
+ { kind: message.kind, obligation: message.obligation });
36
+ }
37
+
38
+ const root = message.threadId === message.messageId;
39
+ if (message.kind === "answer" && message.inReplyTo === null) {
40
+ data("an answer requires inReplyTo", { messageId: message.messageId });
41
+ }
42
+ if (message.kind === "answer" && root) {
43
+ data("an answer cannot be a thread root", { messageId: message.messageId });
44
+ }
45
+ if (message.inReplyTo === null && !root) {
46
+ data("a thread root requires threadId to equal messageId",
47
+ { threadId: message.threadId, messageId: message.messageId });
48
+ }
49
+ if (message.inReplyTo !== null && root) {
50
+ data("a thread root must have inReplyTo null", { inReplyTo: message.inReplyTo });
51
+ }
52
+
53
+ if (message.kind === "handoff" && message.handoff === null) {
54
+ data("a handoff message requires a handoff payload", { messageId: message.messageId });
55
+ }
56
+ if (!room && message.kind === "handoff" && message.obligation !== "acknowledge") {
57
+ data("an addressed handoff requires acknowledgement",
58
+ { messageId: message.messageId, obligation: message.obligation });
59
+ }
60
+ if (message.kind !== "handoff" && message.handoff !== null) {
61
+ data(`a ${message.kind} message must have handoff null`, { kind: message.kind });
62
+ }
63
+ return message;
64
+ }
@@ -74,6 +74,23 @@ export const listOf = inner => (value, field) => {
74
74
  export const nullable = inner => (value, field) =>
75
75
  (value === null ? null : inner(value, field));
76
76
 
77
+ /**
78
+ * A field that may be absent entirely.
79
+ *
80
+ * For a fact that either happened or did not, absence is the honest encoding of
81
+ * "not yet" - and it is what a record written before the field existed already
82
+ * says. Every record carries a schemaVersion, but adding a required field to a
83
+ * kind that outlives an upgrade turns a stored record into an unreadable one:
84
+ * measured, one binding written by the previous build made `acc status` fail
85
+ * outright with "deliveryBinding requires retiredAt". A coordination tool must
86
+ * not refuse to read its own store because it was upgraded.
87
+ */
88
+ export const optional = inner => {
89
+ const check = (value, field) => (value === undefined ? undefined : inner(value, field));
90
+ check.optional = true;
91
+ return check;
92
+ };
93
+
77
94
  export const positiveInteger = (value, field) => {
78
95
  if (!Number.isSafeInteger(value) || value <= 0) {
79
96
  invalid(field, "must be a positive integer", value);
@@ -3,7 +3,10 @@ export { AccError, EXIT, isAccError } from "./errors.mjs";
3
3
  export { assertPortableId, createId } from "./ids.mjs";
4
4
  export { ENVELOPE_VERSION, failure, ok } from "./envelopes.mjs";
5
5
  export { RECORD_KINDS, SCHEMA_VERSION, validateRecord } from "./schema.mjs";
6
+ export { GENERIC_MESSAGE_KINDS, MESSAGE_KINDS, OBLIGATIONS, VALID_OBLIGATIONS,
7
+ assertMessageSemantics }
8
+ from "./conversations.mjs";
6
9
  export { CONFIG_FILENAME, CONFIG_SCHEMA_VERSION, RUNTIME_KEYS, defaultProjectConfig,
7
10
  validateProjectConfig } from "./config.mjs";
8
- export { DELIVERY_STATES, TASK_STATES, advanceDelivery, transitionTask } from "./states.mjs";
11
+ export { RECEIPT_STATES, advanceReceipt } from "./states.mjs";
9
12
  export { assertMatchableResource, normaliseResource } from "./resources.mjs";
@@ -1,8 +1,9 @@
1
+ import { assertMessageSemantics, MESSAGE_KINDS, OBLIGATIONS } from "./conversations.mjs";
1
2
  import { AccError, EXIT } from "./errors.mjs";
2
- import { flag, id, invalid, listOf, nullable, oneOf, plainObject, positiveInteger,
3
+ import { id, invalid, listOf, nullable, oneOf, optional, plainObject, positiveInteger,
3
4
  resourceUri, sequence, text, timestamp } from "./fields.mjs";
4
5
 
5
- export const SCHEMA_VERSION = 2;
6
+ export const SCHEMA_VERSION = 3;
6
7
 
7
8
  const line = text();
8
9
  const prose = text({ max: 4000, multiline: true });
@@ -14,10 +15,22 @@ const artifactKind = oneOf("file", "git", "url", "report", "image", "data");
14
15
  /** @typedef {{ cursor: string, events: AccEvent[] }} EventPage */
15
16
  /** @typedef {{ kind: string, priority: number, sourceId: string, summary: string }} AttentionItem */
16
17
  /** @typedef {{ workspace: object, participants: object[], sessions: object[],
17
- * intents: object[], workstreams: object[], tasks: object[], claims: object[] }} WorkspaceSnapshot */
18
+ * intents: object[], claims: object[], messages: object[], receipts: object[] }} WorkspaceSnapshot */
19
+
20
+ function closedObject(value, field, fields) {
21
+ plainObject(value, field);
22
+ for (const key of Object.keys(value)) {
23
+ if (!Object.hasOwn(fields, key)) invalid(`${field}.${key}`, "is not a known field", value[key]);
24
+ }
25
+ for (const [key, check] of Object.entries(fields)) {
26
+ if (!Object.hasOwn(value, key)) {
27
+ throw new AccError(EXIT.DATA, `${field} requires ${key}`, { field, key });
28
+ }
29
+ check(value[key], `${field}.${key}`);
30
+ }
31
+ return value;
32
+ }
18
33
 
19
- // Field names stay mappable to the A2A Agent Card, Task, Message, and Artifact
20
- // concepts (spec section 11) without importing any A2A transport.
21
34
  const artifactRef = (value, field) => {
22
35
  plainObject(value, field);
23
36
  const known = new Set(["kind", "uri", "description", "sha256"]);
@@ -33,120 +46,84 @@ const artifactRef = (value, field) => {
33
46
  return value;
34
47
  };
35
48
 
36
- // Every event ACC itself appends. Closed on purpose: `type` used to be free
37
- // text, so a record written by hand validated cleanly and `acc doctor` called
38
- // the store healthy. That is not theoretical - a session that could not run the
39
- // CLI wrote its own events, inventing `task.completed`, and the store reported
40
- // no problem.
49
+ const handoffPayload = (value, field) => closedObject(value, field, {
50
+ status: oneOf("complete", "partial", "blocked"),
51
+ completed: listOf(line),
52
+ remaining: listOf(line),
53
+ blockers: listOf(line),
54
+ verification: listOf(artifactRef),
55
+ });
56
+
41
57
  const EVENT_TYPES = Object.freeze([
42
58
  "workspace.materialised",
43
59
  "session.opened", "session.closed",
44
60
  "intent.published", "intent.cleared",
45
- "workstream.created", "workstream.coordinator_acquired",
46
- "workstream.coordinator_released",
47
- "task.created", "task.claimed", "task.transitioned", "task.unblocked",
48
- "task.declined", "task.released",
49
61
  "claim.acquired", "claim.released", "claim.renewed", "claim.force_released",
50
- "message.sent", "decision.recorded", "handoff.created",
51
- // Delivery transitions and request outcomes are templated from their state,
52
- // so the set has to carry each one they can produce.
53
- ...["recorded", "queued", "injected", "seen", "acknowledged", "failed"]
54
- .map(state => `message.${state}`),
55
- ...["accepted", "declined", "review", "done", "released"]
56
- .map(outcome => `work.${outcome}`),
57
- "work.requested",
62
+ "message.recorded", "message.offered", "message.retrieved", "message.acknowledged",
63
+ "message.offer_succeeded", "message.offer_failed",
58
64
  ]);
59
65
 
60
66
  const eventType = oneOf(...EVENT_TYPES);
61
67
 
62
- const RECORDS = Object.freeze({
68
+ // nextTurn is the durable hook projection; the other four are what a native
69
+ // transport proved for one session generation. A mode listed twice would let a
70
+ // reader count capabilities it does not have.
71
+ export const DELIVERY_MODES = Object.freeze(["nextTurn", "livePush", "idleWake", "busyQueue",
72
+ "replyRoute"]);
73
+ const uniqueListOf = check => (value, field) => {
74
+ listOf(check)(value, field);
75
+ if (new Set(value).size !== value.length) invalid(field, "must not repeat entries", value);
76
+ };
77
+ const receiptState = oneOf("queued", "offered", "retrieved", "acknowledged");
78
+
79
+ const DURABLE_RECORDS = Object.freeze({
63
80
  workspace: { workspaceId: id, displayName: line, source: oneOf("config", "git", "directory"),
64
81
  roots: listOf(line), createdAt: timestamp },
65
82
 
66
83
  participant: { participantId: id, workspaceId: id, displayName: line,
67
84
  kind: oneOf("agent", "human"), createdAt: timestamp },
68
85
 
69
- // `enforcement` and `lifecycle` are what this session's harness can actually
70
- // do, declared at attach. The harness name does not imply them: the same
71
- // client guards or does not depending on its model and its approval mode, and
72
- // a peer deciding whether to rely on a claim needs the answer, not the brand.
73
- // A workspace spans every worktree of one repository, so the workspace id
74
- // cannot say which checkout a session is sitting in. Recorded at attach from
75
- // what discovery already resolved: without it nobody can tell which worktrees
76
- // have an owner, and asking cannot answer for the agents that are not running
77
- // - which are exactly the ones a clean-up is looking for.
78
86
  session: { sessionId: id, participantId: id, workspaceId: id, generation: id,
79
87
  harness: line, state: oneOf("open", "closed"), parentSessionId: nullable(id),
80
- checkoutRoot: nullable(line), branch: nullable(line),
81
- // The process behind this session, when it can be named. Null means nobody
82
- // knows - no process table, or an ancestry that did not resolve - and is
83
- // read as "judge this one by age alone", never as "dead".
84
- pid: nullable(positiveInteger),
88
+ checkoutRoot: nullable(line), branch: nullable(line), pid: nullable(positiveInteger),
85
89
  enforcement: oneOf("guarded", "advisory"), lifecycle: oneOf("managed", "manual"),
86
90
  heartbeatCadenceMs: positiveInteger, startedAt: timestamp, heartbeatAt: timestamp },
87
91
 
88
92
  intent: { sessionId: id, workspaceId: id, summary,
89
93
  mode: oneOf("observe", "explore", "edit", "review", "coordinate", "wait"),
90
- resourceHints: listOf(resourceUri), workstreamId: nullable(id),
94
+ resourceHints: listOf(resourceUri),
91
95
  state: oneOf("active", "blocked", "waiting", "done"), updatedAt: timestamp },
92
96
 
93
- workstream: { workstreamId: id, workspaceId: id, title: line, objective: prose,
94
- coordinatorSessionId: nullable(id),
95
- state: oneOf("open", "paused", "complete", "cancelled"), createdAt: timestamp },
96
-
97
- // Two assignees, deliberately. `assigneeParticipantId` is who the work is
98
- // for and survives that agent restarting; `assigneeSessionId` is the exact
99
- // session doing it right now and dies with the process. Asking one field to
100
- // be both would either lose the request when a terminal closes or claim a
101
- // dead session is still working.
102
- task: { taskId: id, workstreamId: nullable(id), workspaceId: id, title: line,
103
- state: oneOf("pending", "in_progress", "review", "done", "blocked"),
104
- assigneeParticipantId: nullable(id), assigneeSessionId: nullable(id),
105
- // Who asked. Without it nothing could tell the requester that their work
106
- // was accepted, declined or finished - the task knew who it was for and
107
- // had no idea who was waiting on it.
108
- requestedByParticipantId: nullable(id),
109
- dependsOn: listOf(id), acceptance: listOf(line), detail: nullable(prose),
110
- createdAt: timestamp },
111
-
112
97
  claim: { claimId: id, workspaceId: id, ownerSessionId: id, resource: resourceUri,
113
98
  mode: oneOf("shared", "exclusive"), enforcement: oneOf("advisory", "guarded"),
114
99
  reason: line, acquiredAt: timestamp, expiresAt: timestamp, generation: id },
115
100
 
116
- // `fromParticipantId` beside the session: a session ends, and the one fact
117
- // that has to outlive it is who was speaking. Resolving the sender by looking
118
- // its session up meant the record could never be retired, and an agent whose
119
- // client had restarted stopped being told about its own unanswered question.
120
- message: { messageId: id, workspaceId: id, fromSessionId: id,
121
- fromParticipantId: id, toParticipantIds: listOf(id),
122
- type: oneOf("note", "question", "answer", "contract_request", "contract_response",
123
- "decision_proposal", "decision_result", "blocker", "review_request",
124
- "review_result", "handoff", "work_request", "work_response"),
125
- subject: line, body: prose, priority: oneOf("low", "normal", "high", "urgent"),
126
- workstreamId: nullable(id), taskId: nullable(id), inReplyTo: nullable(id),
127
- requiresAck: flag, artifacts: listOf(artifactRef), sentAt: timestamp },
101
+ message: { messageId: id, threadId: id, clientMessageId: id, workspaceId: id,
102
+ fromParticipantId: id, fromSessionId: id, toParticipantIds: listOf(id),
103
+ kind: oneOf(...MESSAGE_KINDS), obligation: oneOf(...OBLIGATIONS),
104
+ subject: line, body: prose, inReplyTo: nullable(id), artifacts: listOf(artifactRef),
105
+ handoff: nullable(handoffPayload), sentAt: timestamp },
128
106
 
129
107
  receipt: { messageId: id, workspaceId: id, recipientParticipantId: id,
130
- state: oneOf("recorded", "queued", "injected", "seen", "acknowledged", "failed"),
131
- updatedAt: timestamp },
132
-
133
- decision: { decisionId: id, workspaceId: id, workstreamId: nullable(id), title: line,
134
- outcome: prose, authority: oneOf("human", "workstream", "policy"),
135
- decidedBy: listOf(id), evidence: listOf(artifactRef), supersedes: nullable(id),
136
- decidedAt: timestamp },
137
-
138
- artifact: { kind: artifactKind, uri: resourceUri, description: line },
139
-
140
- handoff: { handoffId: id, workspaceId: id, fromSessionId: id, toParticipantId: nullable(id),
141
- goal: line, status: oneOf("complete", "partial", "blocked"), completed: listOf(line),
142
- remaining: listOf(line), blockers: listOf(line), claimsToRelease: listOf(resourceUri),
143
- verification: listOf(artifactRef), artifacts: listOf(artifactRef), createdAt: timestamp },
108
+ state: receiptState, updatedAt: timestamp },
144
109
 
145
110
  event: { sequence, eventId: id, workspaceId: id, actorSessionId: id, type: eventType,
146
111
  occurredAt: timestamp, payload: plainObject },
147
112
  });
148
113
 
149
- export const RECORD_KINDS = Object.freeze(Object.keys(RECORDS));
114
+ const RECORDS = Object.freeze({
115
+ ...DURABLE_RECORDS,
116
+ deliveryBinding: { sessionId: id, generation: id, adapterId: id, clientVersion: line,
117
+ availableModes: uniqueListOf(oneOf(...DELIVERY_MODES)),
118
+ livePolicy: oneOf("off", "actionable", "all"), opaqueEndpointRef: prose,
119
+ // The lease says how long the endpoint's owner has vouched for it; the
120
+ // retirement says the session gave it up. They were once the same field -
121
+ // clearing expired the lease - which made a renewal by a channel that had
122
+ // not yet noticed indistinguishable from a legitimate extension.
123
+ leaseUntil: timestamp, retiredAt: optional(nullable(timestamp)) },
124
+ });
125
+
126
+ export const RECORD_KINDS = Object.freeze(Object.keys(DURABLE_RECORDS));
150
127
 
151
128
  export function validateRecord(kind, value) {
152
129
  const fields = RECORDS[kind];
@@ -161,19 +138,16 @@ export function validateRecord(kind, value) {
161
138
  }
162
139
  for (const key of Object.keys(value)) {
163
140
  if (key === "schemaVersion" || key === "extensions") continue;
164
- if (!Object.hasOwn(fields, key)) {
165
- invalid(`${kind}.${key}`, "is not a known field", value[key]);
166
- }
141
+ if (!Object.hasOwn(fields, key)) invalid(`${kind}.${key}`, "is not a known field", value[key]);
167
142
  }
168
143
  for (const [field, check] of Object.entries(fields)) {
169
144
  if (!Object.hasOwn(value, field)) {
145
+ if (check.optional === true) continue;
170
146
  throw new AccError(EXIT.DATA, `${kind} requires ${field}`, { kind, field });
171
147
  }
172
148
  check(value[field], field);
173
149
  }
174
- // Forward-compatible metadata is tolerated only inside a named container, so
175
- // an older reader can round-trip a newer writer's record without guessing
176
- // which unknown top-level keys are safe.
177
150
  if (Object.hasOwn(value, "extensions")) plainObject(value.extensions, "extensions");
151
+ if (kind === "message") assertMessageSemantics(value);
178
152
  return value;
179
153
  }