agent-control-plane-core 0.1.0 → 0.2.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/bin/amp-hook.mjs CHANGED
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,10 +1,17 @@
1
1
  {
2
2
  "name": "agent-control-plane-core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Vendor-neutral control-plane contract for coding agents: a normalized ToolCallEvent/Verdict schema with per-agent adapters (Claude Code, Codex) and a conformance suite.",
5
5
  "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/AlexanderMattTurner/agent-control-plane-core.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/AlexanderMattTurner/agent-control-plane-core/issues"
12
+ },
13
+ "homepage": "https://github.com/AlexanderMattTurner/agent-control-plane-core#readme",
6
14
  "license": "MIT",
7
- "packageManager": "pnpm@11.8.0",
8
15
  "engines": {
9
16
  "node": ">=20"
10
17
  },
@@ -34,6 +41,10 @@
34
41
  "./conformance": {
35
42
  "types": "./types/conformance.d.mts",
36
43
  "import": "./src/conformance.mjs"
44
+ },
45
+ "./registry": {
46
+ "types": "./types/registry.d.mts",
47
+ "import": "./src/registry.mjs"
37
48
  }
38
49
  },
39
50
  "files": [
@@ -51,17 +62,6 @@
51
62
  "adapter",
52
63
  "guardrail"
53
64
  ],
54
- "scripts": {
55
- "prepare": "git config core.hooksPath .hooks",
56
- "build": "tsc -p tsconfig.build.json",
57
- "prepublishOnly": "pnpm build",
58
- "test": "node --test test/*.test.mjs",
59
- "test:coverage": "c8 node --test test/*.test.mjs",
60
- "check": "tsc --noEmit",
61
- "lint": "eslint .",
62
- "format": "prettier --write .",
63
- "format:check": "prettier --check ."
64
- },
65
65
  "lint-staged": {
66
66
  "*.{css,scss}": [
67
67
  "prettier --write"
@@ -105,5 +105,14 @@
105
105
  "lint-staged": "^17.0.5",
106
106
  "prettier": "^3.0.0",
107
107
  "typescript": "^5.6.0"
108
+ },
109
+ "scripts": {
110
+ "build": "tsc -p tsconfig.build.json",
111
+ "test": "node --test test/*.test.mjs",
112
+ "test:coverage": "c8 node --test test/*.test.mjs",
113
+ "check": "tsc --noEmit",
114
+ "lint": "eslint .",
115
+ "format": "prettier --write .",
116
+ "format:check": "prettier --check ."
108
117
  }
109
- }
118
+ }
@@ -15,6 +15,11 @@ import {
15
15
  EventKind,
16
16
  Decision,
17
17
  IntegrationMode,
18
+ CallClass,
19
+ CoverageStatus,
20
+ classifyCallClass,
21
+ coverageAllowsVeto,
22
+ canonicalTool,
18
23
  makeEvent,
19
24
  normalizeVerdict,
20
25
  nativeResponse,
@@ -31,6 +36,22 @@ import {
31
36
  export const AGENT = "amp";
32
37
  export const INTEGRATION_MODE = IntegrationMode.EXTERNAL_HOOK;
33
38
 
39
+ /**
40
+ * Hook-coverage matrix row (`docs/hook-coverage-matrix.md`). Every tool call is
41
+ * checked against the permission engine; `amp.mcpPermissions` runs MCP tools
42
+ * through the same rule syntax, and a rule's `context: "subagent"` selector
43
+ * gates calls inside subagents — so builtin, MCP, and subagent are all COVERED.
44
+ * Resumed-thread firing is a strong-but-uncited structural argument, so it is
45
+ * held at UNKNOWN until an item-⑤ probe confirms it.
46
+ */
47
+ /** @type {import("../control-plane.mjs").CoverageMap} */
48
+ export const COVERAGE = Object.freeze({
49
+ [CallClass.BUILTIN]: CoverageStatus.COVERED,
50
+ [CallClass.MCP]: CoverageStatus.COVERED,
51
+ [CallClass.SUBAGENT]: CoverageStatus.COVERED,
52
+ [CallClass.RESUMED]: CoverageStatus.UNKNOWN,
53
+ });
54
+
34
55
  // Amp invokes the delegate for a tool call; the payload carries the tool name +
35
56
  // input and the session context. Pinned by fixtures/amp.json.
36
57
  const CONSUMED = new Set(["tool", "input", "session_id", "cwd"]);
@@ -51,12 +72,17 @@ export function parse(native) {
51
72
  };
52
73
  if (typeof raw.session_id === "string") meta.session_id = raw.session_id;
53
74
  if (typeof raw.cwd === "string") meta.cwd = raw.cwd;
75
+ const nativeTool = asStringOrNull(raw.tool);
76
+ if (nativeTool !== null) meta.native_tool = nativeTool;
54
77
  return makeEvent({
55
78
  event: EventKind.PRE_TOOL,
56
- tool: asStringOrNull(raw.tool),
79
+ tool: canonicalTool(nativeTool),
57
80
  input: asObject(raw.input),
58
81
  response: undefined,
59
- this_call_vetoable: true,
82
+ // Classify on the NATIVE name (MCP detection keys on `mcp__…`).
83
+ this_call_vetoable: coverageAllowsVeto(
84
+ COVERAGE[classifyCallClass(nativeTool, raw)],
85
+ ),
60
86
  meta,
61
87
  });
62
88
  }
@@ -79,4 +105,4 @@ export function render(verdict, event) {
79
105
  }
80
106
 
81
107
  /** @type {import("../control-plane.mjs").Adapter} */
82
- export const ampAdapter = { AGENT, INTEGRATION_MODE, parse, render };
108
+ export const ampAdapter = { AGENT, INTEGRATION_MODE, COVERAGE, parse, render };
@@ -16,6 +16,11 @@ import {
16
16
  EventKind,
17
17
  Decision,
18
18
  IntegrationMode,
19
+ CallClass,
20
+ CoverageStatus,
21
+ classifyCallClass,
22
+ coverageAllowsVeto,
23
+ canonicalTool,
19
24
  makeEvent,
20
25
  normalizeVerdict,
21
26
  nativeResponse,
@@ -36,6 +41,21 @@ export const AGENT = "claude";
36
41
  /** How this adapter attaches to the agent. */
37
42
  export const INTEGRATION_MODE = IntegrationMode.EXTERNAL_HOOK;
38
43
 
44
+ /**
45
+ * Hook-coverage matrix row (`docs/hook-coverage-matrix.md`). `PreToolUse` fires
46
+ * for builtins, for MCP tools (surfaced as `mcp__<server>__<tool>` through the
47
+ * same hook), and for every tool a subagent uses (subagents do not inherit the
48
+ * parent's permissions, so the hook is often their only gate). Resumed sessions
49
+ * re-read hooks from settings and fire per new call — structural, uncontested.
50
+ */
51
+ /** @type {import("../control-plane.mjs").CoverageMap} */
52
+ export const COVERAGE = Object.freeze({
53
+ [CallClass.BUILTIN]: CoverageStatus.COVERED,
54
+ [CallClass.MCP]: CoverageStatus.COVERED,
55
+ [CallClass.SUBAGENT]: CoverageStatus.COVERED,
56
+ [CallClass.RESUMED]: CoverageStatus.COVERED,
57
+ });
58
+
39
59
  /** Claude Code native hook event names (the `hook_event_name` field). */
40
60
  export const HookEvent = Object.freeze({
41
61
  PRE_TOOL_USE: "PreToolUse",
@@ -129,13 +149,20 @@ export function parse(native) {
129
149
  /** @type {Record<string, string>} */ (NATIVE_TO_KIND)[nativeEvent] ??
130
150
  EventKind.UNKNOWN;
131
151
  const response = kind === EventKind.POST_TOOL ? raw.tool_response : undefined;
152
+ const nativeTool = claudeTool(kind, raw);
153
+ const meta = claudeMeta(nativeEvent, raw);
154
+ if (nativeTool !== null) meta.native_tool = nativeTool;
132
155
  return makeEvent({
133
156
  event: kind,
134
- tool: claudeTool(kind, raw),
157
+ tool: canonicalTool(nativeTool),
135
158
  input: claudeInput(kind, raw),
136
159
  response,
137
- this_call_vetoable: true,
138
- meta: claudeMeta(nativeEvent, raw),
160
+ // Classify on the NATIVE name — MCP detection keys on `mcp__…`, which a
161
+ // canonical builtin name would never carry.
162
+ this_call_vetoable: coverageAllowsVeto(
163
+ COVERAGE[classifyCallClass(nativeTool, raw)],
164
+ ),
165
+ meta,
139
166
  });
140
167
  }
141
168
 
@@ -212,6 +239,11 @@ function gatingBody(hookEventName, vd, soleGate) {
212
239
  function nonGatingBody(hookEventName, vd) {
213
240
  /** @type {Record<string, unknown>} */
214
241
  const hookSpecificOutput = { hookEventName };
242
+ // A PostToolUse content transform: `updatedToolOutput` replaces what the model
243
+ // sees (the tool already ran, so this governs only the model's view). The
244
+ // native channel for the whole redaction/sanitize pipeline.
245
+ if (vd.mutated_output !== undefined)
246
+ hookSpecificOutput.updatedToolOutput = vd.mutated_output;
215
247
  if (vd.additional_context !== undefined)
216
248
  hookSpecificOutput.additionalContext = vd.additional_context;
217
249
  /** @type {Record<string, unknown>} */
@@ -224,4 +256,10 @@ function nonGatingBody(hookEventName, vd) {
224
256
  }
225
257
 
226
258
  /** @type {import("../control-plane.mjs").Adapter} */
227
- export const claudeAdapter = { AGENT, INTEGRATION_MODE, parse, render };
259
+ export const claudeAdapter = {
260
+ AGENT,
261
+ INTEGRATION_MODE,
262
+ COVERAGE,
263
+ parse,
264
+ render,
265
+ };
@@ -15,6 +15,11 @@ import {
15
15
  EventKind,
16
16
  Decision,
17
17
  IntegrationMode,
18
+ CallClass,
19
+ CoverageStatus,
20
+ classifyCallClass,
21
+ coverageAllowsVeto,
22
+ canonicalTool,
18
23
  makeEvent,
19
24
  normalizeVerdict,
20
25
  nativeResponse,
@@ -32,6 +37,21 @@ import {
32
37
  export const AGENT = "codex";
33
38
  export const INTEGRATION_MODE = IntegrationMode.EXTERNAL_HOOK;
34
39
 
40
+ /**
41
+ * Hook-coverage matrix row (`docs/hook-coverage-matrix.md`). `PreToolUse`
42
+ * intercepts the shell (Bash) tool ONLY — other builtins and MCP tools never
43
+ * reach it — so builtin is PARTIAL and MCP is UNCOVERED. Subagent and resumed
44
+ * firing are undocumented ⇒ UNKNOWN (treated as uncovered until an item-⑤ probe
45
+ * proves otherwise).
46
+ */
47
+ /** @type {import("../control-plane.mjs").CoverageMap} */
48
+ export const COVERAGE = Object.freeze({
49
+ [CallClass.BUILTIN]: CoverageStatus.PARTIAL,
50
+ [CallClass.MCP]: CoverageStatus.UNCOVERED,
51
+ [CallClass.SUBAGENT]: CoverageStatus.UNKNOWN,
52
+ [CallClass.RESUMED]: CoverageStatus.UNKNOWN,
53
+ });
54
+
35
55
  /** Minimum Codex version whose hook can actually veto a tool call. */
36
56
  export const MIN_ENFORCING_VERSION = Object.freeze([0, 135]);
37
57
 
@@ -96,12 +116,20 @@ export function parse(native) {
96
116
  if (typeof raw.transcript_path === "string")
97
117
  meta.transcript_path = raw.transcript_path;
98
118
 
119
+ // Coverage floor: even on an enforcing Codex, `PreToolUse` fires for Bash only,
120
+ // so an MCP-sourced call is un-vetoable regardless of version (COVERAGE.mcp).
121
+ // Classify on the NATIVE name (MCP detection keys on `mcp__…`).
122
+ const nativeTool = asStringOrNull(raw.tool_name);
123
+ if (nativeTool !== null) meta.native_tool = nativeTool;
124
+ const vetoable =
125
+ enforce && coverageAllowsVeto(COVERAGE[classifyCallClass(nativeTool, raw)]);
126
+
99
127
  return makeEvent({
100
128
  event: kind,
101
- tool: asStringOrNull(raw.tool_name),
129
+ tool: canonicalTool(nativeTool),
102
130
  input: asObject(raw.tool_input),
103
131
  response: undefined,
104
- this_call_vetoable: enforce,
132
+ this_call_vetoable: vetoable,
105
133
  meta,
106
134
  });
107
135
  }
@@ -146,4 +174,10 @@ export function render(verdict, event, { soleGate = false } = {}) {
146
174
  }
147
175
 
148
176
  /** @type {import("../control-plane.mjs").Adapter} */
149
- export const codexAdapter = { AGENT, INTEGRATION_MODE, parse, render };
177
+ export const codexAdapter = {
178
+ AGENT,
179
+ INTEGRATION_MODE,
180
+ COVERAGE,
181
+ parse,
182
+ render,
183
+ };
@@ -23,11 +23,18 @@ import {
23
23
  EventKind,
24
24
  Decision,
25
25
  IntegrationMode,
26
+ CallClass,
27
+ CoverageStatus,
28
+ classifyCallClass,
29
+ coverageAllowsVeto,
30
+ canonicalTool,
31
+ assertAliasTargetsModeled,
26
32
  makeEvent,
27
33
  normalizeVerdict,
28
34
  nativeResponse,
29
35
  collectPassthrough,
30
36
  asObject,
37
+ asString,
31
38
  asStringOrNull,
32
39
  } from "../control-plane.mjs";
33
40
 
@@ -39,15 +46,33 @@ import {
39
46
  export const AGENT = "gemini";
40
47
  export const INTEGRATION_MODE = IntegrationMode.EXTERNAL_HOOK;
41
48
 
49
+ /**
50
+ * Hook-coverage matrix row (`docs/hook-coverage-matrix.md`). `BeforeTool` gates
51
+ * builtins on v0.26+ (COVERED). MCP routing through the same matcher is only
52
+ * MEDIUM-confidence and unproven, subagent firing for a loaded agent is
53
+ * undocumented, and resumed-session behavior has no source — all three are
54
+ * UNKNOWN, held at fail-closed until an item-⑤ probe upgrades them. A guessed ✅
55
+ * here would be a silent fail-open.
56
+ */
57
+ /** @type {import("../control-plane.mjs").CoverageMap} */
58
+ export const COVERAGE = Object.freeze({
59
+ [CallClass.BUILTIN]: CoverageStatus.COVERED,
60
+ [CallClass.MCP]: CoverageStatus.UNKNOWN,
61
+ [CallClass.SUBAGENT]: CoverageStatus.UNKNOWN,
62
+ [CallClass.RESUMED]: CoverageStatus.UNKNOWN,
63
+ });
64
+
42
65
  /** Gemini CLI native hook event names (the `hook_event_name` field). */
43
66
  export const HookEvent = Object.freeze({
44
67
  BEFORE_TOOL: "BeforeTool",
45
68
  AFTER_TOOL: "AfterTool",
69
+ BEFORE_AGENT: "BeforeAgent",
46
70
  });
47
71
 
48
72
  const NATIVE_TO_KIND = Object.freeze({
49
73
  [HookEvent.BEFORE_TOOL]: EventKind.PRE_TOOL,
50
74
  [HookEvent.AFTER_TOOL]: EventKind.POST_TOOL,
75
+ [HookEvent.BEFORE_AGENT]: EventKind.PROMPT_SUBMIT,
51
76
  });
52
77
 
53
78
  // Only the fields the adapter maps are consumed; everything else (timestamp,
@@ -60,6 +85,7 @@ const CONSUMED = new Set([
60
85
  "tool_name",
61
86
  "tool_input",
62
87
  "tool_response",
88
+ "prompt",
63
89
  ]);
64
90
 
65
91
  /**
@@ -83,11 +109,67 @@ function geminiMeta(nativeEvent, raw) {
83
109
  return meta;
84
110
  }
85
111
 
112
+ /**
113
+ * Adapter-scoped native-builtin → canonical tool aliases, applied ONLY when a
114
+ * call classifies as BUILTIN. These names are too generic for the global
115
+ * {@link TOOL_ALIASES} (an MCP server could export a `read_file`), but Gemini
116
+ * CLI removes the ambiguity at parse time: every MCP tool is unconditionally
117
+ * registered — and surfaced in hook payloads — under its fully qualified
118
+ * `mcp_{server}_{tool}` name (gemini-cli docs/tools/mcp-server.md), so a bare
119
+ * builtin name in `tool_name` can only be the builtin. The builtin's native
120
+ * input fields still pass through verbatim (e.g. read_file's `absolute_path`,
121
+ * not Read's `file_path`) — `meta.native_tool` tells a consumer which field
122
+ * dialect to expect. Targets are pinned to {@link MODELED_TOOLS} at import, and
123
+ * every entry must be witnessed by a gemini conformance fixture
124
+ * (`assertToolAliasesCovered`).
125
+ * @type {Readonly<Record<string, string>>}
126
+ */
127
+ export const GEMINI_TOOL_ALIASES = Object.freeze({
128
+ read_file: "Read",
129
+ write_file: "Write",
130
+ web_fetch: "WebFetch",
131
+ });
132
+
133
+ assertAliasTargetsModeled(GEMINI_TOOL_ALIASES);
134
+
135
+ /**
136
+ * Canonicalize a Gemini native tool name: adapter-scoped builtin aliases first
137
+ * (BUILTIN calls only — an MCP- or context-flagged call is never reclassified),
138
+ * then the global {@link canonicalTool} map, else verbatim.
139
+ * @param {string|null} nativeTool
140
+ * @param {string} callClass a {@link CallClass} value
141
+ * @returns {string|null}
142
+ */
143
+ function geminiCanonicalTool(nativeTool, callClass) {
144
+ if (nativeTool !== null && callClass === CallClass.BUILTIN) {
145
+ const scoped = GEMINI_TOOL_ALIASES[nativeTool];
146
+ if (scoped !== undefined) return scoped;
147
+ }
148
+ return canonicalTool(nativeTool);
149
+ }
150
+
151
+ /**
152
+ * @param {string} kind
153
+ * @param {Record<string, unknown>} raw
154
+ * @returns {Record<string, unknown>}
155
+ */
156
+ function geminiInput(kind, raw) {
157
+ if (kind === EventKind.PROMPT_SUBMIT)
158
+ return { prompt: asString(raw.prompt, "") };
159
+ if (kind === EventKind.PRE_TOOL || kind === EventKind.POST_TOOL)
160
+ return asObject(raw.tool_input);
161
+ return {};
162
+ }
163
+
86
164
  /**
87
165
  * Parse a raw Gemini CLI hook payload into a normalized {@link ToolCallEvent}.
88
- * Never throws on an unmodelled event type or tool-input field. Any payload we
89
- * receive comes from a hooks-capable Gemini (v0.26.0+) whose `BeforeTool` can
90
- * pre-empt via exit 2, so `this_call_vetoable` is true.
166
+ * Never throws on an unmodelled event type or tool-input field. A payload comes
167
+ * from a hooks-capable Gemini (v0.26.0+) whose `BeforeTool` can pre-empt builtin
168
+ * tools via exit 2. But MCP firing is only medium-confidence (COVERAGE.mcp is
169
+ * UNKNOWN), so an MCP-sourced call — flagged by `mcp_context` or an `mcp_`-named
170
+ * tool — parses non-vetoable until an item-⑤ probe confirms the hook fires.
171
+ * `BeforeAgent` (fires after the user submits a prompt, before planning) maps to
172
+ * `prompt_submit`: no tool, the submitted text folded into `input.prompt`.
91
173
  * @param {any} native
92
174
  * @returns {ToolCallEvent}
93
175
  */
@@ -100,22 +182,31 @@ export function parse(native) {
100
182
  EventKind.UNKNOWN;
101
183
  const gating = kind === EventKind.PRE_TOOL || kind === EventKind.POST_TOOL;
102
184
  const response = kind === EventKind.POST_TOOL ? raw.tool_response : undefined;
185
+ const nativeTool = gating ? asStringOrNull(raw.tool_name) : null;
186
+ const meta = geminiMeta(nativeEvent, raw);
187
+ if (nativeTool !== null) meta.native_tool = nativeTool;
188
+ // Classify on the NATIVE name (MCP detection keys on the `mcp_…` FQN prefix
189
+ // every Gemini MCP tool carries); the class gates both the veto flag and the
190
+ // builtin-only adapter-scoped aliases.
191
+ const callClass = classifyCallClass(nativeTool, raw);
103
192
  return makeEvent({
104
193
  event: kind,
105
- tool: gating ? asStringOrNull(raw.tool_name) : null,
106
- input: gating ? asObject(raw.tool_input) : {},
194
+ tool: geminiCanonicalTool(nativeTool, callClass),
195
+ input: geminiInput(kind, raw),
107
196
  response,
108
- this_call_vetoable: true,
109
- meta: geminiMeta(nativeEvent, raw),
197
+ this_call_vetoable: coverageAllowsVeto(COVERAGE[callClass]),
198
+ meta,
110
199
  });
111
200
  }
112
201
 
113
202
  /**
114
203
  * Render into Gemini CLI's native external-hook transport. An enforceable deny
115
- * renders as exit 2 (the System Block); everything else exits 0 with a JSON
116
- * decision body (or none, when `allow` abstains). `soleGate` (default false) is
117
- * the same dangerous opt-in as the other adapters: it makes an `allow` emit the
118
- * real `decision: "allow"` instead of abstaining.
204
+ * renders as exit 2 (the System Block on BeforeTool; documented on BeforeAgent
205
+ * as "same as decision: deny" it aborts the turn); everything else exits 0
206
+ * with a JSON decision body (or none, when `allow` abstains). `soleGate`
207
+ * (default false) is the same dangerous opt-in as the other adapters: it makes
208
+ * an `allow` emit the real `decision: "allow"` instead of abstaining (tool
209
+ * events only — BeforeAgent documents no allow behavior to opt into).
119
210
  * @param {Verdict} verdict
120
211
  * @param {ToolCallEvent} event
121
212
  * @param {{ soleGate?: boolean }} [options]
@@ -134,7 +225,10 @@ export function render(verdict, event, { soleGate = false } = {}) {
134
225
  enforced: true,
135
226
  });
136
227
 
137
- const body = decisionBody(vd, soleGate);
228
+ const body =
229
+ event.event === EventKind.PROMPT_SUBMIT
230
+ ? promptSubmitBody(vd)
231
+ : decisionBody(vd, soleGate);
138
232
  return nativeResponse({
139
233
  transport: INTEGRATION_MODE,
140
234
  exit_code: 0,
@@ -171,5 +265,36 @@ function decisionBody(vd, soleGate) {
171
265
  return Object.keys(out).length > 0 ? out : undefined;
172
266
  }
173
267
 
268
+ /**
269
+ * Build the exit-0 stdout body for a `prompt_submit` (BeforeAgent) render.
270
+ * BeforeAgent's documented channels differ from BeforeTool's: `decision:
271
+ * "deny"` blocks the turn and discards the prompt, and context injection is
272
+ * `hookSpecificOutput.additionalContext` (appended to the prompt for this turn
273
+ * only) — there is no `tool_input` to mutate and no documented allow decision,
274
+ * so `allow` always abstains and `mutated_input` has no home here. `deny`/`ask`
275
+ * both surface `decision: "deny"` with the reason, mirroring how BeforeTool
276
+ * renders ask (Gemini has no native ask tier). Returns undefined when there is
277
+ * nothing to emit.
278
+ * @param {Verdict} vd
279
+ * @returns {Record<string, unknown>|undefined}
280
+ */
281
+ function promptSubmitBody(vd) {
282
+ /** @type {Record<string, unknown>} */
283
+ const out = {};
284
+ if (vd.decision === Decision.DENY || vd.decision === Decision.ASK) {
285
+ out.decision = "deny";
286
+ if (vd.reason !== undefined) out.reason = vd.reason;
287
+ }
288
+ if (vd.additional_context !== undefined)
289
+ out.hookSpecificOutput = { additionalContext: vd.additional_context };
290
+ return Object.keys(out).length > 0 ? out : undefined;
291
+ }
292
+
174
293
  /** @type {import("../control-plane.mjs").Adapter} */
175
- export const geminiAdapter = { AGENT, INTEGRATION_MODE, parse, render };
294
+ export const geminiAdapter = {
295
+ AGENT,
296
+ INTEGRATION_MODE,
297
+ COVERAGE,
298
+ parse,
299
+ render,
300
+ };