agents-can-communicate 0.3.0 → 0.3.1

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 (42) hide show
  1. package/README.md +92 -92
  2. package/SECURITY.md +7 -4
  3. package/docs/ADAPTER_AUTHORING.md +26 -22
  4. package/docs/ARCHITECTURE.md +23 -15
  5. package/docs/CAPABILITIES.md +30 -12
  6. package/docs/CLI.md +21 -10
  7. package/docs/CONCEPTS.md +91 -94
  8. package/docs/CONFIGURATION.md +30 -24
  9. package/docs/DESIGN_DECISIONS.md +8 -4
  10. package/docs/GETTING_STARTED.md +76 -100
  11. package/docs/GLOSSARY.md +4 -4
  12. package/docs/HOW_IT_WORKS.md +119 -233
  13. package/docs/MCP.md +8 -8
  14. package/docs/PROTOCOL.md +12 -7
  15. package/docs/RELEASING.md +111 -87
  16. package/docs/SECURITY_MODEL.md +13 -8
  17. package/docs/TROUBLESHOOTING.md +24 -12
  18. package/docs/WHY_ACC.md +18 -16
  19. package/docs/index.md +26 -18
  20. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  21. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +6 -4
  22. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +5 -3
  24. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  25. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +5 -3
  26. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  27. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  28. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +5 -3
  29. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  30. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +5 -3
  31. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  32. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  33. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +44 -14
  34. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  35. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +41 -1
  36. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  38. package/node_modules/@agents-can-communicate/installer/package.json +4 -2
  39. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  40. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  41. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  42. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -47,14 +47,14 @@ diffs. Prefer a conclusion, stable ids or paths, and the next action.
47
47
  For information that needs no response:
48
48
 
49
49
  ```bash
50
- {{ACC}} message --to models --type note --subject "schema verified" \
50
+ {{ACC}} message --to codex --type note --subject "schema verified" \
51
51
  --body "Record v2 accepts nullable pid; no migration is planned."
52
52
  ```
53
53
 
54
54
  For a question, use the kind whose default obligation is a reply:
55
55
 
56
56
  ```bash
57
- {{ACC}} message --to models --type question \
57
+ {{ACC}} message --to codex --type question \
58
58
  --subject "claim boundary" --body "Can I take file:src/parser/** after your commit?"
59
59
  ```
60
60
 
@@ -65,7 +65,9 @@ When the peer should own a concrete piece of work, send one reply-required reque
65
65
  --detail "Check queued -> retrieved and reply -> acknowledged; return only defects."
