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,244 +0,0 @@
|
|
|
1
|
-
import { AccError, EXIT, SCHEMA_VERSION, createId, transitionTask as stepTask, validateRecord }
|
|
2
|
-
from "@agents-can-communicate/protocol";
|
|
3
|
-
|
|
4
|
-
import { ensureMaterialised } from "./materialisation.mjs";
|
|
5
|
-
import { assertKnownParticipants } from "./participants.mjs";
|
|
6
|
-
import { classifySessionPresence } from "./sessions.mjs";
|
|
7
|
-
import { closeRequestReceipt, writeWorkResponse } from "./notify.mjs";
|
|
8
|
-
|
|
9
|
-
// Dependency completion unblocks tasks deterministically, inside the same
|
|
10
|
-
// transaction that completed the dependency. It must never depend on a model
|
|
11
|
-
// remembering to re-evaluate the graph.
|
|
12
|
-
//
|
|
13
|
-
// Exported because a caller may supply its own task id - an adapter mirroring
|
|
14
|
-
// an external tracker, for instance - and that is the only way a create can
|
|
15
|
-
// close a cycle. Without an explicit id the guard would be unreachable.
|
|
16
|
-
export function wouldCycle(tasks, taskId, dependsOn) {
|
|
17
|
-
const byId = new Map(tasks.map(task => [task.taskId, task]));
|
|
18
|
-
const seen = new Set();
|
|
19
|
-
const stack = [...dependsOn];
|
|
20
|
-
while (stack.length > 0) {
|
|
21
|
-
const current = stack.pop();
|
|
22
|
-
if (current === taskId) return true;
|
|
23
|
-
if (seen.has(current)) continue;
|
|
24
|
-
seen.add(current);
|
|
25
|
-
stack.push(...(byId.get(current)?.dependsOn ?? []));
|
|
26
|
-
}
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const blockedBy = (task, byId) => task.dependsOn
|
|
31
|
-
.filter(id => (byId.get(id)?.state ?? "pending") !== "done");
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Write one task inside a transaction the caller already owns.
|
|
35
|
-
*
|
|
36
|
-
* Separated so that `acc request` - which creates the task and tells the
|
|
37
|
-
* recipient about it - can do both as one write. A request that produced a task
|
|
38
|
-
* and then failed to mention it would leave work addressed to an agent that was
|
|
39
|
-
* never told, which is worse than no request at all.
|
|
40
|
-
*/
|
|
41
|
-
export function writeTask(tx, { input, session, workspaceId, now, ids }) {
|
|
42
|
-
const taskId = input.taskId ?? createId("task");
|
|
43
|
-
const existing = tx.list("task");
|
|
44
|
-
const dependsOn = input.dependsOn ?? [];
|
|
45
|
-
for (const dependency of dependsOn) {
|
|
46
|
-
if (tx.get("task", dependency) === null) {
|
|
47
|
-
throw new AccError(EXIT.DATA, "a dependency does not exist", { dependency });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
if (wouldCycle(existing, taskId, dependsOn)) {
|
|
51
|
-
throw new AccError(EXIT.DATA, "the dependency graph would contain a cycle", { taskId });
|
|
52
|
-
}
|
|
53
|
-
// A workstream is optional - "finish these tests for me" should not require
|
|
54
|
-
// inventing a project first - but a named one has to exist, or the task hangs
|
|
55
|
-
// off nothing and nobody notices.
|
|
56
|
-
const workstreamId = input.workstreamId ?? null;
|
|
57
|
-
if (workstreamId !== null && tx.get("workstream", workstreamId) === null) {
|
|
58
|
-
throw new AccError(EXIT.DATA, "the workstream does not exist", { workstreamId });
|
|
59
|
-
}
|
|
60
|
-
const record = validateRecord("task", {
|
|
61
|
-
schemaVersion: SCHEMA_VERSION,
|
|
62
|
-
taskId,
|
|
63
|
-
workstreamId,
|
|
64
|
-
workspaceId,
|
|
65
|
-
title: input.title,
|
|
66
|
-
detail: input.detail ?? null,
|
|
67
|
-
state: blockedBy({ dependsOn }, new Map(existing.map(task => [task.taskId, task])))
|
|
68
|
-
.length > 0 ? "blocked" : "pending",
|
|
69
|
-
// Addressed to a participant, so the request survives that agent closing
|
|
70
|
-
// its terminal. Whoever picks it up is recorded separately.
|
|
71
|
-
assigneeParticipantId: input.assigneeParticipantId ?? null,
|
|
72
|
-
assigneeSessionId: null,
|
|
73
|
-
requestedByParticipantId: input.requestedByParticipantId ?? null,
|
|
74
|
-
dependsOn,
|
|
75
|
-
acceptance: input.acceptance ?? [],
|
|
76
|
-
createdAt: now,
|
|
77
|
-
});
|
|
78
|
-
tx.put("task", taskId, record);
|
|
79
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"), workspaceId,
|
|
80
|
-
actorSessionId: session.sessionId, type: "task.created", occurredAt: now,
|
|
81
|
-
payload: { taskId, state: record.state,
|
|
82
|
-
assigneeParticipantId: record.assigneeParticipantId } });
|
|
83
|
-
return record;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function createTaskService(ports, workstreams) {
|
|
87
|
-
const { store, clock, ids, pidIsAlive } = ports;
|
|
88
|
-
|
|
89
|
-
async function createTask(input) {
|
|
90
|
-
const session = await workstreams.requireOpenSession(input, "create a task");
|
|
91
|
-
const workspaceId = session.workspaceId;
|
|
92
|
-
await ensureMaterialised(ports, { workspaceId, descriptor: input.descriptor,
|
|
93
|
-
reason: "durable_object" });
|
|
94
|
-
// The same rule as addressing a message. `acc task --assignee physcis` was
|
|
95
|
-
// accepted and left work `pending` for a participant nobody has ever been:
|
|
96
|
-
// invisible to every roster, raising `task_unblocked` for nobody, and not
|
|
97
|
-
// even stalled, since nothing was waiting on it that could be told.
|
|
98
|
-
if (input.assigneeParticipantId != null) {
|
|
99
|
-
await assertKnownParticipants(store, workspaceId, [input.assigneeParticipantId]);
|
|
100
|
-
}
|
|
101
|
-
const now = clock.now();
|
|
102
|
-
let record = null;
|
|
103
|
-
await store.transaction(async tx => {
|
|
104
|
-
record = writeTask(tx, { input, session, workspaceId, now, ids });
|
|
105
|
-
// `writeTask` reads and writes on this handle, so its kinds are ours.
|
|
106
|
-
}, { kinds: ["session", "task", "workstream"] });
|
|
107
|
-
return record;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async function claimTask(input) {
|
|
111
|
-
const session = await workstreams.requireOpenSession(input, "claim a task");
|
|
112
|
-
const now = clock.now();
|
|
113
|
-
let record = null;
|
|
114
|
-
await store.transaction(async tx => {
|
|
115
|
-
const existing = tx.get("task", input.taskId);
|
|
116
|
-
if (existing === null) {
|
|
117
|
-
throw new AccError(EXIT.DATA, "the task does not exist", { taskId: input.taskId });
|
|
118
|
-
}
|
|
119
|
-
if (existing.assigneeSessionId !== null
|
|
120
|
-
&& existing.assigneeSessionId !== session.sessionId) {
|
|
121
|
-
// A holder that is gone is not a holder. Closing a session hands its
|
|
122
|
-
// work back, so this is the crash case: no session end ever arrived and
|
|
123
|
-
// presence has decayed. `stale` alone does not release it - the same
|
|
124
|
-
// rule claims follow, because an idle agent may be thinking rather than
|
|
125
|
-
// dead - and taking it over needs `force`. `offline` needs no force at
|
|
126
|
-
// all, silence-derived cases included: a session with no recorded pid,
|
|
127
|
-
// quiet past the unknown floor, reads offline exactly like one whose
|
|
128
|
-
// pid is confirmed dead, and either way the task is simply taken. That
|
|
129
|
-
// is acceptable here where it would not be for a session id: the
|
|
130
|
-
// participant guard below still refuses a different participant, and
|
|
131
|
-
// taking a task back is reversible where replacing an id is not.
|
|
132
|
-
const holder = tx.get("session", existing.assigneeSessionId);
|
|
133
|
-
const presence = holder === null
|
|
134
|
-
? "offline"
|
|
135
|
-
: classifySessionPresence(holder, now, pidIsAlive);
|
|
136
|
-
if (presence === "online") {
|
|
137
|
-
throw new AccError(EXIT.CONFLICT, "the task already has an assignee",
|
|
138
|
-
{ taskId: input.taskId, assigneeSessionId: existing.assigneeSessionId });
|
|
139
|
-
}
|
|
140
|
-
if (presence === "stale" && input.force !== true) {
|
|
141
|
-
throw new AccError(EXIT.CONFLICT,
|
|
142
|
-
"the task is held by a session that has gone quiet; take it with force",
|
|
143
|
-
{ taskId: input.taskId, assigneeSessionId: existing.assigneeSessionId,
|
|
144
|
-
presence });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
// Work addressed to one participant is not picked up by another. Taking
|
|
148
|
-
// an unaddressed task is open to anyone, which is what makes a request
|
|
149
|
-
// with no named recipient a request to the room.
|
|
150
|
-
if (existing.assigneeParticipantId !== null
|
|
151
|
-
&& existing.assigneeParticipantId !== session.participantId) {
|
|
152
|
-
throw new AccError(EXIT.CONFLICT, "the task is addressed to another participant",
|
|
153
|
-
{ taskId: input.taskId,
|
|
154
|
-
assigneeParticipantId: existing.assigneeParticipantId });
|
|
155
|
-
}
|
|
156
|
-
record = { ...existing, assigneeSessionId: session.sessionId,
|
|
157
|
-
assigneeParticipantId: existing.assigneeParticipantId ?? session.participantId,
|
|
158
|
-
state: stepTask(existing.state, "in_progress") };
|
|
159
|
-
// Whoever asked is waiting on an answer, and "someone is on it" is one.
|
|
160
|
-
writeWorkResponse(tx, { task: record, actor: session,
|
|
161
|
-
workspaceId: session.workspaceId, now, ids, outcome: "accepted" });
|
|
162
|
-
tx.put("task", input.taskId, record, tx.generationOf("task", input.taskId));
|
|
163
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
164
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
165
|
-
type: "task.claimed", occurredAt: now, payload: { taskId: input.taskId } });
|
|
166
|
-
}, { kinds: ["message", "receipt", "session", "task"] });
|
|
167
|
-
return record;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
async function transitionTask(input) {
|
|
171
|
-
const session = await workstreams.requireOpenSession(input, "transition a task");
|
|
172
|
-
const now = clock.now();
|
|
173
|
-
let record = null;
|
|
174
|
-
await store.transaction(async tx => {
|
|
175
|
-
const existing = tx.get("task", input.taskId);
|
|
176
|
-
if (existing === null) {
|
|
177
|
-
throw new AccError(EXIT.DATA, "the task does not exist", { taskId: input.taskId });
|
|
178
|
-
}
|
|
179
|
-
record = { ...existing, state: stepTask(existing.state, input.state) };
|
|
180
|
-
tx.put("task", input.taskId, record, tx.generationOf("task", input.taskId));
|
|
181
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
182
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
183
|
-
type: "task.transitioned", occurredAt: now,
|
|
184
|
-
payload: { taskId: input.taskId, state: record.state } });
|
|
185
|
-
|
|
186
|
-
if (record.state === "done" || record.state === "review") {
|
|
187
|
-
writeWorkResponse(tx, { task: record, actor: session,
|
|
188
|
-
workspaceId: session.workspaceId, now, ids, outcome: record.state });
|
|
189
|
-
}
|
|
190
|
-
// Doing the work answers the request that asked for it.
|
|
191
|
-
if (record.state === "done") closeRequestReceipt(tx, { task: record, actor: session, now, ids });
|
|
192
|
-
if (record.state !== "done") return;
|
|
193
|
-
// Unblock dependents here, in the same transaction, so the graph is
|
|
194
|
-
// never left in a state that needs someone to notice it later.
|
|
195
|
-
const byId = new Map(tx.list("task").map(task => [task.taskId, task]));
|
|
196
|
-
byId.set(record.taskId, record);
|
|
197
|
-
for (const dependent of byId.values()) {
|
|
198
|
-
if (dependent.state !== "blocked") continue;
|
|
199
|
-
if (blockedBy(dependent, byId).length > 0) continue;
|
|
200
|
-
const unblocked = { ...dependent, state: "pending" };
|
|
201
|
-
tx.put("task", dependent.taskId, unblocked,
|
|
202
|
-
tx.generationOf("task", dependent.taskId));
|
|
203
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
204
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
205
|
-
type: "task.unblocked", occurredAt: now, payload: { taskId: dependent.taskId } });
|
|
206
|
-
}
|
|
207
|
-
}, { kinds: ["message", "receipt", "task"] });
|
|
208
|
-
return record;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Refuse a request, with a reason.
|
|
213
|
-
*
|
|
214
|
-
* The task returns to unclaimed rather than being deleted: the work is still
|
|
215
|
-
* wanted, it is just not this agent's. Leaving a request pending forever was
|
|
216
|
-
* the only way to say no, and it looks identical to not having read it.
|
|
217
|
-
*/
|
|
218
|
-
async function declineTask(input) {
|
|
219
|
-
const session = await workstreams.requireOpenSession(input, "decline a task");
|
|
220
|
-
const now = clock.now();
|
|
221
|
-
let record = null;
|
|
222
|
-
await store.transaction(async tx => {
|
|
223
|
-
const existing = tx.get("task", input.taskId);
|
|
224
|
-
if (existing === null) {
|
|
225
|
-
throw new AccError(EXIT.DATA, "the task does not exist", { taskId: input.taskId });
|
|
226
|
-
}
|
|
227
|
-
record = { ...existing, assigneeParticipantId: null, assigneeSessionId: null,
|
|
228
|
-
state: "pending" };
|
|
229
|
-
tx.put("task", input.taskId, record, tx.generationOf("task", input.taskId));
|
|
230
|
-
writeWorkResponse(tx, { task: existing, actor: session,
|
|
231
|
-
workspaceId: session.workspaceId, now, ids, outcome: "declined",
|
|
232
|
-
reason: input.reason ?? null });
|
|
233
|
-
// Refusing is also an answer, so the request stops demanding one.
|
|
234
|
-
closeRequestReceipt(tx, { task: existing, actor: session, now, ids });
|
|
235
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
236
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
237
|
-
type: "task.declined", occurredAt: now,
|
|
238
|
-
payload: { taskId: input.taskId, reason: input.reason ?? null } });
|
|
239
|
-
}, { kinds: ["message", "receipt", "task"] });
|
|
240
|
-
return record;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return { createTask, claimTask, transitionTask, declineTask };
|
|
244
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { AccError, EXIT, SCHEMA_VERSION, createId, validateRecord }
|
|
2
|
-
from "@agents-can-communicate/protocol";
|
|
3
|
-
|
|
4
|
-
import { ensureMaterialised } from "./materialisation.mjs";
|
|
5
|
-
import { classifySessionPresence } from "./sessions.mjs";
|
|
6
|
-
|
|
7
|
-
// A workstream groups related collaboration. It may have zero or one
|
|
8
|
-
// coordinator lease, and the coordinator plans - it is never the transport,
|
|
9
|
-
// the durable owner, or an information gatekeeper.
|
|
10
|
-
export function createWorkstreamService(ports, sessions) {
|
|
11
|
-
const { store, clock, ids, pidIsAlive } = ports;
|
|
12
|
-
|
|
13
|
-
async function requireOpenSession(input, action) {
|
|
14
|
-
const existing = await sessions.locateSession(input.sessionId, input.workspaceId);
|
|
15
|
-
if (existing === null || existing.record.state !== "open"
|
|
16
|
-
|| existing.record.generation !== input.generation) {
|
|
17
|
-
throw new AccError(EXIT.CONFLICT, `cannot ${action} from this session generation`,
|
|
18
|
-
{ sessionId: input.sessionId });
|
|
19
|
-
}
|
|
20
|
-
return existing.record;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async function createWorkstream(input) {
|
|
24
|
-
const session = await requireOpenSession(input, "create a workstream");
|
|
25
|
-
const workspaceId = session.workspaceId;
|
|
26
|
-
await ensureMaterialised(ports, { workspaceId, descriptor: input.descriptor,
|
|
27
|
-
reason: "durable_object" });
|
|
28
|
-
const now = clock.now();
|
|
29
|
-
const workstreamId = createId("workstream");
|
|
30
|
-
const record = validateRecord("workstream", {
|
|
31
|
-
schemaVersion: SCHEMA_VERSION,
|
|
32
|
-
workstreamId,
|
|
33
|
-
workspaceId,
|
|
34
|
-
title: input.title,
|
|
35
|
-
objective: input.objective,
|
|
36
|
-
// A workstream never acquires a coordinator merely by being created.
|
|
37
|
-
coordinatorSessionId: null,
|
|
38
|
-
state: "open",
|
|
39
|
-
createdAt: now,
|
|
40
|
-
});
|
|
41
|
-
await store.transaction(async tx => {
|
|
42
|
-
tx.put("workstream", workstreamId, record);
|
|
43
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"), workspaceId,
|
|
44
|
-
actorSessionId: session.sessionId, type: "workstream.created", occurredAt: now,
|
|
45
|
-
payload: { workstreamId } });
|
|
46
|
-
}, { kinds: ["workstream"] });
|
|
47
|
-
return record;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function acquireCoordinator(input) {
|
|
51
|
-
const session = await requireOpenSession(input, "coordinate");
|
|
52
|
-
const now = clock.now();
|
|
53
|
-
const snapshot = await store.snapshot(session.workspaceId,
|
|
54
|
-
{ kinds: ["session", "workstream"] });
|
|
55
|
-
let record = null;
|
|
56
|
-
await store.transaction(async tx => {
|
|
57
|
-
const existing = tx.get("workstream", input.workstreamId);
|
|
58
|
-
if (existing === null) {
|
|
59
|
-
throw new AccError(EXIT.CONFLICT, "the workstream does not exist",
|
|
60
|
-
{ workstreamId: input.workstreamId });
|
|
61
|
-
}
|
|
62
|
-
const held = existing.coordinatorSessionId;
|
|
63
|
-
if (held !== null && held !== session.sessionId) {
|
|
64
|
-
const holder = snapshot.sessions.find(item => item.sessionId === held);
|
|
65
|
-
const presence = holder === undefined ? "offline"
|
|
66
|
-
: classifySessionPresence(holder, now, pidIsAlive);
|
|
67
|
-
// A coordinator lease is replaced only when the holder is genuinely
|
|
68
|
-
// gone or policy says so - not because a peer would like the role.
|
|
69
|
-
if (presence !== "offline" && input.authority !== "human"
|
|
70
|
-
&& input.authority !== "policy") {
|
|
71
|
-
throw new AccError(EXIT.CONFLICT, "the workstream already has a coordinator",
|
|
72
|
-
{ workstreamId: input.workstreamId, coordinatorSessionId: held,
|
|
73
|
-
coordinatorPresence: presence });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
record = { ...existing, coordinatorSessionId: session.sessionId };
|
|
77
|
-
tx.put("workstream", input.workstreamId, record,
|
|
78
|
-
tx.generationOf("workstream", input.workstreamId));
|
|
79
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
80
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
81
|
-
type: "workstream.coordinator_acquired", occurredAt: now,
|
|
82
|
-
payload: { workstreamId: input.workstreamId, replaced: held } });
|
|
83
|
-
}, { kinds: ["workstream"] });
|
|
84
|
-
return record;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function releaseCoordinator(input) {
|
|
88
|
-
const session = await requireOpenSession(input, "release coordination");
|
|
89
|
-
const now = clock.now();
|
|
90
|
-
let record = null;
|
|
91
|
-
await store.transaction(async tx => {
|
|
92
|
-
const existing = tx.get("workstream", input.workstreamId);
|
|
93
|
-
if (existing === null || existing.coordinatorSessionId !== session.sessionId) {
|
|
94
|
-
throw new AccError(EXIT.CONFLICT, "only the coordinator may release the lease",
|
|
95
|
-
{ workstreamId: input.workstreamId });
|
|
96
|
-
}
|
|
97
|
-
record = { ...existing, coordinatorSessionId: null };
|
|
98
|
-
tx.put("workstream", input.workstreamId, record,
|
|
99
|
-
tx.generationOf("workstream", input.workstreamId));
|
|
100
|
-
tx.append({ schemaVersion: SCHEMA_VERSION, eventId: ids.next("event"),
|
|
101
|
-
workspaceId: session.workspaceId, actorSessionId: session.sessionId,
|
|
102
|
-
type: "workstream.coordinator_released", occurredAt: now,
|
|
103
|
-
payload: { workstreamId: input.workstreamId } });
|
|
104
|
-
}, { kinds: ["workstream"] });
|
|
105
|
-
return record;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return { createWorkstream, acquireCoordinator, releaseCoordinator, requireOpenSession };
|
|
109
|
-
}
|