@benjamolina/pi-antigravity-guard 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,11 +14,11 @@ Choose browser login, or choose manual login and paste the complete callback URL
14
14
 
15
15
  ## Text registration is not tool enablement
16
16
 
17
- All seven rows below are registered for text. Every production route is currently **not enabled for tools**, so a context with a declaration, assistant tool call, or tool result fails locally before any network request. Text-only requests retain their released behavior.
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
- | Public ID | Exposed Pi levels | Tool state | Why tools are unavailable |
19
+ | Public ID | Exposed Pi levels | Tool state | Admission |
20
20
  |---|---|---|---|
21
- | `antigravity-gemini-3.8-flash` | off, low, medium, high | Disabled | missing-direct-evidence |
21
+ | `antigravity-gemini-3.8-flash` | off, low, medium, high | Enabled (off) | pi-json-tool-loop/gemini-3.8-flash-off-v1 |
22
22
  | `antigravity-gemini-3.7-flash` | off, low, medium, high | Disabled | missing-direct-evidence |
23
23
  | `antigravity-gemini-3.6-flash` | off, low, medium, high | Disabled | missing-direct-evidence |
24
24
  | `antigravity-gemini-3.1-pro` | off, low, high | Disabled | missing-direct-evidence |
@@ -28,13 +28,17 @@ All seven rows below are registered for text. Every production route is currentl
28
28
 
29
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
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. No route is enabled by this package release.
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.
32
36
 
33
37
  ## Tool preflight boundaries
34
38
 
35
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.
36
40
 
37
- 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.
41
+ Only the evidence-enabled Gemini 3.8 Flash `off` route uses the `parametersJsonSchema` profile. Its hermetically verified preflight preserves ordinary JSON Schema constraints and JSON values in canonical immutable output, including safe local RFC 6901 `$ref` expansion (`#` and `#/...`) with resolved definitions removed. External, malformed, unresolved, cyclic, or otherwise unsafe references fail closed before transport. This is local serialization and safety evidence, not direct live backend acceptance of constrained or reference-based schemas. 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.
38
42
 
39
43
  ## Text catalog limits and safety
40
44
 
@@ -52,6 +56,6 @@ An explicit `maxTokens` must be a positive integer no greater than the row's out
52
56
 
53
57
  - Costs are reported as zero because subscription usage is unpriced, not because access is free.
54
58
  - The catalog is static: it performs no startup or runtime model discovery, catalog synchronization, or dynamic registration.
55
- - It has no account rotation, quota fallback, model substitution, image support, or enabled tools.
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.
56
60
 
57
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
@@ -7,6 +7,8 @@ export interface ToolEvidenceRef {
7
7
  readonly reasoning: StandardReasoningLevel;
8
8
  readonly wireModel: string;
9
9
  }
