agents-can-communicate 0.1.18 → 0.3.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 +87 -69
- package/SECURITY.md +31 -0
- package/bin/acc-bootstrap.mjs +56 -0
- package/bin/acc-claude-channel.mjs +177 -0
- package/bin/acc-hook.mjs +94 -12
- package/bin/acc-mcp.mjs +6 -2
- package/bin/acc.mjs +13 -3
- package/docs/ADAPTER_AUTHORING.md +204 -0
- package/docs/ARCHITECTURE.md +131 -0
- package/docs/CAPABILITIES.md +117 -214
- package/docs/CLI.md +164 -0
- package/docs/CONCEPTS.md +134 -0
- package/docs/CONFIGURATION.md +147 -0
- package/docs/DESIGN_DECISIONS.md +89 -0
- package/docs/GETTING_STARTED.md +145 -0
- package/docs/GLOSSARY.md +26 -0
- package/docs/HOW_IT_WORKS.md +277 -0
- package/docs/MCP.md +94 -0
- package/docs/PROTOCOL.md +200 -0
- package/docs/RELEASING.md +115 -0
- package/docs/SECURITY_MODEL.md +131 -0
- package/docs/TROUBLESHOOTING.md +108 -0
- package/docs/WHY_ACC.md +61 -0
- package/docs/index.md +44 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +228 -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 +269 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.258.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.260.json +23 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +13 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.mcp.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +45 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/channel.mjs +377 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +27 -7
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/native-delivery.mjs +229 -0
- package/node_modules/@agents-can-communicate/adapter-codex/certification.json +150 -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 +199 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +21 -0
- package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.1-remote-workspace.json +25 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +11 -2
- 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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +54 -12
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +121 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +151 -0
- package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +192 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +68 -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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell-0.57.0.json +12 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart-0.57.0.json +8 -0
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +293 -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 +31 -13
- 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 +2 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +22 -22
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +11 -9
- 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 +22 -22
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +52 -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 +36 -17
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +9 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +7 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-activation.mjs +76 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +202 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +101 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +28 -4
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +12 -31
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +70 -5
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +111 -12
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
- 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 +131 -0
- package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
- package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
- package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
- package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
- package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
- package/node_modules/@agents-can-communicate/core/src/service.mjs +21 -10
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
- package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
- 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 +131 -0
- package/node_modules/@agents-can-communicate/hook-runner/package.json +4 -2
- package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +90 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +190 -105
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +70 -10
- package/node_modules/@agents-can-communicate/installer/src/bootstrap-runtime.mjs +144 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +89 -5
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +10 -2
- package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +161 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +112 -12
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +54 -2
- package/node_modules/@agents-can-communicate/installer/src/shell-bootstrap.mjs +210 -0
- 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 +102 -72
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
- 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/fields.mjs +17 -0
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
- package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +64 -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 +68 -26
- package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
- package/package.json +20 -1
- package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
- package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
- 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
|
@@ -1,134 +1,118 @@
|
|
|
1
|
-
import { AccError, EXIT, SCHEMA_VERSION,
|
|
1
|
+
import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
|
|
2
2
|
from "@agents-can-communicate/protocol";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|| (
|
|
4
|
+
import { receiptId, recordMessageInTransaction } from "./conversations.mjs";
|
|
5
|
+
|
|
6
|
+
const listable = (message, receipt) => receipt.state === "queued" || receipt.state === "offered"
|
|
7
|
+
|| (receipt.state === "retrieved" && message.obligation !== "none");
|
|
8
8
|
|
|
9
|
-
/** Narrow, recipient-owned message reads and replies. */
|
|
10
9
|
export function createInboxService(ports, sessions) {
|
|
11
10
|
const { store, clock, ids } = ports;
|
|
12
11
|
|
|
13
12
|
async function requireOpen(input, action) {
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
||
|
|
13
|
+
const located = await sessions.locateSession(input.sessionId, input.workspaceId);
|
|
14
|
+
if (located === null || located.record.state !== "open"
|
|
15
|
+
|| located.record.generation !== input.generation) {
|
|
17
16
|
throw new AccError(EXIT.CONFLICT, `cannot ${action} from this session generation`,
|
|
18
17
|
{ sessionId: input.sessionId });
|
|
19
18
|
}
|
|
20
|
-
return
|
|
19
|
+
return located.record;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireOwnedReceipt(tx, session, messageId) {
|
|
23
|
+
const message = tx.get("message", messageId);
|
|
24
|
+
const id = receiptId(messageId, session.participantId);
|
|
25
|
+
const receipt = tx.get("receipt", id);
|
|
26
|
+
if (message === null || receipt === null) {
|
|
27
|
+
throw new AccError(EXIT.CONFLICT,
|
|
28
|
+
"that message is not addressed to this participant",
|
|
29
|
+
{ messageId, participantId: session.participantId });
|
|
30
|
+
}
|
|
31
|
+
return { id, message, receipt };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function advanceOwned(tx, session, messageId, state, now) {
|
|
35
|
+
const owned = requireOwnedReceipt(tx, session, messageId);
|
|
36
|
+
const nextState = advanceReceipt(owned.receipt.state, state);
|
|
37
|
+
if (nextState === owned.receipt.state) {
|
|
38
|
+
return { message: owned.message, receipt: owned.receipt };
|
|
39
|
+
}
|
|
40
|
+
const receipt = { ...owned.receipt, state: nextState, updatedAt: now };
|
|
41
|
+
tx.put("receipt", owned.id, receipt, tx.generationOf("receipt", owned.id));
|
|
42
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
43
|
+
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
44
|
+
type: `message.${state}`, occurredAt: now,
|
|
45
|
+
payload: { messageId, recipientParticipantId: session.participantId } });
|
|
46
|
+
return { message: owned.message, receipt };
|
|
21
47
|
}
|
|
22
48
|
|
|
23
49
|
async function readInbox(input) {
|
|
24
50
|
const session = await requireOpen(input, "read the inbox");
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
const now = clock.now();
|
|
52
|
+
return store.transaction(tx => {
|
|
53
|
+
const messages = new Map(tx.list("message").map(item => [item.messageId, item]));
|
|
54
|
+
let selected = tx.list("receipt", receipt =>
|
|
55
|
+
receipt.recipientParticipantId === session.participantId)
|
|
56
|
+
.filter(receipt => messages.has(receipt.messageId));
|
|
57
|
+
if (input.messageId === undefined) {
|
|
58
|
+
selected = selected.filter(receipt => listable(messages.get(receipt.messageId), receipt));
|
|
59
|
+
} else {
|
|
60
|
+
selected = selected.filter(receipt => receipt.messageId === input.messageId
|
|
61
|
+
&& receipt.state !== "acknowledged");
|
|
62
|
+
if (selected.length === 0) {
|
|
63
|
+
throw new AccError(EXIT.CONFLICT,
|
|
64
|
+
"that message is not recoverable by this participant",
|
|
65
|
+
{ messageId: input.messageId, participantId: session.participantId });
|
|
66
|
+
}
|
|
39
67
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
selected.sort((left, right) => {
|
|
69
|
+
const a = messages.get(left.messageId);
|
|
70
|
+
const b = messages.get(right.messageId);
|
|
71
|
+
return a.sentAt.localeCompare(b.sentAt) || a.messageId.localeCompare(b.messageId);
|
|
72
|
+
});
|
|
73
|
+
return selected.map(receipt => advanceOwned(tx, session, receipt.messageId,
|
|
74
|
+
"retrieved", now));
|
|
75
|
+
}, { kinds: ["message", "receipt"] });
|
|
76
|
+
}
|
|
46
77
|
|
|
78
|
+
async function acknowledgeMessage(input) {
|
|
79
|
+
const session = await requireOpen(input, "acknowledge a message");
|
|
47
80
|
const now = clock.now();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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;
|
|
81
|
+
return store.transaction(tx => advanceOwned(tx, session, input.messageId,
|
|
82
|
+
"acknowledged", now).receipt,
|
|
83
|
+
{ kinds: ["message", "receipt"] });
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
async function replyToMessage(input) {
|
|
68
87
|
const session = await requireOpen(input, "reply to a message");
|
|
69
88
|
const now = clock.now();
|
|
70
|
-
const replyId =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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") {
|
|
89
|
+
const replyId = ids.next("message");
|
|
90
|
+
return store.transaction(tx => {
|
|
91
|
+
const original = requireOwnedReceipt(tx, session, input.messageId);
|
|
92
|
+
const matchingReply = tx.list("message", message =>
|
|
93
|
+
message.workspaceId === session.workspaceId
|
|
94
|
+
&& message.fromParticipantId === session.participantId
|
|
95
|
+
&& message.clientMessageId === input.clientMessageId).at(0);
|
|
96
|
+
if (original.receipt.state === "acknowledged" && matchingReply === undefined) {
|
|
83
97
|
throw new AccError(EXIT.CONFLICT, "that message is already resolved",
|
|
84
|
-
{ messageId: input.messageId
|
|
98
|
+
{ messageId: input.messageId });
|
|
85
99
|
}
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
100
|
+
const recorded = recordMessageInTransaction({ tx, session, now, messageId: replyId, ids,
|
|
101
|
+
action: "reply to a message", input: {
|
|
102
|
+
clientMessageId: input.clientMessageId,
|
|
103
|
+
toParticipantIds: [original.message.fromParticipantId],
|
|
104
|
+
kind: "answer",
|
|
105
|
+
obligation: "none",
|
|
106
|
+
subject: input.subject ?? `Re: ${original.message.subject}`,
|
|
107
|
+
body: input.body,
|
|
108
|
+
inReplyTo: original.message.messageId,
|
|
109
|
+
artifacts: input.artifacts ?? [],
|
|
110
|
+
handoff: null,
|
|
111
|
+
} });
|
|
112
|
+
const receipt = advanceOwned(tx, session, input.messageId, "acknowledged", now).receipt;
|
|
113
|
+
return { reply: recorded.message, receipt };
|
|
114
|
+
}, { kinds: ["participant", "session", "message", "receipt"] });
|
|
131
115
|
}
|
|
132
116
|
|
|
133
|
-
return { readInbox, replyToMessage };
|
|
117
|
+
return { readInbox, replyToMessage, acknowledgeMessage };
|
|
134
118
|
}
|
|
@@ -3,7 +3,7 @@ export { createCoordinationService } from "./service.mjs";
|
|
|
3
3
|
export { assertPorts } from "./ports.mjs";
|
|
4
4
|
export { classifySessionPresence } from "./sessions.mjs";
|
|
5
5
|
export { defaultPidIsAlive } from "./pid.mjs";
|
|
6
|
-
export { ATTENTION_PRIORITY, computeAttention } from "./
|
|
7
|
-
export {
|
|
6
|
+
export { ATTENTION_PRIORITY, computeAttention } from "./attention.mjs";
|
|
7
|
+
export { SAFE_OFFER_ERROR_CODES } from "./receipts.mjs";
|
|
8
|
+
export { createDeliveryBindingService } from "./delivery-bindings.mjs";
|
|
8
9
|
export { overlaps } from "./claims.mjs";
|
|
9
|
-
export { wouldCycle } from "./tasks.mjs";
|
|
@@ -8,7 +8,8 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
8
8
|
* list(kind: string, predicate?: (record: object) => boolean): object[],
|
|
9
9
|
* put(kind: string, id: string, record: object, expectedGeneration?: string | null): string,
|
|
10
10
|
* append(event: object): object }} CoordinationTransaction
|
|
11
|
-
* @typedef {{ transaction(callback: (tx: CoordinationTransaction) => unknown
|
|
11
|
+
* @typedef {{ transaction(callback: (tx: CoordinationTransaction) => unknown,
|
|
12
|
+
* options?: { kinds?: string[], deadlineAt?: number }): Promise<unknown>,
|
|
12
13
|
* eventsSince(workspaceId: string, cursor: string | null, limit: number): Promise<object>,
|
|
13
14
|
* snapshot(workspaceId: string): Promise<object> }} CoordinationStore
|
|
14
15
|
*/
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
|
|
2
|
+
from "@agents-can-communicate/protocol";
|
|
3
|
+
|
|
4
|
+
import { receiptId } from "./conversations.mjs";
|
|
5
|
+
|
|
6
|
+
export const SAFE_OFFER_ERROR_CODES = Object.freeze([
|
|
7
|
+
"ambiguous_recipient_sessions",
|
|
8
|
+
"delivery_disabled",
|
|
9
|
+
"recipient_busy",
|
|
10
|
+
"recipient_unavailable",
|
|
11
|
+
"transport_error",
|
|
12
|
+
"transport_rejected",
|
|
13
|
+
"unsupported_client_version",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function missingReceipt(messageId, recipientParticipantId) {
|
|
17
|
+
return new AccError(EXIT.CONFLICT, "no receipt exists for that recipient",
|
|
18
|
+
{ messageId, recipientParticipantId });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireReceipt(tx, messageId, recipientParticipantId) {
|
|
22
|
+
const receipt = tx.get("receipt", receiptId(messageId, recipientParticipantId));
|
|
23
|
+
if (receipt === null) throw missingReceipt(messageId, recipientParticipantId);
|
|
24
|
+
const message = tx.get("message", messageId);
|
|
25
|
+
if (message === null) throw missingReceipt(messageId, recipientParticipantId);
|
|
26
|
+
return { message, receipt };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function requireOfferableReceipt(tx, input, { allowRoomNextTurn }) {
|
|
30
|
+
const found = requireReceipt(tx, input.messageId, input.recipientParticipantId);
|
|
31
|
+
if (found.message.toParticipantIds.length === 0
|
|
32
|
+
&& (!allowRoomNextTurn || input.transport !== "next-turn")) {
|
|
33
|
+
throw new AccError(EXIT.CONFLICT, "room messages are not eligible for live offers",
|
|
34
|
+
{ messageId: input.messageId, recipientParticipantId: input.recipientParticipantId });
|
|
35
|
+
}
|
|
36
|
+
return found.receipt;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function requireTarget(tx, input, { mustBeOpen }) {
|
|
40
|
+
const target = tx.get("session", input.targetSessionId);
|
|
41
|
+
if (target === null || (mustBeOpen && target.state !== "open")
|
|
42
|
+
|| target.participantId !== input.recipientParticipantId
|
|
43
|
+
|| target.generation !== input.targetGeneration) {
|
|
44
|
+
throw new AccError(EXIT.CONFLICT,
|
|
45
|
+
"the offer target is not this recipient's session generation",
|
|
46
|
+
{ targetSessionId: input.targetSessionId,
|
|
47
|
+
recipientParticipantId: input.recipientParticipantId });
|
|
48
|
+
}
|
|
49
|
+
return target;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createReceiptService(ports) {
|
|
53
|
+
const { store, clock, ids } = ports;
|
|
54
|
+
|
|
55
|
+
async function readReceipt(input) {
|
|
56
|
+
return store.transaction(tx => requireReceipt(tx, input.messageId,
|
|
57
|
+
input.recipientParticipantId).receipt, { kinds: ["message", "receipt"] });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function recordOfferSucceeded(input) {
|
|
61
|
+
const now = clock.now();
|
|
62
|
+
return store.transaction(tx => {
|
|
63
|
+
const id = receiptId(input.messageId, input.recipientParticipantId);
|
|
64
|
+
const receipt = requireOfferableReceipt(tx, input, { allowRoomNextTurn: true });
|
|
65
|
+
const target = requireTarget(tx, input, { mustBeOpen: true });
|
|
66
|
+
// A retrieval or acknowledgement may win after a transport accepted but
|
|
67
|
+
// before this transaction acquired the writer. That stronger truth must
|
|
68
|
+
// remain in place. The same rule makes a repeated successful commit a
|
|
69
|
+
// read-only operation rather than another success event.
|
|
70
|
+
if (receipt.state !== "queued") return receipt;
|
|
71
|
+
const offered = { ...receipt, state: advanceReceipt(receipt.state, "offered"),
|
|
72
|
+
updatedAt: now };
|
|
73
|
+
tx.put("receipt", id, offered, tx.generationOf("receipt", id));
|
|
74
|
+
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
75
|
+
workspaceId: receipt.workspaceId, actorSessionId: target.sessionId,
|
|
76
|
+
type: "message.offer_succeeded", occurredAt: now,
|
|
77
|
+
payload: { messageId: input.messageId,
|
|
78
|
+
recipientParticipantId: input.recipientParticipantId,
|
|
79
|
+
targetSessionId: input.targetSessionId,
|
|
80
|
+
targetGeneration: input.targetGeneration,
|
|
81
|
+
transport: input.transport, adapterId: input.adapterId,
|
|
82
|
+
clientVersion: input.clientVersion } });
|
|
83
|
+
return offered;
|
|
84
|
+
}, { kinds: ["session", "message", "receipt"], deadlineAt: input.deadlineAt });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function recordOfferFailed(input) {
|
|
88
|
+
if (!SAFE_OFFER_ERROR_CODES.includes(input.safeErrorCode)) {
|
|
89
|
+
throw new AccError(EXIT.DATA, "safeErrorCode is not in the closed offer error set",
|
|
90
|
+
{ safeErrorCode: input.safeErrorCode });
|
|
91
|
+
}
|
|
92
|
+
const now = clock.now();
|
|
93
|
+
return store.transaction(tx => {
|
|
94
|
+
const receipt = requireOfferableReceipt(tx, input, { allowRoomNextTurn: false });
|
|
95
|
+
const target = requireTarget(tx, input, { mustBeOpen: false });
|
|
96
|
+
return tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
97
|
+
workspaceId: receipt.workspaceId, actorSessionId: target.sessionId,
|
|
98
|
+
type: "message.offer_failed", occurredAt: now,
|
|
99
|
+
payload: { messageId: input.messageId,
|
|
100
|
+
recipientParticipantId: input.recipientParticipantId,
|
|
101
|
+
targetSessionId: target.sessionId,
|
|
102
|
+
targetGeneration: target.generation,
|
|
103
|
+
transport: input.transport, adapterId: input.adapterId,
|
|
104
|
+
clientVersion: input.clientVersion, safeErrorCode: input.safeErrorCode } });
|
|
105
|
+
}, { kinds: ["session", "message", "receipt"] });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { readReceipt, recordOfferSucceeded, recordOfferFailed };
|
|
109
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { createClaimService } from "./claims.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { createConversationService } from "./conversations.mjs";
|
|
3
|
+
import { createDeliveryBindingService } from "./delivery-bindings.mjs";
|
|
3
4
|
import { createIntentService } from "./intents.mjs";
|
|
4
5
|
import { createInboxService } from "./inbox.mjs";
|
|
5
6
|
import { defaultPidIsAlive } from "./pid.mjs";
|
|
6
7
|
import { assertPorts } from "./ports.mjs";
|
|
8
|
+
import { createReceiptService } from "./receipts.mjs";
|
|
7
9
|
import { createSessionService } from "./sessions.mjs";
|
|
8
10
|
import { createGuardStateService, createStatusService } from "./status.mjs";
|
|
9
11
|
import { createSyncService } from "./sync.mjs";
|
|
10
|
-
import { createTaskService } from "./tasks.mjs";
|
|
11
|
-
import { createWorkstreamService } from "./workstreams.mjs";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Composition root for the domain services. Everything time- or
|
|
@@ -29,13 +29,13 @@ export function createCoordinationService({ store, clock, ids,
|
|
|
29
29
|
const ports = assertPorts({ store, clock, ids, pidIsAlive });
|
|
30
30
|
const sessions = createSessionService(ports);
|
|
31
31
|
const intents = createIntentService(ports, sessions);
|
|
32
|
-
const workstreams = createWorkstreamService(ports, sessions);
|
|
33
|
-
const tasks = createTaskService(ports, workstreams);
|
|
34
32
|
const claims = createClaimService(ports, sessions);
|
|
35
|
-
const
|
|
33
|
+
const conversations = createConversationService(ports, sessions);
|
|
34
|
+
const deliveryBindings = createDeliveryBindingService(ports, sessions);
|
|
36
35
|
const inbox = createInboxService(ports, sessions);
|
|
36
|
+
const receipts = createReceiptService(ports);
|
|
37
37
|
const sync = createSyncService(ports, sessions);
|
|
38
|
-
const status = createStatusService(ports, sessions);
|
|
38
|
+
const status = createStatusService(ports, sessions, deliveryBindings);
|
|
39
39
|
const guardState = createGuardStateService(ports);
|
|
40
40
|
return Object.freeze({
|
|
41
41
|
store,
|
|
@@ -43,12 +43,23 @@ export function createCoordinationService({ store, clock, ids,
|
|
|
43
43
|
ids,
|
|
44
44
|
policies: Object.freeze({ ...policies }),
|
|
45
45
|
...sessions,
|
|
46
|
+
// Closing a session also retires its live endpoint under the same
|
|
47
|
+
// generation, so nothing can be offered into a session that has left.
|
|
48
|
+
closeSession: async input => {
|
|
49
|
+
const closed = await sessions.closeSession(input);
|
|
50
|
+
await deliveryBindings.clearDeliveryBinding({ sessionId: input.sessionId,
|
|
51
|
+
generation: input.generation });
|
|
52
|
+
return closed;
|
|
53
|
+
},
|
|
46
54
|
...intents,
|
|
47
|
-
...workstreams,
|
|
48
|
-
...tasks,
|
|
49
55
|
...claims,
|
|
50
|
-
...
|
|
56
|
+
...conversations,
|
|
57
|
+
publishDeliveryBinding: deliveryBindings.publishDeliveryBinding,
|
|
58
|
+
clearDeliveryBinding: deliveryBindings.clearDeliveryBinding,
|
|
59
|
+
listDeliveryBindings: deliveryBindings.listDeliveryBindings,
|
|
60
|
+
refreshDeliveryBinding: deliveryBindings.refreshDeliveryBinding,
|
|
51
61
|
...inbox,
|
|
62
|
+
...receipts,
|
|
52
63
|
...sync,
|
|
53
64
|
...status,
|
|
54
65
|
guardState,
|
|
@@ -2,7 +2,6 @@ import { AccError, EXIT, SCHEMA_VERSION, assertPortableId, createId, validateRec
|
|
|
2
2
|
from "@agents-can-communicate/protocol";
|
|
3
3
|
|
|
4
4
|
import { ensureMaterialised, isMaterialised, materialise } from "./materialisation.mjs";
|
|
5
|
-
import { writeWorkResponse } from "./notify.mjs";
|
|
6
5
|
|
|
7
6
|
// A hook-only adapter heartbeats only when its harness gives it a turn, so the
|
|
8
7
|
// staleness window is a multiple of the cadence the session itself declared
|
|
@@ -154,7 +153,12 @@ export function createSessionService(ports) {
|
|
|
154
153
|
// The approved trigger is the SECOND live session, not the first: a lone
|
|
155
154
|
// session must be able to open and close without leaving a trace.
|
|
156
155
|
const live = (await store.ephemeral.list("session")).filter(item => item.state === "open");
|
|
157
|
-
|
|
156
|
+
// Recheck durable state only after our ephemeral writes. Another process
|
|
157
|
+
// may have staged the old ephemeral set after our first precheck, committed
|
|
158
|
+
// materialisation, and retired that set before these puts completed. In
|
|
159
|
+
// that case this attach is the only ephemeral session left, but it still
|
|
160
|
+
// has to run the idempotent promotion pass into the now-durable workspace.
|
|
161
|
+
if (live.length > 1 || await isMaterialised(store, workspaceId)) {
|
|
158
162
|
await materialise(ports, { workspaceId, descriptor: input.descriptor,
|
|
159
163
|
reason: "second_live_session" });
|
|
160
164
|
}
|
|
@@ -243,35 +247,33 @@ export function createSessionService(ports) {
|
|
|
243
247
|
|
|
244
248
|
await store.transaction(async tx => {
|
|
245
249
|
tx.put("session", sessionId, closed, tx.generationOf("session", sessionId));
|
|
246
|
-
// Work in progress goes back on the table. A task held by a session that
|
|
247
|
-
// has gone stayed `in_progress` forever, nobody else could take it, and
|
|
248
|
-
// whoever asked for it was never told - a request handed to an agent that
|
|
249
|
-
// closed its terminal simply vanished.
|
|
250
|
-
for (const task of tx.list("task")) {
|
|
251
|
-
if (task.assigneeSessionId !== sessionId) continue;
|
|
252
|
-
if (task.state === "done") continue;
|
|
253
|
-
const released = { ...task, assigneeSessionId: null, state: "pending" };
|
|
254
|
-
tx.put("task", task.taskId, released, tx.generationOf("task", task.taskId));
|
|
255
|
-
writeWorkResponse(tx, { task, actor: closed, workspaceId: closed.workspaceId,
|
|
256
|
-
now, ids, outcome: "released",
|
|
257
|
-
reason: "the session working on this closed before finishing it" });
|
|
258
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
259
|
-
workspaceId: closed.workspaceId, actorSessionId: sessionId,
|
|
260
|
-
type: "task.released", occurredAt: now, payload: { taskId: task.taskId } });
|
|
261
|
-
}
|
|
262
250
|
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
263
251
|
workspaceId: closed.workspaceId, actorSessionId: sessionId, type: "session.closed",
|
|
264
252
|
occurredAt: now, payload: {} });
|
|
265
|
-
|
|
266
|
-
}, { kinds: ["message", "receipt", "session", "task"] });
|
|
253
|
+
}, { kinds: ["session"] });
|
|
267
254
|
return closed;
|
|
268
255
|
}
|
|
269
256
|
|
|
257
|
+
async function listLiveSessions({ participantId, workspaceId, now = clock.now() }) {
|
|
258
|
+
const resolved = workspaceId ?? store.workspaceId;
|
|
259
|
+
const snapshot = resolved === undefined ? null
|
|
260
|
+
: await store.snapshot(resolved, { kinds: ["workspace", "session"] });
|
|
261
|
+
const records = snapshot?.workspace === null
|
|
262
|
+
? await store.ephemeral.list("session")
|
|
263
|
+
: snapshot?.sessions ?? await store.ephemeral.list("session");
|
|
264
|
+
return records
|
|
265
|
+
.filter(session => session.participantId === participantId
|
|
266
|
+
&& classifySessionPresence(session, now, pidIsAlive) !== "offline")
|
|
267
|
+
.sort((left, right) => left.sessionId.localeCompare(right.sessionId))
|
|
268
|
+
.map(session => ({ sessionId: session.sessionId, generation: session.generation }));
|
|
269
|
+
}
|
|
270
|
+
|
|
270
271
|
return {
|
|
271
272
|
openSession,
|
|
272
273
|
resumeSession,
|
|
273
274
|
heartbeatSession,
|
|
274
275
|
closeSession,
|
|
276
|
+
listLiveSessions,
|
|
275
277
|
locateSession: locate,
|
|
276
278
|
ensureMaterialised: options => ensureMaterialised(ports, options),
|
|
277
279
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classifySessionPresence } from "./sessions.mjs";
|
|
2
|
-
import { computeAttention } from "./
|
|
2
|
+
import { computeAttention } from "./attention.mjs";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Protection level, reported from what is actually enforceable.
|
|
@@ -57,7 +57,7 @@ export function createGuardStateService(ports) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
export function createStatusService(ports, sessions) {
|
|
60
|
+
export function createStatusService(ports, sessions, deliveryBindings) {
|
|
61
61
|
const { store, clock, pidIsAlive } = ports;
|
|
62
62
|
|
|
63
63
|
async function collectStatus(input = {}) {
|
|
@@ -85,6 +85,7 @@ export function createStatusService(ports, sessions) {
|
|
|
85
85
|
const live = classified.filter(item => item.presence !== "offline");
|
|
86
86
|
const claims = snapshot.claims
|
|
87
87
|
.filter(claim => Date.parse(claim.expiresAt) > Date.parse(now));
|
|
88
|
+
const currentBindings = await deliveryBindings.currentBindings(now);
|
|
88
89
|
|
|
89
90
|
return {
|
|
90
91
|
workspaceId,
|
|
@@ -109,12 +110,6 @@ export function createStatusService(ports, sessions) {
|
|
|
109
110
|
presence,
|
|
110
111
|
intent: intents.find(intent => intent.sessionId === session.sessionId)?.summary ?? null,
|
|
111
112
|
})),
|
|
112
|
-
workstreams: snapshot.workstreams.map(workstream => ({
|
|
113
|
-
workstreamId: workstream.workstreamId,
|
|
114
|
-
title: workstream.title,
|
|
115
|
-
state: workstream.state,
|
|
116
|
-
coordinatorSessionId: workstream.coordinatorSessionId,
|
|
117
|
-
})),
|
|
118
113
|
// The owner is named twice on purpose. Every command that reaches a peer
|
|
119
114
|
// takes a participant id, so a claim that gave only a session id sent the
|
|
120
115
|
// reader back through the roster to answer "who is holding this, and how
|
|
@@ -133,13 +128,20 @@ export function createStatusService(ports, sessions) {
|
|
|
133
128
|
.find(session => session.sessionId === claim.ownerSessionId)?.participantId ?? null,
|
|
134
129
|
expiresAt: claim.expiresAt,
|
|
135
130
|
})),
|
|
131
|
+
deliveryBindings: currentBindings.map(({ binding, reachable }) => ({
|
|
132
|
+
adapterId: binding.adapterId,
|
|
133
|
+
clientVersion: binding.clientVersion,
|
|
134
|
+
availableModes: [...binding.availableModes],
|
|
135
|
+
livePolicy: binding.livePolicy,
|
|
136
|
+
reachable,
|
|
137
|
+
leaseUntil: binding.leaseUntil,
|
|
138
|
+
})),
|
|
136
139
|
attention: computeAttention(snapshot, { session: null,
|
|
137
140
|
participantId: input.participantId, now, pidIsAlive }),
|
|
138
141
|
counts: {
|
|
139
142
|
live: live.length,
|
|
140
143
|
stale: live.filter(item => item.presence === "stale").length,
|
|
141
144
|
claims: claims.length,
|
|
142
|
-
tasks: snapshot.tasks.length,
|
|
143
145
|
messages: snapshot.messages.length,
|
|
144
146
|
},
|
|
145
147
|
};
|