66
66
  ```
67
67
 
68
- Participant names come from `{{ACC}} status --json`. A request is not an order.
68
+ Address a peer by its client - `codex`, `claude_code`, `gemini_cli` - when one session of
69
+ it is here; `{{ACC}} status --json` names them all, and two sessions of one client have to be
70
+ named exactly. A request is not an order.
69
71
 
70
72
  ## Treat delivery as evidence
71
73
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -6,7 +6,7 @@ import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
6
6
  import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
7
7
  import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
8
8
  import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
9
- import { LIVE_POLICIES, applyPlan, describeActivation, detectInstallation, livePolicyOf,
9
+ import { LIVE_POLICIES, applyPlan, detectInstallation, livePolicyOf,
10
10
  loadOwnership, planInstallation, rcFileFor, shellOf, shimDirFor }
11
11
  from "@agents-can-communicate/installer";
12
12
  import { AccError, EXIT } from "@agents-can-communicate/protocol";
@@ -192,18 +192,43 @@ export function actedOn(result) {
192
192
  const isInteractive = runtime => typeof runtime.isInteractive === "function"
193
193
  && runtime.isInteractive() === true;
194
194
 
195
- // One default-No question per eligible client, asked only after detection has
196
- // finished and only when a person is at both ends of the terminal. It names
197
- // the client, the mechanism, and the files or service it would touch, and says
198
- // when the choice takes effect.
199
- function questionFor(entry, context) {
200
- const activation = { livePolicy: "actionable", shell: context.shell,
201
- rcFile: rcFileFor(context.home, context.shell), shimDir: shimDirFor(context.stateRoot),
202
- mechanisms: entry.nativeDelivery.activationPlan.mechanisms };
203
- return [`Enable native live delivery for ${entry.displayName} ${entry.version}?`,
204
- ...describeActivation(activation).map(line => ` ${line}`),
205
- " applies to newly started sessions; a new PATH block needs a new or reloaded shell"]
206
- .join("\n");
195
+ /**
196
+ * One default-No question per eligible client, asked only after detection has
197
+ * finished and only when a person is at both ends of the terminal.
198
+ *
199
+ * Written for someone who has never heard of this project. It used to open with
200
+ * "Enable native live delivery" over a list of internal artefact names, which
201
+ * told a first-time reader neither what they would gain nor what it would cost -
202
+ * and left out the part that matters most: saying yes makes their own `claude`
203
+ * start with the vendor's `--dangerously-load-development-channels` flag. A
204
+ * consent prompt that hides the loudest consequence is not consent.
205
+ *
206
+ * So the order is the reader's, not the installer's: what changes for them,
207
+ * what it costs, what it touches, and that "no" costs them nothing.
208
+ */
209
+ function questionFor(entry, context, { onlyEligible = false } = {}) {
210
+ const bootstrap = entry.nativeDelivery.activationPlan.mechanisms
211
+ .find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
212
+ // The presence of a flag is what makes this worth asking about at all: it is
213
+ // why the client will warn at every start from now on. Which flag it is, and
214
+ // that a shim adds it, are answers to a question nobody is asking here - the
215
+ // shim `exec`s and is gone, so nothing runs "through" acc afterwards either.
216
+ const flags = (bootstrap?.prefixArgs ?? []).filter(argument => argument.startsWith("--"));
217
+ void context;
218
+ // Why the default is No, and why only one client is being asked about, are
219
+ // the two questions the bare prompt provokes and never answered: a `[y/N]`
220
+ // reads as "not recommended" on its own, and a single question among four
221
+ // installed clients reads as an omission. Both have the same short answer.
222
+ const aside = ["experimental", ...(onlyEligible ? [`${entry.displayName} only`] : [])]
223
+ .join("; ");
224
+ return [
225
+ `Let idle agents answer each other while you are away? (${aside})`,
226
+ ...(flags.length === 0 || bootstrap === null
227
+ ? [" Yes: they reply without waiting for you."]
228
+ : [` Yes: they reply without waiting for you, and ${entry.displayName} asks you`,
229
+ " to allow development channels every time it starts."]),
230
+ " No: messages still arrive, at the session's next turn.",
231
+ ].join("\n");
207
232
  }
208
233
 
209
234
  /**
@@ -220,6 +245,11 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
220
245
  throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
221
246
  }
222
247
  const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
248
+ // Whether this client is the only one that could be asked at all. When it is,
249
+ // the question says so, because otherwise being asked about one of four
250
+ // installed clients looks like the other three were forgotten.
251
+ const onlyEligible = detected
252
+ .filter(candidate => candidate.nativeDelivery?.state === "eligible").length === 1;
223
253
  const deliveryByAdapter = {};
224
254
  const asked = [];
225
255
  let withheld = 0;
@@ -234,7 +264,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
234
264
  deliveryByAdapter[entry.adapterId] = "off";
235
265
  if (eligible) withheld += 1;
236
266
  } else {
237
- const yes = await runtime.confirm(questionFor(entry, context),
267
+ const yes = await runtime.confirm(questionFor(entry, context, { onlyEligible }),
238
268
  { input: runtime.input, output: runtime.output }) === true;
239
269
  deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
240
270
  asked.push(entry.adapterId);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -55,6 +55,41 @@ function assertCurrentSession(tx, session, action) {
55
55
  }
56
56
  }
57
57
 
58
+ /**
59
+ * Turn what the sender typed into participant ids that exist.
60
+ *
61
+ * An exact participant id is always taken as written. A name that matches no
62
+ * participant is tried as a client name - `codex`, `claude_code` - because that
63
+ * is what an agent knows about its peers: the roster it is handed names the
64
+ * client, and asking it to first look up `codex-fbqX8o` is a lookup it
65
+ * routinely skips. Every shipped example taught this form while nothing
66
+ * implemented it, so an agent following its own skill was refused on its first
67
+ * attempt to reach anyone.
68
+ *
69
+ * One live session of that client resolves. Two refuse and name them, the way
70
+ * `--session` already refuses to choose between two sessions of one client:
71
+ * guessing is how a question is answered by a session that never had the
72
+ * context. The sender's own client is excluded, or the one client an agent is
73
+ * certain is running becomes the easiest thing to address by accident.
74
+ */
75
+ function resolveAddressees(tx, requested, session) {
76
+ const known = new Set(tx.list("participant").map(item => item.participantId));
77
+ return requested.map(name => {
78
+ if (known.has(name)) return name;
79
+ const peers = [...new Set(tx.list("session", item => item.state === "open"
80
+ && item.harness === name && item.participantId !== session.participantId)
81
+ .map(item => item.participantId))].sort();
82
+ if (peers.length === 1) return peers[0];
83
+ if (peers.length > 1) {
84
+ throw new AccError(EXIT.USAGE,
85
+ `could not tell which of ${peers.length} ${name} sessions you meant: `
86
+ + `${peers.join(", ")}. Name one of them, which \`acc status --json\` lists.`,
87
+ { client: name, candidates: peers });
88
+ }
89
+ return name;
90
+ });
91
+ }
92
+
58
93
  function addressedRecipients(tx, message, session) {
59
94
  if (message.toParticipantIds.length === 0) {
60
95
  return [...new Set(tx.list("session", item => item.state === "open"
@@ -86,9 +121,14 @@ function threadFor(tx, messageId, inReplyTo) {
86
121
  return parent.threadId;
87
122
  }
88
123
 
89
- export function recordMessageInTransaction({ tx, session, input, now, messageId, ids,
124
+ export function recordMessageInTransaction({ tx, session, input: raw, now, messageId, ids,
90
125
  action = "send a message" }) {
91
126
  assertCurrentSession(tx, session, action);
127
+ // Resolved before anything else reads the recipients, so the stored message
128
+ // names real participants and a retry compares against the same content. A
129
+ // record that kept the client name would address nobody when it is read back.
130
+ const input = { ...raw,
131
+ toParticipantIds: resolveAddressees(tx, raw.toParticipantIds ?? [], session) };
92
132
  const existing = existingMessage(tx, session, input);
93
133
  if (existing !== undefined) {
94
134
  return { message: existing, recipientParticipantIds: [], created: false };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
- "exports": { ".": "./src/index.mjs" },
6
+ "exports": {
7
+ ".": "./src/index.mjs"
8
+ },
7
9
  "files": [
8
10
  "src/"
9
11
  ]
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "Local-first coordination for independently opened AI agent sessions.",
6
6
  "keywords": [