@benjamolina/pi-antigravity-guard 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/catalog.js +4 -1
- package/dist/context.d.ts +8 -2
- package/dist/context.js +16 -3
- 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 +16 -1
- package/dist/tool-context.js +77 -23
- 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,7 +28,11 @@ 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
|
|
|
@@ -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.js
CHANGED
|
@@ -8,7 +8,7 @@ const CATALOG = freeze(defineCatalog([{
|
|
|
8
8
|
maxTokens: 65_536,
|
|
9
9
|
},
|
|
10
10
|
routes: {
|
|
11
|
-
off: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: false }, tools:
|
|
11
|
+
off: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: false }, tools: enabled({ record: "pi-json-tool-loop", revision: "gemini-3.8-flash-off-v1", publicModelId: "antigravity-gemini-3.8-flash", reasoning: "off", wireModel: "gemini-3.8-flash-tiered" }) },
|
|
12
12
|
low: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "low", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
|
|
13
13
|
medium: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "medium", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
|
|
14
14
|
high: { wireModel: "gemini-3.8-flash-tiered", thinking: { kind: "native-level", thinkingLevel: "high", includeThoughts: true }, tools: disabled("missing-direct-evidence") },
|
|
@@ -109,6 +109,9 @@ const lookup = new Map(CATALOG.map((entry) => [entry.publicId, entry]));
|
|
|
109
109
|
function disabled(reason) {
|
|
110
110
|
return { state: "disabled", contractRevision: 1, reason };
|
|
111
111
|
}
|
|
112
|
+
function enabled(evidence) {
|
|
113
|
+
return { state: "enabled", contractRevision: 1, fixtureEvidence: evidence, directEvidence: evidence };
|
|
114
|
+
}
|
|
112
115
|
/** Constructs deterministic capability data for hermetic serializer tests only. */
|
|
113
116
|
export function createEnabledToolCapability(evidence) {
|
|
114
117
|
return freeze({ state: "enabled", contractRevision: 1, fixtureEvidence: evidence, directEvidence: evidence });
|
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);
|
|
@@ -24,12 +24,21 @@ export function serializeContext(input, injectedSelection, onRecovery) {
|
|
|
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
|
@@ -10,5 +10,20 @@ 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
|
@@ -29,27 +29,45 @@ export function prepareToolContext(tools, choice) {
|
|
|
29
29
|
unsupportedChoice();
|
|
30
30
|
return { declarations, mode: choice === "none" ? "NONE" : "AUTO" };
|
|
31
31
|
}
|
|
32
|
-
export function replayToolHistory(messages, serializePart,
|
|
32
|
+
export function replayToolHistory(messages, serializePart, onDiagnostics, policy, declaredToolCount = 0) {
|
|
33
33
|
const output = [];
|
|
34
34
|
const calls = new Set();
|
|
35
35
|
const results = new Set();
|
|
36
36
|
const recoveryCount = { value: 0 };
|
|
37
|
+
const shape = {
|
|
38
|
+
userMessageCount: 0,
|
|
39
|
+
assistantMessageCount: 0,
|
|
40
|
+
toolResultMessageCount: 0,
|
|
41
|
+
assistantToolCallBlockCount: 0,
|
|
42
|
+
declaredToolCount: safeCount(declaredToolCount),
|
|
43
|
+
};
|
|
44
|
+
let replayMode = "none";
|
|
37
45
|
let pending;
|
|
46
|
+
const appendTurn = (role, parts) => {
|
|
47
|
+
if (!parts.length)
|
|
48
|
+
return;
|
|
49
|
+
const last = output.at(-1);
|
|
50
|
+
if (last?.role === role)
|
|
51
|
+
last.parts.push(...parts);
|
|
52
|
+
else
|
|
53
|
+
output.push({ role, parts });
|
|
54
|
+
};
|
|
38
55
|
const finalize = () => {
|
|
39
56
|
if (!pending)
|
|
40
57
|
return;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
appendTurn("user", pending.calls.map((call) => {
|
|
59
|
+
const actual = pending.results.get(call.id);
|
|
60
|
+
if (actual)
|
|
61
|
+
return actual;
|
|
62
|
+
recoveryCount.value += 1;
|
|
63
|
+
return pending.observations ? observation(call, missingResultText()) : missingResult(call);
|
|
64
|
+
}));
|
|
48
65
|
pending = undefined;
|
|
49
66
|
};
|
|
50
67
|
for (const message of messages) {
|
|
51
68
|
const current = record(message);
|
|
52
69
|
if (value(current, "role") === "toolResult") {
|
|
70
|
+
shape.toolResultMessageCount += 1;
|
|
53
71
|
const id = nonempty(value(current, "toolCallId"), "PI_TOOL_RESULT_FOREIGN");
|
|
54
72
|
if (results.has(id))
|
|
55
73
|
history("PI_TOOL_RESULT_DUPLICATE");
|
|
@@ -68,8 +86,8 @@ export function replayToolHistory(messages, serializePart, onRecovery) {
|
|
|
68
86
|
history("PI_TOOL_RESULT_MEDIA_UNSUPPORTED");
|
|
69
87
|
return value(part, "text");
|
|
70
88
|
}).join("\n\n");
|
|
71
|
-
const response = value(current, "isError") === true ? { error: text } : {
|
|
72
|
-
pending.results.set(id, { functionResponse: { name: call.name,
|
|
89
|
+
const response = value(current, "isError") === true ? { error: text } : { output: text };
|
|
90
|
+
pending.results.set(id, pending.observations ? observation(call, text) : { functionResponse: { name: call.name, response } });
|
|
73
91
|
results.add(id);
|
|
74
92
|
continue;
|
|
75
93
|
}
|
|
@@ -77,53 +95,82 @@ export function replayToolHistory(messages, serializePart, onRecovery) {
|
|
|
77
95
|
const role = value(current, "role");
|
|
78
96
|
if (role !== "user" && role !== "assistant")
|
|
79
97
|
history("PI_TOOL_CALL_INVALID");
|
|
98
|
+
if (role === "user")
|
|
99
|
+
shape.userMessageCount += 1;
|
|
100
|
+
else
|
|
101
|
+
shape.assistantMessageCount += 1;
|
|
80
102
|
const rawContent = value(current, "content");
|
|
81
103
|
if (typeof rawContent === "string") {
|
|
82
104
|
if (!rawContent)
|
|
83
105
|
history("PI_TOOL_CALL_INVALID");
|
|
84
|
-
|
|
106
|
+
appendTurn(role === "assistant" ? "model" : "user", [{ text: rawContent }]);
|
|
85
107
|
continue;
|
|
86
108
|
}
|
|
87
109
|
const content = dense(rawContent);
|
|
88
110
|
const hasCalls = role === "assistant" && content.some((part) => value(record(part), "type") === "toolCall");
|
|
89
111
|
if (!hasCalls) {
|
|
90
|
-
|
|
112
|
+
appendTurn(role === "assistant" ? "model" : "user", content.map((part) => serializePart(record(part), current)));
|
|
91
113
|
continue;
|
|
92
114
|
}
|
|
93
115
|
if (value(current, "stopReason") !== "toolUse")
|
|
94
116
|
history("PI_TOOL_CALL_INVALID");
|
|
95
117
|
const group = [];
|
|
96
|
-
const parts = content.
|
|
118
|
+
const parts = content.flatMap((item) => {
|
|
97
119
|
const part = record(item);
|
|
98
|
-
if (value(part, "type") !== "toolCall")
|
|
99
|
-
|
|
120
|
+
if (value(part, "type") !== "toolCall") {
|
|
121
|
+
if (isEmptyUnsignedText(part))
|
|
122
|
+
return [];
|
|
123
|
+
return [serializePart(part, current)];
|
|
124
|
+
}
|
|
125
|
+
shape.assistantToolCallBlockCount += 1;
|
|
100
126
|
const id = nonempty(value(part, "id"), "PI_TOOL_CALL_INVALID");
|
|
101
127
|
const name = nonempty(value(part, "name"), "PI_TOOL_CALL_INVALID");
|
|
102
128
|
if (!/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) || calls.has(id))
|
|
103
129
|
history(calls.has(id) ? "PI_TOOL_CALL_DUPLICATE" : "PI_TOOL_CALL_INVALID");
|
|
104
130
|
const args = canonicalJson(value(part, "arguments"), name, "$.arguments");
|
|
105
|
-
if (Array.isArray(args) || args === null)
|
|
131
|
+
if (typeof args !== "object" || Array.isArray(args) || args === null)
|
|
106
132
|
history("PI_TOOL_CALL_INVALID");
|
|
107
133
|
calls.add(id);
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
const call = { id, name, args };
|
|
135
|
+
group.push(call);
|
|
136
|
+
return [{ call, signature: policy?.toolCallSignature(part, current), signaturePresent: value(part, "thoughtSignature") !== undefined }];
|
|
110
137
|
});
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
const toolParts = parts.filter((part) => "call" in part);
|
|
139
|
+
const callsAreSigned = policy?.requireSignedToolCalls === true && policy.isSameModel(current) && Boolean(toolParts[0]?.signature) && toolParts.every((part) => !part.signaturePresent || Boolean(part.signature));
|
|
140
|
+
const observations = policy?.requireSignedToolCalls === true && !callsAreSigned;
|
|
141
|
+
if (observations)
|
|
142
|
+
replayMode = "unsigned-observation";
|
|
143
|
+
else if (replayMode === "none" && callsAreSigned)
|
|
144
|
+
replayMode = "signed-function-response";
|
|
145
|
+
appendTurn("model", parts.flatMap((part) => {
|
|
146
|
+
if (!("call" in part))
|
|
147
|
+
return [part];
|
|
148
|
+
if (observations)
|
|
149
|
+
return [];
|
|
150
|
+
return [{ functionCall: { name: part.call.name, args: part.call.args }, ...(part.signature ? { thoughtSignature: part.signature } : {}) }];
|
|
151
|
+
}));
|
|
152
|
+
pending = { calls: group, results: new Map(), observations };
|
|
113
153
|
}
|
|
114
154
|
finalize();
|
|
115
|
-
|
|
155
|
+
onDiagnostics?.({ replayMode, recoveryCount: recoveryCount.value, ...shape });
|
|
116
156
|
return output;
|
|
117
157
|
}
|
|
158
|
+
function observation(call, text) {
|
|
159
|
+
const args = JSON.stringify(call.args);
|
|
160
|
+
const label = args === "{}" ? `\`${call.name}\`` : `\`${call.name}\` (${args})`;
|
|
161
|
+
return { text: `[Observation from ${label}:\n${text}]` };
|
|
162
|
+
}
|
|
163
|
+
function missingResultText() {
|
|
164
|
+
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.";
|
|
165
|
+
}
|
|
118
166
|
function missingResult(call) {
|
|
119
167
|
return {
|
|
120
168
|
functionResponse: {
|
|
121
169
|
name: call.name,
|
|
122
|
-
id: call.id,
|
|
123
170
|
response: {
|
|
124
171
|
error: {
|
|
125
172
|
code: "PI_TOOL_RESULT_MISSING",
|
|
126
|
-
message:
|
|
173
|
+
message: missingResultText(),
|
|
127
174
|
},
|
|
128
175
|
},
|
|
129
176
|
},
|
|
@@ -142,11 +189,18 @@ function dense(value) {
|
|
|
142
189
|
history("PI_TOOL_CALL_INVALID");
|
|
143
190
|
return value;
|
|
144
191
|
}
|
|
192
|
+
function isEmptyUnsignedText(part) {
|
|
193
|
+
const text = value(part, "text");
|
|
194
|
+
return value(part, "type") === "text" && typeof text === "string" && !text.trim() && value(part, "textSignature") === undefined;
|
|
195
|
+
}
|
|
145
196
|
function nonempty(value, code) {
|
|
146
197
|
if (typeof value !== "string" || !value.trim())
|
|
147
198
|
history(code);
|
|
148
199
|
return value;
|
|
149
200
|
}
|
|
201
|
+
function safeCount(value) {
|
|
202
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
203
|
+
}
|
|
150
204
|
function unsupportedChoice() {
|
|
151
205
|
throw new ToolPreflightError("PI_TOOL_CHOICE_UNSUPPORTED", "tools", "$");
|
|
152
206
|
}
|