10
+ export declare const GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE: "gemini-parameters-json-schema";
11
+ export type ToolSchemaProfile = typeof GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE;
10
12
  export type ToolCapability = {
11
13
  readonly state: "disabled";
12
14
  readonly contractRevision: 1;
@@ -18,6 +20,7 @@ export type ToolCapability = {
18
20
  } | {
19
21
  readonly state: "enabled";
20
22
  readonly contractRevision: 1;
23
+ readonly schemaProfile: ToolSchemaProfile;
21
24
  readonly fixtureEvidence: ToolEvidenceRef;
22
25
  readonly directEvidence: ToolEvidenceRef;
23
26
  };
@@ -402,7 +405,7 @@ type Catalog = typeof CATALOG;
402
405
  export type AntigravityPublicModelId = Catalog[number]["publicId"];
403
406
  export type AntigravityWireModelId = Catalog[number]["routes"][keyof Catalog[number]["routes"]]["wireModel"];
404
407
  /** Constructs deterministic capability data for hermetic serializer tests only. */
405
- export declare function createEnabledToolCapability(evidence: ToolEvidenceRef): Extract<ToolCapability, {
408
+ export declare function createEnabledToolCapability(evidence: ToolEvidenceRef, schemaProfile?: ToolSchemaProfile): Extract<ToolCapability, {
406
409
  readonly state: "enabled";
407
410
  }>;
408
411
  export declare function defineCatalog<T extends readonly {
package/dist/catalog.js CHANGED
@@ -1,4 +1,5 @@
1
1
  const ANSWER_RESERVE = 1024;
2
+ export const GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE = "gemini-parameters-json-schema";
2
3
  const CATALOG = freeze(defineCatalog([{
3
4
  publicId: "antigravity-gemini-3.8-flash",
4
5
  descriptor: {
@@ -8,7 +9,7 @@ const CATALOG = freeze(defineCatalog([{
8
9
  maxTokens: 65_536,
9
10
  },
10
11
  routes: {
11
- off: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: false }, tools: disabled("missing-direct-evidence") },
12
+ 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" }, GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE) },
12
13
  low: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
13
14
  medium: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "medium", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
14
15
  high: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "high", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
@@ -109,9 +110,12 @@ const lookup = new Map(CATALOG.map((entry) => [entry.publicId, entry]));
109
110
  function disabled(reason) {
110
111
  return { state: "disabled", contractRevision: 1, reason };
111
112
  }
113
+ function enabled(evidence, schemaProfile = GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE) {
114
+ return { state: "enabled", contractRevision: 1, schemaProfile, fixtureEvidence: evidence, directEvidence: evidence };
115
+ }
112
116
  /** Constructs deterministic capability data for hermetic serializer tests only. */
113
- export function createEnabledToolCapability(evidence) {
114
- return freeze({ state: "enabled", contractRevision: 1, fixtureEvidence: evidence, directEvidence: evidence });
117
+ export function createEnabledToolCapability(evidence, schemaProfile = GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE) {
118
+ return freeze({ state: "enabled", contractRevision: 1, schemaProfile, fixtureEvidence: evidence, directEvidence: evidence });
115
119
  }
116
120
  function freeze(value) {
117
121
  if (typeof value === "object" && value !== null) {
package/dist/context.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
2
2
  import { type GenerationSelection } from "./catalog.ts";
3
+ import { type ToolReplayDiagnostics } from "./tool-context.ts";
3
4
  interface Part {
4
5
  text: string;
5
6
  thought?: true;
@@ -9,6 +10,11 @@ interface Content {
9
10
  role: "user" | "model";
10
11
  parts: object[];
11
12
  }
13
+ interface GeminiFunctionDeclaration {
14
+ readonly name: string;
15
+ readonly description: string;
16
+ readonly parametersJsonSchema: object;
17
+ }
12
18
  export interface GenerationRequest {
13
19
  project: string;
14
20
  model: string;
@@ -18,7 +24,7 @@ export interface GenerationRequest {
18
24
  parts: Part[];
19
25
  };
20
26
  tools?: {
21
- functionDeclarations: unknown;
27
+ functionDeclarations: readonly GeminiFunctionDeclaration[];
22
28
  }[];
23
29
  toolConfig?: {
24
30
  functionCallingConfig: {
@@ -50,6 +56,6 @@ export interface SerializeTextContextInput {
50
56
  }
51
57
  export declare class ContextSerializationError extends Error {
52
58
  }
53
- export declare function serializeContext(input: SerializeTextContextInput, injectedSelection?: GenerationSelection, onRecovery?: (recoveryCount: number) => void): GenerationRequest;
59
+ export declare function serializeContext(input: SerializeTextContextInput, injectedSelection?: GenerationSelection, onDiagnostics?: (diagnostics: ToolReplayDiagnostics) => void): GenerationRequest;
54
60
  export declare function serializeTextContext(input: SerializeTextContextInput): GenerationRequest;
55
61
  export {};
package/dist/context.js CHANGED
@@ -6,7 +6,7 @@ const RESERVED_HEADERS = new Set(["authorization", "host", "content-type", "cont
6
6
  const THINKING_LEVELS = ["low", "medium", "high"];
7
7
  export class ContextSerializationError extends Error {
8
8
  }
9
- export function serializeContext(input, injectedSelection, onRecovery) {
9
+ export function serializeContext(input, injectedSelection, onDiagnostics) {
10
10
  const context = input.context;
11
11
  if (!hasToolContext(context))
12
12
  return serializeTextContext(input);
@@ -20,16 +20,25 @@ export function serializeContext(input, injectedSelection, onRecovery) {
20
20
  fail("Invalid tool capability selection.");
21
21
  if (selection.tools.state !== "enabled")
22
22
  fail(capabilityError(entry.publicId, selection.level, selection.tools));
23
- const prepared = prepareToolContext(field(context, "tools") ?? [], isRecord(options) ? field(options, "toolChoice") : undefined);
23
+ const prepared = prepareToolContext(field(context, "tools") ?? [], isRecord(options) ? field(options, "toolChoice") : undefined, selection.tools.schemaProfile);
24
24
  const { tools: _tools, messages, ...textContext } = context;
25
25
  const { toolChoice: _choice, ...textOptions } = isRecord(options) ? options : {};
26
26
  const text = serializeTextContext({ ...input, context: { ...textContext, messages: [{ role: "user", content: "placeholder", timestamp: 0 }] }, options: textOptions });
27
- const contents = replayToolHistory(isDenseArray(messages), (part, message) => messagePart(part, field(message, "role") === "assistant", field(message, "role") === "assistant" && entry.replay.kind === "same-public-model" && isSameProviderAndModel(message, entry.publicId)), onRecovery);
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);
28
34
  const { systemInstruction, generationConfig } = text.request;
29
35
  return { ...text, request: {
30
36
  contents,
31
37
  ...(systemInstruction ? { systemInstruction } : {}),
32
- ...(prepared ? { tools: [{ functionDeclarations: prepared.declarations }], toolConfig: { functionCallingConfig: { mode: prepared.mode } } } : {}),
38
+ ...(prepared ? {
39
+ tools: [{ functionDeclarations: prepared.declarations.map(({ name, description, parameters }) => ({ name, description, parametersJsonSchema: parameters })) }],
40
+ ...(prepared.mode === "NONE" ? { toolConfig: { functionCallingConfig: { mode: "NONE" } } } : {}),
41
+ } : {}),
33
42
  generationConfig,
34
43
  } };
35
44
  }
@@ -172,6 +181,10 @@ function resolveOutputTokens(maxTokens, maxAllowed, thinking) {
172
181
  }
173
182
  return thinking.kind === "budget" && thinking.budget > 0 ? Math.max(4096, thinking.budget + 1024) : 4096;
174
183
  }
184
+ function geminiRequiresSignedToolReplay(wireModel) {
185
+ const match = /^gemini-(\d+)(?:[.-])/.exec(wireModel);
186
+ return match !== null && Number(match[1]) >= 3;
187
+ }
175
188
  function validThoughtSignature(value) {
176
189
  if (typeof value !== "string" || !value || value.length % 4 !== 0)
177
190
  return undefined;
package/dist/response.js CHANGED
@@ -83,9 +83,18 @@ function candidate(value, events, semantics) {
83
83
  const parts = field(content, "parts");
84
84
  if (!Array.isArray(parts))
85
85
  throw new ResponseSemanticError("Antigravity returned invalid content.");
86
+ const explicitIds = new Set();
87
+ for (const part of parts) {
88
+ const call = field(part, "functionCall");
89
+ if (call !== undefined) {
90
+ const id = field(call, "id");
91
+ if (typeof id === "string")
92
+ explicitIds.add(id);
93
+ }
94
+ }
86
95
  for (const part of parts) {
87
96
  if (field(part, "functionCall") !== undefined) {
88
- const call = toolCall(part, semantics, ids, callCount++);
97
+ const call = toolCall(part, semantics, ids, explicitIds, callCount++);
89
98
  ids.push(call.id);
90
99
  local.push(call);
91
100
  }
@@ -102,8 +111,8 @@ function candidate(value, events, semantics) {
102
111
  const finishReason = field(item, "finishReason");
103
112
  if (finishReason !== undefined) {
104
113
  const hasCalls = semantics.callIndex() + callCount > 0;
105
- const reason = finishReason === "STOP" ? "stop" : finishReason === "MAX_TOKENS" ? "length" : finishReason === "OTHER" && hasCalls ? "toolUse" : undefined;
106
- if (!reason || (hasCalls && reason !== "toolUse"))
114
+ const reason = hasCalls ? "toolUse" : finishReason === "STOP" ? "stop" : finishReason === "MAX_TOKENS" ? "length" : undefined;
115
+ if (!reason)
107
116
  throw new ResponseSemanticError("Antigravity returned an unsupported finish reason.");
108
117
  semantics.setFinish(reason);
109
118
  }
@@ -119,13 +128,14 @@ function candidate(value, events, semantics) {
119
128
  events.push(event);
120
129
  }
121
130
  }
122
- function toolCall(part, semantics, ids, offset) {
131
+ function toolCall(part, semantics, ids, explicitIds, offset) {
123
132
  if (semantics.policy.kind !== "accept" || keys(part, ["functionCall", "thoughtSignature"]))
124
133
  throw new ResponseSemanticError("Antigravity returned unsupported content.");
125
134
  const call = field(part, "functionCall");
126
135
  if (keys(call, ["id", "name", "args"]))
127
136
  throw new ResponseSemanticError("Antigravity returned unsupported content.");
128
- const id = field(call, "id");
137
+ const suppliedId = field(call, "id");
138
+ const id = suppliedId === undefined ? localCallId(semantics, ids, explicitIds, offset) : suppliedId;
129
139
  const name = field(call, "name");
130
140
  const args = field(call, "args");
131
141
  if (typeof id !== "string" || !id.trim() || typeof name !== "string" || !semantics.policy.declaredNames.has(name) || semantics.hasId(id) || ids.includes(id))
@@ -146,6 +156,15 @@ function toolCall(part, semantics, ids, offset) {
146
156
  const signature = validThoughtSignature(field(part, "thoughtSignature"));
147
157
  return { type: "toolCall", callIndex: semantics.callIndex() + offset, id, name, arguments: argumentsValue, argumentsJson, ...(signature ? { signature } : {}) };
148
158
  }
159
+ function localCallId(semantics, ids, explicitIds, offset) {
160
+ let suffix = semantics.callIndex() + offset + 1;
161
+ let id = `pi-gemini-call-${suffix}`;
162
+ while (semantics.hasId(id) || ids.includes(id) || explicitIds.has(id)) {
163
+ suffix++;
164
+ id = `pi-gemini-call-${suffix}`;
165
+ }
166
+ return id;
167
+ }
149
168
  function depth(value, level = 0) {
150
169
  let maximum = level;
151
170
  for (const item of Object.values(value))
package/dist/stream.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Context, Model, SimpleStreamOptions } from "@earendil-works/pi-ai";
2
2
  import { type GenerationSelection } from "./catalog.ts";
3
+ import { type ToolReplayMode } from "./tool-context.ts";
3
4
  import type { ResponseSemantic } from "./response.ts";
4
5
  type StreamErrorKind = "aborted" | "access" | "capability" | "model" | "quota" | "preflight" | "response" | "transport" | "callback";
5
6
  export interface ToolStreamDiagnostics {
@@ -9,7 +10,17 @@ export interface ToolStreamDiagnostics {
9
10
  readonly preflight?: "accepted" | string;
10
11
  readonly preflightCategory?: string;
11
12
  readonly preflightPath?: string;
12
- readonly recoveryCount?: number;
13
+ readonly replayMode: ToolReplayMode;
14
+ readonly recoveryCount: number;
15
+ readonly userMessageCount: number;
16
+ readonly assistantMessageCount: number;
17
+ readonly toolResultMessageCount: number;
18
+ readonly assistantToolCallBlockCount: number;
19
+ readonly declaredToolCount: number;
20
+ readonly failure?: {
21
+ readonly kind: StreamErrorKind;
22
+ readonly status?: number;
23
+ };
13
24
  }
14
25
  type ToolDiagnosticSemantic = {
15
26
  type: "toolDiagnostics";
package/dist/stream.js CHANGED
@@ -55,7 +55,7 @@ export function createPiLifecycleStream(input) {
55
55
  currentBlock = undefined;
56
56
  };
57
57
  let removeAbort = () => { };
58
- const finalize = (reason, errorMessage, details) => {
58
+ const finalize = (reason, errorMessage, details, error) => {
59
59
  if (complete)
60
60
  return;
61
61
  complete = true;
@@ -69,8 +69,9 @@ export function createPiLifecycleStream(input) {
69
69
  }
70
70
  output.stopReason = reason;
71
71
  const diagnostic = details ?? toolDiagnostics;
72
+ const failure = error && isLocalStreamError(error) ? diagnosticFailure(error) : undefined;
72
73
  if (sawToolCall || diagnostic)
73
- output.diagnostics = [{ type: "antigravity-guard.tools", timestamp: input.now(), details: { ...diagnostic, terminal: reason } }];
74
+ output.diagnostics = [{ type: "antigravity-guard.tools", timestamp: input.now(), details: { ...diagnostic, ...(failure ? { failure } : {}), terminal: reason } }];
74
75
  if (reason === "stop" || reason === "length" || reason === "toolUse") {
75
76
  closeBlock();
76
77
  stream.push({ type: "done", reason, message: output });
@@ -153,7 +154,7 @@ export function createPiLifecycleStream(input) {
153
154
  };
154
155
  stream.push({ type: "start", partial: output });
155
156
  if (input.signal) {
156
- const abort = () => finalize("aborted");
157
+ const abort = () => finalize("aborted", undefined, undefined, streamError("aborted", "Generation was cancelled."));
157
158
  input.signal.addEventListener("abort", abort, { once: true });
158
159
  removeAbort = () => input.signal?.removeEventListener("abort", abort);
159
160
  if (input.signal.aborted)
@@ -161,7 +162,7 @@ export function createPiLifecycleStream(input) {
161
162
  }
162
163
  if (!complete)
163
164
  void Promise.resolve().then(() => input.runTransport({ onSemantic, signal })).then(() => { if (!complete)
164
- finalize(signal.aborted ? "aborted" : "error"); }, (error) => finalize(signal.aborted || input.signal?.aborted ? "aborted" : "error", isLocalStreamError(error) ? error.message : undefined, isLocalStreamError(error) ? error.details : undefined));
165
+ finalize(signal.aborted ? "aborted" : "error"); }, (error) => finalize(signal.aborted || input.signal?.aborted ? "aborted" : "error", isLocalStreamError(error) ? error.message : undefined, isLocalStreamError(error) ? error.details : undefined, isLocalStreamError(error) ? error : undefined));
165
166
  return stream;
166
167
  }
167
168
  function isContentSemantic(semantic) {
@@ -173,13 +174,13 @@ export async function executeStreamTransport(input) {
173
174
  const headers = requestHeaders(input);
174
175
  let responseBody = null;
175
176
  let selection;
176
- let recoveryCount = 0;
177
+ let replayDiagnostics = emptyReplayDiagnostics();
177
178
  try {
178
179
  const entry = getCatalogEntry(input.model.id);
179
180
  selection = input.selection ?? resolveGenerationSelection(entry, input.generationOptions?.reasoning);
180
- const original = serializeContext({ context: input.context, model: input.model, options: input.generationOptions, project: input.projectId, requestId: input.requestId }, selection, (count) => { recoveryCount = count; });
181
+ const original = serializeContext({ context: input.context, model: input.model, options: input.generationOptions, project: input.projectId, requestId: input.requestId }, selection, (diagnostics) => { replayDiagnostics = diagnostics; });
181
182
  if (hasToolContext(input.context))
182
- await deliver(input.onSemantic, { type: "toolDiagnostics", details: toolDiagnostics(entry.publicId, selection, recoveryCount, "accepted") });
183
+ await deliver(input.onSemantic, { type: "toolDiagnostics", details: toolDiagnostics(entry.publicId, selection, replayDiagnostics, "accepted") });
183
184
  const payload = await payloadHook(input, original, signal);
184
185
  const response = await abortable(input.fetch(ENDPOINT, { method: "POST", redirect: "error", headers, body: JSON.stringify(payload), signal }), signal);
185
186
  await responseHook(input, response, signal);
@@ -199,11 +200,11 @@ export async function executeStreamTransport(input) {
199
200
  if (signal.aborted)
200
201
  throw streamError("aborted", "Generation was cancelled.");
201
202
  if (error instanceof ToolPreflightError && selection) {
202
- const details = toolDiagnostics(getCatalogEntry(input.model.id).publicId, selection, recoveryCount, error.code, error.path);
203
+ const details = toolDiagnostics(getCatalogEntry(input.model.id).publicId, selection, replayDiagnostics, error.code, error.path);
203
204
  throw streamError("preflight", error.message, undefined, details);
204
205
  }
205
206
  if (error instanceof ContextSerializationError && error.message.startsWith("PI_TOOL_CAPABILITY_NOT_ENABLED") && selection) {
206
- throw streamError("capability", error.message, undefined, toolDiagnostics(getCatalogEntry(input.model.id).publicId, selection, recoveryCount, "PI_TOOL_CAPABILITY_NOT_ENABLED"));
207
+ throw streamError("capability", error.message, undefined, toolDiagnostics(getCatalogEntry(input.model.id).publicId, selection, replayDiagnostics, "PI_TOOL_CAPABILITY_NOT_ENABLED"));
207
208
  }
208
209
  if (error instanceof SseFrameError || error instanceof ResponseSemanticError)
209
210
  throw streamError("response", "Antigravity returned an invalid stream.");
@@ -214,13 +215,31 @@ function validateInput(input) {
214
215
  if (!getCatalogEntry(input.model.id) || input.model.api !== API)
215
216
  throw streamError("response", "The selected Antigravity model or API is unsupported.");
216
217
  }
217
- function toolDiagnostics(publicModelId, selection, recoveryCount, preflight, preflightPath) {
218
+ function toolDiagnostics(publicModelId, selection, replayDiagnostics, preflight, preflightPath) {
218
219
  return {
219
220
  publicModelId,
220
221
  reasoning: selection.level,
221
222
  capabilityState: selection.tools.state,
223
+ replayMode: replayDiagnostics.replayMode,
224
+ recoveryCount: replayDiagnostics.recoveryCount,
225
+ userMessageCount: replayDiagnostics.userMessageCount,
226
+ assistantMessageCount: replayDiagnostics.assistantMessageCount,
227
+ toolResultMessageCount: replayDiagnostics.toolResultMessageCount,
228
+ assistantToolCallBlockCount: replayDiagnostics.assistantToolCallBlockCount,
229
+ declaredToolCount: replayDiagnostics.declaredToolCount,
222
230
  preflight,
223
- ...(preflight === "accepted" ? { recoveryCount } : { preflightCategory: preflightCategory(preflight), ...(preflightPath ? { preflightPath } : {}) }),
231
+ ...(preflight === "accepted" ? {} : { preflightCategory: preflightCategory(preflight), ...(preflightPath ? { preflightPath } : {}) }),
232
+ };
233
+ }
234
+ function emptyReplayDiagnostics() {
235
+ return {
236
+ replayMode: "none",
237
+ recoveryCount: 0,
238
+ userMessageCount: 0,
239
+ assistantMessageCount: 0,
240
+ toolResultMessageCount: 0,
241
+ assistantToolCallBlockCount: 0,
242
+ declaredToolCount: 0,
224
243
  };
225
244
  }
226
245
  function preflightCategory(code) {
@@ -236,9 +255,9 @@ function preflightCategory(code) {
236
255
  }
237
256
  function responsePolicy(payload, selection) {
238
257
  const declarations = payload.request.tools?.[0]?.functionDeclarations;
239
- if (selection.tools.state !== "enabled" || payload.request.toolConfig?.functionCallingConfig.mode !== "AUTO" || !Array.isArray(declarations))
258
+ if (selection.tools.state !== "enabled" || payload.request.toolConfig?.functionCallingConfig.mode === "NONE" || !Array.isArray(declarations))
240
259
  return { kind: "reject" };
241
- const names = declarations.map((item) => typeof item === "object" && item !== null ? Object.getOwnPropertyDescriptor(item, "name")?.value : undefined);
260
+ const names = declarations.map((declaration) => Object.getOwnPropertyDescriptor(declaration, "name")?.value);
242
261
  return names.every((name) => typeof name === "string") ? { kind: "accept", declaredNames: new Set(names) } : { kind: "reject" };
243
262
  }
244
263
  async function payloadHook(input, payload, signal) {
@@ -364,6 +383,18 @@ function isSse(value) { return value?.split(";", 1)[0]?.trim().toLowerCase() ===
364
383
  function inactivityTimeout(input) {
365
384
  return input.inactivityTimeoutMs === undefined ? INACTIVITY_TIMEOUT_MS : input.inactivityTimeoutMs > 0 && Number.isFinite(input.inactivityTimeoutMs) ? Math.min(input.inactivityTimeoutMs, INACTIVITY_TIMEOUT_MS) : 0;
366
385
  }
386
+ function diagnosticFailure(error) {
387
+ if (!isStreamErrorKind(error.kind))
388
+ return undefined;
389
+ const status = error.status;
390
+ return {
391
+ kind: error.kind,
392
+ ...(typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? { status } : {}),
393
+ };
394
+ }
395
+ function isStreamErrorKind(value) {
396
+ return value === "aborted" || value === "access" || value === "capability" || value === "model" || value === "quota" || value === "preflight" || value === "response" || value === "transport" || value === "callback";
397
+ }
367
398
  function streamError(kind, message, status, details) {
368
399
  const error = new StreamTransportError(kind, message, status, details);
369
400
  LOCAL_ERRORS.add(error);
@@ -4,11 +4,26 @@ export interface PreparedToolContext {
4
4
  readonly mode: "AUTO" | "NONE";
5
5
  }
6
6
  export declare function hasToolContext(context: unknown): boolean;
7
- export declare function prepareToolContext(tools: unknown, choice: unknown): PreparedToolContext | undefined;
7
+ export declare function prepareToolContext(tools: unknown, choice: unknown, schemaProfile: unknown): PreparedToolContext | undefined;
8
8
  type WirePart = object;
9
9
  type WireContent = {
10
10
  role: "user" | "model";
11
11
  parts: WirePart[];
12
12
  };
13
- export declare function replayToolHistory(messages: unknown[], serializePart: (part: Record<string, unknown>, message: Record<string, unknown>) => WirePart, onRecovery?: (recoveryCount: number) => void): WireContent[];
13
+ export type ToolReplayMode = "none" | "signed-function-response" | "unsigned-observation";
14
+ export interface ToolReplayDiagnostics {
15
+ readonly replayMode: ToolReplayMode;
16
+ readonly recoveryCount: number;
17
+ readonly userMessageCount: number;
18
+ readonly assistantMessageCount: number;
19
+ readonly toolResultMessageCount: number;
20
+ readonly assistantToolCallBlockCount: number;
21
+ readonly declaredToolCount: number;
22
+ }
23
+ export interface ToolReplayPolicy {
24
+ readonly requireSignedToolCalls: boolean;
25
+ readonly isSameModel: (message: Record<string, unknown>) => boolean;
26
+ readonly toolCallSignature: (part: Record<string, unknown>, message: Record<string, unknown>) => string | undefined;
27
+ }
28
+ export declare function replayToolHistory(messages: unknown[], serializePart: (part: Record<string, unknown>, message: Record<string, unknown>) => WirePart, onDiagnostics?: (diagnostics: ToolReplayDiagnostics) => void, policy?: ToolReplayPolicy, declaredToolCount?: number): WireContent[];
14
29
  export {};
@@ -1,4 +1,5 @@
1
1
  import { canonicalJson, ToolPreflightError } from "./tool-contract.js";
2
+ import { GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE } from "./catalog.js";
2
3
  import { normalizeToolDeclarations } from "./tool-schema.js";
3
4
  export function hasToolContext(context) {
4
5
  if (!isRecord(context))
@@ -16,7 +17,9 @@ export function hasToolContext(context) {
16
17
  return Array.isArray(content) && content.some((part) => isRecord(part) && value(part, "type") === "toolCall");
17
18
  });
18
19
  }
19
- export function prepareToolContext(tools, choice) {
20
+ export function prepareToolContext(tools, choice, schemaProfile) {
21
+ if (schemaProfile !== GEMINI_PARAMETERS_JSON_SCHEMA_PROFILE)
22
+ throw new ToolPreflightError("PI_TOOL_SCHEMA_PROFILE_UNSUPPORTED", "tools", "$");
20
23
  const declarations = normalizeToolDeclarations(tools ?? []);
21
24
  if (!declarations.length) {
22
25
  if (choice === "auto")
@@ -29,27 +32,45 @@ export function prepareToolContext(tools, choice) {
29
32
  unsupportedChoice();
30
33
  return { declarations, mode: choice === "none" ? "NONE" : "AUTO" };
31
34
  }
32
- export function replayToolHistory(messages, serializePart, onRecovery) {
35
+ export function replayToolHistory(messages, serializePart, onDiagnostics, policy, declaredToolCount = 0) {
33
36
  const output = [];
34
37
  const calls = new Set();
35
38
  const results = new Set();
36
39
  const recoveryCount = { value: 0 };
40
+ const shape = {
41
+ userMessageCount: 0,
42
+ assistantMessageCount: 0,
43
+ toolResultMessageCount: 0,
44
+ assistantToolCallBlockCount: 0,
45
+ declaredToolCount: safeCount(declaredToolCount),
46
+ };
47
+ let replayMode = "none";
37
48
  let pending;
49
+ const appendTurn = (role, parts) => {
50
+ if (!parts.length)
51
+ return;
52
+ const last = output.at(-1);
53
+ if (last?.role === role)
54
+ last.parts.push(...parts);
55
+ else
56
+ output.push({ role, parts });
57
+ };
38
58
  const finalize = () => {
39
59
  if (!pending)
40
60
  return;
41
- output.push({ role: "user", parts: pending.calls.map((call) => {
42
- const actual = pending.results.get(call.id);
43
- if (actual)
44
- return actual;
45
- recoveryCount.value += 1;
46
- return missingResult(call);
47
- }) });
61
+ appendTurn("user", pending.calls.map((call) => {
62
+ const actual = pending.results.get(call.id);
63
+ if (actual)
64
+ return actual;
65
+ recoveryCount.value += 1;
66
+ return pending.observations ? observation(call, missingResultText()) : missingResult(call);
67
+ }));
48
68
  pending = undefined;
49
69
  };
50
70
  for (const message of messages) {
51
71
  const current = record(message);
52
72
  if (value(current, "role") === "toolResult") {
73
+ shape.toolResultMessageCount += 1;
53
74
  const id = nonempty(value(current, "toolCallId"), "PI_TOOL_RESULT_FOREIGN");
54
75
  if (results.has(id))
55
76
  history("PI_TOOL_RESULT_DUPLICATE");
@@ -68,8 +89,8 @@ export function replayToolHistory(messages, serializePart, onRecovery) {
68
89
  history("PI_TOOL_RESULT_MEDIA_UNSUPPORTED");
69
90
  return value(part, "text");
70
91
  }).join("\n\n");
71
- const response = value(current, "isError") === true ? { error: text } : { result: text };
72
- pending.results.set(id, { functionResponse: { name: call.name, id, response } });
92
+ const response = value(current, "isError") === true ? { error: text } : { output: text };
93
+ pending.results.set(id, pending.observations ? observation(call, text) : { functionResponse: { name: call.name, response } });
73
94
  results.add(id);
74
95
  continue;
75
96
  }
@@ -77,53 +98,82 @@ export function replayToolHistory(messages, serializePart, onRecovery) {
77
98
  const role = value(current, "role");
78
99
  if (role !== "user" && role !== "assistant")
79
100
  history("PI_TOOL_CALL_INVALID");
101
+ if (role === "user")
102
+ shape.userMessageCount += 1;
103
+ else
104
+ shape.assistantMessageCount += 1;
80
105
  const rawContent = value(current, "content");
81
106
  if (typeof rawContent === "string") {
82
107
  if (!rawContent)
83
108
  history("PI_TOOL_CALL_INVALID");
84
- output.push({ role: role === "assistant" ? "model" : "user", parts: [{ text: rawContent }] });
109
+ appendTurn(role === "assistant" ? "model" : "user", [{ text: rawContent }]);
85
110
  continue;
86
111
  }
87
112
  const content = dense(rawContent);
88
113
  const hasCalls = role === "assistant" && content.some((part) => value(record(part), "type") === "toolCall");
89
114
  if (!hasCalls) {
90
- output.push({ role: role === "assistant" ? "model" : "user", parts: content.map((part) => serializePart(record(part), current)) });
115
+ appendTurn(role === "assistant" ? "model" : "user", content.map((part) => serializePart(record(part), current)));
91
116
  continue;
92
117
  }
93
118
  if (value(current, "stopReason") !== "toolUse")
94
119
  history("PI_TOOL_CALL_INVALID");
95
120
  const group = [];
96
- const parts = content.map((item) => {
121
+ const parts = content.flatMap((item) => {
97
122
  const part = record(item);
98
- if (value(part, "type") !== "toolCall")
99
- return serializePart(part, current);
123
+ if (value(part, "type") !== "toolCall") {
124
+ if (isEmptyUnsignedText(part))
125
+ return [];
126
+ return [serializePart(part, current)];
127
+ }
128
+ shape.assistantToolCallBlockCount += 1;
100
129
  const id = nonempty(value(part, "id"), "PI_TOOL_CALL_INVALID");
101
130
  const name = nonempty(value(part, "name"), "PI_TOOL_CALL_INVALID");
102
131
  if (!/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) || calls.has(id))
103
132
  history(calls.has(id) ? "PI_TOOL_CALL_DUPLICATE" : "PI_TOOL_CALL_INVALID");
104
133
  const args = canonicalJson(value(part, "arguments"), name, "$.arguments");
105
- if (Array.isArray(args) || args === null)
134
+ if (typeof args !== "object" || Array.isArray(args) || args === null)
106
135
  history("PI_TOOL_CALL_INVALID");
107
136
  calls.add(id);
108
- group.push({ id, name });
109
- return { functionCall: { name, args, id } };
137
+ const call = { id, name, args };
138
+ group.push(call);
139
+ return [{ call, signature: policy?.toolCallSignature(part, current), signaturePresent: value(part, "thoughtSignature") !== undefined }];
110
140
  });
111
- output.push({ role: "model", parts });
112
- pending = { calls: group, results: new Map() };
141
+ const toolParts = parts.filter((part) => "call" in part);
142
+ const callsAreSigned = policy?.requireSignedToolCalls === true && policy.isSameModel(current) && Boolean(toolParts[0]?.signature) && toolParts.every((part) => !part.signaturePresent || Boolean(part.signature));
143
+ const observations = policy?.requireSignedToolCalls === true && !callsAreSigned;
144
+ if (observations)
145
+ replayMode = "unsigned-observation";
146
+ else if (replayMode === "none" && callsAreSigned)
147
+ replayMode = "signed-function-response";
148
+ appendTurn("model", parts.flatMap((part) => {
149
+ if (!("call" in part))
150
+ return [part];
151
+ if (observations)
152
+ return [];
153
+ return [{ functionCall: { name: part.call.name, args: part.call.args }, ...(part.signature ? { thoughtSignature: part.signature } : {}) }];
154
+ }));
155
+ pending = { calls: group, results: new Map(), observations };
113
156
  }
114
157
  finalize();
115
- onRecovery?.(recoveryCount.value);
158
+ onDiagnostics?.({ replayMode, recoveryCount: recoveryCount.value, ...shape });
116
159
  return output;
117
160
  }
161
+ function observation(call, text) {
162
+ const args = JSON.stringify(call.args);
163
+ const label = args === "{}" ? `\`${call.name}\`` : `\`${call.name}\` (${args})`;
164
+ return { text: `[Observation from ${label}:\n${text}]` };
165
+ }
166
+ function missingResultText() {
167
+ return "Tool execution did not complete or its result was not recorded. Treat the call as failed; do not assume it had no side effects and do not retry it automatically.";
168
+ }
118
169
  function missingResult(call) {
119
170
  return {
120
171
  functionResponse: {
121
172
  name: call.name,
122
- id: call.id,
123
173
  response: {
124
174
  error: {
125
175
  code: "PI_TOOL_RESULT_MISSING",
126
- message: "Tool execution did not complete or its result was not recorded. Treat the call as failed; do not assume it had no side effects and do not retry it automatically.",
176
+ message: missingResultText(),
127
177
  },
128
178
  },
129
179
  },
@@ -142,11 +192,18 @@ function dense(value) {
142
192
  history("PI_TOOL_CALL_INVALID");
143
193
  return value;
144
194
  }
195
+ function isEmptyUnsignedText(part) {
196
+ const text = value(part, "text");
197
+ return value(part, "type") === "text" && typeof text === "string" && !text.trim() && value(part, "textSignature") === undefined;
198
+ }
145
199
  function nonempty(value, code) {
146
200
  if (typeof value !== "string" || !value.trim())
147
201
  history(code);
148
202
  return value;
149
203
  }
204
+ function safeCount(value) {
205
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
206
+ }
150
207
  function unsupportedChoice() {
151
208
  throw new ToolPreflightError("PI_TOOL_CHOICE_UNSUPPORTED", "tools", "$");
152
209
  }
@@ -1,10 +1,12 @@
1
- import { canonicalJson, denseArray, fail, freeze, ownData, plainObject } from "./tool-contract.js";
2
- const TYPES = new Set(["object", "array", "string", "number", "integer", "boolean"]);
3
- const KEYWORDS = new Set(["type", "description", "nullable", "enum", "const", "properties", "required", "items"]);
1
+ import { denseArray, fail, freeze, ownData, plainObject } from "./tool-contract.js";
4
2
  const MAX_SCHEMA_DEPTH = 32;
5
3
  const MAX_SCHEMA_NODES = 2048;
6
4
  const MAX_SCHEMA_BYTES = 256 * 1024;
7
5
  const MAX_TOTAL_SCHEMA_BYTES = 1024 * 1024;
6
+ const METADATA = new Set(["$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", "$comment"]);
7
+ const SCHEMA_MAPS = new Set(["properties", "patternProperties", "dependentSchemas"]);
8
+ const SCHEMAS = new Set(["additionalItems", "additionalProperties", "contains", "contentSchema", "else", "if", "items", "not", "propertyNames", "then", "unevaluatedItems", "unevaluatedProperties"]);
9
+ const SCHEMA_ARRAYS = new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
8
10
  export function normalizeToolDeclarations(value) {
9
11
  const tools = denseArray(value, "tools", "$");
10
12
  const names = new Set();
@@ -32,94 +34,179 @@ function normalizeDeclaration(value, index, names) {
32
34
  const constrainedSampling = ownData(tool, "constrainedSampling", name, path);
33
35
  if (constrainedSampling !== undefined && constrainedSampling !== false)
34
36
  fail("PI_TOOL_CONSTRAINED_SAMPLING_UNSUPPORTED", name, `${path}.constrainedSampling`);
35
- const parameterRoot = plainObject(ownData(tool, "parameters", name, path), name, "$");
36
- if (ownData(parameterRoot, "type", name, "$") !== "object")
37
+ const root = plainObject(ownData(tool, "parameters", name, path), name, "$");
38
+ if (ownData(root, "type", name, "$") !== "object")
39
+ fail("PI_TOOL_SCHEMA_INVALID", name, "$.type");
40
+ const snapshot = snapshotJson(root, name, "$", { ancestors: new Set(), nodes: 0 });
41
+ if (!isJsonObject(snapshot))
42
+ fail("PI_TOOL_SCHEMA_INVALID", name, "$");
43
+ const parameters = emitSchema(snapshot, snapshot, name, "$", { nodes: 0 }, new Set());
44
+ if (!isJsonObject(parameters) || parameters.type !== "object")
37
45
  fail("PI_TOOL_SCHEMA_INVALID", name, "$.type");
38
- const parameters = normalizeSchema(parameterRoot, name, "$");
39
46
  if (Buffer.byteLength(JSON.stringify(parameters), "utf8") > MAX_SCHEMA_BYTES)
40
47
  fail("PI_TOOL_SCHEMA_LIMIT", name, "$");
41
48
  return freeze({ name, description, parameters });
42
49
  }
43
- function normalizeSchema(value, declaration, path, state = { ancestors: new Set(), nodes: 0 }, depth = 0) {
50
+ function snapshotJson(value, declaration, path, state, depth = 0) {
44
51
  if (depth > MAX_SCHEMA_DEPTH || ++state.nodes > MAX_SCHEMA_NODES)
45
52
  fail("PI_TOOL_SCHEMA_LIMIT", declaration, path);
46
- if (typeof value === "object" && value !== null) {
47
- if (state.ancestors.has(value))
48
- fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
49
- state.ancestors.add(value);
53
+ if (value === null || typeof value === "string" || typeof value === "boolean")
54
+ return value;
55
+ if (typeof value === "number")
56
+ return Number.isFinite(value) ? value : fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
57
+ if (typeof value !== "object" || state.ancestors.has(value))
58
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
59
+ state.ancestors.add(value);
60
+ try {
61
+ if (Array.isArray(value))
62
+ return freeze(snapshotArray(value, declaration, path, state, depth));
63
+ const object = plainObject(value, declaration, path);
64
+ const output = {};
65
+ for (const key of Object.keys(object).sort())
66
+ define(output, key, snapshotJson(ownData(object, key, declaration, path), declaration, pathSegment(path, key), state, depth + 1));
67
+ return freeze(output);
50
68
  }
51
- const schema = plainObject(value, declaration, path);
52
- for (const key of Object.keys(schema))
53
- if (!KEYWORDS.has(key))
54
- fail("PI_TOOL_SCHEMA_UNSUPPORTED", declaration, `${path}.${key}`);
55
- const type = ownData(schema, "type", declaration, path);
56
- if (typeof type !== "string" || !TYPES.has(type))
57
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.type`);
58
- const output = { type };
59
- const description = ownData(schema, "description", declaration, path);
60
- if (description !== undefined) {
61
- if (typeof description !== "string")
62
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.description`);
63
- output.description = description;
69
+ finally {
70
+ state.ancestors.delete(value);
64
71
  }
65
- const nullable = ownData(schema, "nullable", declaration, path);
66
- if (nullable !== undefined) {
67
- if (typeof nullable !== "boolean")
68
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.nullable`);
69
- output.nullable = nullable;
72
+ }
73
+ function snapshotArray(value, declaration, path, state, depth) {
74
+ if (Object.getPrototypeOf(value) !== Array.prototype || Object.getOwnPropertySymbols(value).length || Object.getOwnPropertyNames(value).length !== value.length + 1)
75
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
76
+ const values = denseArray(value, declaration, path);
77
+ const output = [];
78
+ for (let index = 0; index < values.length; index += 1) {
79
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
80
+ if (!descriptor || !("value" in descriptor))
81
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}[${index}]`);
82
+ output.push(snapshotJson(descriptor.value, declaration, `${path}[${index}]`, state, depth + 1));
70
83
  }
71
- const enumValue = ownData(schema, "enum", declaration, path);
72
- const constant = ownData(schema, "const", declaration, path);
73
- if (enumValue !== undefined && constant !== undefined)
74
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.const`);
75
- if (enumValue !== undefined)
76
- output.enum = normalizeEnum(enumValue, type, nullable === true, declaration, `${path}.enum`);
77
- if (constant !== undefined)
78
- output.enum = freeze([validateEnumValue(constant, type, nullable === true, declaration, `${path}.const`)]);
79
- if (type === "object")
80
- normalizeObject(schema, output, declaration, path, state, depth);
81
- else if (type === "array")
82
- normalizeArray(schema, output, declaration, path, state, depth);
83
- else if (ownData(schema, "properties", declaration, path) !== undefined || ownData(schema, "required", declaration, path) !== undefined || ownData(schema, "items", declaration, path) !== undefined)
84
+ return output;
85
+ }
86
+ function emitSchema(value, root, declaration, path, state, targets, depth = 0) {
87
+ if (depth > MAX_SCHEMA_DEPTH || ++state.nodes > MAX_SCHEMA_NODES)
88
+ fail("PI_TOOL_SCHEMA_LIMIT", declaration, path);
89
+ if (typeof value === "boolean")
90
+ return value;
91
+ if (!isJsonObject(value))
84
92
  fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
85
- return freeze(output);
93
+ const reference = value.$ref;
94
+ if (reference !== undefined) {
95
+ const referencePath = pathSegment(path, "$ref");
96
+ const target = resolveReference(reference, root, declaration, referencePath);
97
+ if (isJsonObject(target) && targets.has(target))
98
+ fail("PI_TOOL_SCHEMA_REFERENCE_CYCLE", declaration, referencePath);
99
+ const next = new Set(targets);
100
+ if (isJsonObject(target))
101
+ next.add(target);
102
+ const resolved = emitSchema(target, root, declaration, path, state, next, depth + 1);
103
+ const sibling = emitObject(value, root, declaration, path, state, targets, depth + 1, new Set(["$ref"]));
104
+ if (!Object.keys(sibling).length)
105
+ return resolved;
106
+ if (resolved === true || resolved === false)
107
+ return resolved === true ? sibling : false;
108
+ return freeze({ allOf: freeze([resolved, sibling]) });
109
+ }
110
+ return emitObject(value, root, declaration, path, state, targets, depth, new Set());
86
111
  }
87
- function normalizeObject(schema, output, declaration, path, state, depth) {
88
- if (ownData(schema, "items", declaration, path) !== undefined)
89
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.items`);
90
- const properties = plainObject(ownData(schema, "properties", declaration, path), declaration, `${path}.properties`);
91
- const names = Object.keys(properties).sort();
92
- if (!names.length)
93
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.properties`);
94
- const normalized = {};
95
- for (const name of names)
96
- normalized[name] = normalizeSchema(properties[name], declaration, `${path}.properties.${name}`, state, depth + 1);
97
- output.properties = freeze(normalized);
98
- const required = ownData(schema, "required", declaration, path);
99
- if (required !== undefined) {
100
- const values = denseArray(required, declaration, `${path}.required`);
101
- if (values.some((name) => typeof name !== "string" || !Object.hasOwn(properties, name)) || new Set(values).size !== values.length)
102
- fail("PI_TOOL_SCHEMA_INVALID", declaration, `${path}.required`);
103
- output.required = freeze([...values].sort());
112
+ function emitObject(value, root, declaration, path, state, targets, depth, omitted) {
113
+ const output = {};
114
+ for (const key of Object.keys(value)) {
115
+ const child = value[key];
116
+ if (omitted.has(key) || METADATA.has(key))
117
+ continue;
118
+ if (key === "$defs" || key === "definitions") {
119
+ emitSchemaMap(child, root, declaration, pathSegment(path, key), state, targets, depth + 1);
120
+ continue;
121
+ }
122
+ if (SCHEMA_MAPS.has(key))
123
+ define(output, key, emitSchemaMap(child, root, declaration, pathSegment(path, key), state, targets, depth + 1));
124
+ else if (SCHEMA_ARRAYS.has(key))
125
+ define(output, key, emitSchemaArray(child, root, declaration, pathSegment(path, key), state, targets, depth + 1));
126
+ else if (key === "dependencies")
127
+ define(output, key, emitDependencies(child, root, declaration, pathSegment(path, key), state, targets, depth + 1));
128
+ else if (key === "items" && Array.isArray(child))
129
+ define(output, key, emitSchemaArray(child, root, declaration, pathSegment(path, key), state, targets, depth + 1));
130
+ else if (SCHEMAS.has(key))
131
+ define(output, key, emitSchema(child, root, declaration, pathSegment(path, key), state, targets, depth + 1));
132
+ else
133
+ define(output, key, child);
104
134
  }
135
+ return freeze(output);
105
136
  }
106
- function normalizeArray(schema, output, declaration, path, state, depth) {
107
- if (ownData(schema, "properties", declaration, path) !== undefined || ownData(schema, "required", declaration, path) !== undefined)
137
+ function emitSchemaMap(value, root, declaration, path, state, targets, depth) {
138
+ if (!isJsonObject(value))
108
139
  fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
109
- output.items = normalizeSchema(ownData(schema, "items", declaration, path), declaration, `${path}.items`, state, depth + 1);
140
+ const output = {};
141
+ for (const key of Object.keys(value))
142
+ define(output, key, emitSchema(value[key], root, declaration, pathSegment(path, key), state, targets, depth));
143
+ return freeze(output);
110
144
  }
111
- function normalizeEnum(value, type, nullable, declaration, path) {
112
- const values = denseArray(value, declaration, path);
113
- if (!values.length)
114
- fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
115
- const normalized = values.map((item, index) => validateEnumValue(item, type, nullable, declaration, `${path}[${index}]`));
116
- if (new Set(normalized.map((item) => JSON.stringify(item))).size !== normalized.length)
145
+ function emitSchemaArray(value, root, declaration, path, state, targets, depth) {
146
+ if (!Array.isArray(value))
117
147
  fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
118
- return freeze(normalized);
148
+ return freeze(value.map((item, index) => emitSchema(item, root, declaration, `${path}[${index}]`, state, targets, depth)));
119
149
  }
120
- function validateEnumValue(value, type, nullable, declaration, path) {
121
- const json = canonicalJson(value, declaration, path);
122
- if (typeof json === "object" || (json === null && !nullable) || (json !== null && (type === "integer" ? typeof json !== "number" || !Number.isInteger(json) : typeof json !== type)))
150
+ function emitDependencies(value, root, declaration, path, state, targets, depth) {
151
+ if (!isJsonObject(value))
123
152
  fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
124
- return json;
153
+ const output = {};
154
+ for (const key of Object.keys(value)) {
155
+ const dependency = value[key];
156
+ if (Array.isArray(dependency) && dependency.every((entry) => typeof entry === "string"))
157
+ define(output, key, dependency);
158
+ else
159
+ define(output, key, emitSchema(dependency, root, declaration, pathSegment(path, key), state, targets, depth));
160
+ }
161
+ return freeze(output);
162
+ }
163
+ function resolveReference(reference, root, declaration, path) {
164
+ if (typeof reference !== "string" || (reference !== "#" && !reference.startsWith("#/")))
165
+ fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
166
+ if (reference === "#")
167
+ return root;
168
+ let pointer;
169
+ try {
170
+ pointer = decodeURIComponent(reference.slice(1));
171
+ }
172
+ catch {
173
+ return fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
174
+ }
175
+ let current = root;
176
+ for (const rawToken of pointer.slice(1).split("/")) {
177
+ const token = decodeToken(rawToken, declaration, path);
178
+ if (Array.isArray(current)) {
179
+ if (!/^(0|[1-9][0-9]*)$/.test(token) || !Number.isSafeInteger(Number(token)) || Number(token) >= current.length)
180
+ fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
181
+ current = current[Number(token)];
182
+ }
183
+ else if (isJsonObject(current)) {
184
+ if (!Object.hasOwn(current, token))
185
+ fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
186
+ current = current[token];
187
+ }
188
+ else
189
+ fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
190
+ }
191
+ if (typeof current !== "boolean" && !isJsonObject(current))
192
+ fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
193
+ return current;
194
+ }
195
+ function decodeToken(value, declaration, path) {
196
+ return value.replace(/~(.)?/g, (escape, next) => {
197
+ if (escape === "~0")
198
+ return "~";
199
+ if (escape === "~1")
200
+ return "/";
201
+ return fail("PI_TOOL_SCHEMA_REFERENCE_INVALID", declaration, path);
202
+ });
203
+ }
204
+ function pathSegment(path, key) {
205
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `${path}.${key}` : `${path}[${JSON.stringify(key)}]`;
206
+ }
207
+ function isJsonObject(value) {
208
+ return typeof value === "object" && value !== null && !Array.isArray(value);
209
+ }
210
+ function define(target, key, value) {
211
+ Object.defineProperty(target, key, { value, enumerable: true, configurable: false, writable: false });
125
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjamolina/pi-antigravity-guard",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Pi adapter for Antigravity Guard",
5
5
  "type": "module",
6
6
  "license": "MIT",