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.
- package/README.md +92 -92
- package/SECURITY.md +7 -4
- package/docs/ADAPTER_AUTHORING.md +26 -22
- package/docs/ARCHITECTURE.md +23 -15
- package/docs/CAPABILITIES.md +30 -12
- package/docs/CLI.md +21 -10
- package/docs/CONCEPTS.md +91 -94
- package/docs/CONFIGURATION.md +30 -24
- package/docs/DESIGN_DECISIONS.md +8 -4
- package/docs/GETTING_STARTED.md +76 -100
- package/docs/GLOSSARY.md +4 -4
- package/docs/HOW_IT_WORKS.md +119 -233
- package/docs/MCP.md +8 -8
- package/docs/PROTOCOL.md +12 -7
- package/docs/RELEASING.md +111 -87
- package/docs/SECURITY_MODEL.md +13 -8
- package/docs/TROUBLESHOOTING.md +24 -12
- package/docs/WHY_ACC.md +18 -16
- package/docs/index.md +26 -18
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +6 -4
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +5 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +44 -14
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +41 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +4 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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,
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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);
|
|
@@ -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 };
|