agents-can-communicate 0.1.18 → 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.
Files changed (134) hide show
  1. package/README.md +78 -70
  2. package/SECURITY.md +31 -0
  3. package/bin/acc-hook.mjs +94 -12
  4. package/bin/acc-mcp.mjs +6 -2
  5. package/bin/acc.mjs +6 -1
  6. package/docs/ADAPTER_AUTHORING.md +172 -0
  7. package/docs/ARCHITECTURE.md +131 -0
  8. package/docs/CAPABILITIES.md +102 -214
  9. package/docs/CLI.md +157 -0
  10. package/docs/CONCEPTS.md +134 -0
  11. package/docs/CONFIGURATION.md +143 -0
  12. package/docs/DESIGN_DECISIONS.md +89 -0
  13. package/docs/GETTING_STARTED.md +145 -0
  14. package/docs/GLOSSARY.md +26 -0
  15. package/docs/MCP.md +94 -0
  16. package/docs/PROTOCOL.md +200 -0
  17. package/docs/RELEASING.md +109 -0
  18. package/docs/SECURITY_MODEL.md +131 -0
  19. package/docs/TROUBLESHOOTING.md +102 -0
  20. package/docs/WHY_ACC.md +61 -0
  21. package/docs/index.md +42 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/certification.json +78 -0
  23. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse-Edit.json +19 -0
  24. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/PreToolUse.json +17 -0
  25. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionEnd.json +8 -0
  26. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/SessionStart.json +7 -0
  27. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/UserPromptSubmit.json +9 -0
  28. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/certification-provenance.json +77 -0
  29. package/node_modules/@agents-can-communicate/adapter-claude-code/fixtures/delivery/claude-code-2.1.252.json +19 -0
  30. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +9 -1
  31. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +20 -22
  32. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +12 -4
  33. package/node_modules/@agents-can-communicate/adapter-codex/certification.json +117 -0
  34. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/PreToolUse.json +14 -0
  35. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionEnd.json +7 -0
  36. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/SessionStart.json +9 -0
  37. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/UserPromptSubmit.json +10 -0
  38. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/certification-provenance.json +66 -0
  39. package/node_modules/@agents-can-communicate/adapter-codex/fixtures/delivery/codex-cli-0.152.0.json +19 -0
  40. package/node_modules/@agents-can-communicate/adapter-codex/package.json +8 -1
  41. package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +20 -22
  43. package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +18 -11
  44. package/node_modules/@agents-can-communicate/adapter-gemini-cli/certification.json +52 -0
  45. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  46. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +20 -22
  47. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeAgent.json +8 -0
  48. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool-shell.json +12 -0
  49. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/BeforeTool.json +12 -0
  50. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionEnd.json +8 -0
  51. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/SessionStart.json +8 -0
  52. package/node_modules/@agents-can-communicate/adapter-gemini-cli/fixtures/certification-provenance.json +66 -0
  53. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +8 -1
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +7 -3
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +4 -2
  56. package/node_modules/@agents-can-communicate/adapter-grok/certification.json +3 -0
  57. package/node_modules/@agents-can-communicate/adapter-grok/package.json +2 -1
  58. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +20 -22
  59. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +9 -9
  60. package/node_modules/@agents-can-communicate/adapter-kimi/certification.json +52 -0
  61. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Bash.json +12 -0
  62. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/PreToolUse-Write.json +12 -0
  63. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionHeartbeat.json +7 -0
  64. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/SessionStart.json +9 -0
  65. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/UserPromptSubmit.json +8 -0
  66. package/node_modules/@agents-can-communicate/adapter-kimi/fixtures/certification-provenance.json +66 -0
  67. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +8 -1
  68. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +20 -22
  69. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +7 -3
  70. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +34 -18
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +158 -0
  73. package/node_modules/@agents-can-communicate/adapter-sdk/src/context-projector.mjs +36 -17
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +7 -1
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +13 -4
  77. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  78. package/node_modules/@agents-can-communicate/cli/src/args.mjs +11 -30
  79. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +3 -0
  80. package/node_modules/@agents-can-communicate/cli/src/help.mjs +2 -5
  81. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +9 -2
  82. package/node_modules/@agents-can-communicate/cli/src/main.mjs +100 -121
  83. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  84. package/node_modules/@agents-can-communicate/core/src/attention.mjs +106 -0
  85. package/node_modules/@agents-can-communicate/core/src/conversations.mjs +276 -0
  86. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +81 -0
  87. package/node_modules/@agents-can-communicate/core/src/finish-retries.mjs +97 -0
  88. package/node_modules/@agents-can-communicate/core/src/inbox.mjs +91 -107
  89. package/node_modules/@agents-can-communicate/core/src/index.mjs +3 -3
  90. package/node_modules/@agents-can-communicate/core/src/intents.mjs +0 -1
  91. package/node_modules/@agents-can-communicate/core/src/ports.mjs +2 -1
  92. package/node_modules/@agents-can-communicate/core/src/receipts.mjs +109 -0
  93. package/node_modules/@agents-can-communicate/core/src/service.mjs +11 -10
  94. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +22 -20
  95. package/node_modules/@agents-can-communicate/core/src/status.mjs +11 -9
  96. package/node_modules/@agents-can-communicate/core/src/sync.mjs +3 -294
  97. package/node_modules/@agents-can-communicate/delivery-router/package.json +12 -0
  98. package/node_modules/@agents-can-communicate/delivery-router/src/index.mjs +1 -0
  99. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +117 -0
  100. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  101. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +20 -0
  102. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +115 -63
  103. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  104. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +23 -7
  105. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +20 -5
  106. package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -2
  107. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +108 -12
  108. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +19 -2
  109. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  110. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +79 -0
  111. package/node_modules/@agents-can-communicate/mcp-server/src/resources.mjs +23 -28
  112. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +102 -72
  113. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +54 -97
  114. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  115. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +1 -1
  116. package/node_modules/@agents-can-communicate/protocol/src/conversations.mjs +64 -0
  117. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +4 -1
  118. package/node_modules/@agents-can-communicate/protocol/src/schema.mjs +49 -90
  119. package/node_modules/@agents-can-communicate/protocol/src/states.mjs +13 -40
  120. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  121. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +230 -0
  122. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +77 -28
  123. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +1 -1
  124. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +83 -35
  125. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +112 -0
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +18 -8
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +68 -26
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +113 -35
  129. package/package.json +19 -1
  130. package/node_modules/@agents-can-communicate/core/src/communication.mjs +0 -334
  131. package/node_modules/@agents-can-communicate/core/src/message-signals.mjs +0 -41
  132. package/node_modules/@agents-can-communicate/core/src/notify.mjs +0 -95
  133. package/node_modules/@agents-can-communicate/core/src/tasks.mjs +0 -244
  134. package/node_modules/@agents-can-communicate/core/src/workstreams.mjs +0 -109
