agents-can-communicate 0.1.16 → 0.1.18
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 +63 -133
- package/bin/acc-hook.mjs +2 -0
- package/docs/CAPABILITIES.md +105 -85
- 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 +80 -154
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +3 -1
- 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 +80 -154
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +80 -154
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +13 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/hooks/hooks.json +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +154 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +61 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/hooks.mjs +127 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +101 -0
- 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 +80 -154
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +3 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +125 -189
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +4 -2
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +23 -2
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +1 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +134 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +41 -0
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +1 -1
- package/node_modules/@agents-can-communicate/core/src/service.mjs +3 -0
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +48 -0
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +36 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +77 -33
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +29 -21
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +25 -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/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +23 -7
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +18 -2
- package/package.json +4 -1
|
@@ -1,237 +1,173 @@
|
|
|
1
1
|
const DEFAULT_BUDGET_BYTES = 6_000;
|
|
2
|
-
// Held back from the required lines so the "not shown" note can always be
|
|
3
|
-
// written. A projection that silently drops what it could not fit is how an
|
|
4
|
-
// agent ends up confidently unaware.
|
|
5
|
-
// Enough for the note *and* the command that reads what the note is about. It
|
|
6
|
-
// said only that something had been withheld, and nothing anywhere - not the
|
|
7
|
-
// skills, not the docs - said how to see it. A turn that reports a thing the
|
|
8
|
-
// reader cannot reach is how an agent ends up inventing its own way in.
|
|
9
|
-
const NOTE_RESERVE = 80;
|
|
10
2
|
const FENCE = "```";
|
|
11
|
-
// A peer cannot close a block it cannot name. The fence carries a marker that
|
|
12
|
-
// is stripped from peer content, so forged delimiters stay inside the block.
|
|
13
3
|
const BLOCK = "acc-peer-message";
|
|
14
4
|
|
|
15
5
|
const bytes = value => Buffer.byteLength(value, "utf8");
|
|
16
|
-
|
|
17
|
-
// Written from char codes rather than a literal class: an escaped control
|
|
18
|
-
// range in a regex literal is corrupted silently by editors and patches, and a
|
|
19
|
-
// corrupted range fails open.
|
|
20
6
|
const CONTROL_CHARACTERS = new RegExp(
|
|
21
7
|
`[${String.fromCharCode(0)}-${String.fromCharCode(8)}`
|
|
22
8
|
+ `${String.fromCharCode(11)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]`, "g");
|
|
23
9
|
|
|
24
|
-
/**
|
|
25
|
-
* Render peer-controlled text as displayable data.
|
|
26
|
-
*
|
|
27
|
-
* Two separate jobs. Control sequences become visible escapes, so a message
|
|
28
|
-
* cannot repaint or retitle the human's terminal. Fence markers are stripped,
|
|
29
|
-
* so a message cannot break out of its own data block and continue as if ACC
|
|
30
|
-
* had written the following lines.
|
|
31
|
-
*/
|
|
32
10
|
function escapePeerText(value) {
|
|
33
11
|
return String(value)
|
|
34
12
|
.replaceAll(new RegExp(`${FENCE}${BLOCK}`, "g"), `'${FENCE}${BLOCK}`)
|
|
35
13
|
.replaceAll(FENCE, `'${FENCE}`)
|
|
36
|
-
// The labels that frame this block are ACC's words at the start of a line.
|
|
37
|
-
// A peer writing one would otherwise produce a second line reading as ACC
|
|
38
|
-
// framing a different message - the same break-out the fence rule prevents,
|
|
39
|
-
// and neutralised the same way rather than by reflowing the text, which a
|
|
40
|
-
// handoff body cannot survive.
|
|
41
14
|
.replace(/^(subject:|body:)/gm, "'$1")
|
|
42
15
|
.replace(CONTROL_CHARACTERS,
|
|
43
16
|
character => `\\u${character.codePointAt(0).toString(16).padStart(4, "0")}`);
|
|
44
17
|
}
|
|
45
18
|
|
|
19
|
+
function oneLine(value) {
|
|
20
|
+
return value.replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
46
23
|
function truncate(line, limit) {
|
|
24
|
+
if (limit <= 0) return "";
|
|
47
25
|
if (bytes(line) <= limit) return line;
|
|
48
26
|
let cut = line;
|
|
49
27
|
while (bytes(`${cut}…`) > limit && cut.length > 0) cut = cut.slice(0, -1);
|
|
50
|
-
return `${cut}…`;
|
|
28
|
+
return cut === "" ? "" : `${cut}…`;
|
|
51
29
|
}
|
|
52
30
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
*
|
|
63
|
-
* Every kind carries such an id and every one is the argument to a command:
|
|
64
|
-
* a message to `acc ack --message`, a task to `acc task --task`, a claim to
|
|
65
|
-
* `acc release --claim`. So they are all shown, not only the ones that happened
|
|
66
|
-
* to be noticed first.
|
|
67
|
-
*/
|
|
68
|
-
function attentionLines(attention) {
|
|
69
|
-
return attention.map(item => (typeof item.sourceId === "string" && item.sourceId !== ""
|
|
70
|
-
? `- [${item.kind}] ${item.sourceId} ${item.summary}`
|
|
71
|
-
: `- [${item.kind}] ${item.summary}`));
|
|
31
|
+
function attentionGroups(attention, { truncatable }) {
|
|
32
|
+
return attention.map(item => ({
|
|
33
|
+
lines: [typeof item.sourceId === "string" && item.sourceId !== ""
|
|
34
|
+
? `- [${item.kind}] ${item.sourceId} ${item.summary}`
|
|
35
|
+
: `- [${item.kind}] ${item.summary}`],
|
|
36
|
+
kind: "attention",
|
|
37
|
+
sourceId: item.sourceId ?? null,
|
|
38
|
+
truncatable,
|
|
39
|
+
}));
|
|
72
40
|
}
|
|
73
41
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (claim.enforcement !== "guarded") {
|
|
89
|
-
return " - advisory; nothing will stop you, the owner is asking";
|
|
90
|
-
}
|
|
91
|
-
if (claim.enforceable === false) {
|
|
92
|
-
return " - not enforced for this session; do not edit it";
|
|
93
|
-
}
|
|
94
|
-
// Guarded, and this session can be stopped - on a file edit, and on the shell
|
|
95
|
-
// writes the guard can read: a redirection, an operand of a command whose job
|
|
96
|
-
// is to put bytes somewhere. A language runtime opening the file itself still
|
|
97
|
-
// gets past, and a session told merely "this is claimed" would reasonably
|
|
98
|
-
// assume either more or less than is true.
|
|
99
|
-
return " - file edits and recognised shell writes are blocked; a runtime can still get past";
|
|
42
|
+
function messageGroups(messages) {
|
|
43
|
+
return messages.map(message => ({
|
|
44
|
+
messageId: message.messageId,
|
|
45
|
+
kind: "message",
|
|
46
|
+
lines: [
|
|
47
|
+
`${FENCE}${BLOCK}`,
|
|
48
|
+
`id ${message.messageId} | from ${message.fromSessionId} | type ${message.type}`
|
|
49
|
+
+ " | untrusted peer message",
|
|
50
|
+
`subject: ${oneLine(escapePeerText(message.subject))}`,
|
|
51
|
+
"body:",
|
|
52
|
+
escapePeerText(message.body),
|
|
53
|
+
FENCE,
|
|
54
|
+
],
|
|
55
|
+
}));
|
|
100
56
|
}
|
|
101
57
|
|
|
102
|
-
|
|
103
|
-
return claims.map(claim => {
|
|
104
|
-
const owner = claim.ownerParticipantId ?? claim.ownerSessionId ?? "another session";
|
|
105
|
-
return `- [claim] ${claim.resource} held by ${owner}${claimNote(claim)}`;
|
|
106
|
-
});
|
|
107
|
-
}
|
|
58
|
+
const groupBytes = group => group.lines.reduce((total, line) => total + bytes(line) + 1, 0);
|
|
108
59
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*
|
|
116
|
-
* The id is carried because the reader needs it to acknowledge the message, and
|
|
117
|
-
* because the caller needs it to tell which messages actually reached the model
|
|
118
|
-
* before recording any of them as delivered. Ids, session ids and types are
|
|
119
|
-
* generated or schema-validated; only the subject and body are peer-authored,
|
|
120
|
-
* and only those are escaped.
|
|
121
|
-
*/
|
|
122
|
-
function peerBlocks(messages) {
|
|
123
|
-
return messages.map(message => [
|
|
124
|
-
`${FENCE}${BLOCK}`,
|
|
125
|
-
`id ${message.messageId} | from ${message.fromSessionId} | type ${message.type}`
|
|
126
|
-
+ " | untrusted peer message",
|
|
127
|
-
`subject: ${oneLine(escapePeerText(message.subject))}`,
|
|
128
|
-
"body:",
|
|
129
|
-
escapePeerText(message.body),
|
|
130
|
-
FENCE,
|
|
131
|
-
]);
|
|
60
|
+
function peerCount(sync) {
|
|
61
|
+
const participants = new Set((sync.roster ?? [])
|
|
62
|
+
.filter(item => item.presence !== "offline")
|
|
63
|
+
.map(item => item.participantId ?? item.sessionId)
|
|
64
|
+
.filter(id => id !== sync.currentParticipantId));
|
|
65
|
+
return participants.size;
|
|
132
66
|
}
|
|
133
67
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
* The subject sits on the label's own line, so a newline inside it would push
|
|
138
|
-
* peer text to column 0 where ACC's labels live. Rendering the break visibly
|
|
139
|
-
* keeps the text readable and the frame ACC's.
|
|
140
|
-
*/
|
|
141
|
-
function oneLine(value) {
|
|
142
|
-
return value.replaceAll("\r\n", "\\n").replaceAll("\n", "\\n").replaceAll("\r", "\\n");
|
|
68
|
+
function ambientHeader(count) {
|
|
69
|
+
const noun = count === 1 ? "participant" : "participants";
|
|
70
|
+
return `ACC: ${count} peer ${noun} present. Load the acc skill before shared work.`;
|
|
143
71
|
}
|
|
144
72
|
|
|
73
|
+
function recoveryNote(ids) {
|
|
74
|
+
const first = ids[0];
|
|
75
|
+
const rest = ids.length > 1 ? ` (+${ids.length - 1} more in \`acc inbox\`)` : "";
|
|
76
|
+
return `- read ${first}: \`acc inbox --message ${first}\`${rest}`;
|
|
77
|
+
}
|
|
145
78
|
|
|
146
79
|
/**
|
|
147
|
-
* Project
|
|
80
|
+
* Project only coordination that can change this turn.
|
|
148
81
|
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
82
|
+
* Presence is a short trigger to load the ACC skill. Roster rows and unrelated
|
|
83
|
+
* claims are intentionally absent: a guard checks exact file claims at write
|
|
84
|
+
* time, while intent-aware conflicts already arrive as attention. Repeating
|
|
85
|
+
* the whole workspace on every prompt is neither safer nor cheaper.
|
|
153
86
|
*/
|
|
154
|
-
export function
|
|
87
|
+
export function projectContextResult(sync, { budgetBytes = DEFAULT_BUDGET_BYTES } = {}) {
|
|
155
88
|
const attention = [...(sync.attention ?? [])]
|
|
156
89
|
.sort((left, right) => left.priority - right.priority
|
|
157
|
-
|| left.sourceId.localeCompare(right.sourceId));
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
// context budget, crowding out messages actually addressed to the reader.
|
|
165
|
-
// Stale stays: a session that crashed holding a claim is very much news.
|
|
166
|
-
const roster = (sync.roster ?? []).filter(item => item.presence !== "offline");
|
|
167
|
-
const claims = sync.claims ?? [];
|
|
168
|
-
|
|
169
|
-
// Every entry is a group that appears whole or not at all. Single-line groups
|
|
170
|
-
// may still be truncated - there is no fence in them to leave open.
|
|
171
|
-
const required = [
|
|
172
|
-
...attentionLines(attention).map(line => [line]),
|
|
173
|
-
...claimLines(claims).map(line => [line]),
|
|
174
|
-
...peerBlocks(messages),
|
|
90
|
+
|| (left.sourceId ?? "").localeCompare(right.sourceId ?? ""));
|
|
91
|
+
const urgent = attention.filter(item => item.priority <= 2);
|
|
92
|
+
const informational = attention.filter(item => item.priority > 2);
|
|
93
|
+
const groups = [
|
|
94
|
+
...attentionGroups(urgent, { truncatable: true }),
|
|
95
|
+
...messageGroups(sync.messages ?? []),
|
|
96
|
+
...attentionGroups(informational, { truncatable: false }),
|
|
175
97
|
];
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (const group of required) {
|
|
203
|
-
if (group.length === 1) {
|
|
204
|
-
const candidate = truncate(group[0], Math.max(0, ceiling - used - 1));
|
|
205
|
-
if (candidate === "" || used + bytes(candidate) + 1 > ceiling) { dropped += 1; continue; }
|
|
206
|
-
lines.push(candidate);
|
|
207
|
-
used += bytes(candidate) + 1;
|
|
98
|
+
const peers = peerCount(sync);
|
|
99
|
+
if (groups.length === 0 && (sync.solo === true || peers === 0)) {
|
|
100
|
+
return { text: "", includedMessageIds: [], includedAttentionIds: [] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const fullHeader = groups.length === 0 ? ambientHeader(peers) : "ACC (load the acc skill):";
|
|
104
|
+
const header = truncate(fullHeader, budgetBytes);
|
|
105
|
+
const lines = header === "" ? [] : [header];
|
|
106
|
+
let used = header === "" ? 0 : bytes(header) + 1;
|
|
107
|
+
const included = [];
|
|
108
|
+
const droppedMessages = [];
|
|
109
|
+
let droppedOther = 0;
|
|
110
|
+
|
|
111
|
+
for (const group of groups) {
|
|
112
|
+
if (group.lines.length === 1) {
|
|
113
|
+
const remaining = budgetBytes - used;
|
|
114
|
+
const line = group.truncatable
|
|
115
|
+
? truncate(group.lines[0], remaining)
|
|
116
|
+
: (bytes(group.lines[0]) <= remaining ? group.lines[0] : "");
|
|
117
|
+
if (line !== "" && used + bytes(line) <= budgetBytes) {
|
|
118
|
+
lines.push(line);
|
|
119
|
+
included.push({ group, lineCount: 1 });
|
|
120
|
+
used += bytes(line) + 1;
|
|
121
|
+
} else {
|
|
122
|
+
droppedOther += 1;
|
|
123
|
+
}
|
|
208
124
|
continue;
|
|
209
125
|
}
|
|
210
|
-
const size = group
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const note = `- +${dropped} not shown, over budget; read them with `
|
|
219
|
-
+ "`acc sync --scope full --json`";
|
|
220
|
-
lines.push(note);
|
|
221
|
-
used += bytes(note) + 1;
|
|
126
|
+
const size = groupBytes(group);
|
|
127
|
+
if (used + size <= budgetBytes) {
|
|
128
|
+
lines.push(...group.lines);
|
|
129
|
+
included.push({ group, lineCount: group.lines.length });
|
|
130
|
+
used += size;
|
|
131
|
+
} else {
|
|
132
|
+
droppedMessages.push(group.messageId);
|
|
133
|
+
}
|
|
222
134
|
}
|
|
223
135
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
136
|
+
if (droppedMessages.length > 0) {
|
|
137
|
+
let note = recoveryNote(droppedMessages);
|
|
138
|
+
while (used + bytes(note) + 1 > budgetBytes && included.length > 0) {
|
|
139
|
+
const removed = included.pop();
|
|
140
|
+
lines.splice(-removed.lineCount, removed.lineCount);
|
|
141
|
+
used = lines.reduce((total, line) => total + bytes(line) + 1, 0);
|
|
142
|
+
if (removed.group.kind === "message") droppedMessages.push(removed.group.messageId);
|
|
143
|
+
else droppedOther += 1;
|
|
144
|
+
note = recoveryNote([...new Set(droppedMessages)]);
|
|
145
|
+
}
|
|
146
|
+
if (used + bytes(note) + 1 > budgetBytes && lines.length > 0) {
|
|
147
|
+
lines.length = 0;
|
|
148
|
+
used = 0;
|
|
149
|
+
}
|
|
150
|
+
const fitted = recoveryNote([...new Set(droppedMessages)]);
|
|
151
|
+
// An incomplete id or command is not a recovery path. When a deliberately
|
|
152
|
+
// tiny budget cannot hold the shortest truthful instruction, say nothing
|
|
153
|
+
// and leave every omitted receipt queued for a later turn.
|
|
154
|
+
if (used + bytes(fitted) + 1 <= budgetBytes) lines.push(fitted);
|
|
155
|
+
} else if (droppedOther > 0) {
|
|
156
|
+
const note = `- +${droppedOther} actionable item(s) omitted; run \`acc status --json\``;
|
|
233
157
|
if (used + bytes(note) + 1 <= budgetBytes) lines.push(note);
|
|
234
158
|
}
|
|
235
159
|
|
|
236
|
-
return
|
|
160
|
+
return {
|
|
161
|
+
text: lines.join("\n"),
|
|
162
|
+
includedMessageIds: included
|
|
163
|
+
.filter(item => item.group.kind === "message")
|
|
164
|
+
.map(item => item.group.messageId),
|
|
165
|
+
includedAttentionIds: included
|
|
166
|
+
.filter(item => item.group.kind === "attention" && item.group.sourceId !== null)
|
|
167
|
+
.map(item => item.group.sourceId),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function projectContext(sync, options) {
|
|
172
|
+
return projectContextResult(sync, options).text;
|
|
237
173
|
}
|
|
@@ -7,7 +7,7 @@ export { assertRunner, bakeSkillCommand, defaultCli, defaultRunner, removeInstal
|
|
|
7
7
|
from "./hook-shim.mjs";
|
|
8
8
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
9
9
|
from "./toml-block.mjs";
|
|
10
|
-
export { projectContext } from "./context-projector.mjs";
|
|
10
|
+
export { projectContext, projectContextResult } from "./context-projector.mjs";
|
|
11
11
|
export { shellWriteTargets } from "./shell-writes.mjs";
|
|
12
12
|
export { keepOnlyVersion, ownVersion, stampPluginVersion } from "./own-version.mjs";
|
|
13
13
|
export { editJson, readJson } from "./json-text.mjs";
|
|
@@ -26,6 +26,9 @@ export const COMMANDS = Object.freeze({
|
|
|
26
26
|
message: { required: ["subject", "body"],
|
|
27
27
|
optional: ["session", "generation", "type", "priority", "workstream"],
|
|
28
28
|
repeated: ["to"], flags: ["requires-ack"] },
|
|
29
|
+
inbox: { required: [], optional: ["session", "generation", "message"] },
|
|
30
|
+
reply: { required: ["message", "body"],
|
|
31
|
+
optional: ["session", "generation", "subject", "type", "priority"] },
|
|
29
32
|
// Asking another agent to do something: one call, because a task nobody was
|
|
30
33
|
// told about and a message pointing at no task are each useless.
|
|
31
34
|
request: { required: ["to", "title"],
|
|
@@ -13,8 +13,8 @@ import { COMMANDS } from "./args.mjs";
|
|
|
13
13
|
*/
|
|
14
14
|
const GROUPS = Object.freeze([
|
|
15
15
|
["Set up", ["install", "uninstall", "doctor", "config"]],
|
|
16
|
-
["In a session", ["status", "sync", "work", "claim", "release", "
|
|
17
|
-
"request", "task", "workstream", "decide", "finish"]],
|
|
16
|
+
["In a session", ["status", "sync", "work", "claim", "release", "inbox", "reply",
|
|
17
|
+
"ack", "message", "request", "task", "workstream", "decide", "finish"]],
|
|
18
18
|
["Driven by adapters, not by people", ["attach", "heartbeat", "detach"]],
|
|
19
19
|
["About acc", ["help", "version", "update"]],
|
|
20
20
|
]);
|
|
@@ -31,6 +31,8 @@ const SUMMARY = Object.freeze({
|
|
|
31
31
|
release: "give a claim back",
|
|
32
32
|
ack: "answer a message that asked for one, so it stops asking",
|
|
33
33
|
message: "send a typed message to named participants",
|
|
34
|
+
inbox: "read only unresolved messages addressed to this participant",
|
|
35
|
+
reply: "reply to one message and acknowledge it in the same operation",
|
|
34
36
|
request: "ask another agent to do something: the work and the why, in one call",
|
|
35
37
|
task: "create work, --take it, or move its --state along",
|
|
36
38
|
workstream: "group related work, and steer it with --take / --release",
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
5
5
|
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
6
6
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
|
+
import { createGrokAdapter } from "@agents-can-communicate/adapter-grok";
|
|
7
8
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
8
9
|
import { applyPlan, detectInstallation, loadOwnership, planInstallation }
|
|
9
10
|
from "@agents-can-communicate/installer";
|
|
@@ -23,6 +24,7 @@ export const clientContext = (home, stateRoot) => ({
|
|
|
23
24
|
agentsHome: home,
|
|
24
25
|
codexHome: path.join(home, ".codex"),
|
|
25
26
|
kimiHome: path.join(home, ".kimi-code"),
|
|
27
|
+
grokHome: path.join(home, ".grok"),
|
|
26
28
|
// Where ACC keeps its own state, for the client that has to be told. Codex
|
|
27
29
|
// sandboxes the commands a model runs to the workspace, and ACC's state is
|
|
28
30
|
// outside every workspace on purpose - so an agent there could read the
|
|
@@ -32,7 +34,7 @@ export const clientContext = (home, stateRoot) => ({
|
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
export const ALL_ADAPTERS = () => [createClaudeCodeAdapter(), createCodexAdapter(),
|
|
35
|
-
createGeminiCliAdapter(), createKimiAdapter()];
|
|
37
|
+
createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
40
|
* How long to wait for a client to say its version.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AccError, CONFIG_FILENAME, EXIT, failure, ok }
|
|
2
2
|
from "@agents-can-communicate/protocol";
|
|
3
|
-
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
3
|
+
import { createCoordinationService, noteNudge } from "@agents-can-communicate/core";
|
|
4
4
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
5
5
|
|
|
6
6
|
import { parseArgs, positiveNumber } from "./args.mjs";
|
|
@@ -199,7 +199,28 @@ const HANDLERS = Object.freeze({
|
|
|
199
199
|
type: options.type ?? "note", subject: options.subject, body: options.body,
|
|
200
200
|
priority: options.priority, workstreamId: options.workstream ?? null,
|
|
201
201
|
requiresAck: options.requiresAck === true, descriptor: context.descriptor });
|
|
202
|
-
|
|
202
|
+
// A note that reads like it wants a reply was sent with no ack obligation
|
|
203
|
+
// and no standing reminder; say so once, without blocking the send. Carried
|
|
204
|
+
// in `data` so an adapter reading `--json` sees it, and appended to `text`
|
|
205
|
+
// for a person - the send already succeeded either way.
|
|
206
|
+
const advice = noteNudge(message);
|
|
207
|
+
return { data: advice ? { ...message, advice } : message,
|
|
208
|
+
text: advice ? `sent ${message.messageId}\n${advice}` : `sent ${message.messageId}` };
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
inbox: async ({ options, context }) => {
|
|
212
|
+
const messages = await context.service.readInbox({ sessionId: options.session,
|
|
213
|
+
generation: options.generation, messageId: options.message });
|
|
214
|
+
return { data: messages, text: messages.length === 0
|
|
215
|
+
? "inbox empty" : JSON.stringify(messages, null, 2) };
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
reply: async ({ options, context }) => {
|
|
219
|
+
const result = await context.service.replyToMessage({ sessionId: options.session,
|
|
220
|
+
generation: options.generation, messageId: options.message, body: options.body,
|
|
221
|
+
subject: options.subject, type: options.type, priority: options.priority });
|
|
222
|
+
return { data: result,
|
|
223
|
+
text: `replied ${result.reply.messageId}; acknowledged ${options.message}` };
|
|
203
224
|
},
|
|
204
225
|
|
|
205
226
|
/**
|
|
@@ -20,7 +20,7 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
20
20
|
* answers that from something the caller demonstrably has.
|
|
21
21
|
*/
|
|
22
22
|
const NEEDS_OWNER = Object.freeze(new Set(["work", "claim", "release", "message",
|
|
23
|
-
"request", "ack", "workstream", "task", "finish", "decide"]));
|
|
23
|
+
"inbox", "reply", "request", "ack", "workstream", "task", "finish", "decide"]));
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Reads that are answers about *you*.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { AccError, EXIT, SCHEMA_VERSION, advanceDelivery, createId, validateRecord }
|
|
2
|
+
from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
const receiptId = (messageId, recipient) => `${messageId}--${recipient}`;
|
|
5
|
+
const unresolved = (message, receipt) => receipt.state === "recorded"
|
|
6
|
+
|| receipt.state === "queued"
|
|
7
|
+
|| (message.requiresAck && (receipt.state === "injected" || receipt.state === "seen"));
|
|
8
|
+
|
|
9
|
+
/** Narrow, recipient-owned message reads and replies. */
|
|
10
|
+
export function createInboxService(ports, sessions) {
|
|
11
|
+
const { store, clock, ids } = ports;
|
|
12
|
+
|
|
13
|
+
async function requireOpen(input, action) {
|
|
14
|
+
const existing = await sessions.locateSession(input.sessionId, input.workspaceId);
|
|
15
|
+
if (existing === null || existing.record.state !== "open"
|
|
16
|
+
|| existing.record.generation !== input.generation) {
|
|
17
|
+
throw new AccError(EXIT.CONFLICT, `cannot ${action} from this session generation`,
|
|
18
|
+
{ sessionId: input.sessionId });
|
|
19
|
+
}
|
|
20
|
+
return existing.record;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readInbox(input) {
|
|
24
|
+
const session = await requireOpen(input, "read the inbox");
|
|
25
|
+
const snapshot = await store.snapshot(session.workspaceId,
|
|
26
|
+
{ kinds: ["message", "receipt"] });
|
|
27
|
+
const messages = new Map(snapshot.messages.map(message => [message.messageId, message]));
|
|
28
|
+
let addressed = snapshot.receipts
|
|
29
|
+
.filter(receipt => receipt.recipientParticipantId === session.participantId)
|
|
30
|
+
.map(receipt => ({ message: messages.get(receipt.messageId), receipt }))
|
|
31
|
+
.filter(item => item.message !== undefined);
|
|
32
|
+
if (input.messageId !== undefined) {
|
|
33
|
+
addressed = addressed.filter(item => item.message.messageId === input.messageId
|
|
34
|
+
&& item.receipt.state !== "acknowledged" && item.receipt.state !== "failed");
|
|
35
|
+
if (addressed.length === 0) {
|
|
36
|
+
throw new AccError(EXIT.CONFLICT,
|
|
37
|
+
"that message is not recoverable by this participant",
|
|
38
|
+
{ messageId: input.messageId, participantId: session.participantId });
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
addressed = addressed.filter(item => unresolved(item.message, item.receipt));
|
|
42
|
+
}
|
|
43
|
+
addressed.sort((left, right) => left.message.sentAt.localeCompare(right.message.sentAt)
|
|
44
|
+
|| left.message.messageId.localeCompare(right.message.messageId));
|
|
45
|
+
if (addressed.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const now = clock.now();
|
|
48
|
+
const shown = [];
|
|
49
|
+
await store.transaction(async tx => {
|
|
50
|
+
for (const item of addressed) {
|
|
51
|
+
const id = receiptId(item.message.messageId, session.participantId);
|
|
52
|
+
const current = tx.get("receipt", id);
|
|
53
|
+
const receipt = { ...current, state: advanceDelivery(current.state, "seen"),
|
|
54
|
+
updatedAt: now };
|
|
55
|
+
tx.put("receipt", id, receipt, tx.generationOf("receipt", id));
|
|
56
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
57
|
+
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
58
|
+
type: "message.seen", occurredAt: now,
|
|
59
|
+
payload: { messageId: item.message.messageId,
|
|
60
|
+
recipientParticipantId: session.participantId } });
|
|
61
|
+
shown.push({ message: item.message, receipt });
|
|
62
|
+
}
|
|
63
|
+
}, { kinds: ["receipt"] });
|
|
64
|
+
return shown;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function replyToMessage(input) {
|
|
68
|
+
const session = await requireOpen(input, "reply to a message");
|
|
69
|
+
const now = clock.now();
|
|
70
|
+
const replyId = createId("message");
|
|
71
|
+
let result = null;
|
|
72
|
+
await store.transaction(async tx => {
|
|
73
|
+
const original = tx.get("message", input.messageId);
|
|
74
|
+
const ownReceiptId = receiptId(input.messageId, session.participantId);
|
|
75
|
+
const originalReceipt = tx.get("receipt", ownReceiptId);
|
|
76
|
+
if (original === null || originalReceipt === null
|
|
77
|
+
|| !original.toParticipantIds.includes(session.participantId)) {
|
|
78
|
+
throw new AccError(EXIT.CONFLICT,
|
|
79
|
+
"that message is not addressed to this participant",
|
|
80
|
+
{ messageId: input.messageId, participantId: session.participantId });
|
|
81
|
+
}
|
|
82
|
+
if (originalReceipt.state === "acknowledged" || originalReceipt.state === "failed") {
|
|
83
|
+
throw new AccError(EXIT.CONFLICT, "that message is already resolved",
|
|
84
|
+
{ messageId: input.messageId, state: originalReceipt.state });
|
|
85
|
+
}
|
|
86
|
+
const reply = validateRecord("message", {
|
|
87
|
+
schemaVersion: SCHEMA_VERSION,
|
|
88
|
+
messageId: replyId,
|
|
89
|
+
workspaceId: session.workspaceId,
|
|
90
|
+
fromSessionId: session.sessionId,
|
|
91
|
+
fromParticipantId: session.participantId,
|
|
92
|
+
toParticipantIds: [original.fromParticipantId],
|
|
93
|
+
type: input.type ?? "answer",
|
|
94
|
+
subject: input.subject ?? `Re: ${original.subject}`,
|
|
95
|
+
body: input.body,
|
|
96
|
+
priority: input.priority ?? "normal",
|
|
97
|
+
workstreamId: original.workstreamId,
|
|
98
|
+
taskId: original.taskId,
|
|
99
|
+
inReplyTo: original.messageId,
|
|
100
|
+
requiresAck: false,
|
|
101
|
+
artifacts: input.artifacts ?? [],
|
|
102
|
+
sentAt: now,
|
|
103
|
+
});
|
|
104
|
+
const acknowledged = { ...originalReceipt,
|
|
105
|
+
state: advanceDelivery(originalReceipt.state, "acknowledged"), updatedAt: now };
|
|
106
|
+
const replyReceipt = validateRecord("receipt", {
|
|
107
|
+
schemaVersion: SCHEMA_VERSION,
|
|
108
|
+
messageId: replyId,
|
|
109
|
+
workspaceId: session.workspaceId,
|
|
110
|
+
recipientParticipantId: original.fromParticipantId,
|
|
111
|
+
state: "queued",
|
|
112
|
+
updatedAt: now,
|
|
113
|
+
});
|
|
114
|
+
tx.put("message", replyId, reply);
|
|
115
|
+
tx.put("receipt", receiptId(replyId, original.fromParticipantId), replyReceipt);
|
|
116
|
+
tx.put("receipt", ownReceiptId, acknowledged,
|
|
117
|
+
tx.generationOf("receipt", ownReceiptId));
|
|
118
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
119
|
+
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
120
|
+
type: "message.sent", occurredAt: now,
|
|
121
|
+
payload: { messageId: replyId, type: reply.type,
|
|
122
|
+
recipients: reply.toParticipantIds } });
|
|
123
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
124
|
+
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
125
|
+
type: "message.acknowledged", occurredAt: now,
|
|
126
|
+
payload: { messageId: original.messageId,
|
|
127
|
+
recipientParticipantId: session.participantId } });
|
|
128
|
+
result = { reply, receipt: acknowledged };
|
|
129
|
+
}, { kinds: ["message", "receipt"] });
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { readInbox, replyToMessage };
|
|
134
|
+
}
|
|
@@ -4,5 +4,6 @@ export { assertPorts } from "./ports.mjs";
|
|
|
4
4
|
export { classifySessionPresence } from "./sessions.mjs";
|
|
5
5
|
export { defaultPidIsAlive } from "./pid.mjs";
|
|
6
6
|
export { ATTENTION_PRIORITY, computeAttention } from "./sync.mjs";
|
|
7
|
+
export { looksConsequential, noteNudge } from "./message-signals.mjs";
|
|
7
8
|
export { overlaps } from "./claims.mjs";
|
|
8
9
|
export { wouldCycle } from "./tasks.mjs";
|