agents-can-communicate 0.5.10 → 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.
Files changed (63) hide show
  1. package/README.md +2 -1
  2. package/bin/acc-antigravity-relay.mjs +7 -0
  3. package/bin/entrypoints/acc-antigravity-relay.mjs +116 -0
  4. package/bin/entrypoints/acc-bootstrap.mjs +4 -0
  5. package/bin/entrypoints/acc-hook.mjs +11 -7
  6. package/bin/entrypoints/antigravity-relay-binding.mjs +43 -0
  7. package/docs/ADAPTER_AUTHORING.md +33 -0
  8. package/docs/ARCHITECTURE.md +5 -3
  9. package/docs/CAPABILITIES.md +22 -16
  10. package/docs/CONFIGURATION.md +1 -0
  11. package/docs/GETTING_STARTED.md +10 -4
  12. package/docs/HOW_IT_WORKS.md +7 -1
  13. package/docs/TROUBLESHOOTING.md +72 -0
  14. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +91 -0
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/PreInvocation-1.2.7.json +11 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/SessionStart-1.2.7.json +9 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-error-answers-1.2.7.json +19 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/agentapi-live-push-1.2.7.json +31 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +217 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product-evidence.json +132 -0
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-cli-1.2.7-relay-product.json +29 -0
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/hooks-first-trust-no-workspace-1.2.7.json +37 -0
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/live-push-surfaces-1.2.7.json +38 -0
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +28 -0
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +5 -0
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +329 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +119 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +98 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/hooks.mjs +201 -0
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +708 -0
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +170 -0
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +116 -0
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +123 -0
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +195 -0
  35. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +64 -0
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  38. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  39. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  40. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  41. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  42. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +37 -1
  44. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +25 -5
  45. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
  46. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -1
  47. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +1 -1
  48. package/node_modules/@agents-can-communicate/cli/package.json +1 -1
  49. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +17 -3
  50. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +2 -2
  51. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +1 -0
  52. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  53. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  54. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  55. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +31 -0
  56. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +59 -9
  57. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  58. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +4 -0
  59. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +10 -0
  60. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  62. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  63. package/package.json +3 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-claude-code",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-codex",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "description": "Coordinate this Gemini CLI session with other AI agent sessions working in the same workspace.",
5
5
  "contextFileName": "skills/acc/SKILL.md"
6
6
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-gemini-cli",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-grok",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -3,6 +3,7 @@ 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";
6
7
 
7
8
  // The capability surface, documented in docs/ADAPTER_AUTHORING.md and measured
8
9
  // per client in docs/CAPABILITIES.md. False is the default for every
@@ -115,8 +116,13 @@ export function defineAdapter(manifest) {
115
116
  { evidenceClient: item.client, client });
116
117
  }
117
118
  }
119
+ const floor = {};
120
+ if (manifest.certificationFloor !== undefined) {
121
+ floor.certificationFloor = validateCertificationFloor(manifest.certificationFloor,
122
+ { certification, client: manifest.client.certificationName ?? manifest.client.command });
123
+ }
118
124
  const native = {};
119
- for (const method of ["refreshNativeSession", "retireNativeSession"]) {
125
+ for (const method of ["refreshNativeSession", "retireNativeSession", "nativeActivationHint"]) {
120
126
  if (manifest[method] !== undefined && typeof manifest[method] !== "function") {
121
127
  usage(`optional native method ${method} must be a function`, { id: manifest.id, method });
122
128
  }
@@ -134,7 +140,37 @@ export function defineAdapter(manifest) {
134
140
  return Object.freeze({
135
141
  ...manifest,
136
142
  certification,
143
+ ...floor,
137
144
  ...native,
138
145
  capabilities: assertCapabilities(manifest.capabilities, manifest, certification),
139
146
  });
140
147
  }
148
+
149
+ const FLOOR_PLATFORM = /^(?:darwin|linux|win32)-(?:arm64|x64)$/;
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,6 +2,8 @@ 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
+
5
7
  export const CAPABILITY_SHAPE = Object.freeze({
6
8
  lifecycle: Object.freeze(["sessionStart", "sessionResume", "sessionEnd", "heartbeat",
7
9
  "childSessions"]),
@@ -132,6 +134,16 @@ function falseCapabilities() {
132
134
  Object.fromEntries(names.map(name => [name, false]))]));
133
135
  }
134
136
 
137
+ /**
138
+ * What a client is certified for, judged against the exact version and
139
+ * platform it reports.
140
+ *
141
+ * A version's own captures decide, capability by capability, including a
142
+ * capture that failed. An adapter may also declare a certification floor per
143
+ * platform: a stable version at or above it, with no capture of its own for a
144
+ * capability, is judged by the floor version's evidence for that capability.
145
+ * Versions below the floor, prereleases and other platforms stay uncertified.
146
+ */
135
147
  export function effectiveCapabilities(adapter, { clientVersion, platform } = {}) {
136
148
  const resolved = falseCapabilities();
137
149
  if (typeof clientVersion !== "string" || !VERSION.test(clientVersion)
@@ -139,14 +151,22 @@ export function effectiveCapabilities(adapter, { clientVersion, platform } = {})
139
151
  || typeof platform !== "string" || !PLATFORM.test(platform)
140
152
  || platform.toLowerCase() === "unknown") return freezeCapabilities(resolved);
141
153
  const client = adapter.client?.certificationName ?? adapter.client?.command;
142
- const passing = new Set((adapter.certification?.evidence ?? [])
143
- .filter(item => item.result === "pass" && item.client === client
144
- && item.version === clientVersion && item.platform === platform)
145
- .map(item => item.capability));
154
+ const rows = (adapter.certification?.evidence ?? [])
155
+ .filter(item => item.client === client && item.platform === platform);
156
+ const floor = adapter.certificationFloor?.[platform];
157
+ const floored = typeof floor === "string" && parseStableVersion(floor) !== null
158
+ && parseStableVersion(clientVersion) !== null
159
+ && compareStableVersions(clientVersion, floor) >= 0;
160
+ const certified = capability => {
161
+ const own = rows.filter(item => item.version === clientVersion && item.capability === capability);
162
+ const judged = own.length > 0 || !floored ? own
163
+ : rows.filter(item => item.version === floor && item.capability === capability);
164
+ return judged.some(item => item.result === "pass");
165
+ };
146
166
  for (const [group, names] of Object.entries(CAPABILITY_SHAPE)) {
147
167
  for (const name of names) {
148
168
  resolved[group][name] = adapter.capabilities?.[group]?.[name] === true
149
- && passing.has(`${group}.${name}`);
169
+ && certified(`${group}.${name}`);
150
170
  }
151
171
  }
152
172
  return freezeCapabilities(resolved);
@@ -60,6 +60,7 @@ const ownBinary = name => {
60
60
  export const defaultRunner = () => ownBinary("acc-hook.mjs");
61
61
  export const defaultBootstrap = () => ownBinary("acc-bootstrap.mjs");
62
62
  export const defaultChannel = () => ownBinary("acc-claude-channel.mjs");
63
+ export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
63
64
 
64
65
  export const runnerExists = async runner => {
65
66
  try {
@@ -7,7 +7,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_PLATFORMS, NATIVE
7
7
  validateNativeActivationPlan, validateNativeDeliveryContract, validateNativeHandshake }
8
8
  from "./native-delivery.mjs";
9
9
  export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
10
- export { assertRunner, bakeSkillCommand, defaultBootstrap, defaultChannel, defaultCli,
10
+ export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultBootstrap, defaultChannel, defaultCli,
11
11
  defaultRunner, removeInstalledTree, runnerExists, writeCliShim, writeHookShim }
12
12
  from "./hook-shim.mjs";
13
13
  export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
@@ -11,7 +11,7 @@ import { NATIVE_REASON_CODES, TIMESTAMP } from "./native-vocabulary.mjs";
11
11
  export function nativeAttemptFrom(value) {
12
12
  if (!value || typeof value.at !== "string" || !TIMESTAMP.test(value.at)
13
13
  || !Number.isFinite(Date.parse(value.at))
14
- || !["sessionStart", "beforeTurn", "channelReady"].includes(value.event)
14
+ || !["sessionStart", "beforeTurn", "channelReady", "relayReady"].includes(value.event)
15
15
  || !["active", "off", "degraded", "unsupported"].includes(value.state)
16
16
  || !(value.reasonCode === null || NATIVE_REASON_CODES.includes(value.reasonCode))
17
17
  || !["off", "actionable", "all"].includes(value.policy)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/cli",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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, { shell = null, env = {}, dataHome } = {}) => ({
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 = () => [createClaudeCodeAdapter(), createCodexAdapter(),
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.
@@ -7,7 +7,7 @@ import { canonicalManagerRoot, readControl, readManagedJson } from "./state.mjs"
7
7
  import { commandPrefix } from "./command-prefix.mjs";
8
8
 
9
9
  export const ENTRY_KINDS = Object.freeze([
10
- "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel",
10
+ "acc", "acc-hook", "acc-mcp", "acc-bootstrap", "acc-claude-channel", "acc-antigravity-relay",
11
11
  ]);
12
12
 
13
13
  const BOOTSTRAP_FLAGS = new Map([["--adapter", "adapter"], ["--real-executable", "realExecutable"],
@@ -43,7 +43,7 @@ export function invokedDirectly(url) {
43
43
 
44
44
  function unavailable(kind) {
45
45
  // Never reflect control file content or payloads into a client diagnostic.
46
- if (kind === "acc-hook" || kind === "acc-claude-channel") {
46
+ if (kind === "acc-hook" || kind === "acc-claude-channel" || kind === "acc-antigravity-relay") {
47
47
  process.stderr.write("acc: coordination unavailable during runtime update; session may continue\n");
48
48
  process.exitCode = 0;
49
49
  } else if (kind === "acc-bootstrap") {
@@ -16,6 +16,7 @@ export const stablePaths = root => ({
16
16
  cli: path.join(root, "bin", "acc.mjs"), runner: path.join(root, "bin", "acc-hook.mjs"),
17
17
  bootstrap: path.join(root, "bin", "acc-bootstrap.mjs"),
18
18
  channel: path.join(root, "bin", "acc-claude-channel.mjs"),
19
+ antigravityRelay: path.join(root, "bin", "acc-antigravity-relay.mjs"),
19
20
  });
20
21
 
21
22
  /** Publish immutable launcher modules, then atomically replace each tiny entry.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/core",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/delivery-router",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/hook-runner",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
+ }
@@ -14,7 +14,7 @@ import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runti
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
20
  import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
@@ -232,7 +232,10 @@ async function openContext({ event, adapterId, dataHome, runtime, env, deadline
232
232
 
233
233
  // What this turn is shown and which complete groups it may commit as
234
234
  // offered. Runs after the heartbeat and the bounded native retry.
235
- async function projectTurn({ binding, context, adapter, adapterId }) {
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 }) {
236
239
  const sync = await context.service.sync({ sessionId: binding.accSessionId,
237
240
  cursor: null, scope: "delta" });
238
241
 
@@ -255,8 +258,14 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
255
258
  // Only this hook's payload selected the binding. Supply its own pair as
256
259
  // trusted context, outside peer bodies, rather than exporting inheritable
257
260
  // credentials or teaching the CLI to guess from a public roster.
258
- const owner = ownerHeader(binding, context.workspaceCwd, context.workspaceRef);
259
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}`;
260
269
  // A peer can join after this prompt has begun. The current turn must already
261
270
  // have its own arguments when it needs inbox/reply, without reattaching or
262
271
  // waiting for another user prompt. Solo emits identity, not a peer notice.
@@ -332,7 +341,7 @@ async function projectTurn({ binding, context, adapter, adapterId }) {
332
341
  // The entry point owns the transport boundary. This handler only prepares
333
342
  // offer inputs; recording them here would claim delivery before stdout's
334
343
  // callback proves that the bytes crossed.
335
- const outcome = { stdout: "", ...adapter.injectOutcome?.(projected) };
344
+ const outcome = { stdout: "", ...render(projected) };
336
345
  const writableOffers = outcome.stdout === "" ? [] : offerInputs;
337
346
  return { ...outcome,
338
347
  stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
@@ -485,7 +494,9 @@ const HANDLERS = {
485
494
  const started = await HANDLERS.sessionStart(input);
486
495
  const fresh = await loadSessionBinding({ runtimeDir: paths.root,
487
496
  harnessSessionId: event.sessionId });
488
- const turn = await projectTurn({ ...input, binding: fresh });
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 });
489
500
  return { ...turn, nativeBinding: started.nativeBinding };
490
501
  }
491
502
  const current = await context.service.locateSession(binding.accSessionId);
@@ -499,7 +510,9 @@ const HANDLERS = {
499
510
  const started = await HANDLERS.sessionStart(input);
500
511
  const fresh = await loadSessionBinding({ runtimeDir: paths.root,
501
512
  harnessSessionId: event.sessionId });
502
- const turn = await projectTurn({ ...input, binding: fresh });
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 });
503
516
  return { ...turn, nativeBinding: started.nativeBinding };
504
517
  }
505
518
  // A turn is the clearest sign a session is alive. Never a reason to fail:
@@ -511,10 +524,38 @@ const HANDLERS = {
511
524
  const nativeBinding = await bindNative({ adapter, event, hookBinding: binding,
512
525
  clientVersion: binding.clientVersion, platform: binding.platform, context, paths,
513
526
  deadline });
514
- const turn = await projectTurn(input);
527
+ const activationHint = await nativeActivationHintFor({ adapter, event, nativeBinding,
528
+ binding, context, paths, deadline });
529
+ const turn = await projectTurn({ ...input, activationHint });
515
530
  return nativeBinding === undefined ? turn : { ...turn, nativeBinding };
516
531
  },
517
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
+
518
559
  async beforeTool({ binding, context, event, adapter }) {
519
560
  if (binding === null) return { decision: "allow" };
520
561
  if (event.targets.length === 0) {
@@ -582,6 +623,13 @@ const HANDLERS = {
582
623
  * only thing this function refuses to do is fail closed.
583
624
  */
584
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 = [],
585
633
  runtime = defaultRuntime(), budgetMs = DEFAULT_BUDGET_MS,
586
634
  readProcessTable = defaultReadProcessTable,
587
635
  probeClientVersion = defaultProbeClientVersion,
@@ -594,7 +642,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
594
642
  const adapter = adapters?.[adapterId];
595
643
  if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
596
644
 
597
- const event = await adapter.normalizeHook(payload);
645
+ const event = await adapter.normalizeHook(payload, { args });
598
646
  const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
599
647
  const handler = HANDLERS[event.kind];
600
648
  const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
@@ -610,8 +658,10 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
610
658
  const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
611
659
  harnessSessionId: event.sessionId });
612
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.
613
663
  const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
614
- binding, paths: context.paths,
664
+ binding, paths: context.paths, payload,
615
665
  readProcessTable, probeClientVersion, platform, deadline });
616
666
  return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
617
667
  { event, binding, context, adapter });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.5.10",
3
+ "version": "0.6.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agents-can-communicate",
3
- "version": "0.5.10",
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",