@@ -0,0 +1,81 @@
1
+ import { AccError, EXIT, SCHEMA_VERSION, validateRecord }
2
+ from "@agents-can-communicate/protocol";
3
+
4
+ function conflict(sessionId) {
5
+ return new AccError(EXIT.CONFLICT,
6
+ "cannot publish a delivery binding for anything but this open session generation",
7
+ { sessionId });
8
+ }
9
+
10
+ export function createDeliveryBindingService(ports, sessions) {
11
+ const { store } = ports;
12
+
13
+ async function currentBindings(now) {
14
+ const bindings = await store.ephemeral.list("deliveryBinding");
15
+ const current = [];
16
+ for (const binding of bindings) {
17
+ const located = await sessions.locateSession(binding.sessionId);
18
+ if (located?.record.state !== "open"
19
+ || located.record.generation !== binding.generation) continue;
20
+ current.push({ binding, participantId: located.record.participantId,
21
+ reachable: Date.parse(binding.leaseUntil) > Date.parse(now) });
22
+ }
23
+ return current.sort((left, right) =>
24
+ left.binding.sessionId.localeCompare(right.binding.sessionId));
25
+ }
26
+
27
+ async function publishDeliveryBinding(input) {
28
+ const located = await sessions.locateSession(input.sessionId);
29
+ if (located?.record.state !== "open" || located.record.generation !== input.generation) {
30
+ throw conflict(input.sessionId);
31
+ }
32
+ let binding;
33
+ try {
34
+ binding = validateRecord("deliveryBinding", {
35
+ schemaVersion: SCHEMA_VERSION,
36
+ sessionId: input.sessionId,
37
+ generation: input.generation,
38
+ adapterId: input.adapterId,
39
+ clientVersion: input.clientVersion,
40
+ availableModes: Array.isArray(input.availableModes)
41
+ ? [...input.availableModes] : input.availableModes,
42
+ livePolicy: input.livePolicy,
43
+ opaqueEndpointRef: input.opaqueEndpointRef,
44
+ leaseUntil: input.leaseUntil,
45
+ });
46
+ } catch (error) {
47
+ if (error?.details?.field === "opaqueEndpointRef") {
48
+ throw new AccError(error.code, error.message, { field: "opaqueEndpointRef" });
49
+ }
50
+ throw error;
51
+ }
52
+ await store.ephemeral.update("deliveryBinding", binding.sessionId, async () => {
53
+ // This check runs while the ephemeral key's writer lock is held. A stale
54
+ // publisher that passed the earlier check cannot replace a successor's
55
+ // endpoint after that successor becomes current.
56
+ const current = await sessions.locateSession(input.sessionId);
57
+ if (current?.record.state !== "open"
58
+ || current.record.generation !== input.generation) {
59
+ throw conflict(input.sessionId);
60
+ }
61
+ return binding;
62
+ });
63
+ const stillCurrent = await sessions.locateSession(input.sessionId);
64
+ if (stillCurrent?.record.state !== "open"
65
+ || stillCurrent.record.generation !== input.generation) {
66
+ // Do not delete here: a successor generation may have published between
67
+ // our put and this recheck. A stale record is filtered on every read and
68
+ // is safe; deleting a successor's endpoint is not.
69
+ throw conflict(input.sessionId);
70
+ }
71
+ return binding;
72
+ }
73
+
74
+ async function listDeliveryBindings({ participantId, now }) {
75
+ return (await currentBindings(now))
76
+ .filter(item => item.participantId === participantId && item.reachable)
77
+ .map(item => item.binding);
78
+ }
79
+
80
+ return { publishDeliveryBinding, listDeliveryBindings, currentBindings };
81
+ }
@@ -0,0 +1,97 @@
1
+ import { AccError, EXIT } from "@agents-can-communicate/protocol";
2
+
3
+ const PAGE_SIZE = 256;
4
+
5
+ export const projectReleasedClaim = claim => ({
6
+ claimId: claim.claimId,
7
+ resource: claim.resource,
8
+ mode: claim.mode,
9
+ });
10
+
11
+ function conflict(message) {
12
+ return new AccError(EXIT.CONFLICT,
13
+ "clientMessageId belongs to a different finish generation",
14
+ { clientMessageId: message.clientMessageId, messageId: message.messageId });
15
+ }
16
+
17
+ async function allEvents(store, workspaceId) {
18
+ const events = [];
19
+ let cursor = null;
20
+ while (true) {
21
+ const page = await store.eventsSince(workspaceId, cursor, PAGE_SIZE);
22
+ if (page.events.length === 0) return events;
23
+ events.push(...page.events);
24
+ cursor = page.cursor;
25
+ }
26
+ }
27
+
28
+ function claimProjection(events, releaseIndex) {
29
+ const release = events[releaseIndex];
30
+ const claimId = release.payload.claimId;
31
+ const facts = events.slice(0, releaseIndex + 1).findLast(event =>
32
+ (event.type === "claim.acquired" || event.type === "claim.renewed")
33
+ && event.payload.claimId === claimId
34
+ && typeof event.payload.resource === "string");
35
+ return facts === undefined ? null : {
36
+ claimId,
37
+ resource: facts.payload.resource,
38
+ mode: facts.payload.mode,
39
+ };
40
+ }
41
+
42
+ function correlatedReleaseIndexes(events, recordedIndex, closedIndex, session, message) {
43
+ const close = events[closedIndex];
44
+ const payload = close.payload;
45
+ const keys = Object.keys(payload).sort();
46
+ const expectedKeys = ["cause", "messageId", "releasedClaimIds", "sessionGeneration"];
47
+ if (keys.length !== expectedKeys.length
48
+ || !keys.every((key, index) => key === expectedKeys[index])
49
+ || payload.cause !== "finish"
50
+ || payload.messageId !== message.messageId
51
+ || payload.sessionGeneration !== session.generation
52
+ || !Array.isArray(payload.releasedClaimIds)
53
+ || payload.releasedClaimIds.some(claimId => typeof claimId !== "string")
54
+ || new Set(payload.releasedClaimIds).size !== payload.releasedClaimIds.length) {
55
+ throw conflict(message);
56
+ }
57
+
58
+ const between = events.slice(recordedIndex + 1, closedIndex);
59
+ if (between.length !== payload.releasedClaimIds.length
60
+ || between.some(event => event.type !== "claim.released"
61
+ || event.actorSessionId !== session.sessionId
62
+ || event.payload.authority !== null
63
+ || event.payload.reason !== null)) {
64
+ throw conflict(message);
65
+ }
66
+ const actualIds = between.map(event => event.payload.claimId);
67
+ if (actualIds.some((claimId, index) => claimId !== payload.releasedClaimIds[index])) {
68
+ throw conflict(message);
69
+ }
70
+ return actualIds.map((claimId, offset) => recordedIndex + offset + 1);
71
+ }
72
+
73
+ export async function reconstructFinishRetry({ store, workspaceId, session, message }) {
74
+ const events = await allEvents(store, workspaceId);
75
+ const recordedIndex = events.findIndex(event => event.type === "message.recorded"
76
+ && event.payload.messageId === message.messageId
77
+ && event.actorSessionId === session.sessionId);
78
+ if (session.state !== "closed" || recordedIndex === -1) throw conflict(message);
79
+
80
+ const laterOpen = events.slice(recordedIndex + 1).some(event =>
81
+ event.type === "session.opened" && event.actorSessionId === session.sessionId);
82
+ if (laterOpen) throw conflict(message);
83
+
84
+ const closedOffset = events.slice(recordedIndex + 1).findIndex(event =>
85
+ event.type === "session.closed" && event.actorSessionId === session.sessionId);
86
+ if (closedOffset === -1) throw conflict(message);
87
+ const closedIndex = recordedIndex + closedOffset + 1;
88
+ const releaseIndexes = correlatedReleaseIndexes(events, recordedIndex, closedIndex,
89
+ session, message);
90
+ const releasedClaims = releaseIndexes.map(index => claimProjection(events, index));
91
+ if (releasedClaims.includes(null)) throw conflict(message);
92
+ const snapshot = await store.snapshot(workspaceId, { kinds: ["claim"] });
93
+ if (snapshot.claims.some(claim => claim.ownerSessionId === session.sessionId)) {
94
+ throw conflict(message);
95
+ }
96
+ return releasedClaims;
97
+ }
@@ -1,134 +1,118 @@
1
- import { AccError, EXIT, SCHEMA_VERSION, advanceDelivery, createId, validateRecord }
1
+ import { AccError, EXIT, SCHEMA_VERSION, advanceReceipt }
2
2
  from "@agents-can-communicate/protocol";
3
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"));
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 existing = await sessions.locateSession(input.sessionId, input.workspaceId);
15
- if (existing === null || existing.record.state !== "open"
16
- || existing.record.generation !== input.generation) {
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 existing.record;
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 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 });
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
- } 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 [];
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
- 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;
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 = 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") {
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, state: originalReceipt.state });
98
+ { messageId: input.messageId });
85
99
  }
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;
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 "./sync.mjs";
7
- export { looksConsequential, noteNudge } from "./message-signals.mjs";
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";
@@ -35,7 +35,6 @@ export function createIntentService(ports, sessions) {
35
35
  summary: input.summary,
36
36
  mode: input.mode,
37
37
  resourceHints: input.resourceHints ?? [],
38
- workstreamId: input.workstreamId ?? null,
39
38
  state: input.state ?? "active",
40
39
  updatedAt: now,
41
40
  });
@@ -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): Promise<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 { createCommunicationService } from "./communication.mjs";
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 communication = createCommunicationService(ports, sessions, claims);
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,
@@ -44,11 +44,12 @@ export function createCoordinationService({ store, clock, ids,
44
44
  policies: Object.freeze({ ...policies }),
45
45
  ...sessions,
46
46
  ...intents,
47
- ...workstreams,
48
- ...tasks,
49
47
  ...claims,
50
- ...communication,
48
+ ...conversations,
49
+ publishDeliveryBinding: deliveryBindings.publishDeliveryBinding,
50
+ listDeliveryBindings: deliveryBindings.listDeliveryBindings,
51
51
  ...inbox,
52
+ ...receipts,
52
53
  ...sync,
53
54
  ...status,
54
55
  guardState,