Skip to content
Source
Sign inSign up
Open History to see the last commit

advertisedJsonSchema.ts

106 lines4.6 KB

1import * as Predicate from "effect/Predicate";
2import { Tool } from "effect/ai";
3​
4type Json = unknown;
5type JsonObject = { [key: string]: Json };
6​
7const isNullSchema = (value: Json) =>
8 Predicate.isObject(value) && value.type === "null" && Object.keys(value).length === 1;
9​
10const isBareAnyOf = (value: Json): value is { anyOf: Array<Json> } =>
11 Predicate.isObject(value) && Array.isArray(value.anyOf) && Object.keys(value).length === 1;
12​
13// A `Schema.fromJsonString` fallback. Advertised beside object shapes, it invites stringified input.
14const isJsonStringSchema = (value: Json) =>
15 Predicate.isObject(value) &&
16 value.type === "string" &&
17 value.contentMediaType === "application/json";
18​
19const SAFE_INTEGER_BOUNDS: Record<string, number> = {
20 maximum: Number.MAX_SAFE_INTEGER,
21 minimum: Number.MIN_SAFE_INTEGER,
22};
23​
24const tagOf = (branch: JsonObject, key: string): Json => {
25 const property = Predicate.isObject(branch.properties) ? branch.properties[key] : undefined;
26 return Predicate.isObject(property) && Array.isArray(property.enum) && property.enum.length === 1
27 ? property.enum[0]
28 : undefined;
29};
30​
31/** One object for a tagged union of objects: each property once, the tag listing every value. */
32const mergeTaggedObjects = (branches: ReadonlyArray<Json>): JsonObject | undefined => {
33 if (branches.length < 2) return undefined;
34 const objects = branches.filter(
35 (branch): branch is JsonObject & { properties: JsonObject } =>
36 Predicate.isObject(branch) &&
37 branch.type === "object" &&
38 Predicate.isObject(branch.properties),
39 );
40 if (objects.length !== branches.length) return undefined;
41 const tag = Object.keys(objects[0]!.properties).find((key) => {
42 const values = objects.map((object) => tagOf(object, key));
43 return values.every((value) => value !== undefined) && new Set(values).size === values.length;
44 });
45 if (tag === undefined) return undefined;
46 const properties: JsonObject = {
47 [tag]: { type: "string", enum: objects.map((object) => tagOf(object, tag)) },
48 };
49 for (const object of objects) {
50 for (const [name, property] of Object.entries(object.properties)) {
51 if (name === tag) continue;
52 const seen = properties[name];
53 if (seen === undefined) properties[name] = property;
54 else if (JSON.stringify(seen) !== JSON.stringify(property)) {
55 const options = Predicate.isObject(seen) && isBareAnyOf(seen) ? seen.anyOf : [seen];
56 if (!options.some((option) => JSON.stringify(option) === JSON.stringify(property)))
57 properties[name] = { anyOf: [...options, property] };
58 }
59 }
60 }
61 const [first = [], ...others] = objects.map((object) =>
62 Array.isArray(object.required) ? object.required : [],
63 );
64 const requiredByAll = first.filter(
65 (name) => name !== tag && others.every((required) => required.includes(name)),
66 );
67 return { type: "object", properties, required: [tag, ...requiredByAll] };
68};
69​
70const compact = (schema: Json, optional: boolean): Json => {
71 if (Array.isArray(schema)) return schema.map((item) => compact(item, false));
72 if (!Predicate.isObject(schema)) return schema;
73 const required = new Set(Array.isArray(schema.required) ? schema.required : []);
74 let result: JsonObject = {};
75 for (const [key, value] of Object.entries(schema)) {
76 if (key === "additionalProperties" && value === true) continue;
77 if (SAFE_INTEGER_BOUNDS[key] === value) continue;
78 result[key] =
79 key === "properties" && Predicate.isObject(value)
80 ? Object.fromEntries(
81 Object.entries(value).map(([name, property]) => [
82 name,
83 compact(property, !required.has(name)),
84 ]),
85 )
86 : compact(value, false);
87 }
88 if (Array.isArray(result.anyOf)) {
89 const branches = result.anyOf
90 .flatMap((branch) => (isBareAnyOf(branch) ? branch.anyOf : [branch]))
91 .filter((branch) => !(optional && isNullSchema(branch)) && !isJsonStringSchema(branch));
92 const { anyOf: _anyOf, ...rest } = result;
93 const only = branches.length === 1 ? branches[0] : mergeTaggedObjects(branches);
94 result = Predicate.isObject(only) ? { ...only, ...rest } : { ...rest, anyOf: branches };
95 }
96 return result;
97};
98​
99/**
100 * The input schema T3 advertises for a tool. Drops JSON noise that every thread pays for:
101 * `additionalProperties: true`, safe-integer bounds, nested `anyOf`, JSON-string fallbacks, and
102 * the `null` branch of optional properties. A tagged union of objects becomes one object.
103 * Decoding still uses the full schema, so each tag's own required fields still apply.
104 */
105export const advertisedJsonSchema = (tool: Tool.Any) =>
106 compact(Tool.getJsonSchema(tool), false) as ReturnType<typeof Tool.getJsonSchema>;