agents-can-communicate 0.4.1 → 0.4.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/README.md +11 -4
- package/docs/ARCHITECTURE.md +7 -1
- package/docs/CLI.md +23 -3
- package/docs/GETTING_STARTED.md +11 -3
- package/docs/RELEASING.md +1 -1
- package/docs/TROUBLESHOOTING.md +43 -5
- package/docs/UPGRADING.md +31 -19
- 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 +3 -3
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +10 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +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/index.mjs +2 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt-writer.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +65 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +17 -52
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +20 -45
- package/node_modules/@agents-can-communicate/cli/src/native-delivery-status.mjs +88 -0
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +64 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- 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/native-attempt.mjs +43 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +17 -27
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +36 -0
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +23 -10
- package/node_modules/@agents-can-communicate/installer/src/index.mjs +3 -1
- package/node_modules/@agents-can-communicate/installer/src/live-policy.mjs +10 -3
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +6 -1
- 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/package.json +1 -1
|
@@ -2,10 +2,15 @@ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { detectInstallation, livePolicyOf, loadOwnership, shellOf, verifyOwned }
|
|
5
|
+
import { describeDeliveryFallback, detectInstallation, livePolicyOf, loadOwnership, shellOf, verifyOwned }
|
|
6
6
|
from "@agents-can-communicate/installer";
|
|
7
7
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
8
8
|
|
|
9
|
+
import { describeNative, nativeRemediation, nativeState, updateNativeRuntime } from "./native-delivery-status.mjs";
|
|
10
|
+
export { describeNative } from "./native-delivery-status.mjs";
|
|
11
|
+
|
|
12
|
+
import { nativeSessionLines, updateNativeSessions } from "./native-session-diagnostics.mjs";
|
|
13
|
+
|
|
9
14
|
import { ALL_ADAPTERS, clientContext, probeTimeout } from "./install-command.mjs";
|
|
10
15
|
import { describePresence } from "./main.mjs";
|
|
11
16
|
import { platformPaths } from "./platform-paths.mjs";
|
|
@@ -66,21 +71,6 @@ export function staleInstall({ recorded, running }) {
|
|
|
66
71
|
return recorded === running ? null : { recorded, running };
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
const RUNTIME_LABEL = Object.freeze({ active: "active", waiting: "waiting for a live session",
|
|
70
|
-
inactive: "not enabled", degraded: "degraded", unsupported: "unsupported" });
|
|
71
|
-
|
|
72
|
-
/** One human clause for a native-delivery state, next-action implied, never overclaiming. */
|
|
73
|
-
export function describeNative(native) {
|
|
74
|
-
if (native.eligibility === "unsupported") {
|
|
75
|
-
return `unsupported${native.reasonCode ? ` (${native.reasonCode})` : ""}`;
|
|
76
|
-
}
|
|
77
|
-
const enabled = native.configured ? `enabled (${native.policy})` : "eligible, not enabled";
|
|
78
|
-
const runtime = RUNTIME_LABEL[native.runtime] ?? native.runtime;
|
|
79
|
-
const degraded = native.eligibility === "degraded" && native.reasonCode
|
|
80
|
-
? ` - ${native.reasonCode}` : "";
|
|
81
|
-
return `${native.eligibility} - ${enabled} - ${runtime}${degraded}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
74
|
/**
|
|
85
75
|
* The runner version behind whatever ACC wrote for one client.
|
|
86
76
|
*
|
|
@@ -117,25 +107,6 @@ async function findShims(root, depth) {
|
|
|
117
107
|
return found;
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
// One closed native-delivery report per adapter, built only from detection,
|
|
121
|
-
// ownership, and later the live binding facts - never inferred from a
|
|
122
|
-
// configured shim alone. eligibility is what the client could do; configured is
|
|
123
|
-
// whether a policy was recorded; policy is that recorded policy; runtime is
|
|
124
|
-
// filled in from current bindings; modes and reasonCode carry the closed
|
|
125
|
-
// detail. runtime "active" never means the model read anything.
|
|
126
|
-
function nativeState(detected, recordedPolicy) {
|
|
127
|
-
const native = detected ?? { state: "unsupported", reasonCode: "native_delivery_unsupported" };
|
|
128
|
-
const eligibility = native.state === "eligible" ? "eligible"
|
|
129
|
-
: native.state === "degraded" ? "degraded" : "unsupported";
|
|
130
|
-
const policy = recordedPolicy ?? "off";
|
|
131
|
-
const configured = policy !== "off";
|
|
132
|
-
const modes = native.state === "eligible" && Array.isArray(native.probe?.modes)
|
|
133
|
-
? [...native.probe.modes] : [];
|
|
134
|
-
const runtime = eligibility === "unsupported" ? "unsupported"
|
|
135
|
-
: !configured ? "inactive" : "waiting";
|
|
136
|
-
return { eligibility, configured, policy, runtime, modes, reasonCode: native.reasonCode ?? null };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
110
|
export async function diagnoseAdapters({ options, runtime, detect = detectInstallation }) {
|
|
140
111
|
// The same home `acc install --home` writes to, or the real one. Reading a
|
|
141
112
|
// different home than install wrote to reports every adapter as missing.
|
|
@@ -203,7 +174,9 @@ export async function diagnoseAdapters({ options, runtime, detect = detectInstal
|
|
|
203
174
|
// divergence legible rather than hidden behind a single reassuring number.
|
|
204
175
|
return { ...entry, stale, wired, bundleVersion, owned: { modified: owned.modified,
|
|
205
176
|
missing: owned.missing, intact: owned.intact.length },
|
|
206
|
-
nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed)
|
|
177
|
+
nativeDelivery: nativeState(entry.nativeDelivery, livePolicyOf(installed), {
|
|
178
|
+
contract: adapters.find(adapter => adapter.id === entry.adapterId)?.nativeDelivery,
|
|
179
|
+
activation: installed?.nativeActivation }), remediation };
|
|
207
180
|
}));
|
|
208
181
|
}
|
|
209
182
|
|
|
@@ -259,22 +232,10 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
259
232
|
// front.
|
|
260
233
|
const service = context.service ?? await context.openService();
|
|
261
234
|
const status = await service.collectStatus({});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const bindingsByAdapter = new Map();
|
|
265
|
-
for (const binding of status.deliveryBindings ?? []) {
|
|
266
|
-
const existing = bindingsByAdapter.get(binding.adapterId);
|
|
267
|
-
if (existing === undefined || binding.reachable) bindingsByAdapter.set(binding.adapterId, binding);
|
|
268
|
-
}
|
|
235
|
+
updateNativeRuntime(adapters, status.deliveryBindings);
|
|
236
|
+
await updateNativeSessions(adapters, { service, status, root, now: clock.now() });
|
|
269
237
|
for (const adapter of adapters) {
|
|
270
|
-
|
|
271
|
-
if (native.eligibility === "unsupported") continue;
|
|
272
|
-
const binding = bindingsByAdapter.get(adapter.adapterId);
|
|
273
|
-
native.runtime = binding === undefined ? (native.configured ? "waiting" : "inactive")
|
|
274
|
-
: binding.reachable ? "active" : "degraded";
|
|
275
|
-
if (binding !== undefined && Array.isArray(binding.availableModes)) {
|
|
276
|
-
native.modes = binding.availableModes.filter(mode => mode !== "nextTurn");
|
|
277
|
-
}
|
|
238
|
+
adapter.remediation.push(...nativeRemediation(adapter));
|
|
278
239
|
}
|
|
279
240
|
|
|
280
241
|
const data = {
|
|
@@ -301,13 +262,17 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
301
262
|
const text = [`store healthy; ${describePresence(status.counts)}; `
|
|
302
263
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
303
264
|
...adapters.filter(adapter => (adapter.present || adapter.installed)
|
|
265
|
+
&& adapter.nativeDelivery.reasonCode === "native_delivery_unsupported"
|
|
304
266
|
&& typeof adapter.deliveryDiagnostic === "string")
|
|
305
267
|
.map(adapter => ` ${adapter.deliveryDiagnostic}`),
|
|
306
268
|
// One concise native-delivery line per detected client, distinguishing
|
|
307
269
|
// eligibility, the recorded policy, and the live runtime state. It never
|
|
308
270
|
// claims that "active" means a model read anything.
|
|
309
271
|
...adapters.filter(adapter => adapter.present)
|
|
310
|
-
.map(adapter => ` ${adapter.displayName}
|
|
272
|
+
.map(adapter => ` ${adapter.displayName} live delivery: `
|
|
273
|
+
+ `${describeNative(adapter.nativeDelivery, { clientVersion: adapter.version })}; `
|
|
274
|
+
+ `fallback: ${describeDeliveryFallback(adapter)}`),
|
|
275
|
+
...nativeSessionLines(adapters),
|
|
311
276
|
...(manager === null ? [] : [` automatic updates ${manager.auto ? "on" : "off"}; ACC ${manager.active.version}`
|
|
312
277
|
+ (manager.pin ? `; pinned to ${manager.pin}` : ""), ...(manager.notice ? [` ${manager.notice}`] : [])]),
|
|
313
278
|
...data.remediation.map(line => ` ${line}`),
|
|
@@ -148,8 +148,8 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
148
148
|
...operations.filter(operation => operation.applied)
|
|
149
149
|
.flatMap(operation => describeChanges(operation, home)),
|
|
150
150
|
...operations.filter(operation => operation.applied
|
|
151
|
-
&& typeof operation.deliveryDiagnostic === "string")
|
|
152
|
-
.map(operation => ` ${operation.deliveryDiagnostic}`),
|
|
151
|
+
&& typeof (operation.deliverySummary ?? operation.deliveryDiagnostic) === "string")
|
|
152
|
+
.map(operation => ` ${operation.deliverySummary ?? operation.deliveryDiagnostic}`),
|
|
153
153
|
...operations.filter(operation => operation.applied)
|
|
154
154
|
.flatMap(operation => (operation.needsAction ?? [])
|
|
155
155
|
.map(step => ` ${operation.adapterId}: ${step}`)),
|
|
@@ -203,42 +203,21 @@ export function actedOn(result) {
|
|
|
203
203
|
const isInteractive = runtime => typeof runtime.isInteractive === "function"
|
|
204
204
|
&& runtime.isInteractive() === true;
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
* Written for someone who has never heard of this project. It used to open with
|
|
211
|
-
* "Enable native live delivery" over a list of internal artefact names, which
|
|
212
|
-
* told a first-time reader neither what they would gain nor what it would cost -
|
|
213
|
-
* and left out the part that matters most: saying yes makes their own `claude`
|
|
214
|
-
* start with the vendor's `--dangerously-load-development-channels` flag. A
|
|
215
|
-
* consent prompt that hides the loudest consequence is not consent.
|
|
216
|
-
*
|
|
217
|
-
* So the order is the reader's, not the installer's: what changes for them,
|
|
218
|
-
* what it costs, what it touches, and that "no" costs them nothing.
|
|
219
|
-
*/
|
|
220
|
-
function questionFor(entry, context, { onlyEligible = false } = {}) {
|
|
221
|
-
const bootstrap = entry.nativeDelivery.activationPlan.mechanisms
|
|
206
|
+
// One default-No question per supported client. Disclose token use and any
|
|
207
|
+
// repeated channel prompt; declining only promises a verified fallback.
|
|
208
|
+
function questionFor(entry) {
|
|
209
|
+
const bootstrap = entry.nativeDelivery.activationPlan?.mechanisms
|
|
222
210
|
.find(mechanism => mechanism.kind === "shell-bootstrap") ?? null;
|
|
223
|
-
|
|
224
|
-
// why the client will warn at every start from now on. Which flag it is, and
|
|
225
|
-
// that a shim adds it, are answers to a question nobody is asking here - the
|
|
226
|
-
// shim `exec`s and is gone, so nothing runs "through" acc afterwards either.
|
|
227
|
-
const flags = (bootstrap?.prefixArgs ?? []).filter(argument => argument.startsWith("--"));
|
|
228
|
-
void context;
|
|
229
|
-
// Why the default is No, and why only one client is being asked about, are
|
|
230
|
-
// the two questions the bare prompt provokes and never answered: a `[y/N]`
|
|
231
|
-
// reads as "not recommended" on its own, and a single question among four
|
|
232
|
-
// installed clients reads as an omission. Both have the same short answer.
|
|
233
|
-
const aside = ["experimental", ...(onlyEligible ? [`${entry.displayName} only`] : [])]
|
|
234
|
-
.join("; ");
|
|
211
|
+
const warns = (bootstrap?.prefixArgs ?? []).some(argument => argument.startsWith("--"));
|
|
235
212
|
return [
|
|
236
|
-
`Let
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
213
|
+
`Let ${entry.displayName} answer peer requests while idle? (experimental)`,
|
|
214
|
+
" Yes: automatic turns can spend tokens without waiting for you.",
|
|
215
|
+
...(warns ? [" Allow development channels when prompted; check the client's startup notice."] : []),
|
|
216
|
+
...(entry.nativeDelivery.state !== "eligible"
|
|
217
|
+
? [" Save consent now; delivery waits for an available client service."] : []),
|
|
218
|
+
entry.capabilities?.delivery?.nextTurn === true
|
|
219
|
+
? " No: use next-turn hooks (when enabled) or acc inbox."
|
|
220
|
+
: " No: read messages with acc inbox; automatic next-turn delivery is unavailable.",
|
|
242
221
|
].join("\n");
|
|
243
222
|
}
|
|
244
223
|
|
|
@@ -250,22 +229,18 @@ function questionFor(entry, context, { onlyEligible = false } = {}) {
|
|
|
250
229
|
* client is off unless a person answers yes here, and a dry run or a
|
|
251
230
|
* non-interactive stdin/stdout makes no interactive choice at all.
|
|
252
231
|
*/
|
|
253
|
-
export async function decideDelivery({ options, detected, recorded, runtime, dryRun
|
|
232
|
+
export async function decideDelivery({ options, detected, recorded, runtime, dryRun }) {
|
|
254
233
|
const explicit = options.delivery;
|
|
255
234
|
if (explicit !== undefined && !LIVE_POLICIES.includes(explicit)) {
|
|
256
235
|
throw new AccError(EXIT.USAGE, `unknown delivery policy: ${explicit}`, { delivery: explicit });
|
|
257
236
|
}
|
|
258
237
|
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
259
|
-
// Whether this client is the only one that could be asked at all. When it is,
|
|
260
|
-
// the question says so, because otherwise being asked about one of four
|
|
261
|
-
// installed clients looks like the other three were forgotten.
|
|
262
|
-
const onlyEligible = detected
|
|
263
|
-
.filter(candidate => candidate.nativeDelivery?.state === "eligible").length === 1;
|
|
264
238
|
const deliveryByAdapter = {};
|
|
265
239
|
const asked = [];
|
|
266
240
|
let withheld = 0;
|
|
267
241
|
for (const entry of detected) {
|
|
268
|
-
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
242
|
+
const eligible = entry.nativeDelivery?.state === "eligible"
|
|
243
|
+
|| entry.nativeDelivery?.consentAvailable === true;
|
|
269
244
|
const previous = livePolicyOf(recordedById.get(entry.adapterId));
|
|
270
245
|
if (explicit !== undefined) {
|
|
271
246
|
deliveryByAdapter[entry.adapterId] = explicit;
|
|
@@ -275,7 +250,7 @@ export async function decideDelivery({ options, detected, recorded, runtime, dry
|
|
|
275
250
|
deliveryByAdapter[entry.adapterId] = "off";
|
|
276
251
|
if (eligible) withheld += 1;
|
|
277
252
|
} else {
|
|
278
|
-
const yes = await runtime.confirm(questionFor(entry
|
|
253
|
+
const yes = await runtime.confirm(questionFor(entry),
|
|
279
254
|
{ input: runtime.input, output: runtime.output }) === true;
|
|
280
255
|
deliveryByAdapter[entry.adapterId] = yes ? "actionable" : "off";
|
|
281
256
|
asked.push(entry.adapterId);
|
|
@@ -333,7 +308,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
333
308
|
// Decided only after detection, from the operator's explicit answer, the
|
|
334
309
|
// recorded consent, or a default of off; never from inference.
|
|
335
310
|
const decided = action === "install"
|
|
336
|
-
? await decideDelivery({ options, detected, recorded, runtime, dryRun
|
|
311
|
+
? await decideDelivery({ options, detected, recorded, runtime, dryRun })
|
|
337
312
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
338
313
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
339
314
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
2
|
+
|
|
3
|
+
export function describeNative(native, { clientVersion } = {}) {
|
|
4
|
+
const differs = native.runtime === "active" && native.sessionPolicy
|
|
5
|
+
&& native.sessionPolicy !== native.policy;
|
|
6
|
+
const enabled = (native.configured ? `enabled (${native.policy})` : "off")
|
|
7
|
+
+ (differs ? " for new sessions" : "");
|
|
8
|
+
const availability = native.eligibility === "eligible" ? "available"
|
|
9
|
+
: `${native.eligibility === "unsupported" ? "unavailable" : "readiness unverified"}: `
|
|
10
|
+
+ describeNativeReason(native.reasonCode, { clientVersion, minimumVersion: native.minimumVersion });
|
|
11
|
+
const runtime = native.runtime === "active"
|
|
12
|
+
? `local transport active${differs ? ` (session policy: ${native.sessionPolicy})` : ""}`
|
|
13
|
+
: native.runtime === "degraded" ? "channel unreachable"
|
|
14
|
+
: native.configured && native.runtime === "waiting"
|
|
15
|
+
? "no live channel bound in this workspace" : null;
|
|
16
|
+
return [availability, enabled, ...(runtime ? [runtime] : [])].join("; ");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function nativeRemediation(entry) {
|
|
20
|
+
const native = entry.nativeDelivery;
|
|
21
|
+
if (!(entry.present || entry.installed) || native.eligibility === "unsupported") return [];
|
|
22
|
+
const steps = [];
|
|
23
|
+
if (!native.configured && native.runtime !== "active") {
|
|
24
|
+
steps.push(`acc install --adapter ${entry.adapterId} --delivery actionable`
|
|
25
|
+
+ " # opt in to automatic peer requests; may spend tokens");
|
|
26
|
+
} else if (native.configured && native.activation === "missing") {
|
|
27
|
+
steps.push(`acc install --adapter ${entry.adapterId}`
|
|
28
|
+
+ " # complete the missing native launch setup from a supported shell");
|
|
29
|
+
} else if (native.configured && native.runtime !== "active") {
|
|
30
|
+
steps.push(`${entry.displayName}: no verified live channel in this workspace; `
|
|
31
|
+
+ "open a new terminal, start a new client session and check its integration/channel prompts; "
|
|
32
|
+
+ "then run acc doctor here");
|
|
33
|
+
}
|
|
34
|
+
if (native.reasonCode === "native_endpoint_unavailable") {
|
|
35
|
+
steps.push(`${entry.displayName}: check the client's local delivery service setup; `
|
|
36
|
+
+ "ACC does not start or restart that service");
|
|
37
|
+
} else if (native.reasonCode === "native_session_unavailable") {
|
|
38
|
+
steps.push(`${entry.displayName}: open a session connected to the client's local delivery service`);
|
|
39
|
+
}
|
|
40
|
+
return steps;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// One closed native-delivery report per adapter, built only from detection,
|
|
44
|
+
// ownership, and later the live binding facts - never inferred from a
|
|
45
|
+
// configured shim alone. eligibility is what the client could do; configured is
|
|
46
|
+
// whether a policy was recorded; policy is that recorded policy; runtime is
|
|
47
|
+
// filled in from current bindings; modes and reasonCode carry the closed
|
|
48
|
+
// detail. runtime "active" never means the model read anything.
|
|
49
|
+
export function nativeState(detected, recordedPolicy, { contract, activation } = {}) {
|
|
50
|
+
const native = detected ?? { state: "unsupported", reasonCode: "native_delivery_unsupported" };
|
|
51
|
+
const eligibility = native.state === "eligible" ? "eligible"
|
|
52
|
+
: native.state === "degraded" ? "degraded" : "unsupported";
|
|
53
|
+
const policy = recordedPolicy ?? "off";
|
|
54
|
+
const configured = policy !== "off";
|
|
55
|
+
const modes = native.state === "eligible" && Array.isArray(native.probe?.modes)
|
|
56
|
+
? [...native.probe.modes] : [];
|
|
57
|
+
const runtime = eligibility === "unsupported" ? "unsupported"
|
|
58
|
+
: !configured ? "inactive" : "waiting";
|
|
59
|
+
const setupRequired = contract?.activationKinds?.some(kind => kind !== "native-service") === true;
|
|
60
|
+
return { eligibility, configured, policy, runtime, modes, reasonCode: native.reasonCode ?? null,
|
|
61
|
+
minimumVersion: native.eligibility?.minimumVersion ?? null,
|
|
62
|
+
policySource: contract?.policySource ?? null,
|
|
63
|
+
activation: !setupRequired ? "not_required"
|
|
64
|
+
: activation?.mechanisms?.length > 0 ? "recorded" : "missing", sessionPolicy: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Native runtime is workspace-specific. A hook-only binding cannot prove it;
|
|
68
|
+
// legacy launch policies may outlive the policy selected for future launches.
|
|
69
|
+
export function updateNativeRuntime(adapters, bindings) {
|
|
70
|
+
const byAdapter = new Map();
|
|
71
|
+
for (const binding of bindings ?? []) {
|
|
72
|
+
if (!binding.availableModes?.includes("livePush")) continue;
|
|
73
|
+
const previous = byAdapter.get(binding.adapterId);
|
|
74
|
+
if (previous === undefined || binding.reachable) byAdapter.set(binding.adapterId, binding);
|
|
75
|
+
}
|
|
76
|
+
for (const entry of adapters) {
|
|
77
|
+
const native = entry.nativeDelivery;
|
|
78
|
+
if (native.eligibility === "unsupported") continue;
|
|
79
|
+
const binding = byAdapter.get(entry.adapterId);
|
|
80
|
+
const disabled = native.policySource === "installation-record" && !native.configured;
|
|
81
|
+
native.runtime = disabled ? "inactive"
|
|
82
|
+
: binding === undefined ? (native.configured ? "waiting" : "inactive")
|
|
83
|
+
: binding.reachable ? "active" : "degraded";
|
|
84
|
+
native.sessionPolicy = disabled || !binding ? null
|
|
85
|
+
: native.policySource === "installation-record" ? native.policy : binding.livePolicy;
|
|
86
|
+
if (binding) native.modes = binding.availableModes.filter(mode => mode !== "nextTurn");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { listSessionBindings, loadNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { describeNativeReason } from "@agents-can-communicate/installer";
|
|
3
|
+
|
|
4
|
+
// Session diagnostics are read only here, never projected into agent context.
|
|
5
|
+
// Verify the owner generation against core before attributing any last attempt.
|
|
6
|
+
export async function updateNativeSessions(adapters, { service, status, root, now }) {
|
|
7
|
+
const owners = await listSessionBindings({ runtimeDir: root }).catch(() => []);
|
|
8
|
+
const transports = new Map();
|
|
9
|
+
for (const entry of adapters) {
|
|
10
|
+
const native = entry.nativeDelivery;
|
|
11
|
+
native.sessions = [];
|
|
12
|
+
if (native.reasonCode === "native_delivery_unsupported") continue;
|
|
13
|
+
for (const peer of status.participants.filter(p => p.harness === entry.adapterId)) {
|
|
14
|
+
const current = (await service.locateSession(peer.sessionId))?.record;
|
|
15
|
+
if (current?.state !== "open") continue;
|
|
16
|
+
const matches = owners.filter(owner => owner.accSessionId === peer.sessionId
|
|
17
|
+
&& owner.generation === current.generation);
|
|
18
|
+
const owner = matches.length === 1 ? matches[0] : null;
|
|
19
|
+
if (!transports.has(peer.participantId)) {
|
|
20
|
+
transports.set(peer.participantId, await service.listDeliveryBindings({
|
|
21
|
+
participantId: peer.participantId, now, includeExpired: true }));
|
|
22
|
+
}
|
|
23
|
+
const binding = transports.get(peer.participantId).find(b => b.sessionId === peer.sessionId
|
|
24
|
+
&& b.generation === current.generation && b.adapterId === entry.adapterId
|
|
25
|
+
&& b.availableModes.includes("livePush"));
|
|
26
|
+
const runtime = native.policySource === "installation-record" && !native.configured
|
|
27
|
+
? "inactive" : !binding ? "unbound"
|
|
28
|
+
: Date.parse(binding.leaseUntil) > Date.parse(now) ? "active" : "degraded";
|
|
29
|
+
native.sessions.push({ sessionId: peer.sessionId, participantId: peer.participantId,
|
|
30
|
+
presence: peer.presence, runtime,
|
|
31
|
+
lastAttempt: owner === null ? null : await loadNativeAttempt({ runtimeDir: root, ...owner }) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const failures = {
|
|
37
|
+
client_process_unknown: "the client process could not be identified; start a new client session",
|
|
38
|
+
handshake_failed: "the session handshake failed; check the client's integration/channel setup",
|
|
39
|
+
handshake_timeout: "the session handshake timed out; check the local channel and retry on the next turn",
|
|
40
|
+
handshake_version_mismatch: "the session and CLI versions differ; start a new client session",
|
|
41
|
+
session_generation_stale: "the session was replaced during the handshake",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
function describeAttempt(attempt) {
|
|
45
|
+
if (attempt === null) return "no native binding attempt observed for this generation; "
|
|
46
|
+
+ "check client hooks; an older hook runtime or a failed diagnostic write can also leave no record";
|
|
47
|
+
let detail;
|
|
48
|
+
if (attempt.state === "off") {
|
|
49
|
+
const source = attempt.policySource === "bootstrap-environment" ? "launch" : "installation";
|
|
50
|
+
detail = attempt.policyStatus === "missing" ? `${source} delivery policy was absent`
|
|
51
|
+
: attempt.policyStatus === "invalid" ? `${source} delivery policy was invalid`
|
|
52
|
+
: attempt.policyStatus === "unavailable" ? `${source} delivery policy could not be read`
|
|
53
|
+
: `${source} delivery policy was off`;
|
|
54
|
+
} else if (attempt.state === "active") detail = "local handshake succeeded";
|
|
55
|
+
else detail = failures[attempt.reasonCode] ?? describeNativeReason(attempt.reasonCode);
|
|
56
|
+
return `last attempt ${attempt.at}: ${detail}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nativeSessionLines(adapters) {
|
|
60
|
+
return adapters.flatMap(entry => (entry.nativeDelivery.sessions ?? []).map(session =>
|
|
61
|
+
` ${entry.displayName} session ${session.participantId} (${session.sessionId}): `
|
|
62
|
+
+ `${session.runtime === "active" ? "local transport active" : session.runtime}; `
|
|
63
|
+
+ describeAttempt(session.lastAttempt)));
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { storeNativeAttempt } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
|
|
3
|
+
|
|
4
|
+
import { establishNativeBinding, LIVE_POLICIES, livePolicyFrom } from "./native-binding.mjs";
|
|
5
|
+
|
|
6
|
+
// Reserve time for turn projection, status and output before optional disk I/O.
|
|
7
|
+
export function nativeDiagnosticDeadline(hookDeadline) {
|
|
8
|
+
const now = Date.now();
|
|
9
|
+
return hookDeadline - now >= 750 ? now + 250 : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The diagnostic has its own bounded writer; it never rewrites hook ownership.
|
|
13
|
+
// A failed or slow disk leaves the functional hook result intact.
|
|
14
|
+
export async function bindNative({ adapter, event, hookBinding, clientVersion, platform,
|
|
15
|
+
context, paths, deadline }) {
|
|
16
|
+
const assertBudget = () => {
|
|
17
|
+
if (Date.now() >= deadline) throw new Error("hook deadline expired");
|
|
18
|
+
};
|
|
19
|
+
assertBudget();
|
|
20
|
+
const policySource = adapter?.nativeDelivery?.policySource === "installation-record"
|
|
21
|
+
? "installation-record" : "bootstrap-environment";
|
|
22
|
+
const raw = context.env?.ACC_NATIVE_DELIVERY_POLICY;
|
|
23
|
+
const { policy, policyStatus } = policySource === "installation-record"
|
|
24
|
+
? await readInstalledLivePolicyState({ dataHome: context.dataHome, adapterId: adapter.id })
|
|
25
|
+
: { policy: livePolicyFrom(context.env), policyStatus: raw === undefined ? "missing"
|
|
26
|
+
: !LIVE_POLICIES.includes(raw) ? "invalid" : raw === "off" ? "off" : "enabled" };
|
|
27
|
+
assertBudget();
|
|
28
|
+
const result = await establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
29
|
+
livePolicy: policy, service: context.service, runtimeDir: paths.root,
|
|
30
|
+
clock: context.service.clock, env: context.env,
|
|
31
|
+
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
32
|
+
const diagnosticDeadline = nativeDiagnosticDeadline(deadline);
|
|
33
|
+
if (adapter.nativeDelivery && diagnosticDeadline !== null) {
|
|
34
|
+
await storeNativeAttempt({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
35
|
+
accSessionId: hookBinding.accSessionId, generation: hookBinding.generation, deadlineAt: diagnosticDeadline, nativeAttempt: {
|
|
36
|
+
at: context.service.clock.now(), event: event.kind, state: result.state,
|
|
37
|
+
reasonCode: result.reasonCode, policy, policySource, policyStatus,
|
|
38
|
+
clientProcess: Number.isInteger(hookBinding.clientPid) && hookBinding.clientPid > 0
|
|
39
|
+
? "identified" : "unknown",
|
|
40
|
+
} }).catch(() => {});
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
@@ -3,10 +3,9 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
3
3
|
import { realpath } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
|
|
6
|
-
import { clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
|
|
6
|
+
import { clearNativeAttempt, clearSessionBinding, effectiveCapabilities, loadSessionBinding, storeSessionBinding }
|
|
7
7
|
from "@agents-can-communicate/adapter-sdk";
|
|
8
8
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
9
|
-
import { readInstalledLivePolicy } from "@agents-can-communicate/installer";
|
|
10
9
|
import { AccError, createId } from "@agents-can-communicate/protocol";
|
|
11
10
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
12
11
|
import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
@@ -14,7 +13,7 @@ import { createGitProbe, discoverWorkspace, platformDataHome, runtimePaths }
|
|
|
14
13
|
|
|
15
14
|
import { resolveClientPid } from "./client-pid.mjs";
|
|
16
15
|
import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
|
|
17
|
-
import {
|
|
16
|
+
import { bindNative, nativeDiagnosticDeadline } from "./native-attempt.mjs";
|
|
18
17
|
import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
|
|
19
18
|
import { withSessionLifecycle } from "./session-lifecycle.mjs";
|
|
20
19
|
import { appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
|
|
@@ -305,22 +304,6 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
305
304
|
offerInputs: writableOffers };
|
|
306
305
|
}
|
|
307
306
|
|
|
308
|
-
// One bounded, fail-open native handshake for this exact session generation.
|
|
309
|
-
// New adapters read durable consent from their installation record; legacy
|
|
310
|
-
// adapters keep the environment exported by an owned shell bootstrap.
|
|
311
|
-
async function bindNative({ adapter, event, hookBinding, clientVersion, platform, context, paths,
|
|
312
|
-
deadline }) {
|
|
313
|
-
assertHookBudget(deadline);
|
|
314
|
-
const livePolicy = adapter?.nativeDelivery?.policySource === "installation-record"
|
|
315
|
-
? await readInstalledLivePolicy({ dataHome: context.dataHome, adapterId: adapter.id })
|
|
316
|
-
: livePolicyFrom(context.env);
|
|
317
|
-
assertHookBudget(deadline);
|
|
318
|
-
return establishNativeBinding({ adapter, event, hookBinding, clientVersion, platform,
|
|
319
|
-
livePolicy, service: context.service, runtimeDir: paths.root,
|
|
320
|
-
clock: context.service.clock, env: context.env,
|
|
321
|
-
timeoutMs: Math.max(1, Math.min(750, deadline - Date.now())) });
|
|
322
|
-
}
|
|
323
|
-
|
|
324
307
|
const HANDLERS = {
|
|
325
308
|
async sessionStart({ event, context, adapter, adapterId, binding, paths,
|
|
326
309
|
readProcessTable, probeClientVersion, platform, deadline }) {
|
|
@@ -356,12 +339,12 @@ const HANDLERS = {
|
|
|
356
339
|
branch: context.descriptor.git?.branch ?? null,
|
|
357
340
|
};
|
|
358
341
|
if (event.kind === "beforeTurn" && binding !== null) {
|
|
359
|
-
// A
|
|
360
|
-
//
|
|
361
|
-
//
|
|
342
|
+
// A genuine prompt can resume after an ephemeral removal or a durable
|
|
343
|
+
// close. Recheck after probes: a CLI replacement can run outside the
|
|
344
|
+
// native lifecycle lock. Never adopt that replacement's identity.
|
|
362
345
|
const previous = await context.service.locateSession(binding.accSessionId);
|
|
363
|
-
if (previous
|
|
364
|
-
|| previous.record.generation !== binding.generation) {
|
|
346
|
+
if (previous !== null && (previous.record.state !== "closed"
|
|
347
|
+
|| previous.record.generation !== binding.generation)) {
|
|
365
348
|
throw new Error("the completed hook owner changed during turn registration");
|
|
366
349
|
}
|
|
367
350
|
}
|
|
@@ -432,6 +415,11 @@ const HANDLERS = {
|
|
|
432
415
|
await context.service.clearDeliveryBinding(owner);
|
|
433
416
|
}
|
|
434
417
|
await clearSessionBinding({ runtimeDir: paths.root, deadlineAt: deadline, harnessSessionId: event.sessionId });
|
|
418
|
+
const diagnosticDeadline = nativeDiagnosticDeadline(deadline);
|
|
419
|
+
if (diagnosticDeadline !== null) {
|
|
420
|
+
await clearNativeAttempt({ runtimeDir: paths.root, harnessSessionId: event.sessionId,
|
|
421
|
+
...binding, deadlineAt: diagnosticDeadline });
|
|
422
|
+
}
|
|
435
423
|
return {};
|
|
436
424
|
},
|
|
437
425
|
|
|
@@ -450,9 +438,11 @@ const HANDLERS = {
|
|
|
450
438
|
return { ...turn, nativeBinding: started.nativeBinding };
|
|
451
439
|
}
|
|
452
440
|
const current = await context.service.locateSession(binding.accSessionId);
|
|
453
|
-
if (current
|
|
454
|
-
|
|
455
|
-
//
|
|
441
|
+
if (current === null
|
|
442
|
+
|| current.record.state === "closed" && current.record.generation === binding.generation) {
|
|
443
|
+
// A solo detach removes its ephemeral record; a durable close retains it.
|
|
444
|
+
// Both end the ACC incarnation while the native conversation may continue.
|
|
445
|
+
// Only a genuine user turn can start another one; tool hooks cannot. Reuse
|
|
456
446
|
// the crash-safe opening path, retaining its full published client facts
|
|
457
447
|
// and its single native handshake rather than binding a second time.
|
|
458
448
|
const started = await HANDLERS.sessionStart(input);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Shared presentation of observed delivery facts. Vendor probes provide closed
|
|
2
|
+
// reasons; installation and doctor must not turn a version floor into readiness.
|
|
3
|
+
const REASONS = Object.freeze({
|
|
4
|
+
native_delivery_unsupported: "this adapter has no native delivery channel",
|
|
5
|
+
platform_not_captured: "native delivery is not verified on this platform",
|
|
6
|
+
version_unavailable: "the client version could not be verified",
|
|
7
|
+
prerelease_not_captured: "this client version is not a verified stable release",
|
|
8
|
+
below_minimum_version: "the client is below the native delivery minimum version",
|
|
9
|
+
known_bad_version: "this client version has a known native delivery failure",
|
|
10
|
+
native_endpoint_unavailable: "the client's local delivery service is unavailable",
|
|
11
|
+
native_session_unavailable: "the local delivery service has no loaded client session",
|
|
12
|
+
feature_probe_failed: "the native protocol probe did not succeed",
|
|
13
|
+
probe_timeout: "the native protocol probe timed out",
|
|
14
|
+
probe_version_mismatch: "the local service and CLI versions differ",
|
|
15
|
+
protocol_mismatch: "the local service did not confirm the required protocol",
|
|
16
|
+
unsupported_shell: "automatic launch setup requires zsh",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function describeNativeReason(reason, { clientVersion, minimumVersion } = {}) {
|
|
20
|
+
if (reason === "below_minimum_version" && clientVersion && minimumVersion) {
|
|
21
|
+
return `client ${clientVersion} needs version ${minimumVersion} or newer for native delivery`;
|
|
22
|
+
}
|
|
23
|
+
return REASONS[reason] ?? reason ?? "readiness is unverified";
|
|
24
|
+
}
|
|
25
|
+
export const describeDeliveryFallback = entry => entry.capabilities?.delivery?.nextTurn === true
|
|
26
|
+
? "next-turn hooks (when enabled) or acc inbox" : "acc inbox";
|
|
27
|
+
|
|
28
|
+
export function describeInstallDelivery(entry, policy, effectivePolicy) {
|
|
29
|
+
const state = policy === "off" ? "off" : effectivePolicy === "off"
|
|
30
|
+
? `consent saved (${policy}); not active` : `enabled (${policy}); waiting for a verified session`;
|
|
31
|
+
const reason = entry.nativeDelivery?.reasonCode;
|
|
32
|
+
return `${entry.displayName ?? entry.adapterId} live delivery: ${state}`
|
|
33
|
+
+ (reason ? `; ${describeNativeReason(reason, { clientVersion: entry.version,
|
|
34
|
+
minimumVersion: entry.nativeDelivery?.eligibility?.minimumVersion })}` : "")
|
|
35
|
+
+ `; fallback: ${describeDeliveryFallback(entry)}`;
|
|
36
|
+
}
|