agents-can-communicate 0.6.0 → 0.6.2
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/docs/ADAPTER_AUTHORING.md +19 -16
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/CAPABILITIES.md +17 -15
- package/docs/CONCEPTS.md +2 -2
- package/docs/GETTING_STARTED.md +5 -5
- package/docs/GLOSSARY.md +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +4 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -2
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +6 -32
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +73 -22
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +2 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +1 -2
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +12 -6
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +16 -4
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +23 -13
- package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +18 -1
- package/package.json +1 -1
|
@@ -118,22 +118,25 @@ example is not a capture. Failed experiments stay in the manifest as `fail`; the
|
|
|
118
118
|
the false value and can never enable it.
|
|
119
119
|
|
|
120
120
|
`effectiveCapabilities(adapter, { clientVersion, platform })` returns the full boolean
|
|
121
|
-
shape for the installed client
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
121
|
+
shape for the installed client, resolving each capability independently from the evidence
|
|
122
|
+
that client can reach:
|
|
123
|
+
|
|
124
|
+
1. Take the rows for that capability, and keep those whose version is at or below the
|
|
125
|
+
client's. A prerelease is ordered by its release triple, so `1.3.0-rc.1` is judged as
|
|
126
|
+
`1.3.0`, and a version that cannot be read reaches every row.
|
|
127
|
+
2. Among those, prefer the rows that name this platform; with none, use them all. A loss
|
|
128
|
+
recorded for one platform at 1.3.0 says nothing about that platform at 1.2.3, where
|
|
129
|
+
another platform's passing capture is the only evidence in reach.
|
|
130
|
+
3. The highest version left decides. The capability is on when every row at that version
|
|
131
|
+
passes, so a recorded loss wins a tie between platforms.
|
|
132
|
+
|
|
133
|
+
A client older than every row gets nothing, and so does a capability whose deciding row
|
|
134
|
+
records a failure. `capabilityEvidence(adapter, facts, capability)` returns that verdict
|
|
135
|
+
with its reason - `undeclared`, `unobserved`, `older-than-evidence` or `recorded-failure` -
|
|
136
|
+
and the version that decided, so a refusal can name the evidence rather than the client.
|
|
137
|
+
|
|
138
|
+
Recording a regression is an ordinary capture with `result: "fail"` at the version where
|
|
139
|
+
the loss was observed; it applies forward until a later row passes again.
|
|
137
140
|
|
|
138
141
|
The backing methods for delivery are `renderContextResult()` for `nextTurn`,
|
|
139
142
|
`offerMessage()` for `livePush`, and `routeReply()` for `replyRoute`.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -87,12 +87,12 @@ if registration fails. This does not create a session or revive a closed one.
|
|
|
87
87
|
|
|
88
88
|
## Certified capability versus current reachability
|
|
89
89
|
|
|
90
|
-
Ordinary hook capabilities say
|
|
91
|
-
|
|
90
|
+
Ordinary hook capabilities say a captured behaviour passed, and apply forward from the
|
|
91
|
+
version that recorded it until a later capture changes it. Native live delivery uses a separate contract: installation checks the
|
|
92
92
|
captured minimum, platform, and current feature probe, then each session publishes a binding
|
|
93
93
|
only after a generation-bound handshake. The router trusts that admission and verifies the
|
|
94
94
|
binding, recipient policy, declared adapter capability, and adapter response; it does not add
|
|
95
|
-
a third
|
|
95
|
+
a third certification check. A binding says what that current generation
|
|
96
96
|
exposes and whether its lease is current, while recipient policy says whether it may spend a
|
|
97
97
|
turn.
|
|
98
98
|
|
package/docs/CAPABILITIES.md
CHANGED
|
@@ -3,22 +3,25 @@
|
|
|
3
3
|
Use this page to set expectations after installation. Integration means ACC can introduce
|
|
4
4
|
peer awareness and coordination instructions; it does not guarantee what a model will do
|
|
5
5
|
with them. Delivery also varies independently from awareness. The durable inbox works for
|
|
6
|
-
every participant,
|
|
6
|
+
every participant, a client at or after a captured version may add next-turn delivery, and the
|
|
7
7
|
experimental Codex LocalDaemon and Claude Code Channel paths can deliver while a session is idle.
|
|
8
8
|
|
|
9
9
|
Capability honesty separates four questions that are easy to collapse:
|
|
10
10
|
|
|
11
|
-
1. **Certified support** —
|
|
12
|
-
real-client fixture?
|
|
11
|
+
1. **Certified support** — does the evidence this client can reach show the capability
|
|
12
|
+
passing in a shipped real-client fixture?
|
|
13
13
|
2. **Current reachability** — does one current session generation expose a live binding
|
|
14
14
|
whose lease is valid now?
|
|
15
15
|
3. **Recipient policy** — did that recipient opt into spending a turn for this message
|
|
16
16
|
kind?
|
|
17
17
|
4. **Fallback** — what durable path remains when any earlier answer is no?
|
|
18
18
|
|
|
19
|
-
A source method or vendor documentation is not certification.
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
A source method or vendor documentation is not certification. A capture applies forward: from
|
|
20
|
+
the version that recorded it until a later capture changes that capability, and to every
|
|
21
|
+
platform until one of them records something of its own. A client older than every capture
|
|
22
|
+
degrades to false, and so does a capability a capture recorded as failing. Native
|
|
23
|
+
minimum-based eligibility is separate, and stays per-platform. No weaker session inherits a
|
|
24
|
+
stronger peer's capability.
|
|
22
25
|
|
|
23
26
|
Run `acc doctor` in the project when observed behavior differs from this page. It reports
|
|
24
27
|
the installed client version, platform, effective capability, and fallback instead of
|
|
@@ -26,10 +29,9 @@ assuming that a newer or differently packaged client behaves like a captured one
|
|
|
26
29
|
|
|
27
30
|
## Certified support
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
own says otherwise.
|
|
32
|
+
Each column names the version that recorded the capture, on `darwin-arm64`. Every later
|
|
33
|
+
version of that client reads it, on every platform, until a capture of its own says
|
|
34
|
+
otherwise. A client older than the version named here is unproven and gets nothing.
|
|
33
35
|
|
|
34
36
|
| Capability | Antigravity 1.2.7 | Codex 0.147.0 | Claude Code 2.1.233 | Gemini CLI 0.57.0 | Grok 1.0.13 | Kimi 0.36.1 |
|
|
35
37
|
|---|---:|---:|---:|---:|---:|---:|
|
|
@@ -46,7 +48,7 @@ own says otherwise.
|
|
|
46
48
|
Every other capability in the closed shape defaults to false, including session resume,
|
|
47
49
|
child sessions, startup or safe-point injection, and before-read guards.
|
|
48
50
|
|
|
49
|
-
The native rows
|
|
51
|
+
The native rows are `no` at the hook versions this matrix names.
|
|
50
52
|
Separate installed-client captures establish Codex `livePush` on 0.152.1 and
|
|
51
53
|
0.153.4, Claude Code `livePush` plus `replyRoute` on 2.1.258 and 2.1.260, and
|
|
52
54
|
Antigravity CLI `livePush` on 1.2.7 and later through a relay the agent starts in its own shell.
|
|
@@ -58,12 +60,12 @@ The limitations belong next to the adapters they affect:
|
|
|
58
60
|
|
|
59
61
|
| Adapter | Exact limitation and evidence |
|
|
60
62
|
|---|---|
|
|
61
|
-
| Antigravity CLI | 1.2.7 on darwin-arm64, captured in print mode, and later
|
|
62
|
-
| Codex |
|
|
63
|
+
| Antigravity CLI | 1.2.7 on darwin-arm64, captured in print mode, and every later release by the forward rule. Only `SessionStart`, `PreInvocation`, `PostInvocation` and `Stop` load; `SessionEnd`, `PreToolUse` and `PostToolUse` are accepted into the config file and silently dropped, so there is no tool guard and no session-end deregistration - a session goes offline by presence age or an explicit `acc finish`. Payloads carry no `hook_event_name`, so each registered command passes its own event name. The end-of-turn `Stop` continuation reaches the model and is a bounded nudge, not a gate: ACC continues a turn at most once and fails open, and the client caps consecutive continuations itself (vendor 1.1.9). `agy agentapi send-message` can wake an idle session - captured - but only with that session's language-server address and CSRF token, which exist in the agent's own shell and in no hook. ACC does not take that token, so live push and reply routing are false. A peer message that arrives while the model writes its last answer is carried by the `Stop` continuation instead. A write that parses can register nothing, so install and doctor read `agy -p "/hooks"` back instead of trusting the file. Live push (1.2.7, darwin-arm64, TUI only, experimental, recorded opt-in) runs through a relay the agent starts once per conversation from its own shell - the only process holding the session endpoint - after ACC's context asks it to; the operator approves that command at the client's permission prompt. An idle session wakes; a busy one sees the message after its running answer, or at the next model invocation when the turn waits on a tool. Print mode and the first session in a folder trusted at that launch get no relay. |
|
|
64
|
+
| Codex | Next-turn context, captured on 0.147.0, requires plugin trust. The observed stock 0.153.4 upgrade from ACC 0.3.1 to 0.4 required fresh review of five modified hook definitions; a subsequent restart retained all five active (activation evidence, not new event certification). LocalDaemon native delivery was captured through the installed package on 0.152.1 and 0.153.4, darwin-arm64; minimum 0.152.1, recorded opt-in, current feature probe and exact thread/cwd/process/version/protocol checks are required. Ordinary launch preserves the receiver workspace without ACC arguments or daemon ownership. Embedded or unreachable sessions keep their inbox. Native `replyRoute` remains false. |
|
|
63
65
|
| Claude Code | 2.1.233 next-turn delivery waits for the next user prompt. A 2.1.258 Channel capture proved idle offer, busy queue-after-turn, explicit reply, duplicate suppression, and durable fallback, so `delivery.livePush` and `delivery.replyRoute` are live capabilities behind the native contract (experimental, off until opted in; Claude's development-channel warning is vendor-owned and visible). |
|
|
64
|
-
| Gemini CLI |
|
|
66
|
+
| Gemini CLI | Package-shipped next-turn certification starts at 0.57.0. Its TUI has no captured external wake or queue interface and `--acp` changes launch ownership, so native delivery is fallback-only; live push and reply routing remain false. |
|
|
65
67
|
| Grok | The Grok 1.0.24 installed-client check observed own CLI arguments after a terminal result, followed by owned work, message, and finish calls. This identity-only path does not certify peer-context injection. Documentation-shaped payloads do not count as real captures. The public leader surface exposed no proven addressed injection into an ordinary TUI session, so native delivery is `awaiting_compatibility_capture`; all capabilities remain false. |
|
|
66
|
-
| Kimi Code |
|
|
68
|
+
| Kimi Code | Next-turn and guard evidence was captured on 0.36.1, plus a 60-second heartbeat. Its server/queue APIs do not prove a transparent binding to an independently opened session, so native delivery is fallback-only. |
|
|
67
69
|
| Generic MCP | As a receiver, tool polling is not next-turn injection, live push, or a native reply route. It has no write guard or client-lifecycle evidence. Outgoing messages may use an eligible recipient's opted-in native adapter. |
|
|
68
70
|
|
|
69
71
|
`certification.json` beside each adapter is machine-readable. `COMPATIBILITY.md` records the
|
package/docs/CONCEPTS.md
CHANGED
|
@@ -123,8 +123,8 @@ recoverable rather than creating a terminal failure state.
|
|
|
123
123
|
|
|
124
124
|
## Durable first, faster delivery second
|
|
125
125
|
|
|
126
|
-
The durable inbox is universal.
|
|
127
|
-
messages at the next normal turn. Codex LocalDaemon and Claude Code Channel add
|
|
126
|
+
The durable inbox is universal. An adapter whose captures reach the installed client may
|
|
127
|
+
also offer messages at the next normal turn. Codex LocalDaemon and Claude Code Channel add
|
|
128
128
|
experimental live delivery, off by default and subject to recipient policy and
|
|
129
129
|
current reachability. Codex can retain an eligible daemon thread after its terminal
|
|
130
130
|
exits. [Capabilities](CAPABILITIES.md) gives the current limits.
|
package/docs/GETTING_STARTED.md
CHANGED
|
@@ -53,7 +53,7 @@ are active; `acc doctor` leaves current readiness unverified and directs you to
|
|
|
53
53
|
Read the delivery summary for each client. Live delivery needs an explicit opt-in and a
|
|
54
54
|
verified channel in the current session. Codex can save your choice even when its local
|
|
55
55
|
service is not running yet. Declining uses the reported fallback: `acc inbox`, or next-turn
|
|
56
|
-
hooks
|
|
56
|
+
hooks wherever the client is at or after a captured version.
|
|
57
57
|
|
|
58
58
|
An interactive install asks one default-No question for all selected clients that need a
|
|
59
59
|
choice. Use `--delivery actionable|all` for explicit noninteractive consent. Use
|
|
@@ -131,10 +131,10 @@ or follow the interaction through [How ACC works](HOW_IT_WORKS.md).
|
|
|
131
131
|
Every message is recorded before ACC attempts faster delivery. Every participant has a
|
|
132
132
|
durable inbox, which is the universal recovery path.
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
not wake an idle session. Grok, generic MCP clients
|
|
137
|
-
|
|
134
|
+
From the version each capture records onward, Codex, Claude Code, Gemini CLI, Kimi Code
|
|
135
|
+
and Antigravity CLI can receive a message at the next normal turn, on any platform. That
|
|
136
|
+
does not wake an idle session. Grok, generic MCP clients and clients older than every
|
|
137
|
+
capture use the durable inbox instead.
|
|
138
138
|
|
|
139
139
|
Antigravity CLI carries one extra condition: its hooks are given a project directory only
|
|
140
140
|
when the session has an open workspace. A session started without one attaches nothing, and
|
package/docs/GLOSSARY.md
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
- **Live push** — optional delivery through Claude Code Channel or Codex LocalDaemon to an already-running session, behind recipient opt-in. Both wait for an active turn to finish; [Capabilities](CAPABILITIES.md) gives the measured eligibility and limits.
|
|
20
20
|
- **Recipient policy** — `off`, `actionable`, or `all`; opt-in permission to spend a turn, not a capability.
|
|
21
21
|
- **Delivery binding** — ephemeral, generation-bound reachability data owned by an adapter.
|
|
22
|
-
- **Fallback** — durable inbox or
|
|
22
|
+
- **Fallback** — durable inbox or certified next-turn recovery when live delivery is unavailable.
|
|
23
23
|
- **Managed / manual lifecycle** — whether hooks report ACC presence automatically; never ownership of the external client process.
|
|
24
24
|
- **MCP participation** — manual incoming polling without native lifecycle, context, guards, or receive wake; outgoing messages can use an eligible recipient's opted-in native route.
|
|
25
25
|
- **Bounded discovery** — inbox/history summary pages with limits and cursors; bodies require selection.
|
|
@@ -9,10 +9,10 @@ import { bindNativeSession, nativeActivationHint, offerMessage, planNativeActiva
|
|
|
9
9
|
probeNativeDelivery, refreshNativeSession } from "./native-delivery.mjs";
|
|
10
10
|
import { PROTOCOL_CONTRACT } from "./relay-endpoint.mjs";
|
|
11
11
|
|
|
12
|
-
// The version this client has been captured on
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
12
|
+
// The version this client has been captured on. Nothing earlier was measured,
|
|
13
|
+
// and later releases - this client ships every few days - are judged by this
|
|
14
|
+
// capture until one of their own says otherwise, which is how certification
|
|
15
|
+
// evidence reads everywhere since 0.6.2.
|
|
16
16
|
export const ANTIGRAVITY_CLI_VERSION = "1.2.7";
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -65,7 +65,6 @@ export function createAntigravityAdapter() {
|
|
|
65
65
|
client: { command: "agy", certificationName: "antigravity-cli",
|
|
66
66
|
versionArgs: ["--version"] },
|
|
67
67
|
certification,
|
|
68
|
-
certificationFloor: { "darwin-arm64": ANTIGRAVITY_CLI_VERSION },
|
|
69
68
|
capabilities: {
|
|
70
69
|
lifecycle: { sessionStart: true },
|
|
71
70
|
context: { beforeTurnInjection: true },
|
|
@@ -46,8 +46,8 @@ export function createClaudeCodeAdapter() {
|
|
|
46
46
|
guards: { beforeWrite: true, beforeShell: true },
|
|
47
47
|
// nextTurn is the certified 2.1.233 hook projection; livePush and
|
|
48
48
|
// replyRoute rest on the 2.1.258 Channel capture and the native contract
|
|
49
|
-
// below. effectiveCapabilities()
|
|
50
|
-
//
|
|
49
|
+
// below. effectiveCapabilities() carries each of those captures forward
|
|
50
|
+
// to later clients; the native contract is the separate live rule.
|
|
51
51
|
delivery: { nextTurn: true, livePush: true, replyRoute: true },
|
|
52
52
|
},
|
|
53
53
|
// The first passing capture is the shipped minimum; the research lower
|
|
@@ -3,7 +3,6 @@ import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protoc
|
|
|
3
3
|
import { CAPABILITY_SHAPE, freezeCapabilities, validateCertification }
|
|
4
4
|
from "./certification.mjs";
|
|
5
5
|
import { validateNativeDeliveryContract } from "./native-delivery.mjs";
|
|
6
|
-
import { parseStableVersion } from "./native-vocabulary.mjs";
|
|
7
6
|
|
|
8
7
|
// The capability surface, documented in docs/ADAPTER_AUTHORING.md and measured
|
|
9
8
|
// per client in docs/CAPABILITIES.md. False is the default for every
|
|
@@ -116,10 +115,13 @@ export function defineAdapter(manifest) {
|
|
|
116
115
|
{ evidenceClient: item.client, client });
|
|
117
116
|
}
|
|
118
117
|
}
|
|
119
|
-
|
|
118
|
+
// A floor said "judge later versions by this capture". Evidence now does that
|
|
119
|
+
// by itself, on every platform, so the field has nothing left to express and
|
|
120
|
+
// an adapter still declaring one is stale rather than strict. See
|
|
121
|
+
// docs/design/2026-09-22-certification-applies-forward.md.
|
|
120
122
|
if (manifest.certificationFloor !== undefined) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
usage("certificationFloor was removed: certification evidence applies forward from the "
|
|
124
|
+
+ "version that recorded it", { id: manifest.id });
|
|
123
125
|
}
|
|
124
126
|
const native = {};
|
|
125
127
|
for (const method of ["refreshNativeSession", "retireNativeSession", "nativeActivationHint"]) {
|
|
@@ -140,37 +142,9 @@ export function defineAdapter(manifest) {
|
|
|
140
142
|
return Object.freeze({
|
|
141
143
|
...manifest,
|
|
142
144
|
certification,
|
|
143
|
-
...floor,
|
|
144
145
|
...native,
|
|
145
146
|
capabilities: assertCapabilities(manifest.capabilities, manifest, certification),
|
|
146
147
|
});
|
|
147
148
|
}
|
|
148
149
|
|
|
149
|
-
const FLOOR_PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
* An optional per-platform floor: later stable versions are judged by this
|
|
153
|
-
* version's evidence until a capture of their own says otherwise. It can only
|
|
154
|
-
* name a version that has passing evidence on that platform, so a floor never
|
|
155
|
-
* certifies anything nobody captured.
|
|
156
|
-
*/
|
|
157
|
-
function validateCertificationFloor(value, { certification, client }) {
|
|
158
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
159
|
-
usage("certificationFloor must map a platform to a stable version");
|
|
160
|
-
}
|
|
161
|
-
for (const [platform, version] of Object.entries(value)) {
|
|
162
|
-
if (!FLOOR_PLATFORM.test(platform)) {
|
|
163
|
-
usage(`certificationFloor names an unknown platform ${platform}`, { platform });
|
|
164
|
-
}
|
|
165
|
-
if (parseStableVersion(version) === null) {
|
|
166
|
-
usage(`certificationFloor ${platform} must be a stable version`, { platform, version });
|
|
167
|
-
}
|
|
168
|
-
const proven = certification.evidence.some(item => item.result === "pass"
|
|
169
|
-
&& item.client === client && item.version === version && item.platform === platform);
|
|
170
|
-
if (!proven) {
|
|
171
|
-
usage(`certificationFloor ${version} on ${platform} has no passing evidence`,
|
|
172
|
-
{ platform, version });
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return Object.freeze({ ...value });
|
|
176
|
-
}
|
|
@@ -2,8 +2,6 @@ import path from "node:path";
|
|
|
2
2
|
|
|
3
3
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
4
4
|
|
|
5
|
-
import { compareStableVersions, parseStableVersion } from "./native-vocabulary.mjs";
|
|
6
|
-
|
|
7
5
|
export const CAPABILITY_SHAPE = Object.freeze({
|
|
8
6
|
lifecycle: Object.freeze(["sessionStart", "sessionResume", "sessionEnd", "heartbeat",
|
|
9
7
|
"childSessions"]),
|
|
@@ -20,6 +18,26 @@ const EVIDENCE_KEYS = new Set([...REQUIRED_TEXT, "limitations", "result"]);
|
|
|
20
18
|
const RESULTS = new Set(["pass", "fail"]);
|
|
21
19
|
const VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
22
20
|
const PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
|
|
21
|
+
const TRIPLE = /^(\d+)\.(\d+)\.(\d+)(?:[-+][0-9A-Za-z.-]+)?$/;
|
|
22
|
+
|
|
23
|
+
/** A client version as the triple that orders it. A prerelease is ordered by
|
|
24
|
+
* its release triple: 1.3.0-rc.1 is judged as 1.3.0, because a prerelease of a
|
|
25
|
+
* version ACC has already observed is not an older client. Anything unreadable
|
|
26
|
+
* - a probe that failed, "unknown", a vendor string - returns null.
|
|
27
|
+
*/
|
|
28
|
+
function versionOrder(text) {
|
|
29
|
+
const match = typeof text === "string" && text.toLowerCase() !== "unknown"
|
|
30
|
+
? TRIPLE.exec(text) : null;
|
|
31
|
+
return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function compareVersionOrder(left, right) {
|
|
35
|
+
if (left === null || right === null) return left === right ? 0 : left === null ? -1 : 1;
|
|
36
|
+
for (let index = 0; index < 3; index += 1) {
|
|
37
|
+
if (left[index] !== right[index]) return left[index] < right[index] ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
23
41
|
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
24
42
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
25
43
|
|
|
@@ -144,29 +162,62 @@ function falseCapabilities() {
|
|
|
144
162
|
* capability, is judged by the floor version's evidence for that capability.
|
|
145
163
|
* Versions below the floor, prereleases and other platforms stay uncertified.
|
|
146
164
|
*/
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Why a capability is on or off for one client, so a refusal can name the
|
|
167
|
+
* evidence that refused it instead of the version that asked.
|
|
168
|
+
*
|
|
169
|
+
* A capture says where a behaviour was observed, not which single release may
|
|
170
|
+
* use it. Clients ship almost daily, so evidence applies forward from the
|
|
171
|
+
* version that recorded it until a later capture changes that capability, and
|
|
172
|
+
* across platforms until one of them records something of its own. A version
|
|
173
|
+
* that cannot be read is judged by the newest evidence: a hook that runs has
|
|
174
|
+
* already proven the integration is installed.
|
|
175
|
+
*
|
|
176
|
+
* `reason` is one of `undeclared` (the adapter does not claim it), `unobserved`
|
|
177
|
+
* (nobody has captured it), `older-than-evidence` (this client predates the
|
|
178
|
+
* first capture, whose version is returned) or `recorded-failure` (a capture at
|
|
179
|
+
* the returned version recorded the loss).
|
|
180
|
+
*/
|
|
181
|
+
export function capabilityEvidence(adapter, { clientVersion, platform } = {}, capability) {
|
|
182
|
+
const [group, name] = capability.split(".");
|
|
183
|
+
if (adapter.capabilities?.[group]?.[name] !== true) {
|
|
184
|
+
return { granted: false, reason: "undeclared", version: null };
|
|
185
|
+
}
|
|
153
186
|
const client = adapter.client?.certificationName ?? adapter.client?.command;
|
|
154
|
-
const
|
|
155
|
-
.filter(item => item.client === client && item.
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
const named = (adapter.certification?.evidence ?? [])
|
|
188
|
+
.filter(item => item.client === client && item.capability === capability);
|
|
189
|
+
if (named.length === 0) return { granted: false, reason: "unobserved", version: null };
|
|
190
|
+
const asked = versionOrder(clientVersion);
|
|
191
|
+
const reachable = asked === null ? named
|
|
192
|
+
: named.filter(item => compareVersionOrder(versionOrder(item.version), asked) <= 0);
|
|
193
|
+
// Version first, platform second: a loss recorded on one platform at 1.3.0
|
|
194
|
+
// says nothing about that platform at 1.2.3, where the only evidence in
|
|
195
|
+
// reach is another platform's passing capture.
|
|
196
|
+
const own = reachable.filter(item => item.platform === platform);
|
|
197
|
+
const usable = own.length > 0 ? own : reachable;
|
|
198
|
+
if (usable.length === 0) {
|
|
199
|
+
const first = named.reduce((earliest, item) =>
|
|
200
|
+
compareVersionOrder(versionOrder(item.version), versionOrder(earliest.version)) < 0
|
|
201
|
+
? item : earliest);
|
|
202
|
+
return { granted: false, reason: "older-than-evidence", version: first.version };
|
|
203
|
+
}
|
|
204
|
+
const newest = usable.reduce((best, item) =>
|
|
205
|
+
compareVersionOrder(versionOrder(item.version), versionOrder(best.version)) > 0 ? item : best);
|
|
206
|
+
const deciding = versionOrder(newest.version);
|
|
207
|
+
// Two platforms can disagree at the deciding version when neither is the
|
|
208
|
+
// platform in hand. Withholding a body costs a trip to acc inbox; a false
|
|
209
|
+
// "delivered" loses the message, so a recorded loss wins the tie.
|
|
210
|
+
const granted = usable
|
|
211
|
+
.filter(item => compareVersionOrder(versionOrder(item.version), deciding) === 0)
|
|
212
|
+
.every(item => item.result === "pass");
|
|
213
|
+
return { granted, reason: granted ? null : "recorded-failure", version: newest.version };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function effectiveCapabilities(adapter, facts = {}) {
|
|
217
|
+
const resolved = falseCapabilities();
|
|
166
218
|
for (const [group, names] of Object.entries(CAPABILITY_SHAPE)) {
|
|
167
219
|
for (const name of names) {
|
|
168
|
-
resolved[group][name] = adapter
|
|
169
|
-
&& certified(`${group}.${name}`);
|
|
220
|
+
resolved[group][name] = capabilityEvidence(adapter, facts, `${group}.${name}`).granted;
|
|
170
221
|
}
|
|
171
222
|
}
|
|
172
223
|
return freezeCapabilities(resolved);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Capability contract, context projection, config ownership, and the binding
|
|
2
2
|
// that survives between two ephemeral hook processes.
|
|
3
3
|
export { CAPABILITY_SHAPE, assertCapabilities, defineAdapter } from "./capabilities.mjs";
|
|
4
|
-
export { effectiveCapabilities, validateCertification }
|
|
4
|
+
export { capabilityEvidence, effectiveCapabilities, validateCertification }
|
|
5
|
+
from "./certification.mjs";
|
|
5
6
|
export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE_REASON_CODES,
|
|
6
7
|
compareStableVersions, evaluateNativeEligibility, evaluateVersionContract, parseStableVersion,
|
|
7
8
|
validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
|
|
@@ -58,8 +58,7 @@ export function createSessionService(ports) {
|
|
|
58
58
|
const ephemeral = await store.ephemeral.get("session", sessionId);
|
|
59
59
|
const resolved = workspaceId ?? store.workspaceId ?? ephemeral?.workspaceId;
|
|
60
60
|
if (resolved === undefined) return null;
|
|
61
|
-
const durable =
|
|
62
|
-
.find(session => session.sessionId === sessionId) ?? null;
|
|
61
|
+
const durable = await store.stateRecord(resolved, "session", sessionId);
|
|
63
62
|
if (durable !== null) return { record: durable, durable: true };
|
|
64
63
|
return ephemeral !== null && ephemeral.workspaceId === resolved
|
|
65
64
|
? { record: ephemeral, durable: false } : null;
|
|
@@ -4,7 +4,8 @@ import { readFile, realpath } from "node:fs/promises";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
|
-
import { clearNativeAttempt, clearSessionBinding, effectiveCapabilities,
|
|
7
|
+
import { capabilityEvidence, clearNativeAttempt, clearSessionBinding, effectiveCapabilities,
|
|
8
|
+
loadSessionBinding, storeSessionBinding }
|
|
8
9
|
from "@agents-can-communicate/adapter-sdk";
|
|
9
10
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
10
11
|
import { AccError, createId } from "@agents-can-communicate/protocol";
|
|
@@ -301,12 +302,17 @@ async function projectTurn({ binding, context, adapter, adapterId,
|
|
|
301
302
|
? { text: await adapter.renderContext?.(projectionInput, projectionOptions) ?? "",
|
|
302
303
|
offeredMessageIds: [], includedAttentionIds: [] }
|
|
303
304
|
: await adapter.renderContextResult(projectionInput, projectionOptions);
|
|
304
|
-
|
|
305
|
-
|
|
305
|
+
// Name the evidence that refused, never the version that asked. "not
|
|
306
|
+
// certified" told the person their own client was at fault for existing;
|
|
307
|
+
// these two say what acc knows and what would change it.
|
|
308
|
+
const evidence = capabilityEvidence(adapter, binding, "delivery.nextTurn");
|
|
309
|
+
const refusal = {
|
|
310
|
+
"older-than-evidence": () => `client ${binding.clientVersion} is older than `
|
|
311
|
+
+ `${evidence.version}, the first version acc verified for nextTurn`,
|
|
312
|
+
"recorded-failure": () => `acc recorded that nextTurn stopped working in ${evidence.version}`,
|
|
313
|
+
}[evidence.reason] ?? (() => "this client has no nextTurn evidence");
|
|
306
314
|
const reason = !effective.delivery.nextTurn
|
|
307
|
-
?
|
|
308
|
-
? `client ${binding.clientVersion} on ${binding.platform} is not certified for nextTurn`
|
|
309
|
-
: "the client version or platform is unknown"
|
|
315
|
+
? refusal()
|
|
310
316
|
: !hasStructuredRenderer ? "this adapter lacks structured delivery metadata" : null;
|
|
311
317
|
const degradation = reason !== null && messages.length > 0
|
|
312
318
|
? `acc: ${messages.length} pending message(s) withheld because ${reason}; read `
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
|
-
import { effectiveCapabilities, evaluateNativeEligibility,
|
|
4
|
+
import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
|
|
5
|
+
validateNativeActivationPlan }
|
|
5
6
|
from "@agents-can-communicate/adapter-sdk";
|
|
6
7
|
|
|
7
8
|
import { resolveExecutable, shellOf, shimDirFor } from "./native-activation.mjs";
|
|
@@ -186,9 +187,20 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
186
187
|
} else if (typeof adapter.deliveryFallback?.diagnostic === "string") {
|
|
187
188
|
const downgraded = adapter.capabilities?.delivery?.nextTurn === true
|
|
188
189
|
&& entry.capabilities?.delivery?.nextTurn !== true;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// Name the evidence, not the version in hand: "has no certified
|
|
191
|
+
// next-turn delivery" told people their own client was the problem,
|
|
192
|
+
// and left them nothing to act on.
|
|
193
|
+
const evidence = capabilityEvidence(adapter,
|
|
194
|
+
{ clientVersion: entry.version, platform }, "delivery.nextTurn");
|
|
195
|
+
const named = entry.version ?? "unknown version";
|
|
196
|
+
const why = evidence.reason === "older-than-evidence"
|
|
197
|
+
? `${adapter.displayName} ${named} is older than ${evidence.version}, the first `
|
|
198
|
+
+ "version acc verified for next-turn delivery; "
|
|
199
|
+
: evidence.reason === "recorded-failure"
|
|
200
|
+
? `acc recorded that ${adapter.displayName} next-turn delivery stopped working `
|
|
201
|
+
+ `in ${evidence.version}; `
|
|
202
|
+
: `${adapter.displayName} next-turn delivery has not been captured; `;
|
|
203
|
+
entry.deliveryDiagnostic = (downgraded ? why : "") + adapter.deliveryFallback.diagnostic;
|
|
192
204
|
}
|
|
193
205
|
if (entry.deliveryDiagnostic !== null) entry.diagnostics.push(entry.deliveryDiagnostic);
|
|
194
206
|
}
|
|
@@ -58,28 +58,34 @@ function ephemeralDirectory(paths, kind, id) {
|
|
|
58
58
|
return path.join(paths.retained, "ephemeral", kind, id);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// The newest marker decides a record's state, so only that one is read and
|
|
62
|
+
// checked. Every name is still validated first - an unparsable sequence fails
|
|
63
|
+
// closed before anything is selected - and names are ordered as numbers, since
|
|
64
|
+
// a sequence can outgrow its padding. Reading every marker made each lookup
|
|
65
|
+
// cost an open and a full ancestor check per marker ever written: thousands
|
|
66
|
+
// for a delivery binding renewed every forty seconds, and seconds for one
|
|
67
|
+
// status in a workspace that had been in use for weeks.
|
|
61
68
|
async function latestEphemeralMarker(paths, root, kind, id) {
|
|
62
69
|
const directory = ephemeralDirectory(paths, kind, id);
|
|
63
|
-
const
|
|
64
|
-
for (const filePath of await listJsonFiles(directory, { root })) {
|
|
70
|
+
const named = (await listJsonFiles(directory, { root })).map(filePath => {
|
|
65
71
|
const sequence = path.basename(filePath, ".json");
|
|
72
|
+
if (typeof sequence !== "string" || !/^\d+$/.test(sequence)
|
|
73
|
+
|| BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))) {
|
|
74
|
+
data("invalid ephemeral retention marker", { filePath });
|
|
75
|
+
}
|
|
76
|
+
return { filePath, sequence, order: BigInt(sequence) };
|
|
77
|
+
}).sort((left, right) => (left.order < right.order ? 1 : left.order > right.order ? -1 : 0));
|
|
78
|
+
for (const { filePath, sequence } of named) {
|
|
66
79
|
const found = await readJsonIfPresent(filePath, root);
|
|
67
80
|
if (found === null) continue;
|
|
68
81
|
const marker = found.value;
|
|
69
|
-
if (
|
|
70
|
-
|| BigInt(sequence) < 1n || sequence !== pad(BigInt(sequence))
|
|
71
|
-
|| !(marker?.state === "present" || marker?.state === "deleted")) {
|
|
82
|
+
if (!(marker?.state === "present" || marker?.state === "deleted")) {
|
|
72
83
|
data("invalid ephemeral retention marker", { filePath });
|
|
73
84
|
}
|
|
74
|
-
assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state },
|
|
75
|
-
|
|
76
|
-
markers.push(marker);
|
|
85
|
+
assertMarker(found, { area: "ephemeral", kind, id, sequence, state: marker.state }, filePath);
|
|
86
|
+
return marker;
|
|
77
87
|
}
|
|
78
|
-
return
|
|
79
|
-
const leftSequence = BigInt(left.sequence);
|
|
80
|
-
const rightSequence = BigInt(right.sequence);
|
|
81
|
-
return leftSequence < rightSequence ? -1 : leftSequence > rightSequence ? 1 : 0;
|
|
82
|
-
}).at(-1) ?? null;
|
|
88
|
+
return null;
|
|
83
89
|
}
|
|
84
90
|
|
|
85
91
|
export async function ephemeralIsDeleted(paths, root, kind, id) {
|
|
@@ -91,6 +97,10 @@ export async function markEphemeral(paths, options, kind, id, state) {
|
|
|
91
97
|
data("invalid ephemeral retention state", { state });
|
|
92
98
|
}
|
|
93
99
|
const previous = await latestEphemeralMarker(paths, options.root, kind, id);
|
|
100
|
+
// A marker that repeats the current state changes nothing, and every renewal
|
|
101
|
+
// of a live record used to append one. History now grows only with a real
|
|
102
|
+
// change: published after a deletion, or deleted.
|
|
103
|
+
if (previous?.state === state) return previous;
|
|
94
104
|
const sequence = pad(previous === null ? 1n : BigInt(previous.sequence) + 1n);
|
|
95
105
|
const record = { retentionVersion: RETENTION_VERSION, area: "ephemeral", kind, id,
|
|
96
106
|
sequence, state };
|
|
@@ -265,6 +265,23 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
265
265
|
* cost grow with the number of messages the workspace had ever carried, and
|
|
266
266
|
* the hook budget is five seconds after which it allows the write.
|
|
267
267
|
*/
|
|
268
|
+
/**
|
|
269
|
+
* One state record by id, or null - the checks a listing applies to each
|
|
270
|
+
* record (the path names it, a deleted generation is absent, the record
|
|
271
|
+
* validates, it belongs to this workspace) without reading every other
|
|
272
|
+
* record of its kind. Looking one session up by listing them all cost a full
|
|
273
|
+
* pass over every session the workspace ever had, several times a hook.
|
|
274
|
+
*/
|
|
275
|
+
async function stateRecord(workspace, kind, id) {
|
|
276
|
+
const filePath = statePath(paths, kind, id);
|
|
277
|
+
const found = await readJsonIfPresent(filePath, root);
|
|
278
|
+
if (found === null) return null;
|
|
279
|
+
const envelope = assertStateBinding(found.value, kind, id, filePath);
|
|
280
|
+
if (await stateGenerationIsDeleted(paths, root, kind, envelope.id, envelope.generation)) return null;
|
|
281
|
+
validateRecord(kind, envelope.record);
|
|
282
|
+
return envelope.record.workspaceId === workspace ? envelope.record : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
268
285
|
async function snapshot(workspace, { kinds } = {}) {
|
|
269
286
|
const wanted = kinds === undefined ? null : new Set(kinds);
|
|
270
287
|
const of = async kind => {
|
|
@@ -348,6 +365,6 @@ export async function openFilesystemStore({ root, clock, ids, workspaceId, failA
|
|
|
348
365
|
},
|
|
349
366
|
});
|
|
350
367
|
|
|
351
|
-
return Object.freeze({ transaction, eventsSince, snapshot, ephemeral, paths, root,
|
|
368
|
+
return Object.freeze({ transaction, eventsSince, snapshot, stateRecord, ephemeral, paths, root,
|
|
352
369
|
workspaceId });
|
|
353
370
|
}
|