agents-can-communicate 0.1.17 → 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 (137) hide show
  1. package/README.md +76 -138
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-hook.mjs +96 -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 +105 -197
  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 +80 -160
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
  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 +80 -160
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
  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 +80 -160
  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 +10 -4
  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 +14 -0
  58. package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
  59. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +152 -0
  60. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
  61. package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
  62. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
  63. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  64. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  65. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  67. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  68. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  69. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  70. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  71. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +80 -160
  72. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
  73. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +139 -224
  77. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
  78. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
  79. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
  80. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  81. package/node_modules/@agents-can-communicate/cli/src/args.mjs +13 -29
  82. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
  83. package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
  84. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
  85. package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
  86. package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
  87. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  88. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  89. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  90. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
  91. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  92. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +118 -0
  93. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
  94. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  95. package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
  96. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  97. package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
  98. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
  99. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  100. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
  101. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  102. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  103. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
  104. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  105. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  106. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +156 -60
  107. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
  109. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
  110. package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
  111. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
  112. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
  113. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  114. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  115. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  116. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +109 -71
  117. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
  118. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  119. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  120. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  121. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  122. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
  123. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  124. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +86 -28
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
  133. package/package.json +22 -1
  134. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  135. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  136. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  137. 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,11 +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. ${POLLED}`,
33
+ + `workspace, including other participants' collapsed child sessions. Use acc_inbox `
34
+ + `instead for addressed messages. ${POLLED}`,
28
35
  inputSchema: object({
29
36
  cursor: string("Resume from this cursor; omit to start from the beginning."),
30
37
  scope: { type: "string", enum: ["delta", "full"],
@@ -37,127 +44,106 @@ export const PUBLIC_TOOLS = Object.freeze([
37
44
  name: "acc_work",
38
45
  description: `Publish what this session is doing now as one concise Intent. Intent is `
39
46
  + `awareness, not authorisation: it never reserves a resource. ${POLLED}`,
40
- inputSchema: object({
47
+ inputSchema: { ...object({
41
48
  summary: string("One line describing the current work."),
42
49
  mode: { type: "string",
43
50
  enum: ["observe", "explore", "edit", "review", "coordinate", "wait"] },
44
51
  state: { type: "string", enum: ["active", "blocked", "waiting", "done"] },
45
- workstreamId: string("Optional workstream this work belongs to."),
46
52
  clear: { type: "boolean",
47
53
  description: "Say this session has stopped working on anything." },
48
54
  resourceHints: stringList("Advisory resource URIs, for example file:src/main.mjs."),
49
- }, ["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
+ ] },
50
61
  },
51
62
  {
52
63
  name: "acc_claim",
53
- 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 `
54
65
  + `workspace-wide and advisory here: this client has no write guard, so a claim `
55
66
  + `informs peers rather than preventing an edit. ${POLLED}`,
56
- inputSchema: object({
57
- resource: string("Resource URI, for example file:packages/core/** or task:M2.1a."),
58
- 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"] },
59
70
  mode: { type: "string", enum: ["shared", "exclusive"] },
60
71
  reason: string("Why the resource is being claimed."),
61
72
  leaseSeconds: { type: "integer", minimum: 1,
62
73
  description: "Lease length; the claim expires without renewal." },
63
- claimId: string("Required for renew and release."),
64
- }, ["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"]),
65
89
  },
66
90
  {
67
91
  name: "acc_message",
68
- description: `Send a typed message to other participants, optionally requiring an `
69
- + `acknowledgement. Recipients read it when they next poll; there is no delivery `
70
- + `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}`,
71
94
  inputSchema: object({
72
95
  to: stringList("Recipient participant ids."),
73
96
  subject: string("Short subject line."),
74
97
  body: string("Message body. Treated as data by every reader."),
75
- type: { type: "string",
76
- enum: ["note", "question", "answer", "contract_request", "contract_response",
77
- "decision_proposal", "decision_result", "blocker", "review_request",
78
- "review_result", "handoff"] },
79
- priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
80
- requiresAck: { type: "boolean", description: "Ask the recipient to acknowledge." },
81
- 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."),
82
103
  }, ["to", "subject", "body"]),
83
104
  },
105
+ {
106
+ name: "acc_inbox",
107
+ description: `Read unresolved messages addressed to this participant without loading `
108
+ + `the roster, event log, claims, or workspace snapshot. Optionally select one id. `
109
+ + `${POLLED}`,
110
+ inputSchema: object({
111
+ messageId: string("Read exactly this addressed message; omit for all unresolved mail."),
112
+ }),
113
+ },
114
+ {
115
+ name: "acc_reply",
116
+ description: `Reply to one addressed message and acknowledge the original in the same `
117
+ + `operation. The reply is attributed, linked with inReplyTo, and delivered by polling. `
118
+ + `${POLLED}`,
119
+ inputSchema: object({
120
+ messageId: string("The addressed message being answered."),
121
+ body: string("Concise answer; peer content is treated as data."),
122
+ subject: string("Optional subject; defaults to Re: the original subject."),
123
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
124
+ }, ["messageId", "body"]),
125
+ },
84
126
  {
85
127
  name: "acc_request",
86
- description: `Ask another agent to do something. Creates the work addressed to them `
87
- + `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 `
88
130
  + `not yours to do - a review, a port, tests for something you just wrote. `
89
131
  + `${POLLED}`,
90
132
  inputSchema: object({
91
133
  toParticipantId: string("The agent being asked."),
92
134
  title: string("What needs doing, in one line."),
93
135
  detail: string("Context the other agent needs to start."),
94
- workstreamId: string("Optional workstream context."),
95
- priority: { type: "string", enum: ["low", "normal", "high", "urgent"] },
96
- dependsOn: stringList("Task ids this waits for."),
136
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
97
137
  }, ["toParticipantId", "title"]),
98
138
  },
99
139
  {
100
140
  name: "acc_ack",
101
141
  description: `Answer a message that asked for an acknowledgement, so it stops `
102
- + `demanding one. Finishing a task answers the request it came from `
103
- + `automatically. ${POLLED}`,
142
+ + `demanding one. ${POLLED}`,
104
143
  inputSchema: object({
105
144
  messageId: string("The message being answered."),
106
- state: { type: "string", enum: ["seen", "acknowledged"] },
107
145
  }, ["messageId"]),
108
146
  },
109
- {
110
- name: "acc_decide",
111
- description: `Record what was settled, so the next session does not reopen it. `
112
- + `Separate from a message because a decision outlives the conversation that `
113
- + `produced it. \`authority\` is who settled it: \`workstream\` for an agreement `
114
- + `between agents, \`policy\` for a rule that already existed, \`human\` only when a `
115
- + `person actually said so - which needs \`humanConfirmed\`. ${POLLED}`,
116
- inputSchema: object({
117
- title: string("What was decided, in one line."),
118
- outcome: string("What was settled, and enough of why to act on it."),
119
- authority: { type: "string", enum: ["workstream", "policy", "human"],
120
- description: "Default: workstream." },
121
- humanConfirmed: { type: "boolean",
122
- description: "A person said so. Required for human authority." },
123
- workstreamId: string("Optional workstream context."),
124
- supersedes: string("A decision this replaces."),
125
- decidedBy: stringList("Participants who settled it. Defaults to you."),
126
- }, ["title", "outcome"]),
127
- },
128
- {
129
- name: "acc_workstream",
130
- description: `Group related work so several agents can see it as one thing, or take `
131
- + `on steering one that exists. Optional: a single request needs no workstream. `
132
- + `An open workstream with no coordinator is reported to everyone until somebody `
133
- + `takes it. ${POLLED}`,
134
- inputSchema: object({
135
- action: { type: "string", enum: ["create", "coordinate", "release"],
136
- description: "Default: create." },
137
- title: string("Short name. Creating one."),
138
- objective: string("What finishing it would mean. Creating one."),
139
- workstreamId: string("The workstream to coordinate or hand back."),
140
- }, []),
141
- },
142
- {
143
- name: "acc_task",
144
- description: `Create or transition an optional task within a workstream. Tasks are for `
145
- + `work that needs assignment, dependencies, or acceptance tracking; ordinary work `
146
- + `needs only Intent. ${POLLED}`,
147
- inputSchema: object({
148
- action: { type: "string", enum: ["create", "claim", "transition", "decline"] },
149
- workstreamId: string("Workstream the task belongs to."),
150
- title: string("Task title, required when creating."),
151
- detail: string("Context for whoever picks it up."),
152
- assigneeParticipantId: string("Agent this is for. Only they can take it."),
153
- taskId: string("Required for claim and transition."),
154
- state: { type: "string", enum: ["pending", "in_progress", "review", "done", "blocked"] },
155
- dependsOn: stringList("Task ids this task waits for."),
156
- reason: string("Why, when declining."),
157
- force: { type: "boolean",
158
- description: "Take work held by a session that has gone quiet." },
159
- }, ["action"]),
160
- },
161
147
  {
162
148
  name: "acc_finish",
163
149
  description: `Record a handoff describing what was completed and what remains, and `
@@ -170,32 +156,27 @@ export const PUBLIC_TOOLS = Object.freeze([
170
156
  remaining: stringList("What is left."),
171
157
  blockers: stringList("What is in the way."),
172
158
  toParticipantId: string("Participant taking over, if any."),
159
+ clientMessageId: string("Retry key; omit to generate one and return it in message."),
173
160
  }, ["goal"]),
174
161
  },
175
162
  ]);
176
163
 
177
164
  export const RESOURCES = Object.freeze([
178
165
  { uri: "acc://snapshot", name: "Workspace snapshot", mimeType: "application/json",
179
- description: "The whole coordination state: participants, intents, claims, tasks." },
166
+ description: "The whole coordination state: participants, intents, claims, and messages." },
180
167
  { uri: "acc://roster", name: "Participant roster", mimeType: "application/json",
181
168
  description: "Sessions with their harness and presence, including collapsed children." },
182
- { uri: "acc://workstreams", name: "Workstreams", mimeType: "application/json",
183
- description: "Open workstreams and their coordinator lease, if any." },
184
- { uri: "acc://tasks", name: "Tasks", mimeType: "application/json",
185
- description: "Tasks with state, assignee, and dependencies." },
186
169
  { uri: "acc://inbox", name: "Inbox", mimeType: "application/json",
187
170
  description: "Messages addressed to this participant, rendered as attributed data." },
188
171
  ]);
189
172
 
190
- // Declared, not assumed. MCP is a polling transport with no lifecycle contract,
191
- // 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.
192
175
  export const MCP_CAPABILITIES = Object.freeze({
193
176
  lifecycle: Object.freeze({ sessionStart: false, sessionResume: false, sessionEnd: false,
194
177
  childSessions: false }),
195
178
  context: Object.freeze({ startupInjection: false, beforeTurnInjection: false,
196
179
  safePointInjection: false }),
197
180
  guards: Object.freeze({ beforeRead: false, beforeWrite: false, beforeShell: false }),
198
- delivery: Object.freeze({ polling: true, activeNotification: false,
199
- wakeDormantSession: false }),
200
- execution: Object.freeze({ launch: false, resume: false, terminate: false }),
181
+ delivery: Object.freeze({ nextTurn: false, livePush: false, replyRoute: false }),
201
182
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.1.17",
3
+ "version": "0.2.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
+ }
@@ -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, 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,70 @@ 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);
67
+ const receiptState = oneOf("queued", "offered", "retrieved", "acknowledged");
61
68
 
62
- const RECORDS = Object.freeze({
69
+ const DURABLE_RECORDS = Object.freeze({
63
70
  workspace: { workspaceId: id, displayName: line, source: oneOf("config", "git", "directory"),
64
71
  roots: listOf(line), createdAt: timestamp },
65
72
 
66
73
  participant: { participantId: id, workspaceId: id, displayName: line,
67
74
  kind: oneOf("agent", "human"), createdAt: timestamp },
68
75
 
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
76
  session: { sessionId: id, participantId: id, workspaceId: id, generation: id,
79
77
  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),
78
+ checkoutRoot: nullable(line), branch: nullable(line), pid: nullable(positiveInteger),
85
79
  enforcement: oneOf("guarded", "advisory"), lifecycle: oneOf("managed", "manual"),
86
80
  heartbeatCadenceMs: positiveInteger, startedAt: timestamp, heartbeatAt: timestamp },
87
81
 
88
82
  intent: { sessionId: id, workspaceId: id, summary,
89
83
  mode: oneOf("observe", "explore", "edit", "review", "coordinate", "wait"),
90
- resourceHints: listOf(resourceUri), workstreamId: nullable(id),
84
+ resourceHints: listOf(resourceUri),
91
85
  state: oneOf("active", "blocked", "waiting", "done"), updatedAt: timestamp },
92
86
 
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
87
  claim: { claimId: id, workspaceId: id, ownerSessionId: id, resource: resourceUri,
113
88
  mode: oneOf("shared", "exclusive"), enforcement: oneOf("advisory", "guarded"),
114
89
  reason: line, acquiredAt: timestamp, expiresAt: timestamp, generation: id },
115
90
 
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 },
91
+ message: { messageId: id, threadId: id, clientMessageId: id, workspaceId: id,
92
+ fromParticipantId: id, fromSessionId: id, toParticipantIds: listOf(id),
93
+ kind: oneOf(...MESSAGE_KINDS), obligation: oneOf(...OBLIGATIONS),
94
+ subject: line, body: prose, inReplyTo: nullable(id), artifacts: listOf(artifactRef),
95
+ handoff: nullable(handoffPayload), sentAt: timestamp },
128
96
 
129
97
  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 },
98
+ state: receiptState, updatedAt: timestamp },
144
99
 
145
100
  event: { sequence, eventId: id, workspaceId: id, actorSessionId: id, type: eventType,
146
101
  occurredAt: timestamp, payload: plainObject },
147
102
  });
148
103
 
149
- export const RECORD_KINDS = Object.freeze(Object.keys(RECORDS));
104
+ const RECORDS = Object.freeze({
105
+ ...DURABLE_RECORDS,
106
+ deliveryBinding: { sessionId: id, generation: id, adapterId: id, clientVersion: line,
107
+ availableModes: listOf(oneOf("nextTurn", "livePush", "replyRoute")),
108
+ livePolicy: oneOf("off", "actionable", "all"), opaqueEndpointRef: prose,
109
+ leaseUntil: timestamp },
110
+ });
111
+
112
+ export const RECORD_KINDS = Object.freeze(Object.keys(DURABLE_RECORDS));
150
113
 
151
114
  export function validateRecord(kind, value) {
152
115
  const fields = RECORDS[kind];
@@ -161,9 +124,7 @@ export function validateRecord(kind, value) {
161
124
  }
162
125
  for (const key of Object.keys(value)) {
163
126
  if (key === "schemaVersion" || key === "extensions") continue;
164
- if (!Object.hasOwn(fields, key)) {
165
- invalid(`${kind}.${key}`, "is not a known field", value[key]);
166
- }
127
+ if (!Object.hasOwn(fields, key)) invalid(`${kind}.${key}`, "is not a known field", value[key]);
167
128
  }
168
129
  for (const [field, check] of Object.entries(fields)) {
169
130
  if (!Object.hasOwn(value, field)) {
@@ -171,9 +132,7 @@ export function validateRecord(kind, value) {
171
132
  }
172
133
  check(value[field], field);
173
134
  }
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
135
  if (Object.hasOwn(value, "extensions")) plainObject(value.extensions, "extensions");
136
+ if (kind === "message") assertMessageSemantics(value);
178
137
  return value;
179
138
  }
@@ -1,55 +1,28 @@
1
1
  import { AccError, EXIT } from "./errors.mjs";
2
2
 
3
- // recorded -> queued -> injected -> seen -> acknowledged, with failed branching
4
- // off before the message was ever exposed. States are monotonic: one
5
- // recipient's receipt can only move forwards, and never rewrites another
6
- // recipient's state.
7
- export const DELIVERY_STATES = Object.freeze(
8
- ["recorded", "queued", "injected", "seen", "acknowledged", "failed"]);
3
+ export const RECEIPT_STATES = Object.freeze(
4
+ ["queued", "offered", "retrieved", "acknowledged"]);
9
5
 
10
- const DELIVERY_NEXT = Object.freeze({
11
- recorded: ["queued", "injected", "seen", "acknowledged", "failed"],
12
- queued: ["injected", "seen", "acknowledged", "failed"],
13
- injected: ["seen", "acknowledged"],
14
- seen: ["acknowledged"],
6
+ const RECEIPT_NEXT = Object.freeze({
7
+ queued: ["offered", "retrieved", "acknowledged"],
8
+ offered: ["retrieved", "acknowledged"],
9
+ retrieved: ["acknowledged"],
15
10
  acknowledged: [],
16
- failed: [],
17
11
  });
18
12
 
19
- export const TASK_STATES = Object.freeze(
20
- ["pending", "in_progress", "review", "done", "blocked"]);
21
-
22
- const TASK_NEXT = Object.freeze({
23
- pending: ["in_progress", "blocked"],
24
- in_progress: ["review", "done", "blocked"],
25
- review: ["in_progress", "done", "blocked"],
26
- blocked: ["pending", "in_progress"],
27
- done: [],
28
- });
29
-
30
- function step(machine, allowed, label, current, next) {
31
- if (!machine.includes(current)) {
32
- throw new AccError(EXIT.DATA, `unknown ${label} state: ${String(current)}`,
13
+ export function advanceReceipt(current, next) {
14
+ if (!RECEIPT_STATES.includes(current)) {
15
+ throw new AccError(EXIT.DATA, `unknown receipt state: ${String(current)}`,
33
16
  { current, next });
34
17
  }
35
- if (!machine.includes(next)) {
36
- throw new AccError(EXIT.DATA, `unknown ${label} state: ${String(next)}`,
18
+ if (!RECEIPT_STATES.includes(next)) {
19
+ throw new AccError(EXIT.DATA, `unknown receipt state: ${String(next)}`,
37
20
  { current, next });
38
21
  }
39
- // Re-declaring the current state is idempotent. Adapters retry at safe
40
- // points, and a repeated receipt is not a protocol violation.
41
22
  if (current === next) return next;
42
- if (!allowed[current].includes(next)) {
23
+ if (!RECEIPT_NEXT[current].includes(next)) {
43
24
  throw new AccError(EXIT.CONFLICT,
44
- `illegal ${label} transition from ${current} to ${next}`, { current, next });
25
+ `illegal receipt transition from ${current} to ${next}`, { current, next });
45
26
  }
46
27
  return next;
47
28
  }
48
-
49
- export function advanceDelivery(current, next) {
50
- return step(DELIVERY_STATES, DELIVERY_NEXT, "delivery", current, next);
51
- }
52
-
53
- export function transitionTask(current, next) {
54
- return step(TASK_STATES, TASK_NEXT, "task", current, next);
55
- }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.1.17",
3
+ "version": "0.2.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {