@benjamolina/pi-antigravity-guard 0.3.0 → 0.4.1

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 CHANGED
@@ -12,11 +12,35 @@ Install the package in Pi, then run:
12
12
 
13
13
  Choose browser login, or choose manual login and paste the complete callback URL. The loopback callback uses fixed port `51121`; browser callback failures, remote shells, and port conflicts can use the manual callback-URL flow.
14
14
 
15
- Start a fresh text-only session with `--no-builtin-tools` and disable any active extension-provided tools. That flag does not disable extension tools by itself. Tools, tool history, and images are unsupported and rejected rather than silently changed.
15
+ ## Text registration is not tool enablement
16
16
 
17
- ## Supported static catalog
17
+ All seven rows below are registered for text. The exact `antigravity-gemini-3.8-flash` `off` route is enabled for tools from recorded direct evidence; every other production route is **not enabled for tools**, so its context with a declaration, assistant tool call, or tool result fails locally before any network request. Text-only requests retain their released behavior.
18
18
 
19
- The provider registers only these evidence-admitted public IDs, in this order. `off` is available for every row even though it is omitted from Pi's level map; omitted reasoning also selects that row's `off` route. Unsupported levels are not advertised and are rejected before transport.
19
+ | Public ID | Exposed Pi levels | Tool state | Admission |
20
+ |---|---|---|---|
21
+ | `antigravity-gemini-3.8-flash` | off, low, medium, high | Enabled (off) | pi-json-tool-loop/gemini-3.8-flash-off-v1 |
22
+ | `antigravity-gemini-3.7-flash` | off, low, medium, high | Disabled | missing-direct-evidence |
23
+ | `antigravity-gemini-3.6-flash` | off, low, medium, high | Disabled | missing-direct-evidence |
24
+ | `antigravity-gemini-3.1-pro` | off, low, high | Disabled | missing-direct-evidence |
25
+ | `antigravity-claude-sonnet-4.6` | off, high | Disabled | claude-continuity-unproven |
26
+ | `antigravity-claude-opus-4.6-thinking` | off, high | Disabled | claude-continuity-unproven |
27
+ | `antigravity-gpt-oss-120b` | off, medium | Disabled | missing-direct-evidence |
28
+
29
+ `off` is available for every row even though it is omitted from Pi's level map; omitted reasoning also selects that row's `off` route. Unsupported levels are not advertised and are rejected before transport. `antigravity-gemini-3.5-flash` is not registered or advertised as supported because its recorded HTTP-200 response lacks the strict terminal metadata required for admission. A future `fixture-qualified` route remains disabled for ordinary tool use; only separately authorized direct validation for the exact route may make it enabled.
30
+
31
+ Claude Sonnet and Claude Opus remain disabled until declaration, call, result replay, thinking-signature, resume, interruption, and continuation evidence is recorded for each exact route. Only Gemini 3.8 Flash `off` is enabled by this package release.
32
+
33
+ ## Opt-in direct tool-loop validation
34
+
35
+ Maintainers can run `npm run build:pi` followed by `npx tsx scripts/pi-tool-loop-probe.ts --live --expect-enabled` to verify the directly admitted Gemini 3.8 Flash `off` route. The isolated runner invokes Pi in `--mode json`, passes the deterministic non-terminating `pi_evidence_echo` prompt positionally, drains its event stream, and completes after Pi emits `agent_end` and the child exits. It stores only the passing route and sanitized assertions; it never stores message contents, tool details, names, IDs, arguments, credentials, headers, or raw provider output. Other routes remain fail-closed.
36
+
37
+ ## Tool preflight boundaries
38
+
39
+ Tool-bearing requests are never silently downgraded to text. When an exact route is eventually enabled, declarations are retained in order and omitted or `auto` choice serializes as `AUTO`; explicit `none` serializes as `NONE`. Forced, required, named, unknown, and constrained-sampling choices are rejected before transport.
40
+
41
+ Schemas must use the documented strict object-rooted subset. Unsupported keywords, references, unions, lossy constraints, empty objects, and oversized input are rejected rather than cleaned. Tool results must match the exact call ID and name, remain text-only, and are replayed in assistant source-call order; image-bearing or ambiguous results are rejected. A missing persisted result uses the documented failed response only during context reconstruction.
42
+
43
+ ## Text catalog limits and safety
20
44
 
21
45
  | Public ID | Exposed Pi levels | Context / output |
22
46
  |---|---|---:|
