feat(agent): pi-schema's Static types TypeBox-built schemas too, without depending on TypeBox

This commit is contained in:
Mario Zechner 2026-11-01 11:12:44 +00:00
parent a6bde34bb6
commit 6291b9bcc3
12 changed files with 304 additions and 10 deletions

View file

@ -4,7 +4,7 @@
### Breaking Changes
- Tool schemas are JSON Schema typed through `@earendil-works/pi-schema`: `defineTool()` and `ToolRegistration` take any JSON Schema object as `parameters` and `structuredOutputSchema`, and type `args` with pi-schema's `Static` (`ToolArgs<TParameters>`; other schemas give `unknown`). The built-in tools (`read`, `write`, `edit`, `bash`, `powershell`, `codemode`, `tool_search`, the model tools) are built with pi-schema, with the same JSON Schema up to key order. `structuredOutput` is validated with pi-schema, so `invalid_structured_output` messages use its wording. `typebox` is no longer a dependency. Migration: replace `Type.Object({ n: Type.Number() })` from `@earendil-works/pi-ai` with `obj({ n: num() })` from `@earendil-works/pi-schema` (closed by default; `{ open: true }` allows other properties).
- Tool schemas are JSON Schema typed through `@earendil-works/pi-schema`: `defineTool()` and `ToolRegistration` take any JSON Schema object as `parameters` and `structuredOutputSchema`, and type `args` with pi-schema's `Static` (`ToolArgs<TParameters>`; TypeBox schemas give TypeBox's static type for the common kinds, as pi-schema's `Static` describes; other schemas give `unknown`). The built-in tools (`read`, `write`, `edit`, `bash`, `powershell`, `codemode`, `tool_search`, the model tools) are built with pi-schema, with the same JSON Schema up to key order. `structuredOutput` is validated with pi-schema, so `invalid_structured_output` messages use its wording. `typebox` is no longer a dependency. Migration: replace `Type.Object({ n: Type.Number() })` from `@earendil-works/pi-ai` with `obj({ n: num() })` from `@earendil-works/pi-schema` (closed by default; `{ open: true }` allows other properties).
- `SqliteExecutor` (re-exported from Chord) gains `runCounted(sql, ...params)`, resolving with the rows the statement changed; custom `SqliteDatabase` implementations add it.
- `Storage.commit()` stores a group of commits in one call: `commit(batches, receive, context)` resolves with a `CommitResult` (`{ stored, refused? }`) and replaces `commit(writes, adopt, context)`. Each batch is one atomic commit with its own seq, checked against the state the batches before it left. The first batch Durable's own checks refuse (global IDs, document actions, copy sources) is not stored, nor anything after it; the batches before it are, and the call resolves with their number in `stored` and the refusal in `refused`. Any other failure (a driver or I/O error, a failed `COMMIT`) rejects the whole call as an unknown outcome and stores nothing the backend could roll back. `receive(index, seq)` is called once per stored batch, in order, synchronously, once they are durable and before the next call starts; receipts made before a rejection stand. Every batch has at least one write. The optional `publishing(publish, context)` runs each commit's publication inside the storage's own scope (`StoreStorage`: the attached host's batch). Memory, SQLite, JSONL, IndexedDB and `StoreStorage` store a call in one transaction or append, and the storage conformance suite checks the contract. Migration for a custom Storage:

View file

@ -333,8 +333,8 @@ export interface ToolExecutionApi<TDetails extends JsonValue = JsonValue> extend
}
/**
* The arguments type of a tool with parameter schema `TParameters`: `Static<TParameters>` for schemas built with
* `@earendil-works/pi-schema`'s builders, `unknown` for other JSON Schema, and `any` for `ToolRegistration<any>`.
* The arguments type of a tool with parameter schema `TParameters`: pi-schema's `Static<TParameters>` (the builders'
* type, TypeBox's for the common TypeBox kinds, else `unknown`), and `any` for `ToolRegistration<any>`.
*/
export type ToolArgs<TParameters> = IsAny<TParameters> extends true ? any : Static<TParameters>;

View file

@ -0,0 +1,61 @@
// Tool arguments typed from a TypeBox-built parameter schema. Durable does not depend on TypeBox: the schema below
// has TypeBox 1.x's type shapes as its d.mts declare them; pi-schema's own tests check the shapes against TypeBox.
import { defineTool, type ToolRegistration } from "@earendil-works/pi-durable";
import { describe, expectTypeOf, it } from "vitest";
interface TString {
"~kind": "String";
type: "string";
}
interface TInteger {
"~kind": "Integer";
type: "integer";
}
interface TArray<Items> {
"~kind": "Array";
type: "array";
items: Items;
}
interface TObject<Properties> {
"~kind": "Object";
type: "object";
properties: Properties;
required: string[];
}
type TOptional<Schema> = Schema & { "~optional": true };
const parameters: TObject<{ path: TString; offset: TOptional<TInteger>; tags: TArray<TString> }> = {
"~kind": "Object",
type: "object",
properties: {
path: { "~kind": "String", type: "string" },
offset: { "~kind": "Integer", type: "integer", "~optional": true },
tags: { "~kind": "Array", type: "array", items: { "~kind": "String", type: "string" } },
},
required: ["path", "tags"],
};
type Args = { path: string; offset?: number; tags: string[] };
describe("tool arguments of a TypeBox schema", () => {
it("are typed by the schema in execute() and prepareArguments()", () => {
const tool = defineTool({
name: "read",
description: "Reads a file",
parameters,
async execute(args) {
expectTypeOf(args).toEqualTypeOf<Args>();
return {};
},
});
expectTypeOf<Parameters<typeof tool.execute>[0]>().toEqualTypeOf<Args>();
expectTypeOf<
ReturnType<NonNullable<ToolRegistration<typeof parameters>["prepareArguments"]>>
>().toEqualTypeOf<Args>();
});
});