@benjamolina/pi-antigravity-guard 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ import { canonicalJson, ToolPreflightError } from "./tool-contract.js";
2
+ import { normalizeToolDeclarations } from "./tool-schema.js";
3
+ export function hasToolContext(context) {
4
+ if (!isRecord(context))
5
+ return false;
6
+ const tools = value(context, "tools");
7
+ if (Array.isArray(tools) && tools.length > 0)
8
+ return true;
9
+ const messages = value(context, "messages");
10
+ return Array.isArray(messages) && messages.some((message) => {
11
+ if (!isRecord(message))
12
+ return false;
13
+ if (value(message, "role") === "toolResult")
14
+ return true;
15
+ const content = value(message, "content");
16
+ return Array.isArray(content) && content.some((part) => isRecord(part) && value(part, "type") === "toolCall");
17
+ });
18
+ }
19
+ export function prepareToolContext(tools, choice) {
20
+ const declarations = normalizeToolDeclarations(tools ?? []);
21
+ if (!declarations.length) {
22
+ if (choice === "auto")
23
+ throw new ToolPreflightError("PI_TOOL_CHOICE_WITHOUT_DECLARATIONS", "tools", "$");
24
+ if (choice !== undefined && choice !== "none")
25
+ unsupportedChoice();
26
+ return undefined;
27
+ }
28
+ if (choice !== undefined && choice !== "auto" && choice !== "none")
29
+ unsupportedChoice();
30
+ return { declarations, mode: choice === "none" ? "NONE" : "AUTO" };
31
+ }
32
+ export function replayToolHistory(messages, serializePart, onRecovery) {
33
+ const output = [];
34
+ const calls = new Set();
35
+ const results = new Set();
36
+ const recoveryCount = { value: 0 };
37
+ let pending;
38
+ const finalize = () => {
39
+ if (!pending)
40
+ 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
+ }) });
48
+ pending = undefined;
49
+ };
50
+ for (const message of messages) {
51
+ const current = record(message);
52
+ if (value(current, "role") === "toolResult") {
53
+ const id = nonempty(value(current, "toolCallId"), "PI_TOOL_RESULT_FOREIGN");
54
+ if (results.has(id))
55
+ history("PI_TOOL_RESULT_DUPLICATE");
56
+ if (!pending)
57
+ history(calls.has(id) ? "PI_TOOL_RESULT_SEPARATED" : "PI_TOOL_RESULT_FOREIGN");
58
+ const call = pending.calls.find((item) => item.id === id);
59
+ if (!call)
60
+ history(calls.has(id) ? "PI_TOOL_RESULT_SEPARATED" : "PI_TOOL_RESULT_FOREIGN");
61
+ if (value(current, "toolName") !== call.name)
62
+ history("PI_TOOL_RESULT_NAME_MISMATCH");
63
+ if (value(current, "addedToolNames") !== undefined && dense(value(current, "addedToolNames")).length)
64
+ history("PI_TOOL_CALL_INVALID");
65
+ const text = dense(value(current, "content")).map((item) => {
66
+ const part = record(item);
67
+ if (value(part, "type") !== "text" || typeof value(part, "text") !== "string")
68
+ history("PI_TOOL_RESULT_MEDIA_UNSUPPORTED");
69
+ return value(part, "text");
70
+ }).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 } });
73
+ results.add(id);
74
+ continue;
75
+ }
76
+ finalize();
77
+ const role = value(current, "role");
78
+ if (role !== "user" && role !== "assistant")
79
+ history("PI_TOOL_CALL_INVALID");
80
+ const rawContent = value(current, "content");
81
+ if (typeof rawContent === "string") {
82
+ if (!rawContent)
83
+ history("PI_TOOL_CALL_INVALID");
84
+ output.push({ role: role === "assistant" ? "model" : "user", parts: [{ text: rawContent }] });
85
+ continue;
86
+ }
87
+ const content = dense(rawContent);
88
+ const hasCalls = role === "assistant" && content.some((part) => value(record(part), "type") === "toolCall");
89
+ if (!hasCalls) {
90
+ output.push({ role: role === "assistant" ? "model" : "user", parts: content.map((part) => serializePart(record(part), current)) });
91
+ continue;
92
+ }
93
+ if (value(current, "stopReason") !== "toolUse")
94
+ history("PI_TOOL_CALL_INVALID");
95
+ const group = [];
96
+ const parts = content.map((item) => {
97
+ const part = record(item);
98
+ if (value(part, "type") !== "toolCall")
99
+ return serializePart(part, current);
100
+ const id = nonempty(value(part, "id"), "PI_TOOL_CALL_INVALID");
101
+ const name = nonempty(value(part, "name"), "PI_TOOL_CALL_INVALID");
102
+ if (!/^[A-Za-z_][A-Za-z0-9_.:-]*$/.test(name) || calls.has(id))
103
+ history(calls.has(id) ? "PI_TOOL_CALL_DUPLICATE" : "PI_TOOL_CALL_INVALID");
104
+ const args = canonicalJson(value(part, "arguments"), name, "$.arguments");
105
+ if (Array.isArray(args) || args === null)
106
+ history("PI_TOOL_CALL_INVALID");
107
+ calls.add(id);
108
+ group.push({ id, name });
109
+ return { functionCall: { name, args, id } };
110
+ });
111
+ output.push({ role: "model", parts });
112
+ pending = { calls: group, results: new Map() };
113
+ }
114
+ finalize();
115
+ onRecovery?.(recoveryCount.value);
116
+ return output;
117
+ }
118
+ function missingResult(call) {
119
+ return {
120
+ functionResponse: {
121
+ name: call.name,
122
+ id: call.id,
123
+ response: {
124
+ error: {
125
+ 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.",
127
+ },
128
+ },
129
+ },
130
+ };
131
+ }
132
+ function history(code) {
133
+ throw new ToolPreflightError(code, "history", "$");
134
+ }
135
+ function record(value) {
136
+ if (!isRecord(value))
137
+ history("PI_TOOL_CALL_INVALID");
138
+ return value;
139
+ }
140
+ function dense(value) {
141
+ if (!Array.isArray(value) || Object.keys(value).length !== value.length)
142
+ history("PI_TOOL_CALL_INVALID");
143
+ return value;
144
+ }
145
+ function nonempty(value, code) {
146
+ if (typeof value !== "string" || !value.trim())
147
+ history(code);
148
+ return value;
149
+ }
150
+ function unsupportedChoice() {
151
+ throw new ToolPreflightError("PI_TOOL_CHOICE_UNSUPPORTED", "tools", "$");
152
+ }
153
+ function isRecord(value) {
154
+ if (typeof value !== "object" || value === null)
155
+ return false;
156
+ const prototype = Object.getPrototypeOf(value);
157
+ return prototype === Object.prototype || prototype === null;
158
+ }
159
+ function value(record, name) {
160
+ const descriptor = Object.getOwnPropertyDescriptor(record, name);
161
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
162
+ }
@@ -0,0 +1,17 @@
1
+ export type JsonPrimitive = null | boolean | number | string;
2
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;
3
+ export interface JsonObject {
4
+ readonly [key: string]: JsonValue;
5
+ }
6
+ export declare class ToolPreflightError extends Error {
7
+ readonly code: string;
8
+ readonly declaration: string;
9
+ readonly path: string;
10
+ constructor(code: string, declaration: string, path: string);
11
+ }
12
+ export declare function ownData(value: unknown, name: string, declaration: string, path: string): unknown;
13
+ export declare function plainObject(value: unknown, declaration: string, path: string): Record<string, unknown>;
14
+ export declare function denseArray(value: unknown, declaration: string, path: string): readonly unknown[];
15
+ export declare function canonicalJson(value: unknown, declaration: string, path: string, ancestors?: Set<object>): JsonValue;
16
+ export declare function freeze<T>(value: T): T;
17
+ export declare function fail(code: string, declaration: string, path: string): never;
@@ -0,0 +1,65 @@
1
+ export class ToolPreflightError extends Error {
2
+ code;
3
+ declaration;
4
+ path;
5
+ constructor(code, declaration, path) {
6
+ super(`${code}: declaration ${declaration} at ${path}`);
7
+ this.code = code;
8
+ this.declaration = declaration;
9
+ this.path = path;
10
+ }
11
+ }
12
+ export function ownData(value, name, declaration, path) {
13
+ const object = plainObject(value, declaration, path);
14
+ const descriptor = Object.getOwnPropertyDescriptor(object, name);
15
+ if (!descriptor || !("value" in descriptor))
16
+ return undefined;
17
+ return descriptor.value;
18
+ }
19
+ export function plainObject(value, declaration, path) {
20
+ if (typeof value !== "object" || value === null || Array.isArray(value) || (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) || Object.getOwnPropertySymbols(value).length > 0)
21
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
22
+ for (const name of Object.getOwnPropertyNames(value)) {
23
+ const descriptor = Object.getOwnPropertyDescriptor(value, name);
24
+ if (!descriptor || !("value" in descriptor))
25
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
26
+ }
27
+ return value;
28
+ }
29
+ export function denseArray(value, declaration, path) {
30
+ if (!Array.isArray(value) || Object.keys(value).length !== value.length)
31
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
32
+ return value;
33
+ }
34
+ export function canonicalJson(value, declaration, path, ancestors = new Set()) {
35
+ if (value === null || typeof value === "string" || typeof value === "boolean")
36
+ return value;
37
+ if (typeof value === "number") {
38
+ if (Number.isFinite(value))
39
+ return value;
40
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
41
+ }
42
+ if (typeof value !== "object")
43
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
44
+ if (ancestors.has(value))
45
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
46
+ ancestors.add(value);
47
+ try {
48
+ if (Array.isArray(value))
49
+ return Object.freeze(denseArray(value, declaration, path).map((item, index) => canonicalJson(item, declaration, `${path}[${index}]`, ancestors)));
50
+ const object = plainObject(value, declaration, path);
51
+ const output = {};
52
+ for (const key of Object.keys(object).sort())
53
+ output[key] = canonicalJson(object[key], declaration, `${path}.${key}`, ancestors);
54
+ return Object.freeze(output);
55
+ }
56
+ finally {
57
+ ancestors.delete(value);
58
+ }
59
+ }
60
+ export function freeze(value) {
61
+ return Object.freeze(value);
62
+ }
63
+ export function fail(code, declaration, path) {
64
+ throw new ToolPreflightError(code, declaration, path);
65
+ }
@@ -0,0 +1,7 @@
1
+ import { type JsonObject } from "./tool-contract.ts";
2
+ export interface ToolDeclaration {
3
+ readonly name: string;
4
+ readonly description: string;
5
+ readonly parameters: JsonObject;
6
+ }
7
+ export declare function normalizeToolDeclarations(value: unknown): readonly ToolDeclaration[];
@@ -0,0 +1,125 @@
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"]);
4
+ const MAX_SCHEMA_DEPTH = 32;
5
+ const MAX_SCHEMA_NODES = 2048;
6
+ const MAX_SCHEMA_BYTES = 256 * 1024;
7
+ const MAX_TOTAL_SCHEMA_BYTES = 1024 * 1024;
8
+ export function normalizeToolDeclarations(value) {
9
+ const tools = denseArray(value, "tools", "$");
10
+ const names = new Set();
11
+ const declarations = tools.map((tool, index) => normalizeDeclaration(tool, index, names));
12
+ let totalSchemaBytes = 0;
13
+ for (const declaration of declarations) {
14
+ totalSchemaBytes += Buffer.byteLength(JSON.stringify(declaration.parameters), "utf8");
15
+ if (totalSchemaBytes > MAX_TOTAL_SCHEMA_BYTES)
16
+ fail("PI_TOOL_SCHEMA_LIMIT", declaration.name, "$");
17
+ }
18
+ return freeze(declarations);
19
+ }
20
+ function normalizeDeclaration(value, index, names) {
21
+ const path = `$[${index}]`;
22
+ const tool = plainObject(value, `tool-${index}`, path);
23
+ const name = ownData(tool, "name", `tool-${index}`, path);
24
+ if (typeof name !== "string" || !/^[A-Za-z_][A-Za-z0-9_.:-]{0,63}$/.test(name))
25
+ fail("PI_TOOL_DECLARATION_INVALID", `tool-${index}`, `${path}.name`);
26
+ if (names.has(name))
27
+ fail("PI_TOOL_DECLARATION_DUPLICATE", name, `${path}.name`);
28
+ names.add(name);
29
+ const description = ownData(tool, "description", name, path);
30
+ if (typeof description !== "string" || !description.trim())
31
+ fail("PI_TOOL_DECLARATION_INVALID", name, `${path}.description`);
32
+ const constrainedSampling = ownData(tool, "constrainedSampling", name, path);
33
+ if (constrainedSampling !== undefined && constrainedSampling !== false)
34
+ 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
+ fail("PI_TOOL_SCHEMA_INVALID", name, "$.type");
38
+ const parameters = normalizeSchema(parameterRoot, name, "$");
39
+ if (Buffer.byteLength(JSON.stringify(parameters), "utf8") > MAX_SCHEMA_BYTES)
40
+ fail("PI_TOOL_SCHEMA_LIMIT", name, "$");
41
+ return freeze({ name, description, parameters });
42
+ }
43
+ function normalizeSchema(value, declaration, path, state = { ancestors: new Set(), nodes: 0 }, depth = 0) {
44
+ if (depth > MAX_SCHEMA_DEPTH || ++state.nodes > MAX_SCHEMA_NODES)
45
+ 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);
50
+ }
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;
64
+ }
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;
70
+ }
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
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
85
+ return freeze(output);
86
+ }
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());
104
+ }
105
+ }
106
+ function normalizeArray(schema, output, declaration, path, state, depth) {
107
+ if (ownData(schema, "properties", declaration, path) !== undefined || ownData(schema, "required", declaration, path) !== undefined)
108
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
109
+ output.items = normalizeSchema(ownData(schema, "items", declaration, path), declaration, `${path}.items`, state, depth + 1);
110
+ }
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)
117
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
118
+ return freeze(normalized);
119
+ }
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)))
123
+ fail("PI_TOOL_SCHEMA_INVALID", declaration, path);
124
+ return json;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benjamolina/pi-antigravity-guard",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Pi adapter for Antigravity Guard",
5
5
  "type": "module",
6
6
  "license": "MIT",