@@ -28,16 +52,10 @@ The provider registers only these evidence-admitted public IDs, in this order. `
28
52
  | `antigravity-claude-opus-4.6-thinking` | off, high | 250,000 / 64,000 |
29
53
  | `antigravity-gpt-oss-120b` | off, medium | 131,072 / 32,768 |
30
54
 
31
- Gemini 3.8 remains compatible with the released mapping: `antigravity-gemini-3.8-flash` sends `gemini-3.8-flash-tiered`; `off` sends native `thinkingLevel: "low"` with hidden thoughts, and `low`, `medium`, and `high` send their matching native level with visible thoughts.
32
-
33
- `antigravity-gemini-3.5-flash` is not registered or advertised as supported because its recorded HTTP-200 response lacks the strict terminal metadata required for admission.
34
-
35
- ## Limits and safety
36
-
37
55
  An explicit `maxTokens` must be a positive integer no greater than the row's output limit. For a route with a positive finite thinking budget, it must also be greater than that budget. When `maxTokens` is omitted, the provider uses 4,096 unless a larger finite thinking budget needs a 1,024-token answer reserve, always bounded by the row's output limit. Custom `thinkingBudgets` are unsupported and rejected.
38
56
 
39
57
  - Costs are reported as zero because subscription usage is unpriced, not because access is free.
40
58
  - The catalog is static: it performs no startup or runtime model discovery, catalog synchronization, or dynamic registration.
41
- - It has no account rotation, quota fallback, model substitution, tool support, or image support.
59
+ - It has no account rotation, quota fallback, model substitution, image support, or enabled tools beyond the directly admitted Gemini 3.8 Flash `off` route.
42
60
 
43
61
  To remove it, disable or uninstall `@benjamolina/pi-antigravity-guard` and reload Pi. Removal does not delete Pi-managed credentials or revoke tokens.
package/dist/catalog.d.ts CHANGED
@@ -1,5 +1,26 @@
1
1
  export type StandardReasoningLevel = "off" | "minimal" | "low" | "medium" | "high";
2
2
  type NativeThinkingLevel = "low" | "medium" | "high";
3
+ export interface ToolEvidenceRef {
4
+ readonly record: string;
5
+ readonly revision: string;
6
+ readonly publicModelId: string;
7
+ readonly reasoning: StandardReasoningLevel;
8
+ readonly wireModel: string;
9
+ }
10
+ export type ToolCapability = {
11
+ readonly state: "disabled";
12
+ readonly contractRevision: 1;
13
+ readonly reason: "missing-direct-evidence" | "stale-or-conflicting-evidence" | "claude-continuity-unproven";
14
+ } | {
15
+ readonly state: "fixture-qualified";
16
+ readonly contractRevision: 1;
17
+ readonly fixtureEvidence: ToolEvidenceRef;
18
+ } | {
19
+ readonly state: "enabled";
20
+ readonly contractRevision: 1;
21
+ readonly fixtureEvidence: ToolEvidenceRef;
22
+ readonly directEvidence: ToolEvidenceRef;
23
+ };
3
24
  type NativeRoute = {
4
25
  readonly wireModel: string;
5
26
  readonly thinking: {
@@ -19,6 +40,9 @@ type IntegerBudgetRoute = {
19
40
  };
20
41
  };
21
42
  export type GenerationRoute = NativeRoute | IntegerBudgetRoute;
43
+ type ConfiguredGenerationRoute = GenerationRoute & {
44
+ readonly tools: ToolCapability;
45
+ };
22
46
  export interface CatalogEntry {
23
47
  readonly publicId: string;
24
48
  readonly descriptor: {
@@ -27,7 +51,7 @@ export interface CatalogEntry {
27
51
  readonly contextWindow: number;
28
52
  readonly maxTokens: number;
29
53
  };
30
- readonly routes: Readonly<Partial<Record<StandardReasoningLevel, GenerationRoute>>>;
54
+ readonly routes: Readonly<Partial<Record<StandardReasoningLevel, ConfiguredGenerationRoute>>>;
31
55
  readonly replay: {
32
56
  readonly kind: "same-public-model" | "strip";
33
57
  };
@@ -57,6 +81,7 @@ declare const CATALOG: readonly [{
57
81
  readonly thinkingLevel: "low";
58
82
  readonly includeThoughts: false;
59
83
  };
84
+ readonly tools: ToolCapability;
60
85
  };
61
86
  readonly low: {
62
87
  readonly wireModel: "gemini-3.8-flash-tiered";
@@ -65,6 +90,7 @@ declare const CATALOG: readonly [{
65
90
  readonly thinkingLevel: "low";
66
91
  readonly includeThoughts: true;
67
92
  };
93
+ readonly tools: ToolCapability;
68
94
  };
69
95
  readonly medium: {
70
96
  readonly wireModel: "gemini-3.8-flash-tiered";
@@ -73,6 +99,7 @@ declare const CATALOG: readonly [{
73
99
  readonly thinkingLevel: "medium";
74
100
  readonly includeThoughts: true;
75
101
  };
102
+ readonly tools: ToolCapability;
76
103
  };
77
104
  readonly high: {
78
105
  readonly wireModel: "gemini-3.8-flash-tiered";
@@ -81,6 +108,7 @@ declare const CATALOG: readonly [{
81
108
  readonly thinkingLevel: "high";
82
109
  readonly includeThoughts: true;
83
110
  };
111
+ readonly tools: ToolCapability;
84
112
  };
85
113
  };
86
114
  readonly replay: {
@@ -111,6 +139,7 @@ declare const CATALOG: readonly [{
111
139
  readonly budget: 0;
112
140
  readonly includeThoughts: false;
113
141
  };
142
+ readonly tools: ToolCapability;
114
143
  };
115
144
  readonly low: {
116
145
  readonly wireModel: "gemini-3.7-flash-low";
@@ -119,6 +148,7 @@ declare const CATALOG: readonly [{
119
148
  readonly budget: 1000;
120
149
  readonly includeThoughts: true;
121
150
  };
151
+ readonly tools: ToolCapability;
122
152
  };
123
153
  readonly medium: {
124
154
  readonly wireModel: "gemini-3.7-flash-medium";
@@ -127,6 +157,7 @@ declare const CATALOG: readonly [{
127
157
  readonly budget: 4000;
128
158
  readonly includeThoughts: true;
129
159
  };
160
+ readonly tools: ToolCapability;
130
161
  };
131
162
  readonly high: {
132
163
  readonly wireModel: "gemini-3.7-flash-high";
@@ -135,6 +166,7 @@ declare const CATALOG: readonly [{
135
166
  readonly budget: -1;
136
167
  readonly includeThoughts: true;
137
168
  };
169
+ readonly tools: ToolCapability;
138
170
  };
139
171
  };
140
172
  readonly replay: {
@@ -163,6 +195,7 @@ declare const CATALOG: readonly [{
163
195
  readonly thinking: {
164
196
  readonly kind: "omit";
165
197
  };
198
+ readonly tools: ToolCapability;
166
199
  };
167
200
  readonly low: {
168
201
  readonly wireModel: "gemini-3.6-flash-low";
@@ -171,6 +204,7 @@ declare const CATALOG: readonly [{
171
204
  readonly budget: 1000;
172
205
  readonly includeThoughts: true;
173
206
  };
207
+ readonly tools: ToolCapability;
174
208
  };
175
209
  readonly medium: {
176
210
  readonly wireModel: "gemini-3.6-flash-medium";
@@ -179,6 +213,7 @@ declare const CATALOG: readonly [{
179
213
  readonly budget: 4000;
180
214
  readonly includeThoughts: true;
181
215
  };
216
+ readonly tools: ToolCapability;
182
217
  };
183
218
  readonly high: {
184
219
  readonly wireModel: "gemini-3.6-flash-high";
@@ -187,6 +222,7 @@ declare const CATALOG: readonly [{
187
222
  readonly budget: -1;
188
223
  readonly includeThoughts: true;
189
224
  };
225
+ readonly tools: ToolCapability;
190
226
  };
191
227
  };
192
228
  readonly replay: {
@@ -215,6 +251,7 @@ declare const CATALOG: readonly [{
215
251
  readonly thinking: {
216
252
  readonly kind: "omit";
217
253
  };
254
+ readonly tools: ToolCapability;
218
255
  };
219
256
  readonly low: {
220
257
  readonly wireModel: "gemini-3.1-pro-low";
@@ -223,6 +260,7 @@ declare const CATALOG: readonly [{
223
260
  readonly budget: 1001;
224
261
  readonly includeThoughts: true;
225
262
  };
263
+ readonly tools: ToolCapability;
226
264
  };
227
265
  readonly high: {
228
266
  readonly wireModel: "gemini-pro-agent";
@@ -231,6 +269,7 @@ declare const CATALOG: readonly [{
231
269
  readonly budget: 10001;
232
270
  readonly includeThoughts: true;
233
271
  };
272
+ readonly tools: ToolCapability;
234
273
  };
235
274
  };
236
275
  readonly replay: {
@@ -261,6 +300,7 @@ declare const CATALOG: readonly [{
261
300
  readonly budget: 0;
262
301
  readonly includeThoughts: false;
263
302
  };
303
+ readonly tools: ToolCapability;
264
304
  };
265
305
  readonly high: {
266
306
  readonly wireModel: "claude-sonnet-4-6";
@@ -269,6 +309,7 @@ declare const CATALOG: readonly [{
269
309
  readonly budget: 1024;
270
310
  readonly includeThoughts: true;
271
311
  };
312
+ readonly tools: ToolCapability;
272
313
  };
273
314
  };
274
315
  readonly replay: {
@@ -299,6 +340,7 @@ declare const CATALOG: readonly [{
299
340
  readonly budget: 0;
300
341
  readonly includeThoughts: false;
301
342
  };
343
+ readonly tools: ToolCapability;
302
344
  };
303
345
  readonly high: {
304
346
  readonly wireModel: "claude-opus-4-6-thinking";
@@ -307,6 +349,7 @@ declare const CATALOG: readonly [{
307
349
  readonly budget: 1024;
308
350
  readonly includeThoughts: true;
309
351
  };
352
+ readonly tools: ToolCapability;
310
353
  };
311
354
  };
312
355
  readonly replay: {
@@ -335,6 +378,7 @@ declare const CATALOG: readonly [{
335
378
  readonly thinking: {
336
379
  readonly kind: "omit";
337
380
  };
381
+ readonly tools: ToolCapability;
338
382
  };
339
383
  readonly medium: {
340
384
  readonly wireModel: "gpt-oss-120b-medium";
@@ -343,6 +387,7 @@ declare const CATALOG: readonly [{
343
387
  readonly budget: 8192;
344
388
  readonly includeThoughts: true;
345
389
  };
390
+ readonly tools: ToolCapability;
346
391
  };
347
392
  };
348
393
  readonly replay: {
@@ -356,6 +401,10 @@ declare const CATALOG: readonly [{
356
401
  type Catalog = typeof CATALOG;
357
402
  export type AntigravityPublicModelId = Catalog[number]["publicId"];
358
403
  export type AntigravityWireModelId = Catalog[number]["routes"][keyof Catalog[number]["routes"]]["wireModel"];
404
+ /** Constructs deterministic capability data for hermetic serializer tests only. */
405
+ export declare function createEnabledToolCapability(evidence: ToolEvidenceRef): Extract<ToolCapability, {
406
+ readonly state: "enabled";
407
+ }>;
359
408
  export declare function defineCatalog<T extends readonly {
360
409
  readonly publicId: string;
361
410
  readonly descriptor?: CatalogEntry["descriptor"];
@@ -367,6 +416,13 @@ export declare function defineCatalog<T extends readonly {
367
416
  export declare function listCatalogEntries(): readonly CatalogEntry[];
368
417
  export declare function getCatalogEntry(publicId: unknown): CatalogEntry | undefined;
369
418
  export declare function resolveGenerationRoute(entry: CatalogEntry, reasoning: unknown): GenerationRoute;
419
+ export interface GenerationSelection {
420
+ readonly level: StandardReasoningLevel;
421
+ readonly route: GenerationRoute;
422
+ readonly tools: ToolCapability;
423
+ }
424
+ export declare function resolveGenerationSelection(entry: CatalogEntry, reasoning: unknown): GenerationSelection;
425
+ export declare function resolveToolCapability(capability: ToolCapability, publicModelId: string, reasoning: StandardReasoningLevel, wireModel: string): ToolCapability;
370
426
  export declare function toPiModelDescriptor(entry: CatalogEntry): {
371
427
  id: string;
372
428
  name: string;
package/dist/catalog.js CHANGED
@@ -8,10 +8,10 @@ const CATALOG = freeze(defineCatalog([{
8
8
  maxTokens: 65_536,
9
9
  },
10
10
  routes: {
11
- off: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: false } },
12
- low: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: true } },
13
- medium: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "medium", includeThoughts: true } },
14
- high: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "high", includeThoughts: true } },
11
+ off: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: false }, tools: enabled({ record: "pi-json-tool-loop", revision: "gemini-3.8-flash-off-v1", publicModelId: "antigravity-gemini-3.8-flash", reasoning: "off", wireModel: "gemini-3.8-flash-tiered" }) },
12
+ low: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
13
+ medium: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "medium", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
14
+ high: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "high", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
15
15
  },
16
16
  replay: { kind: "same-public-model" },
17
17
  response: { kind: "gemini-envelope", family: "gemini" },
@@ -24,10 +24,10 @@ const CATALOG = freeze(defineCatalog([{
24
24
  maxTokens: 65_536,
25
25
  },
26
26
  routes: {
27
- off: { wireModel: "gemini-3.7-flash-low", thinking: { kind: "budget", budget: 0, includeThoughts: false } },
28
- low: { wireModel: "gemini-3.7-flash-low", thinking: { kind: "budget", budget: 1000, includeThoughts: true } },
29
- medium: { wireModel: "gemini-3.7-flash-medium", thinking: { kind: "budget", budget: 4000, includeThoughts: true } },
30
- high: { wireModel: "gemini-3.7-flash-high", thinking: { kind: "budget", budget: -1, includeThoughts: true } },
27
+ off: { wireModel: "gemini-3.7-flash-low", thinking: { kind: "budget", budget: 0, includeThoughts: false }, tools: disabled("missing-direct-evidence") },
28
+ low: { wireModel: "gemini-3.7-flash-low", thinking: { kind: "budget", budget: 1000, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
29
+ medium: { wireModel: "gemini-3.7-flash-medium", thinking: { kind: "budget", budget: 4000, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
30
+ high: { wireModel: "gemini-3.7-flash-high", thinking: { kind: "budget", budget: -1, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
31
31
  },
32
32
  replay: { kind: "strip" },
33
33
  response: { kind: "gemini-envelope", family: "gemini" },
@@ -40,10 +40,10 @@ const CATALOG = freeze(defineCatalog([{
40
40
  maxTokens: 65_536,
41
41
  },
42
42
  routes: {
43
- off: { wireModel: "gemini-3.6-flash-low", thinking: { kind: "omit" } },
44
- low: { wireModel: "gemini-3.6-flash-low", thinking: { kind: "budget", budget: 1000, includeThoughts: true } },
45
- medium: { wireModel: "gemini-3.6-flash-medium", thinking: { kind: "budget", budget: 4000, includeThoughts: true } },
46
- high: { wireModel: "gemini-3.6-flash-high", thinking: { kind: "budget", budget: -1, includeThoughts: true } },
43
+ off: { wireModel: "gemini-3.6-flash-low", thinking: { kind: "omit" }, tools: disabled("missing-direct-evidence") },
44
+ low: { wireModel: "gemini-3.6-flash-low", thinking: { kind: "budget", budget: 1000, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
45
+ medium: { wireModel: "gemini-3.6-flash-medium", thinking: { kind: "budget", budget: 4000, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
46
+ high: { wireModel: "gemini-3.6-flash-high", thinking: { kind: "budget", budget: -1, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
47
47
  },
48
48
  replay: { kind: "strip" },
49
49
  response: { kind: "gemini-envelope", family: "gemini" },
@@ -56,9 +56,9 @@ const CATALOG = freeze(defineCatalog([{
56
56
  maxTokens: 65_535,
57
57
  },
58
58
  routes: {
59
- off: { wireModel: "gemini-3.1-pro-low", thinking: { kind: "omit" } },
60
- low: { wireModel: "gemini-3.1-pro-low", thinking: { kind: "budget", budget: 1001, includeThoughts: true } },
61
- high: { wireModel: "gemini-pro-agent", thinking: { kind: "budget", budget: 10001, includeThoughts: true } },
59
+ off: { wireModel: "gemini-3.1-pro-low", thinking: { kind: "omit" }, tools: disabled("missing-direct-evidence") },
60
+ low: { wireModel: "gemini-3.1-pro-low", thinking: { kind: "budget", budget: 1001, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
61
+ high: { wireModel: "gemini-pro-agent", thinking: { kind: "budget", budget: 10001, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
62
62
  },
63
63
  replay: { kind: "strip" },
64
64
  response: { kind: "gemini-envelope", family: "gemini" },
@@ -71,8 +71,8 @@ const CATALOG = freeze(defineCatalog([{
71
71
  maxTokens: 64_000,
72
72
  },
73
73
  routes: {
74
- off: { wireModel: "claude-sonnet-4-6", thinking: { kind: "budget", budget: 0, includeThoughts: false } },
75
- high: { wireModel: "claude-sonnet-4-6", thinking: { kind: "budget", budget: 1024, includeThoughts: true } },
74
+ off: { wireModel: "claude-sonnet-4-6", thinking: { kind: "budget", budget: 0, includeThoughts: false }, tools: disabled("claude-continuity-unproven") },
75
+ high: { wireModel: "claude-sonnet-4-6", thinking: { kind: "budget", budget: 1024, includeThoughts: true }, tools: disabled("claude-continuity-unproven") },
76
76
  },
77
77
  replay: { kind: "strip" },
78
78
  response: { kind: "gemini-envelope", family: "claude" },
@@ -85,8 +85,8 @@ const CATALOG = freeze(defineCatalog([{
85
85
  maxTokens: 64_000,
86
86
  },
87
87
  routes: {
88
- off: { wireModel: "claude-opus-4-6-thinking", thinking: { kind: "budget", budget: 0, includeThoughts: false } },
89
- high: { wireModel: "claude-opus-4-6-thinking", thinking: { kind: "budget", budget: 1024, includeThoughts: true } },
88
+ off: { wireModel: "claude-opus-4-6-thinking", thinking: { kind: "budget", budget: 0, includeThoughts: false }, tools: disabled("claude-continuity-unproven") },
89
+ high: { wireModel: "claude-opus-4-6-thinking", thinking: { kind: "budget", budget: 1024, includeThoughts: true }, tools: disabled("claude-continuity-unproven") },
90
90
  },
91
91
  replay: { kind: "strip" },
92
92
  response: { kind: "gemini-envelope", family: "claude" },
@@ -99,13 +99,23 @@ const CATALOG = freeze(defineCatalog([{
99
99
  maxTokens: 32_768,
100
100
  },
101
101
  routes: {
102
- off: { wireModel: "gpt-oss-120b-medium", thinking: { kind: "omit" } },
103
- medium: { wireModel: "gpt-oss-120b-medium", thinking: { kind: "budget", budget: 8192, includeThoughts: true } },
102
+ off: { wireModel: "gpt-oss-120b-medium", thinking: { kind: "omit" }, tools: disabled("missing-direct-evidence") },
103
+ medium: { wireModel: "gpt-oss-120b-medium", thinking: { kind: "budget", budget: 8192, includeThoughts: true }, tools: disabled("missing-direct-evidence") },
104
104
  },
105
105
  replay: { kind: "strip" },
106
106
  response: { kind: "gemini-envelope", family: "gpt-oss" },
107
107
  }]));
108
108
  const lookup = new Map(CATALOG.map((entry) => [entry.publicId, entry]));
109
+ function disabled(reason) {
110
+ return { state: "disabled", contractRevision: 1, reason };
111
+ }
112
+ function enabled(evidence) {
113
+ return { state: "enabled", contractRevision: 1, fixtureEvidence: evidence, directEvidence: evidence };
114
+ }
115
+ /** Constructs deterministic capability data for hermetic serializer tests only. */
116
+ export function createEnabledToolCapability(evidence) {
117
+ return freeze({ state: "enabled", contractRevision: 1, fixtureEvidence: evidence, directEvidence: evidence });
118
+ }
109
119
  function freeze(value) {
110
120
  if (typeof value === "object" && value !== null) {
111
121
  Object.freeze(value);
@@ -147,10 +157,25 @@ export function getCatalogEntry(publicId) {
147
157
  return typeof publicId === "string" ? lookup.get(publicId) : undefined;
148
158
  }
149
159
  export function resolveGenerationRoute(entry, reasoning) {
160
+ return resolveGenerationSelection(entry, reasoning).route;
161
+ }
162
+ export function resolveGenerationSelection(entry, reasoning) {
150
163
  const level = reasoning === undefined || reasoning === "off" ? "off" : reasoning;
151
164
  if (typeof level !== "string" || !entry.routes[level])
152
165
  throw new Error("Unsupported reasoning level");
153
- return entry.routes[level];
166
+ const typedLevel = level;
167
+ const configured = entry.routes[typedLevel];
168
+ const { tools, ...route } = configured;
169
+ return { level: typedLevel, route, tools: resolveToolCapability(tools, entry.publicId, typedLevel, route.wireModel) };
170
+ }
171
+ export function resolveToolCapability(capability, publicModelId, reasoning, wireModel) {
172
+ if (capability.state === "disabled")
173
+ return capability;
174
+ const evidence = capability.state === "enabled" ? [capability.fixtureEvidence, capability.directEvidence] : [capability.fixtureEvidence];
175
+ if (capability.contractRevision !== 1 || evidence.some((item) => item.publicModelId !== publicModelId || item.reasoning !== reasoning || item.wireModel !== wireModel)) {
176
+ return freeze(disabled("stale-or-conflicting-evidence"));
177
+ }
178
+ return capability;
154
179
  }
155
180
  export function toPiModelDescriptor(entry) {
156
181
  return {
package/dist/context.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
2
+ import { type GenerationSelection } from "./catalog.ts";
3
+ import { type ToolReplayDiagnostics } from "./tool-context.ts";
2
4
  interface Part {
3
5
  text: string;
4
6
  thought?: true;
@@ -6,7 +8,12 @@ interface Part {
6
8
  }
7
9
  interface Content {
8
10
  role: "user" | "model";
9
- parts: Part[];
11
+ parts: object[];
12
+ }
13
+ interface GeminiFunctionDeclaration {
14
+ readonly name: string;
15
+ readonly description: string;
16
+ readonly parametersJsonSchema: object;
10
17
  }
11
18
  export interface GenerationRequest {
12
19
  project: string;
@@ -16,6 +23,14 @@ export interface GenerationRequest {
16
23
  systemInstruction?: {
17
24
  parts: Part[];
18
25
  };
26
+ tools?: {
27
+ functionDeclarations: readonly GeminiFunctionDeclaration[];
28
+ }[];
29
+ toolConfig?: {
30
+ functionCallingConfig: {
31
+ mode: "AUTO" | "NONE";
32
+ };
33
+ };
19
34
  generationConfig: {
20
35
  temperature: number;
21
36
  maxOutputTokens: number;
@@ -41,5 +56,6 @@ export interface SerializeTextContextInput {
41
56
  }
42
57
  export declare class ContextSerializationError extends Error {
43
58
  }
59
+ export declare function serializeContext(input: SerializeTextContextInput, injectedSelection?: GenerationSelection, onDiagnostics?: (diagnostics: ToolReplayDiagnostics) => void): GenerationRequest;
44
60
  export declare function serializeTextContext(input: SerializeTextContextInput): GenerationRequest;
45
61
  export {};
package/dist/context.js CHANGED
@@ -1,10 +1,47 @@
1
- import { getCatalogEntry, resolveGenerationRoute } from "./catalog.js";
1
+ import { getCatalogEntry, resolveGenerationSelection } from "./catalog.js";
2
+ import { hasToolContext, prepareToolContext, replayToolHistory } from "./tool-context.js";
2
3
  const MAX_TEXT_BYTES = 8 * 1024 * 1024;
3
4
  const PROVIDER = "antigravity-guard";
4
5
  const RESERVED_HEADERS = new Set(["authorization", "host", "content-type", "content-length"]);
5
6
  const THINKING_LEVELS = ["low", "medium", "high"];
6
7
  export class ContextSerializationError extends Error {
7
8
  }
9
+ export function serializeContext(input, injectedSelection, onDiagnostics) {
10
+ const context = input.context;
11
+ if (!hasToolContext(context))
12
+ return serializeTextContext(input);
13
+ const options = input.options;
14
+ const entry = isRecord(input.model) ? getCatalogEntry(field(input.model, "id", true)) : undefined;
15
+ if (!entry || !isRecord(context))
16
+ return serializeTextContext(input);
17
+ const selected = resolveGenerationSelection(entry, isRecord(options) ? option(options, "reasoning") : undefined);
18
+ const selection = injectedSelection ?? selected;
19
+ if (selection.level !== selected.level || selection.route.wireModel !== selected.route.wireModel)
20
+ fail("Invalid tool capability selection.");
21
+ if (selection.tools.state !== "enabled")
22
+ fail(capabilityError(entry.publicId, selection.level, selection.tools));
23
+ const prepared = prepareToolContext(field(context, "tools") ?? [], isRecord(options) ? field(options, "toolChoice") : undefined);
24
+ const { tools: _tools, messages, ...textContext } = context;
25
+ const { toolChoice: _choice, ...textOptions } = isRecord(options) ? options : {};
26
+ const text = serializeTextContext({ ...input, context: { ...textContext, messages: [{ role: "user", content: "placeholder", timestamp: 0 }] }, options: textOptions });
27
+ const sameModel = (message) => field(message, "role") === "assistant" && entry.replay.kind === "same-public-model" && isSameProviderAndModel(message, entry.publicId);
28
+ const signedToolReplay = geminiRequiresSignedToolReplay(selection.route.wireModel) ? {
29
+ requireSignedToolCalls: true,
30
+ isSameModel: sameModel,
31
+ toolCallSignature: (part, message) => sameModel(message) ? validThoughtSignature(field(part, "thoughtSignature")) : undefined,
32
+ } : undefined;
33
+ const contents = replayToolHistory(isDenseArray(messages), (part, message) => messagePart(part, field(message, "role") === "assistant", sameModel(message)), onDiagnostics, signedToolReplay, prepared?.declarations.length ?? 0);
34
+ const { systemInstruction, generationConfig } = text.request;
35
+ return { ...text, request: {
36
+ contents,
37
+ ...(systemInstruction ? { systemInstruction } : {}),
38
+ ...(prepared ? {
39
+ tools: [{ functionDeclarations: prepared.declarations.map(({ name, description, parameters }) => ({ name, description, parametersJsonSchema: parameters })) }],
40
+ ...(prepared.mode === "NONE" ? { toolConfig: { functionCallingConfig: { mode: "NONE" } } } : {}),
41
+ } : {}),
42
+ generationConfig,
43
+ } };
44
+ }
8
45
  export function serializeTextContext(input) {
9
46
  try {
10
47
  if (!isRecord(input))
@@ -17,7 +54,10 @@ export function serializeTextContext(input) {
17
54
  const entry = isRecord(model) ? getCatalogEntry(field(model, "id", true)) : undefined;
18
55
  if (!isRecord(context) || !entry || typeof project !== "string" || typeof requestId !== "string")
19
56
  fail("Invalid text context.");
20
- const route = resolveGenerationRoute(entry, option(options, "reasoning"));
57
+ const selection = resolveGenerationSelection(entry, option(options, "reasoning"));
58
+ const route = selection.route;
59
+ if (isToolBearingContext(context) && selection.tools.state !== "enabled")
60
+ fail(capabilityError(entry.publicId, selection.level, selection.tools));
21
61
  const systemPrompt = field(context, "systemPrompt");
22
62
  const tools = field(context, "tools");
23
63
  if (systemPrompt !== undefined && typeof systemPrompt !== "string")
@@ -62,42 +102,64 @@ export function serializeTextContext(input) {
62
102
  fail("Invalid text context.");
63
103
  }
64
104
  }
105
+ function capabilityError(publicId, reasoning, capability) {
106
+ const reason = capability.state === "disabled" ? capability.reason : capability.state;
107
+ return `PI_TOOL_CAPABILITY_NOT_ENABLED: ${publicId}/${reasoning} is ${capability.state} (${reason}).`;
108
+ }
109
+ function isToolBearingContext(context) {
110
+ const tools = field(context, "tools");
111
+ if (tools !== undefined && isDenseArray(tools).length > 0)
112
+ return true;
113
+ const messages = isDenseArray(field(context, "messages", true));
114
+ for (const message of messages) {
115
+ if (!isRecord(message))
116
+ continue;
117
+ if (field(message, "role") === "toolResult")
118
+ return true;
119
+ const content = field(message, "content");
120
+ if (!Array.isArray(content))
121
+ continue;
122
+ for (const part of content)
123
+ if (isRecord(part) && field(part, "type") === "toolCall")
124
+ return true;
125
+ }
126
+ return false;
127
+ }
65
128
  function messageParts(content, assistant, sameProviderAndModel) {
66
129
  if (typeof content === "string")
67
130
  return content ? [{ text: content }] : fail("A text conversation is required.");
68
131
  const parts = isDenseArray(content);
69
132
  if (!parts.length)
70
133
  fail("A text conversation is required.");
71
- return parts.map((part) => {
72
- if (!isRecord(part))
134
+ return parts.map((part) => messagePart(isRecord(part) ? part : fail("Only text context is supported by this provider."), assistant, sameProviderAndModel));
135
+ }
136
+ function messagePart(part, assistant, sameProviderAndModel) {
137
+ const type = field(part, "type", true);
138
+ if (type === "text") {
139
+ const text = field(part, "text", true);
140
+ if (typeof text !== "string")
73
141
  fail("Only text context is supported by this provider.");
74
- const type = field(part, "type", true);
75
- if (type === "text") {
76
- const text = field(part, "text", true);
77
- if (typeof text !== "string")
78
- fail("Only text context is supported by this provider.");
79
- const thoughtSignature = sameProviderAndModel ? validThoughtSignature(field(part, "textSignature")) : undefined;
80
- if (!text && !thoughtSignature)
81
- fail("Only text context is supported by this provider.");
82
- return { text, ...(thoughtSignature ? { thoughtSignature } : {}) };
83
- }
84
- if (type === "thinking" && assistant && sameProviderAndModel) {
85
- const text = field(part, "thinking", true);
86
- if (typeof text !== "string")
87
- fail("Only text context is supported by this provider.");
88
- const thoughtSignature = validThoughtSignature(field(part, "thinkingSignature"));
89
- if (!text && !thoughtSignature)
90
- fail("Only text context is supported by this provider.");
91
- return { thought: true, text, ...(thoughtSignature ? { thoughtSignature } : {}) };
92
- }
93
- if (type === "thinking" && assistant) {
94
- const text = field(part, "thinking", true);
95
- if (typeof text !== "string" || !text)
96
- fail("Only text context is supported by this provider.");
97
- return { text };
98
- }
99
- fail("Only text context is supported by this provider.");
100
- });
142
+ const thoughtSignature = sameProviderAndModel ? validThoughtSignature(field(part, "textSignature")) : undefined;
143
+ if (!text && !thoughtSignature)
144
+ fail("Only text context is supported by this provider.");
145
+ return { text, ...(thoughtSignature ? { thoughtSignature } : {}) };
146
+ }
147
+ if (type === "thinking" && assistant && sameProviderAndModel) {
148
+ const text = field(part, "thinking", true);
149
+ if (typeof text !== "string")
150
+ fail("Only text context is supported by this provider.");
151
+ const thoughtSignature = validThoughtSignature(field(part, "thinkingSignature"));
152
+ if (!text && !thoughtSignature)
153
+ fail("Only text context is supported by this provider.");
154
+ return { thought: true, text, ...(thoughtSignature ? { thoughtSignature } : {}) };
155
+ }
156
+ if (type === "thinking" && assistant) {
157
+ const text = field(part, "thinking", true);
158
+ if (typeof text !== "string" || !text)
159
+ fail("Only text context is supported by this provider.");
160
+ return { text };
161
+ }
162
+ fail("Only text context is supported by this provider.");
101
163
  }
102
164
  function isSameProviderAndModel(message, publicId) {
103
165
  return field(message, "provider") === PROVIDER && field(message, "model") === publicId;
@@ -119,6 +181,10 @@ function resolveOutputTokens(maxTokens, maxAllowed, thinking) {
119
181
  }
120
182
  return thinking.kind === "budget" && thinking.budget > 0 ? Math.max(4096, thinking.budget + 1024) : 4096;
121
183
  }
184
+ function geminiRequiresSignedToolReplay(wireModel) {
185
+ const match = /^gemini-(\d+)(?:[.-])/.exec(wireModel);
186
+ return match !== null && Number(match[1]) >= 3;
187
+ }
122
188
  function validThoughtSignature(value) {
123
189
  if (typeof value !== "string" || !value || value.length % 4 !== 0)
124
190
  return undefined;