@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 +10 -6
- package/dist/catalog.d.ts +4 -1
- package/dist/catalog.js +7 -3
- package/dist/context.d.ts +8 -2
- package/dist/context.js +17 -4
- package/dist/response.js +24 -5
- package/dist/stream.d.ts +12 -1
- package/dist/stream.js +44 -13
- package/dist/tool-context.d.ts +17 -2
- package/dist/tool-context.js +81 -24
- package/dist/tool-schema.js +161 -74
- package/package.json +1 -1
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.
|
|
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 |
|
|
19
|
+
| Public ID | Exposed Pi levels | Tool state | Admission |
|
|
20
20
|
|---|---|---|---|
|
|
21
|
-
| `antigravity-gemini-3.8-flash` | off, low, medium, high |
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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 ? {
|
|
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 =
|
|
106
|
-
if (!reason
|
|
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
|
|
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
|
|
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
|
|
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, (
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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" ? {
|
|
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
|
|
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((
|
|
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);
|
package/dist/tool-context.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/dist/tool-context.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 } : {
|
|
72
|
-
pending.results.set(id, { functionResponse: { name: call.name,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
121
|
+
const parts = content.flatMap((item) => {
|
|
97
122
|
const part = record(item);
|
|
98
|
-
if (value(part, "type") !== "toolCall")
|
|
99
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/dist/tool-schema.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
|
36
|
-
if (ownData(
|
|
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
|
|
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 === "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
if (
|
|
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
|
|
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
|
|
112
|
-
|
|
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(
|
|
148
|
+
return freeze(value.map((item, index) => emitSchema(item, root, declaration, `${path}[${index}]`, state, targets, depth)));
|
|
119
149
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
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
|
-
|
|
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
|
}
|