agents-can-communicate 0.5.9 → 0.6.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 +2 -1
- package/bin/acc-antigravity-relay.mjs +7 -0
- package/bin/entrypoints/acc-antigravity-relay.mjs +116 -0
- package/bin/entrypoints/acc-bootstrap.mjs +4 -0
- package/bin/entrypoints/acc-hook.mjs +11 -7
- package/bin/entrypoints/antigravity-relay-binding.mjs +43 -0
- package/docs/ADAPTER_AUTHORING.md +33 -0
- package/docs/ARCHITECTURE.md +27 -4
- package/docs/CAPABILITIES.md +22 -16
- package/docs/CLI.md +21 -3
- package/docs/CONCEPTS.md +7 -0
- package/docs/CONFIGURATION.md +1 -0
- package/docs/GETTING_STARTED.md +10 -4
- package/docs/HOW_IT_WORKS.md +7 -1
- package/docs/TROUBLESHOOTING.md +99 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +91 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/PreInvocation-1.2.7.json +11 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/SessionStart-1.2.7.json +9 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-error-answers-1.2.7.json +19 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-live-push-1.2.7.json +31 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +217 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product-evidence.json +132 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product.json +29 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/hooks-first-trust-no-workspace-1.2.7.json +37 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/live-push-surfaces-1.2.7.json +38 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +28 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +5 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +329 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +119 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +98 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/hooks.mjs +201 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +708 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +170 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +116 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +123 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +195 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +64 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +2 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/hooks.mjs +5 -0
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +14 -2
- 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 +14 -2
- 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-grok/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +14 -2
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +37 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +25 -5
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +6 -2
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +1 -1
- package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +118 -0
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +17 -3
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/command-prefix.mjs +26 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +6 -4
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +3 -1
- package/node_modules/@agents-can-communicate/cli/src/session-owner.mjs +27 -6
- 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 +31 -0
- package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +16 -3
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +70 -17
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/detect.mjs +4 -0
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -0
- 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 +3 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { createAntigravityAdapter } from "@agents-can-communicate/adapter-antigravity";
|
|
4
5
|
import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-code";
|
|
5
6
|
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
6
7
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
@@ -27,7 +28,8 @@ export { decideDelivery } from "./install-delivery-consent.mjs";
|
|
|
27
28
|
// Each client keeps its own directory under the user's home, and an adapter
|
|
28
29
|
// pointed at the home itself writes beside them rather than inside them. That
|
|
29
30
|
// install reports success and the client never reads a byte of it.
|
|
30
|
-
export const clientContext = (home, stateRoot,
|
|
31
|
+
export const clientContext = (home, stateRoot,
|
|
32
|
+
{ shell = null, env = {}, dataHome, cwd = process.cwd() } = {}) => ({
|
|
31
33
|
home,
|
|
32
34
|
...(dataHome === undefined ? {} : { dataHome }),
|
|
33
35
|
configDir: path.join(home, ".claude"),
|
|
@@ -42,6 +44,18 @@ export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHom
|
|
|
42
44
|
// shim would exec. Detection reads them; nothing here writes.
|
|
43
45
|
shell,
|
|
44
46
|
env,
|
|
47
|
+
// Antigravity CLI keeps its hooks in the `~/.gemini` tree Gemini CLI also
|
|
48
|
+
// uses, and reads none of the same files. Two locations load - the
|
|
49
|
+
// machine-wide `~/.gemini/config/hooks.json` and a project's own
|
|
50
|
+
// `.agents/hooks.json`, the second only while that project is an open
|
|
51
|
+
// workspace. Global is the default (issue #178), because a workspace
|
|
52
|
+
// registration in a folder nobody opened as one registers nothing and says
|
|
53
|
+
// nothing. ACC_ANTIGRAVITY_HOOKS is carried through exactly as it was set,
|
|
54
|
+
// wrong values included, so the adapter can refuse one by name rather than
|
|
55
|
+
// quietly installing somewhere else.
|
|
56
|
+
antigravityWorkspace: cwd,
|
|
57
|
+
antigravityHookLocation: typeof env.ACC_ANTIGRAVITY_HOOKS === "string"
|
|
58
|
+
&& env.ACC_ANTIGRAVITY_HOOKS !== "" ? env.ACC_ANTIGRAVITY_HOOKS : "global",
|
|
45
59
|
// Where ACC keeps its own state, for the client that has to be told. Codex
|
|
46
60
|
// sandboxes the commands a model runs to the workspace, and ACC's state is
|
|
47
61
|
// outside every workspace on purpose - so an agent there could read the
|
|
@@ -50,8 +64,8 @@ export const clientContext = (home, stateRoot, { shell = null, env = {}, dataHom
|
|
|
50
64
|
stateRoot,
|
|
51
65
|
});
|
|
52
66
|
|
|
53
|
-
export const ALL_ADAPTERS = () => [
|
|
54
|
-
createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
|
|
67
|
+
export const ALL_ADAPTERS = () => [createAntigravityAdapter(), createClaudeCodeAdapter(),
|
|
68
|
+
createCodexAdapter(), createGeminiCliAdapter(), createGrokAdapter(), createKimiAdapter()];
|
|
55
69
|
|
|
56
70
|
/**
|
|
57
71
|
* How long to wait for a client to say its version.
|
|
@@ -24,6 +24,7 @@ import { canonicalClaim } from "./claim-spelling.mjs";
|
|
|
24
24
|
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
25
25
|
import { resolveOwner } from "./session-owner.mjs";
|
|
26
26
|
import { discoverWorkspace } from "./workspace-discovery.mjs";
|
|
27
|
+
import { resolveSavedHookWorkspace } from "./hook-workspace.mjs";
|
|
27
28
|
|
|
28
29
|
const DEFAULT_CADENCE_MS = 30_000;
|
|
29
30
|
|
|
@@ -68,14 +69,17 @@ async function claimOn(options, context) {
|
|
|
68
69
|
* looking is safe.
|
|
69
70
|
*/
|
|
70
71
|
async function locateContext(options, runtime) {
|
|
71
|
-
const
|
|
72
|
+
const dataHome = runtime.dataHome ?? platformDataHome({ platform: runtime.platform,
|
|
73
|
+
env: runtime.env });
|
|
74
|
+
const discovery = {
|
|
72
75
|
cwd: options.cwd ?? runtime.cwd,
|
|
73
76
|
env: runtime.env,
|
|
74
77
|
gitProbe: runtime.gitProbe ?? createGitProbe(),
|
|
75
78
|
explicitConfig: options.workspace,
|
|
76
|
-
}
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
+
};
|
|
80
|
+
const saved = await resolveSavedHookWorkspace({ ...discovery, dataHome,
|
|
81
|
+
reference: options.workspace });
|
|
82
|
+
const descriptor = saved?.descriptor ?? await discoverWorkspace(discovery);
|
|
79
83
|
const paths = runtimePaths({
|
|
80
84
|
dataHome,
|
|
81
85
|
workspaceId: descriptor.id,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Shared with CLI parsing, but dependency-free: immutable launchers cannot load
|
|
2
|
+
// workspace modules before runtime admission. Consume values, never search for
|
|
3
|
+
// command words that might appear inside a path or a message.
|
|
4
|
+
export const GLOBAL_OPTIONS = Object.freeze(["json", "workspace", "cwd"]);
|
|
5
|
+
|
|
6
|
+
export function commandPrefix(argv) {
|
|
7
|
+
const leading = [];
|
|
8
|
+
let offset = 0;
|
|
9
|
+
while (offset < argv.length) {
|
|
10
|
+
const token = argv[offset];
|
|
11
|
+
const name = token.startsWith("--") ? token.slice(2).split("=", 1)[0] : "";
|
|
12
|
+
if (!GLOBAL_OPTIONS.includes(name)) break;
|
|
13
|
+
leading.push(token);
|
|
14
|
+
offset += 1;
|
|
15
|
+
if (name !== "json" && !token.includes("=")) {
|
|
16
|
+
const value = argv[offset];
|
|
17
|
+
if (value === undefined || value.startsWith("--")
|
|
18
|
+
&& GLOBAL_OPTIONS.includes(value.slice(2).split("=", 1)[0])) {
|
|
19
|
+
return { error: `option --${name} requires a value` };
|
|
20
|
+
}
|
|
21
|
+
leading.push(value);
|
|
22
|
+
offset += 1;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { command: argv[offset], leading, rest: argv.slice(offset + 1) };
|
|
26
|
+
}
|
|
@@ -4,9 +4,10 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
4
4
|
import { scheduleWorker } from "./schedule.mjs";
|
|
5
5
|
import { acquireRuntime } from "./leases.mjs";
|
|
6
6
|
import { canonicalManagerRoot, readControl, readManagedJson } from "./state.mjs";
|
|
7
|
+
import { commandPrefix } from "./command-prefix.mjs";
|
|
7
8
|
|
|
8
9
|
export const ENTRY_KINDS = Object.freeze([
|
|
9
|
-
"acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
|
|
10
|
+
"acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel", "acc-antigravity-relay",
|
|
10
11
|
]);
|
|
11
12
|
|
|
12
13
|
const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
|
|
@@ -42,7 +43,7 @@ export function invokedDirectly(url) {
|
|
|
42
43
|
|
|
43
44
|
function unavailable(kind) {
|
|
44
45
|
// Never reflect control file content or payloads into a client diagnostic.
|
|
45
|
-
if (kind === "acc-hook" || kind === "acc-claude-channel") {
|
|
46
|
+
if (kind === "acc-hook" || kind === "acc-claude-channel" || kind === "acc-antigravity-relay") {
|
|
46
47
|
process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
|
|
47
48
|
process.exitCode = 0;
|
|
48
49
|
} else if (kind === "acc-bootstrap") {
|
|
@@ -81,6 +82,7 @@ async function updateImplementation(packageRoot, control) {
|
|
|
81
82
|
/** The lease lasts until OS process death, including callbacks after main returns. */
|
|
82
83
|
export async function runEntry({ kind, packageRoot, managerRoot, managedRequired = false }) {
|
|
83
84
|
if (!ENTRY_KINDS.includes(kind)) throw new Error("unknown ACC entry point");
|
|
85
|
+
const command = kind === "acc" ? commandPrefix(process.argv.slice(2)).command : null;
|
|
84
86
|
const bootstrapOptions = kind === "acc-bootstrap" ? parseBootstrapOptions(process.argv.slice(2)) : null;
|
|
85
87
|
if (kind === "acc-bootstrap" && bootstrapOptions === null) {
|
|
86
88
|
if (process.env.ACC_BOOTSTRAP_DEBUG === "1") {
|
|
@@ -107,7 +109,7 @@ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired
|
|
|
107
109
|
if (control !== null) {
|
|
108
110
|
selected = control.active.root;
|
|
109
111
|
managed = root;
|
|
110
|
-
const update = kind === "acc" && ["update", "doctor"].includes(
|
|
112
|
+
const update = kind === "acc" && ["update", "doctor"].includes(command)
|
|
111
113
|
? await updateImplementation(packageRoot, control) : null;
|
|
112
114
|
if (update !== null) {
|
|
113
115
|
selected = update;
|
|
@@ -116,7 +118,7 @@ export async function runEntry({ kind, packageRoot, managerRoot, managedRequired
|
|
|
116
118
|
const lease = await acquireRuntime(root, { pid: process.pid, kind });
|
|
117
119
|
selected = lease.runtime.root;
|
|
118
120
|
const quiet = kind === "acc" && ["update", "install", "uninstall", "help", "version",
|
|
119
|
-
"--help", "-h", "--version", "-v", "-V"].includes(
|
|
121
|
+
"--help", "-h", "--version", "-v", "-V"].includes(command);
|
|
120
122
|
if (!quiet) await scheduleWorker(root, control);
|
|
121
123
|
}
|
|
122
124
|
} else if (managedRequired) throw new Error("managed runtime is not initialized");
|
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { ENTRY_KINDS } from "./entry.mjs";
|
|
5
5
|
import { managedDirectory, syncDirectory } from "./state.mjs";
|
|
6
6
|
|
|
7
|
-
const MODULES = ["entry.mjs", "
|
|
7
|
+
const MODULES = ["entry.mjs", "command-prefix.mjs", "state.mjs", "generation-files.mjs",
|
|
8
|
+
"mutex.mjs", "leases.mjs", "schedule.mjs", "policy.mjs"];
|
|
8
9
|
async function durableFile(file, bytes, mode = 0o600) {
|
|
9
10
|
const handle = await open(file, "wx", mode);
|
|
10
11
|
try { await handle.writeFile(bytes); await handle.sync(); }
|
|
@@ -15,6 +16,7 @@ export const stablePaths = root => ({
|
|
|
15
16
|
cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
|
|
16
17
|
bootstrap: path.join(root, "bin", "acc-bootstrap.mjs"),
|
|
17
18
|
channel: path.join(root, "bin", "acc-claude-channel.mjs"),
|
|
19
|
+
antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
|
|
18
20
|
});
|
|
19
21
|
|
|
20
22
|
/** Publish immutable launcher modules, then atomically replace each tiny entry.
|
|
@@ -15,6 +15,23 @@ const unresolved = command => new AccError(EXIT.USAGE,
|
|
|
15
15
|
+ "session IDs and a shared checkout do not establish CLI ownership.",
|
|
16
16
|
{ command, reasonCode: "caller_identity_unresolved" });
|
|
17
17
|
|
|
18
|
+
async function observationOwner(options, context) {
|
|
19
|
+
if (options.session === undefined) return null;
|
|
20
|
+
const existing = await context.service.locateSession(options.session);
|
|
21
|
+
if (existing === null) {
|
|
22
|
+
throw new AccError(EXIT.CONFLICT,
|
|
23
|
+
"the selected session is absent from this workspace; use --cwd from the ACC hook header "
|
|
24
|
+
+ "or the directory where your manual session attached. Do not attach a replacement to recover context.",
|
|
25
|
+
{ reasonCode: "caller_workspace_mismatch", sessionId: options.session,
|
|
26
|
+
workspaceId: context.descriptor.id });
|
|
27
|
+
}
|
|
28
|
+
if (options.generation !== undefined && existing.record.generation !== options.generation) {
|
|
29
|
+
throw new AccError(EXIT.CONFLICT, "cannot observe as a replaced session generation",
|
|
30
|
+
{ reasonCode: "caller_generation_mismatch", sessionId: options.session });
|
|
31
|
+
}
|
|
32
|
+
return existing.record;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
/**
|
|
19
36
|
* Accept caller-supplied credentials; never discover credentials from peers.
|
|
20
37
|
*
|
|
@@ -30,22 +47,26 @@ export async function resolveOwner({ command, options, context, env = {} }) {
|
|
|
30
47
|
const explicit = options.session !== undefined && options.generation !== undefined;
|
|
31
48
|
const configured = typeof env.ACC_SESSION === "string" && typeof env.ACC_GENERATION === "string";
|
|
32
49
|
if (!explicit && !configured) {
|
|
33
|
-
if (soft)
|
|
50
|
+
if (soft) {
|
|
51
|
+
await observationOwner(options, context);
|
|
52
|
+
return options;
|
|
53
|
+
}
|
|
34
54
|
throw unresolved(command);
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
const owner = explicit ? options : { session: env.ACC_SESSION, generation: env.ACC_GENERATION };
|
|
38
58
|
if (options.session !== undefined && options.session !== owner.session) {
|
|
39
59
|
// A public observation scope is allowed; it does not request credentials.
|
|
40
|
-
if (soft)
|
|
60
|
+
if (soft) {
|
|
61
|
+
await observationOwner(options, context);
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
41
64
|
throw unresolved(command);
|
|
42
65
|
}
|
|
43
66
|
// A supplied generation must reach the core unchanged so stale calls fail.
|
|
44
67
|
const resolved = { ...options, session: owner.session,
|
|
45
68
|
generation: options.generation ?? owner.generation };
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
resolved.participant = participants.find(item => item.sessionId === owner.session)?.participantId;
|
|
49
|
-
}
|
|
69
|
+
const observed = soft ? await observationOwner(resolved, context) : null;
|
|
70
|
+
if (command === "status" && options.participant === undefined) resolved.participant = observed.participantId;
|
|
50
71
|
return resolved;
|
|
51
72
|
}
|
|
@@ -41,3 +41,34 @@ export async function bindNative({ adapter, event, hookBinding, clientVersion, p
|
|
|
41
41
|
}
|
|
42
42
|
return result;
|
|
43
43
|
}
|
|
44
|
+
|
|
45
|
+
const HINT_MAX_BYTES = 512;
|
|
46
|
+
const HINT_MAX_MS = 250;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* One line an adapter asks the agent to act on, when the agent itself can fix
|
|
50
|
+
* a degraded native binding - Antigravity's relay is started from the agent's
|
|
51
|
+
* own shell, and nothing else can start it. Bounded and fail-open: a slow or
|
|
52
|
+
* failing adapter costs the turn nothing, and anything but one short line is
|
|
53
|
+
* dropped rather than trusted into model context.
|
|
54
|
+
*/
|
|
55
|
+
export async function nativeActivationHintFor({ adapter, event, nativeBinding, binding, context,
|
|
56
|
+
paths, deadline }) {
|
|
57
|
+
if (typeof adapter?.nativeActivationHint !== "function" || nativeBinding?.state !== "degraded") return null;
|
|
58
|
+
const budget = Math.min(HINT_MAX_MS, deadline - Date.now() - HINT_MAX_MS);
|
|
59
|
+
if (budget <= 0) return null;
|
|
60
|
+
let timer = null;
|
|
61
|
+
try {
|
|
62
|
+
const line = await Promise.race([
|
|
63
|
+
Promise.resolve(adapter.nativeActivationHint({ event, nativeBinding, runtimeDir: paths.root,
|
|
64
|
+
clientPid: binding?.clientPid, env: context.env })),
|
|
65
|
+
new Promise(resolve => { timer = setTimeout(resolve, budget, null); }),
|
|
66
|
+
]);
|
|
67
|
+
return typeof line === "string" && line !== "" && !/[\r\n]/.test(line)
|
|
68
|
+
&& Buffer.byteLength(line, "utf8") <= HINT_MAX_BYTES ? line : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
} finally {
|
|
72
|
+
if (timer !== null) clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const shellQuote = value => `'${value.replaceAll("'", "'\\''")}'`;
|
|
4
|
+
|
|
5
|
+
export const ownerHeader = (binding, cwd, workspaceRef) => "ACC CLI (append): --session "
|
|
4
6
|
+ assertPortableId(binding.accSessionId, "sessionId") + " --generation "
|
|
5
|
-
+ assertPortableId(binding.generation, "generation")
|
|
7
|
+
+ assertPortableId(binding.generation, "generation") + " --cwd " + shellQuote(cwd)
|
|
8
|
+
+ (workspaceRef === undefined ? "" : " --workspace " + shellQuote(workspaceRef));
|
|
6
9
|
|
|
7
10
|
export function ownerOnlyOutcome(inject, owner, budgetBytes) {
|
|
8
11
|
if (Buffer.byteLength(owner, "utf8") > budgetBytes) {
|
|
@@ -20,7 +23,7 @@ export async function appendToolOwner(result, { event, binding, context, adapter
|
|
|
20
23
|
const current = await context.service.locateSession(binding.accSessionId, context.descriptor.id);
|
|
21
24
|
if (current?.record.state !== "open" || current.record.generation !== binding.generation) return result;
|
|
22
25
|
|
|
23
|
-
const owner = ownerHeader(binding);
|
|
26
|
+
const owner = ownerHeader(binding, context.workspaceCwd, context.workspaceRef);
|
|
24
27
|
const injected = adapter.injectToolOwnerOutcome({ owner, tool: event.tool });
|
|
25
28
|
if (injected === null) return result;
|
|
26
29
|
const fitted = ownerOnlyOutcome(() => injected, owner,
|
|
@@ -28,3 +31,13 @@ export async function appendToolOwner(result, { event, binding, context, adapter
|
|
|
28
31
|
return { ...result, stdout: fitted.stdout,
|
|
29
32
|
stderr: [result.stderr, fitted.stderr].filter(Boolean).join("\n") };
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
// SessionStart can also be a context reset. Return the binding just resumed by
|
|
36
|
+
// the hook; never require the model to remember it or attach another session.
|
|
37
|
+
export function appendStartOwner(result, { event, context, adapter }) {
|
|
38
|
+
if (event.kind !== "sessionStart" || result.accSessionId === undefined
|
|
39
|
+
|| typeof adapter.injectStartOwnerOutcome !== "function") return result;
|
|
40
|
+
return { ...result, ...ownerOnlyOutcome(text => adapter.injectStartOwnerOutcome(text),
|
|
41
|
+
ownerHeader(result, context.workspaceCwd, context.workspaceRef),
|
|
42
|
+
context.descriptor.policy?.contextBudgetBytes ?? 6_000) };
|
|
43
|
+
}
|
|
@@ -9,15 +9,15 @@ import { clearNativeAttempt, clearSessionBinding, effectiveCapabilities, loadSes
|
|
|
9
9
|
import { createCoordinationService } from "@agents-can-communicate/core";
|
|
10
10
|
import { AccError, createId } from "@agents-can-communicate/protocol";
|
|
11
11
|
import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
|
|
12
|
-
import { clearPin, createGitProbe,
|
|
12
|
+
import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runtimePaths, writePin }
|
|
13
13
|
from "@agents-can-communicate/cli";
|
|
14
14
|
|
|
15
15
|
import { resolveClientPid } from "./client-pid.mjs";
|
|
16
16
|
import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
|
|
17
|
-
import { bindNative, nativeDiagnosticDeadline } from "./native-attempt.mjs";
|
|
17
|
+
import { bindNative, nativeActivationHintFor, nativeDiagnosticDeadline } from "./native-attempt.mjs";
|
|
18
18
|
import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
|
|
19
19
|
import { withSessionLifecycle } from "./session-lifecycle.mjs";
|
|
20
|
-
import { appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
|
|
20
|
+
import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
|
|
21
21
|
|
|
22
22
|
// Kept cohesive above 300 lines because every handler shares one fail-open
|
|
23
23
|
// hook boundary, binding lifecycle, and client-specific outcome contract.
|
|
@@ -211,12 +211,13 @@ async function runtimeFacts(fromUrl) {
|
|
|
211
211
|
// unreachable from there.
|
|
212
212
|
const managerRootFor = dataHome => path.join(dataHome, "acc", "runtime");
|
|
213
213
|
|
|
214
|
-
async function openContext({
|
|
214
|
+
async function openContext({ event, adapterId, dataHome, runtime, env, deadline }) {
|
|
215
215
|
assertHookBudget(deadline);
|
|
216
|
-
const
|
|
216
|
+
const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
|
|
217
|
+
const { descriptor, workspaceCwd, workspaceRef } = await resolveHookWorkspace({ adapterId, event,
|
|
218
|
+
dataHome: resolvedDataHome, env: env ?? {}, clock: runtime.clock, deadlineAt: deadline,
|
|
217
219
|
gitProbe: createGitProbe({ deadlineAt: deadline }) });
|
|
218
220
|
assertHookBudget(deadline);
|
|
219
|
-
const resolvedDataHome = dataHome ?? platformDataHome({ env: env ?? {} });
|
|
220
221
|
const paths = runtimePaths({
|
|
221
222
|
dataHome: resolvedDataHome,
|
|
222
223
|
workspaceId: descriptor.id,
|
|
@@ -224,13 +225,17 @@ async function openContext({ cwd, dataHome, runtime, env, deadline }) {
|
|
|
224
225
|
});
|
|
225
226
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
226
227
|
ids: runtime.ids, workspaceId: descriptor.id, deadlineAt: deadline });
|
|
227
|
-
return { descriptor, paths,
|
|
228
|
+
return { descriptor, paths, workspaceCwd, workspaceRef,
|
|
229
|
+
dataHome: resolvedDataHome, env: env ?? {}, realpath: runtime.realpath ?? realpath,
|
|
228
230
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
// What this turn is shown and which complete groups it may commit as
|
|
232
234
|
// offered. Runs after the heartbeat and the bounded native retry.
|
|
233
|
-
|
|
235
|
+
// `render` is how the projected text reaches this client: before a turn it is
|
|
236
|
+
// the adapter's context envelope, at the end of one it is a continuation.
|
|
237
|
+
async function projectTurn({ binding, context, adapter, adapterId,
|
|
238
|
+
render = text => adapter.injectOutcome?.(text), activationHint = null }) {
|
|
234
239
|
const sync = await context.service.sync({ sessionId: binding.accSessionId,
|
|
235
240
|
cursor: null, scope: "delta" });
|
|
236
241
|
|
|
@@ -253,8 +258,14 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
253
258
|
// Only this hook's payload selected the binding. Supply its own pair as
|
|
254
259
|
// trusted context, outside peer bodies, rather than exporting inheritable
|
|
255
260
|
// credentials or teaching the CLI to guess from a public roster.
|
|
256
|
-
const owner = ownerHeader(binding);
|
|
257
261
|
const totalBudget = context.descriptor.policy?.contextBudgetBytes ?? 6_000;
|
|
262
|
+
// An adapter's one-line ask rides with the owner line, which every projected
|
|
263
|
+
// turn carries - and only when both fit in half the budget, so peer bodies
|
|
264
|
+
// always keep the rest.
|
|
265
|
+
const header = ownerHeader(binding, context.workspaceCwd, context.workspaceRef);
|
|
266
|
+
const owner = activationHint === null
|
|
267
|
+
|| byteLength(header) + 1 + byteLength(activationHint) > totalBudget / 2
|
|
268
|
+
? header : `${header}\n${activationHint}`;
|
|
258
269
|
// A peer can join after this prompt has begun. The current turn must already
|
|
259
270
|
// have its own arguments when it needs inbox/reply, without reattaching or
|
|
260
271
|
// waiting for another user prompt. Solo emits identity, not a peer notice.
|
|
@@ -330,7 +341,7 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
|
|
|
330
341
|
// The entry point owns the transport boundary. This handler only prepares
|
|
331
342
|
// offer inputs; recording them here would claim delivery before stdout's
|
|
332
343
|
// callback proves that the bytes crossed.
|
|
333
|
-
const outcome = { stdout: "", ...
|
|
344
|
+
const outcome = { stdout: "", ...render(projected) };
|
|
334
345
|
const writableOffers = outcome.stdout === "" ? [] : offerInputs;
|
|
335
346
|
return { ...outcome,
|
|
336
347
|
stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
|
|
@@ -483,7 +494,9 @@ const HANDLERS = {
|
|
|
483
494
|
const started = await HANDLERS.sessionStart(input);
|
|
484
495
|
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
485
496
|
harnessSessionId: event.sessionId });
|
|
486
|
-
const
|
|
497
|
+
const activationHint = await nativeActivationHintFor({ adapter, event,
|
|
498
|
+
nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
|
|
499
|
+
const turn = await projectTurn({ ...input, binding: fresh, activationHint });
|
|
487
500
|
return { ...turn, nativeBinding: started.nativeBinding };
|
|
488
501
|
}
|
|
489
502
|
const current = await context.service.locateSession(binding.accSessionId);
|
|
@@ -497,7 +510,9 @@ const HANDLERS = {
|
|
|
497
510
|
const started = await HANDLERS.sessionStart(input);
|
|
498
511
|
const fresh = await loadSessionBinding({ runtimeDir: paths.root,
|
|
499
512
|
harnessSessionId: event.sessionId });
|
|
500
|
-
const
|
|
513
|
+
const activationHint = await nativeActivationHintFor({ adapter, event,
|
|
514
|
+
nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
|
|
515
|
+
const turn = await projectTurn({ ...input, binding: fresh, activationHint });
|
|
501
516
|
return { ...turn, nativeBinding: started.nativeBinding };
|
|
502
517
|
}
|
|
503
518
|
// A turn is the clearest sign a session is alive. Never a reason to fail:
|
|
@@ -509,10 +524,38 @@ const HANDLERS = {
|
|
|
509
524
|
const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
|
|
510
525
|
clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
|
|
511
526
|
deadline });
|
|
512
|
-
const
|
|
527
|
+
const activationHint = await nativeActivationHintFor({ adapter, event, nativeBinding,
|
|
528
|
+
binding, context, paths, deadline });
|
|
529
|
+
const turn = await projectTurn({ ...input, activationHint });
|
|
513
530
|
return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
|
|
514
531
|
},
|
|
515
532
|
|
|
533
|
+
/**
|
|
534
|
+
* The end of a turn, for a client that can hold one open.
|
|
535
|
+
*
|
|
536
|
+
* A peer message that arrives while the model is producing its last answer
|
|
537
|
+
* has no next invocation to ride on, and would otherwise wait for the next
|
|
538
|
+
* user prompt - which can be hours. A client whose end-of-turn hook can
|
|
539
|
+
* continue the turn (`continueTurnOutcome`) gets that message now instead.
|
|
540
|
+
*
|
|
541
|
+
* Only for peer bodies the projector actually offers. Continuing a turn costs
|
|
542
|
+
* the operator a model invocation, so an owner header, an attention count or
|
|
543
|
+
* a degradation notice is never a reason to. The adapter owns the shape and
|
|
544
|
+
* the ceiling, and reads its own counter from the payload handed back to it;
|
|
545
|
+
* when it declines, it prints nothing, no offer is recorded, and the body
|
|
546
|
+
* stays queued for the next invocation. Every failure ends the turn normally.
|
|
547
|
+
*/
|
|
548
|
+
async turnEnd(input) {
|
|
549
|
+
const { binding, context, adapter, payload } = input;
|
|
550
|
+
if (typeof adapter.continueTurnOutcome !== "function" || binding === null) return {};
|
|
551
|
+
const current = await context.service.locateSession(binding.accSessionId);
|
|
552
|
+
if (current === null || current.record.state !== "open"
|
|
553
|
+
|| current.record.generation !== binding.generation) return {};
|
|
554
|
+
const turn = await projectTurn({ ...input,
|
|
555
|
+
render: text => adapter.continueTurnOutcome({ reason: text, payload }) });
|
|
556
|
+
return (turn.offerInputs ?? []).length === 0 ? {} : turn;
|
|
557
|
+
},
|
|
558
|
+
|
|
516
559
|
async beforeTool({ binding, context, event, adapter }) {
|
|
517
560
|
if (binding === null) return { decision: "allow" };
|
|
518
561
|
if (event.targets.length === 0) {
|
|
@@ -580,6 +623,13 @@ const HANDLERS = {
|
|
|
580
623
|
* only thing this function refuses to do is fail closed.
|
|
581
624
|
*/
|
|
582
625
|
export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
626
|
+
// The arguments the client's hook command carried, after the adapter id.
|
|
627
|
+
// Most clients name the event inside the payload and their adapters ignore
|
|
628
|
+
// this; Antigravity CLI sends no `hook_event_name` at all, and two of its
|
|
629
|
+
// four events hand over byte-identical envelopes - so for that client the
|
|
630
|
+
// registered command's own argument is the only thing that knows which hook
|
|
631
|
+
// ran. Passed to every adapter, read by the ones that need it.
|
|
632
|
+
args = [],
|
|
583
633
|
runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
|
|
584
634
|
readProcessTable = defaultReadProcessTable,
|
|
585
635
|
probeClientVersion = defaultProbeClientVersion,
|
|
@@ -592,8 +642,8 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
592
642
|
const adapter = adapters?.[adapterId];
|
|
593
643
|
if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
|
|
594
644
|
|
|
595
|
-
const event = await adapter.normalizeHook(payload);
|
|
596
|
-
const context = await openContext({
|
|
645
|
+
const event = await adapter.normalizeHook(payload, { args });
|
|
646
|
+
const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
|
|
597
647
|
const handler = HANDLERS[event.kind];
|
|
598
648
|
const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
|
|
599
649
|
const invoke = async () => {
|
|
@@ -608,10 +658,13 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
|
|
|
608
658
|
const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
|
|
609
659
|
harnessSessionId: event.sessionId });
|
|
610
660
|
assertHookBudget(deadline);
|
|
661
|
+
// The raw payload goes to a handler only to be handed back to the adapter
|
|
662
|
+
// that produced it; nothing in core reads it.
|
|
611
663
|
const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
|
|
612
|
-
binding, paths: context.paths,
|
|
664
|
+
binding, paths: context.paths, payload,
|
|
613
665
|
readProcessTable, probeClientVersion, platform, deadline });
|
|
614
|
-
return appendToolOwner(result, { event,
|
|
666
|
+
return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
|
|
667
|
+
{ event, binding, context, adapter });
|
|
615
668
|
};
|
|
616
669
|
const work = lifecycle
|
|
617
670
|
? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
|
|
@@ -159,6 +159,10 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
159
159
|
try {
|
|
160
160
|
const detected = await adapter.detect({ ...context, clientVersion: entry.version,
|
|
161
161
|
platform, nativeDelivery: entry.nativeDelivery });
|
|
162
|
+
// An adapter that knows it cannot be installed right now says so here,
|
|
163
|
+
// rather than by throwing out of install and taking every other client
|
|
164
|
+
// on the machine down with it.
|
|
165
|
+
if (detected.blocked) entry.blocked = detected.blocked;
|
|
162
166
|
if (detected.outgoingDelivery) entry.outgoingDelivery = detected.outgoingDelivery;
|
|
163
167
|
if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
|
|
164
168
|
entry.diagnostics = [...(detected.diagnostics ?? [])];
|
|
@@ -78,6 +78,16 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
78
78
|
+ `if it is, wire it with --adapter ${entry.adapterId}` });
|
|
79
79
|
continue;
|
|
80
80
|
}
|
|
81
|
+
// An adapter can refuse itself for a reason that is nobody's fault and that
|
|
82
|
+
// no file on disk shows: Antigravity CLI cannot be wired until somebody
|
|
83
|
+
// picks between a machine-wide and a per-workspace hook registration. It is
|
|
84
|
+
// skipped by name so the rest of the run proceeds - but never when ACC has
|
|
85
|
+
// a record for it, because that record is the only account of what was
|
|
86
|
+
// written and an unremovable install is the worse failure.
|
|
87
|
+
if (entry.blocked !== undefined && recordedById.get(entry.adapterId) === undefined) {
|
|
88
|
+
skipped.push({ adapterId: entry.adapterId, reason: entry.blocked.reason });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
81
91
|
// The version doing the installing is whichever `acc` came first on PATH,
|
|
82
92
|
// and the shim it writes pins that copy's node and runner. So a second ACC
|
|
83
93
|
// on the machine quietly replaces every client's wiring with its own, older
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-can-communicate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"accManagedUpdateProtocol": 2,
|
|
5
5
|
"accStoreVersion": 6,
|
|
6
6
|
"type": "module",
|
|
@@ -70,6 +70,7 @@
|
|
|
70
70
|
"LICENSE"
|
|
71
71
|
],
|
|
72
72
|
"dependencies": {
|
|
73
|
+
"@agents-can-communicate/adapter-antigravity": "*",
|
|
73
74
|
"@agents-can-communicate/adapter-claude-code": "*",
|
|
74
75
|
"@agents-can-communicate/adapter-codex": "*",
|
|
75
76
|
"@agents-can-communicate/adapter-gemini-cli": "*",
|
|
@@ -86,6 +87,7 @@
|
|
|
86
87
|
"@agents-can-communicate/storage-filesystem": "*"
|
|
87
88
|
},
|
|
88
89
|
"bundleDependencies": [
|
|
90
|
+
"@agents-can-communicate/adapter-antigravity",
|
|
89
91
|
"@agents-can-communicate/adapter-claude-code",
|
|
90
92
|
"@agents-can-communicate/adapter-codex",
|
|
91
93
|
"@agents-can-communicate/adapter-gemini-cli",
|