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.
- package/README.md +76 -138
- package/SECURITY.md +31 -0
- package/bin/acc-hook.mjs +96 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +6 -1
- package/docs/ADAPTER_AUTHORING.md +172 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +105 -197
- package/docs/CLI.md +157 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +143 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +109 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +102 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +42 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +15 -5
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +21 -12
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
- 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 +80 -160
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +14 -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 +152 -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/certification.json +52 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +80 -160
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +10 -4
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +139 -224
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +13 -29
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +5 -6
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +12 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +109 -109
- 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/attention.mjs +106 -0
- package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
- package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +118 -0
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +3 -2
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +14 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +70 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -258
- package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +156 -60
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +109 -71
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +74 -93
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
- package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +86 -28
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +121 -27
- package/package.json +22 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
- package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
- package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
|
@@ -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.
|
|
@@ -108,6 +110,8 @@ export function describeChanges(operation, home) {
|
|
|
108
110
|
}
|
|
109
111
|
return [
|
|
110
112
|
...(operation.removed ?? []).map(file => ` removed ${shorten(file, home)}`),
|
|
113
|
+
...(operation.removedDirectories ?? [])
|
|
114
|
+
.map(file => ` removed ${shorten(file, home)}`),
|
|
111
115
|
...edited,
|
|
112
116
|
...(operation.kept ?? [])
|
|
113
117
|
.map(file => ` kept ${shorten(file, home)} - changed since ACC wrote it`),
|
|
@@ -120,6 +124,9 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
120
124
|
+ (failed.length > 0 ? `; ${failed.length} failed` : ""),
|
|
121
125
|
...operations.filter(operation => operation.applied)
|
|
122
126
|
.flatMap(operation => describeChanges(operation, home)),
|
|
127
|
+
...operations.filter(operation => operation.applied
|
|
128
|
+
&& typeof operation.deliveryDiagnostic === "string")
|
|
129
|
+
.map(operation => ` ${operation.deliveryDiagnostic}`),
|
|
123
130
|
...skipped.map(entry => ` skip ${entry.adapterId}: ${entry.reason}`),
|
|
124
131
|
...failed.map(entry => ` ${entry.adapterId}: ${entry.error}`),
|
|
125
132
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
@@ -163,7 +170,8 @@ export function failureOf({ action, acted, failed = [] }) {
|
|
|
163
170
|
export function actedOn(result) {
|
|
164
171
|
return result.operations.filter(operation => operation.applied
|
|
165
172
|
&& (result.action === "install"
|
|
166
|
-
|| (operation.removed?.length ?? 0) + (operation.
|
|
173
|
+
|| (operation.removed?.length ?? 0) + (operation.removedDirectories?.length ?? 0)
|
|
174
|
+
+ (operation.changes?.length ?? 0) > 0)).length;
|
|
167
175
|
}
|
|
168
176
|
|
|
169
177
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
@@ -198,7 +206,8 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
198
206
|
? []
|
|
199
207
|
: (Array.isArray(options.adapter) ? options.adapter : [options.adapter]);
|
|
200
208
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
201
|
-
accVersion, allowDowngrade: options.downgrade === true, requested
|
|
209
|
+
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
210
|
+
delivery: options.delivery ?? "off" });
|
|
202
211
|
const result = await applyPlan({ plan, adapters, context, dataHome, dryRun, accVersion });
|
|
203
212
|
|
|
204
213
|
const acted = actedOn(result);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
// Kept cohesive above 300 lines because this is the single CLI composition boundary: every
|
|
2
|
+
// handler shares one owner-resolution, result, and error contract with `main`. Splitting the
|
|
3
|
+
// dispatch table would either duplicate that contract or expose storage/runtime context solely
|
|
4
|
+
// to pass it across a module boundary, making command behavior harder to audit as one surface.
|
|
5
|
+
import { AccError, CONFIG_FILENAME, EXIT, GENERIC_MESSAGE_KINDS, VALID_OBLIGATIONS,
|
|
6
|
+
failure, ok }
|
|
2
7
|
from "@agents-can-communicate/protocol";
|
|
3
8
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
4
9
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
@@ -80,8 +85,11 @@ async function locateContext(options, runtime) {
|
|
|
80
85
|
async function withService({ descriptor, paths }, runtime) {
|
|
81
86
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
82
87
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
88
|
+
const service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
|
|
83
89
|
return { descriptor, paths,
|
|
84
|
-
service
|
|
90
|
+
service,
|
|
91
|
+
deliveryRouter: typeof runtime.createDeliveryRouter === "function"
|
|
92
|
+
? runtime.createDeliveryRouter({ service, clock: runtime.clock }) : null };
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
async function openContext(options, runtime) {
|
|
@@ -107,6 +115,49 @@ async function openDiagnosticContext(options, runtime) {
|
|
|
107
115
|
|
|
108
116
|
const human = value => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
109
117
|
|
|
118
|
+
const clientMessageId = (options, context) =>
|
|
119
|
+
options.clientMessageId ?? context.service.ids.next("client");
|
|
120
|
+
|
|
121
|
+
function obligationFor(kind, explicit, addressed) {
|
|
122
|
+
if (!GENERIC_MESSAGE_KINDS.includes(kind)) {
|
|
123
|
+
const command = kind === "answer" ? "reply" : kind === "handoff" ? "finish" : "message";
|
|
124
|
+
throw usage(kind === "answer" || kind === "handoff"
|
|
125
|
+
? `${kind} messages require acc ${command}` : `unknown message type: ${kind}`);
|
|
126
|
+
}
|
|
127
|
+
const obligation = explicit ?? VALID_OBLIGATIONS[kind][0];
|
|
128
|
+
if (!VALID_OBLIGATIONS[kind].includes(obligation)
|
|
129
|
+
|| (!addressed && obligation !== "none")) {
|
|
130
|
+
throw usage(`message obligation ${obligation} is invalid for ${kind}`);
|
|
131
|
+
}
|
|
132
|
+
return obligation;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function recordedText(message, delivery) {
|
|
136
|
+
const diagnostics = delivery.map(item => item.outcome === "offered"
|
|
137
|
+
? `live offered to ${item.recipientParticipantId} via ${item.transport}`
|
|
138
|
+
: item.outcome === "queued"
|
|
139
|
+
? `live offer unavailable (${item.errorCode ?? "durable_fallback"})`
|
|
140
|
+
: `delivery already ${item.outcome}`);
|
|
141
|
+
return [`recorded ${message.messageId}`, ...diagnostics].join("; ");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function recordAndOffer({ record, router, selectMessage = value => value }) {
|
|
145
|
+
const recorded = await record();
|
|
146
|
+
const message = selectMessage(recorded);
|
|
147
|
+
if (router === null || router === undefined
|
|
148
|
+
|| !Array.isArray(message?.toParticipantIds) || message.toParticipantIds.length === 0) {
|
|
149
|
+
return { recorded, delivery: [] };
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return { recorded, delivery: await router.offer(message) };
|
|
153
|
+
} catch {
|
|
154
|
+
return { recorded, delivery: message.toParticipantIds.map(recipientParticipantId => ({
|
|
155
|
+
recipientParticipantId, outcome: "queued", transport: "durable",
|
|
156
|
+
errorCode: "transport_error",
|
|
157
|
+
})) };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
110
161
|
/** How many are here, and how many only look it. */
|
|
111
162
|
export function describePresence({ live = 0, stale = 0 } = {}) {
|
|
112
163
|
if (live === 0) return "0 live";
|
|
@@ -168,8 +219,7 @@ const HANDLERS = Object.freeze({
|
|
|
168
219
|
if (options.summary === undefined) throw usage("work requires --summary");
|
|
169
220
|
const intent = await context.service.setIntent({ sessionId: options.session,
|
|
170
221
|
generation: options.generation, summary: options.summary, mode: options.mode ?? "edit",
|
|
171
|
-
state: options.state,
|
|
172
|
-
resourceHints: options.hint ?? [] });
|
|
222
|
+
state: options.state, resourceHints: options.hint ?? [] });
|
|
173
223
|
return { data: intent, text: `intent: ${intent.summary}` };
|
|
174
224
|
},
|
|
175
225
|
|
|
@@ -194,122 +244,72 @@ const HANDLERS = Object.freeze({
|
|
|
194
244
|
},
|
|
195
245
|
|
|
196
246
|
message: async ({ options, context }) => {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
247
|
+
const kind = options.type ?? "note";
|
|
248
|
+
const toParticipantIds = options.to ?? [];
|
|
249
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
|
|
250
|
+
context.service.sendMessage({ sessionId: options.session,
|
|
251
|
+
generation: options.generation, clientMessageId: clientMessageId(options, context),
|
|
252
|
+
toParticipantIds, kind,
|
|
253
|
+
obligation: obligationFor(kind, options.obligation, toParticipantIds.length > 0),
|
|
254
|
+
subject: options.subject, body: options.body, descriptor: context.descriptor }) });
|
|
255
|
+
const message = routed.recorded;
|
|
256
|
+
return { data: { message, delivery: routed.delivery },
|
|
257
|
+
text: recordedText(message, routed.delivery) };
|
|
203
258
|
},
|
|
204
259
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
* quoted peer text explaining why.
|
|
211
|
-
*/
|
|
212
|
-
request: async ({ options, context }) => {
|
|
213
|
-
const { task, message } = await context.service.requestWork({
|
|
214
|
-
sessionId: options.session, generation: options.generation,
|
|
215
|
-
toParticipantId: options.to, title: options.title, detail: options.detail,
|
|
216
|
-
workstreamId: options.workstream, priority: options.priority,
|
|
217
|
-
dependsOn: options.dependsOn ?? [], descriptor: context.descriptor });
|
|
218
|
-
return { data: { task, message },
|
|
219
|
-
text: `requested ${task.taskId} of ${options.to}` };
|
|
260
|
+
inbox: async ({ options, context }) => {
|
|
261
|
+
const messages = await context.service.readInbox({ sessionId: options.session,
|
|
262
|
+
generation: options.generation, messageId: options.message });
|
|
263
|
+
return { data: messages, text: messages.length === 0
|
|
264
|
+
? "inbox empty" : JSON.stringify(messages, null, 2) };
|
|
220
265
|
},
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
*
|
|
232
|
-
* `--human` is the caller stating that a person actually decided this. The
|
|
233
|
-
* core refuses `--authority human` without it, because a peer proposal
|
|
234
|
-
* becoming a human decision on its own is the one way this record could
|
|
235
|
-
* launder an agent's opinion into a ruling.
|
|
236
|
-
*/
|
|
237
|
-
decide: async ({ options, context }) => {
|
|
238
|
-
const decision = await context.service.recordDecision({
|
|
239
|
-
sessionId: options.session, generation: options.generation,
|
|
240
|
-
title: options.title, outcome: options.outcome,
|
|
241
|
-
authority: options.authority ?? "workstream",
|
|
242
|
-
workstreamId: options.workstream ?? null,
|
|
243
|
-
decidedBy: options.decidedBy,
|
|
244
|
-
supersedes: options.supersedes ?? null,
|
|
245
|
-
humanConfirmed: options.human === true,
|
|
246
|
-
descriptor: context.descriptor });
|
|
247
|
-
return { data: decision,
|
|
248
|
-
text: `${decision.decisionId} ${decision.authority}: ${decision.title}` };
|
|
267
|
+
reply: async ({ options, context }) => {
|
|
268
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter,
|
|
269
|
+
selectMessage: value => value.reply, record: () => context.service.replyToMessage({
|
|
270
|
+
sessionId: options.session,
|
|
271
|
+
generation: options.generation, messageId: options.message, body: options.body,
|
|
272
|
+
subject: options.subject, clientMessageId: clientMessageId(options, context) }) });
|
|
273
|
+
const result = routed.recorded;
|
|
274
|
+
return { data: { message: result.reply, delivery: routed.delivery },
|
|
275
|
+
text: recordedText(result.reply, routed.delivery) };
|
|
249
276
|
},
|
|
250
277
|
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
text: `${acted.workstreamId} ${options.take === true ? "coordinated" : "released"}` };
|
|
268
|
-
}
|
|
269
|
-
if (options.title === undefined) throw usage("workstream requires --title");
|
|
270
|
-
if (options.objective === undefined) throw usage("workstream requires --objective");
|
|
271
|
-
const workstream = await context.service.createWorkstream({ ...owner,
|
|
272
|
-
title: options.title, objective: options.objective,
|
|
273
|
-
descriptor: context.descriptor });
|
|
274
|
-
return { data: workstream, text: `${workstream.workstreamId} ${workstream.state}` };
|
|
278
|
+
request: async ({ options, context }) => {
|
|
279
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter, record: () =>
|
|
280
|
+
context.service.sendMessage({
|
|
281
|
+
sessionId: options.session,
|
|
282
|
+
generation: options.generation,
|
|
283
|
+
clientMessageId: clientMessageId(options, context),
|
|
284
|
+
toParticipantIds: [options.to],
|
|
285
|
+
kind: "request",
|
|
286
|
+
obligation: "reply",
|
|
287
|
+
subject: options.title,
|
|
288
|
+
body: options.detail ?? options.title,
|
|
289
|
+
descriptor: context.descriptor,
|
|
290
|
+
}) });
|
|
291
|
+
const message = routed.recorded;
|
|
292
|
+
return { data: { message, delivery: routed.delivery },
|
|
293
|
+
text: recordedText(message, routed.delivery) };
|
|
275
294
|
},
|
|
276
295
|
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (options.take === true) {
|
|
282
|
-
if (options.task === undefined) throw usage("task --take requires --task");
|
|
283
|
-
const taken = await context.service.claimTask({ ...owner, taskId: options.task,
|
|
284
|
-
force: options.force === true });
|
|
285
|
-
return { data: taken, text: `${taken.taskId} ${taken.state}` };
|
|
286
|
-
}
|
|
287
|
-
if (options.decline === true) {
|
|
288
|
-
if (options.task === undefined) throw usage("task --decline requires --task");
|
|
289
|
-
const refused = await context.service.declineTask({ ...owner,
|
|
290
|
-
taskId: options.task, reason: options.reason });
|
|
291
|
-
return { data: refused, text: `${refused.taskId} declined` };
|
|
292
|
-
}
|
|
293
|
-
if (options.state !== undefined) {
|
|
294
|
-
if (options.task === undefined) throw usage("task --state requires --task");
|
|
295
|
-
const moved = await context.service.transitionTask({ ...owner,
|
|
296
|
-
taskId: options.task, state: options.state });
|
|
297
|
-
return { data: moved, text: `${moved.taskId} ${moved.state}` };
|
|
298
|
-
}
|
|
299
|
-
if (options.title === undefined) throw usage("task requires --title");
|
|
300
|
-
const task = await context.service.createTask({ ...owner,
|
|
301
|
-
workstreamId: options.workstream, title: options.title, detail: options.detail,
|
|
302
|
-
assigneeParticipantId: options.assignee, taskId: options.task,
|
|
303
|
-
dependsOn: options.dependsOn ?? [], descriptor: context.descriptor });
|
|
304
|
-
return { data: task, text: `${task.taskId} ${task.state}` };
|
|
296
|
+
ack: async ({ options, context }) => {
|
|
297
|
+
const receipt = await context.service.acknowledgeMessage({ sessionId: options.session,
|
|
298
|
+
generation: options.generation, messageId: options.message });
|
|
299
|
+
return { data: receipt, text: `${receipt.messageId} ${receipt.state}` };
|
|
305
300
|
},
|
|
306
301
|
|
|
307
302
|
finish: async ({ options, context }) => {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
303
|
+
const routed = await recordAndOffer({ router: context.deliveryRouter,
|
|
304
|
+
selectMessage: value => value.message, record: () => context.service.finishSession({
|
|
305
|
+
sessionId: options.session,
|
|
306
|
+
generation: options.generation, clientMessageId: clientMessageId(options, context),
|
|
307
|
+
goal: options.goal, status: options.status,
|
|
308
|
+
toParticipantId: options.to, completed: options.completed ?? [],
|
|
309
|
+
remaining: options.remaining ?? [], blockers: options.blocker ?? [] }) });
|
|
310
|
+
const handoff = routed.recorded;
|
|
311
|
+
return { data: { message: handoff.message, delivery: routed.delivery },
|
|
312
|
+
text: recordedText(handoff.message, routed.delivery) };
|
|
313
313
|
},
|
|
314
314
|
|
|
315
315
|
status: async ({ options, context }) => {
|
|
@@ -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,106 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
import { overlaps } from "./claims.mjs";
|
|
4
|
+
import { classifySessionPresence } from "./sessions.mjs";
|
|
5
|
+
|
|
6
|
+
export const ATTENTION_PRIORITY = Object.freeze({
|
|
7
|
+
reply_required: 1,
|
|
8
|
+
acknowledgement_required: 2,
|
|
9
|
+
recipient_unavailable: 3,
|
|
10
|
+
claim_conflict: 4,
|
|
11
|
+
claim_contended: 5,
|
|
12
|
+
claim_expired: 6,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
function obligationItems(snapshot, participantId) {
|
|
16
|
+
const messages = new Map((snapshot.messages ?? []).map(item => [item.messageId, item]));
|
|
17
|
+
return (snapshot.receipts ?? []).flatMap(receipt => {
|
|
18
|
+
if (receipt.recipientParticipantId !== participantId
|
|
19
|
+
|| receipt.state === "acknowledged") return [];
|
|
20
|
+
const message = messages.get(receipt.messageId);
|
|
21
|
+
const kind = message?.obligation === "reply" ? "reply_required"
|
|
22
|
+
: message?.obligation === "acknowledge" ? "acknowledgement_required" : null;
|
|
23
|
+
const action = kind === "reply_required" ? "a reply" : "acknowledgement";
|
|
24
|
+
return kind === null ? [] : [{ kind, priority: ATTENTION_PRIORITY[kind],
|
|
25
|
+
sourceId: message.messageId,
|
|
26
|
+
summary: `message ${message.messageId} from ${message.fromParticipantId} is a `
|
|
27
|
+
+ `${message.kind} requiring ${action}` }];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function presenceByParticipant(snapshot, now, pidIsAlive) {
|
|
32
|
+
const online = new Set();
|
|
33
|
+
for (const session of snapshot.sessions ?? []) {
|
|
34
|
+
if (classifySessionPresence(session, now, pidIsAlive) === "online") {
|
|
35
|
+
online.add(session.participantId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return online;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function unavailableRecipients(snapshot, participantId, now, pidIsAlive) {
|
|
42
|
+
const online = presenceByParticipant(snapshot, now, pidIsAlive);
|
|
43
|
+
const receipts = snapshot.receipts ?? [];
|
|
44
|
+
return (snapshot.messages ?? []).flatMap(message => {
|
|
45
|
+
if (message.fromParticipantId !== participantId || message.toParticipantIds?.length === 0
|
|
46
|
+
|| message.obligation === "none") return [];
|
|
47
|
+
return receipts.filter(receipt => receipt.messageId === message.messageId
|
|
48
|
+
&& receipt.state !== "acknowledged"
|
|
49
|
+
&& !online.has(receipt.recipientParticipantId))
|
|
50
|
+
.map(receipt => ({ kind: "recipient_unavailable",
|
|
51
|
+
priority: ATTENTION_PRIORITY.recipient_unavailable,
|
|
52
|
+
sourceId: message.messageId,
|
|
53
|
+
summary: `${message.subject} - ${receipt.recipientParticipantId} is unavailable` }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function expiredClaims(snapshot, session, now) {
|
|
58
|
+
if (session == null) return [];
|
|
59
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId === session.sessionId
|
|
60
|
+
&& Date.parse(claim.expiresAt) <= Date.parse(now))
|
|
61
|
+
.map(claim => ({ kind: "claim_expired", priority: ATTENTION_PRIORITY.claim_expired,
|
|
62
|
+
sourceId: claim.claimId,
|
|
63
|
+
summary: `${claim.resource} - your claim has run out, and peers can write to it` }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function claimConflicts(snapshot, session, now) {
|
|
67
|
+
const mine = (snapshot.intents ?? []).find(intent => intent.sessionId === session?.sessionId);
|
|
68
|
+
if (mine === undefined) return [];
|
|
69
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId !== session.sessionId
|
|
70
|
+
&& Date.parse(claim.expiresAt) > Date.parse(now)
|
|
71
|
+
&& mine.resourceHints.some(hint => overlaps(hint, claim.resource)))
|
|
72
|
+
.map(claim => ({ kind: "claim_conflict", priority: ATTENTION_PRIORITY.claim_conflict,
|
|
73
|
+
sourceId: claim.claimId,
|
|
74
|
+
summary: `${claim.resource} is claimed by ${claim.ownerSessionId}` }));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function claimContended(snapshot, session, now) {
|
|
78
|
+
if (session == null) return [];
|
|
79
|
+
const peerIntents = (snapshot.intents ?? [])
|
|
80
|
+
.filter(intent => intent.sessionId !== session.sessionId);
|
|
81
|
+
return (snapshot.claims ?? []).filter(claim => claim.ownerSessionId === session.sessionId
|
|
82
|
+
&& Date.parse(claim.expiresAt) > Date.parse(now)).flatMap(claim => {
|
|
83
|
+
const peer = peerIntents.find(intent => intent.resourceHints
|
|
84
|
+
.some(hint => overlaps(hint, claim.resource)));
|
|
85
|
+
if (peer === undefined) return [];
|
|
86
|
+
const participant = (snapshot.sessions ?? [])
|
|
87
|
+
.find(item => item.sessionId === peer.sessionId)?.participantId ?? "a peer";
|
|
88
|
+
return [{ kind: "claim_contended", priority: ATTENTION_PRIORITY.claim_contended,
|
|
89
|
+
sourceId: claim.claimId,
|
|
90
|
+
summary: `${claim.resource} - ${participant} means to work on what you hold` }];
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function computeAttention(snapshot, { session, participantId, now, pidIsAlive }) {
|
|
95
|
+
if (typeof pidIsAlive !== "function") {
|
|
96
|
+
throw new AccError(EXIT.USAGE, "computeAttention requires a pidIsAlive probe", {});
|
|
97
|
+
}
|
|
98
|
+
return [
|
|
99
|
+
...obligationItems(snapshot, participantId),
|
|
100
|
+
...unavailableRecipients(snapshot, participantId, now, pidIsAlive),
|
|
101
|
+
...claimConflicts(snapshot, session, now),
|
|
102
|
+
...claimContended(snapshot, session, now),
|
|
103
|
+
...expiredClaims(snapshot, session, now),
|
|
104
|
+
].sort((left, right) => left.priority - right.priority
|
|
105
|
+
|| left.sourceId.localeCompare(right.sourceId));
|
|
106
|
+
}
|