mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(ai,agent,coding-agent): validate tool arguments with pi-schema, drop TypeBox
pi-schema: opt-in coercion, compile(schema, { coerce: true }) with coerce() and
parse(), the superset of TypeBox's Value.Convert rules and pi-ai's old coercion;
unresolvedRefs option; url and json-pointer-uri-fragment formats.
pi-ai: Tool takes any JSON Schema; validation through pi-schema; Type, Static
and TSchema are no longer exported (breaking). pi-agent-core and pi-durable:
tools on pi-schema, no TypeBox dependency. Coding agent: extensions keep Type,
Static, TSchema and StringEnum through an extension view of pi-ai; built-in
tools stay on TypeBox.
This commit is contained in:
parent
2c8d1feb14
commit
6536f80129
139 changed files with 5115 additions and 1305 deletions
|
|
@ -1,9 +1,10 @@
|
|||
// Open a Harness with a registry of extensions.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/05-harness.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
AgentDoc,
|
||||
createRegistry,
|
||||
|
|
@ -23,7 +24,7 @@ const context = BACKGROUND_CONTEXT;
|
|||
const read = defineTool({
|
||||
name: "read",
|
||||
description: "Read a file",
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
parameters: obj({ path: str() }),
|
||||
execute: async (args) => ({ output: [{ type: "text", text: `contents of ${args.path}` }] }),
|
||||
});
|
||||
const Files = defineExtension({ name: "files", tools: [read] });
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// Per-conversation agent choices and Harness-wide settings.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/06-configuration.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
AgentDoc,
|
||||
createRegistry,
|
||||
|
|
@ -25,7 +26,7 @@ function exampleTool(name: string, description: string): AppTool {
|
|||
const tool = defineTool({
|
||||
name,
|
||||
description,
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
parameters: obj({ path: str() }),
|
||||
execute: async (args) => ({ output: [{ type: "text", text: `${name} ${args.path}` }] }),
|
||||
});
|
||||
return { ...tool, snippet: `Use ${name} for files.` };
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// Reload extension code through the registry.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/09-registry-reload.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import { createRegistry, defineExtension, defineTool, Harness, MemoryStorage, wrapTool } from "../../src/index.ts";
|
||||
|
||||
const context = BACKGROUND_CONTEXT;
|
||||
|
|
@ -12,7 +13,7 @@ function exampleTool(name: string, description: string) {
|
|||
return defineTool({
|
||||
name,
|
||||
description,
|
||||
parameters: Type.Object({ path: Type.String() }),
|
||||
parameters: obj({ path: str() }),
|
||||
execute: async (args) => ({ output: [{ type: "text", text: `${name} ${args.path}` }] }),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// An extension that keeps its own per-conversation document.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/10-extension-state.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
createRegistry,
|
||||
defineDoc,
|
||||
|
|
@ -38,7 +39,7 @@ const Todo = defineExtension({
|
|||
defineTool({
|
||||
name: "todo",
|
||||
description: "Add an item to your todo list",
|
||||
parameters: Type.Object({ item: Type.String() }),
|
||||
parameters: obj({ item: str() }),
|
||||
execute: async (args, api, callContext) => {
|
||||
await api.commit(async (tx) => {
|
||||
(await tx.doc(Todos, api.conversationId)).items.push(args.item);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// view, and then only what changes after that.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/20-late-join.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxText, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { obj } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
createRegistry,
|
||||
defineExtension,
|
||||
|
|
@ -27,7 +28,7 @@ registry.install(
|
|||
defineTool({
|
||||
name: "count",
|
||||
description: "Counts to ten",
|
||||
parameters: Type.Object({}),
|
||||
parameters: obj({}),
|
||||
execute: async (_args, api) => {
|
||||
for (let n = 1; n <= 10; n++) {
|
||||
api.output(`${n}\n`);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
// Uses OpenAI when OPENAI_API_KEY is set, and a scripted faux model otherwise.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/21-subagent-foreground.ts
|
||||
|
||||
import type { Context } from "@earendil-works/chord";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { type AssistantMessage, type FauxResponseStep, Type } from "@earendil-works/pi-ai";
|
||||
import type { AssistantMessage, FauxResponseStep } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxText, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
AssistantEntry,
|
||||
type ConversationId,
|
||||
|
|
@ -42,7 +44,7 @@ const Subagent: Extension = defineExtension({
|
|||
defineTool({
|
||||
name: "subagent",
|
||||
description: "Delegate a self-contained task to a subagent and get its answer back.",
|
||||
parameters: Type.Object({ task: Type.String({ description: "What the subagent should do" }) }),
|
||||
parameters: obj({ task: str({ description: "What the subagent should do" }) }),
|
||||
// Safe to rerun after a crash: a rerun finds the child it already created and the submission it already made.
|
||||
replay: "safe",
|
||||
execute: async (args, api, callContext) => {
|
||||
|
|
|
|||
|
|
@ -5,14 +5,16 @@
|
|||
// Uses OpenAI when OPENAI_API_KEY is set, and a scripted faux model otherwise.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/22-subagent-background.ts
|
||||
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { type AssistantMessage, type FauxResponseStep, Type } from "@earendil-works/pi-ai";
|
||||
import type { AssistantMessage, FauxResponseStep } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxText, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";
|
||||
import { bool, obj, oneOf, opt, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
AssistantEntry,
|
||||
type ConversationId,
|
||||
|
|
@ -134,11 +136,11 @@ const subagentTool = defineTool({
|
|||
"Manage persistent subagents that work in the background. Actions: spawn (name, message), send (name, message; " +
|
||||
"followUp: true queues it after the current answer instead of steering), stop (name: aborts its current work), " +
|
||||
"status (name, or all subagents without one). Answers are reported back to you when they arrive.",
|
||||
parameters: Type.Object({
|
||||
action: Type.Union([Type.Literal("spawn"), Type.Literal("send"), Type.Literal("stop"), Type.Literal("status")]),
|
||||
name: Type.Optional(Type.String()),
|
||||
message: Type.Optional(Type.String()),
|
||||
followUp: Type.Optional(Type.Boolean()),
|
||||
parameters: obj({
|
||||
action: oneOf(["spawn", "send", "stop", "status"]),
|
||||
name: opt(str()),
|
||||
message: opt(str()),
|
||||
followUp: opt(bool()),
|
||||
}),
|
||||
// A call interrupted by a crash is not rerun: repeating `stop` could stop newer work. The model sees that the call
|
||||
// was interrupted and can check with `status`.
|
||||
|
|
|
|||
|
|
@ -2,13 +2,14 @@
|
|||
// extension's own document, and switching back restores the full tool set.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/26-plan-mode.ts
|
||||
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { arr, obj, str } from "@earendil-works/pi-schema";
|
||||
import { NodeExecutionEnv } from "../../src/env/node.ts";
|
||||
import {
|
||||
createRegistry,
|
||||
|
|
@ -39,7 +40,7 @@ const PlanDoc = defineDoc<{ steps: string[] }>({
|
|||
const submitPlan = defineTool({
|
||||
name: "submit_plan",
|
||||
description: "Submit the plan as a list of steps.",
|
||||
parameters: Type.Object({ steps: Type.Array(Type.String()) }),
|
||||
parameters: obj({ steps: arr(str()) }),
|
||||
execute: async (args, api, callContext) => {
|
||||
await api.commit(async (tx) => {
|
||||
(await tx.doc(PlanDoc, api.conversationId)).steps = args.steps;
|
||||
|
|
|
|||
|
|
@ -2,13 +2,15 @@
|
|||
// with; stored choices are names, so they survive a restart and bind to whatever code the new process installs.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/30-reload-and-restart.ts
|
||||
|
||||
import { mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { type ToolResultMessage, Type } from "@earendil-works/pi-ai";
|
||||
import type { ToolResultMessage } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { obj } from "@earendil-works/pi-schema";
|
||||
import { createRegistry, defineExtension, defineTool, Harness, ToolResultEntry } from "../../src/index.ts";
|
||||
import { openNodeSqliteStorage } from "../../src/storage/sqlite/node.ts";
|
||||
|
||||
|
|
@ -24,7 +26,7 @@ function loadVersioned(version: string) {
|
|||
defineTool({
|
||||
name: "version",
|
||||
description: "Report the tool's code version",
|
||||
parameters: Type.Object({}),
|
||||
parameters: obj({}),
|
||||
execute: async () => {
|
||||
started();
|
||||
await gate;
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// tests, a failure starts a fix run, and input sent meanwhile waits for them too.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/32-tests-after-edits.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { obj, str } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
createRegistry,
|
||||
defineExtension,
|
||||
|
|
@ -34,7 +35,7 @@ const runTests = () => {
|
|||
const edit = defineTool({
|
||||
name: "edit",
|
||||
description: "Replace a file's content",
|
||||
parameters: Type.Object({ path: Type.String(), content: Type.String() }),
|
||||
parameters: obj({ path: str(), content: str() }),
|
||||
execute: async (args) => {
|
||||
files.set(args.path, args.content);
|
||||
return { output: [{ type: "text", text: `wrote ${args.path}` }] };
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// message, with a fresh budget. A host learns that a run stopped from `pi.live.runFailure`.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/33-turn-limit-and-continue.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { num, obj } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
ConversationBusy,
|
||||
createRegistry,
|
||||
|
|
@ -21,7 +22,7 @@ const context = BACKGROUND_CONTEXT;
|
|||
const step = defineTool({
|
||||
name: "step",
|
||||
description: "Do one step of the work",
|
||||
parameters: Type.Object({ n: Type.Number() }),
|
||||
parameters: obj({ n: num() }),
|
||||
execute: async (args) => ({ output: [{ type: "text", text: `step ${args.n} done` }] }),
|
||||
});
|
||||
const call = (n: number) => fauxAssistantMessage(fauxToolCall("step", { n }), { stopReason: "toolUse" });
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@
|
|||
// state; `whenIdle: "reject"` refuses it when no run is working.
|
||||
// Run from packages/durable:
|
||||
// node --conditions=source --experimental-strip-types test/examples/34-submit-in-commit.ts
|
||||
|
||||
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
||||
import { Type } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai/models";
|
||||
import { fauxAssistantMessage, fauxProvider, fauxToolCall } from "@earendil-works/pi-ai/providers/faux";
|
||||
import { obj } from "@earendil-works/pi-schema";
|
||||
import {
|
||||
ConversationIdle,
|
||||
createRegistry,
|
||||
|
|
@ -38,7 +39,7 @@ const working = new Promise<void>((resolve) => {
|
|||
const build = defineTool({
|
||||
name: "build",
|
||||
description: "Build the project",
|
||||
parameters: Type.Object({}),
|
||||
parameters: obj({}),
|
||||
execute: async () => {
|
||||
await working;
|
||||
return { output: [{ type: "text", text: "built" }] };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue