Swap the server engine to pi-ai/pi-agent-core (Band B+C of docs/pi-engine-swap.md)
The Vercel AI SDK survives only as Band D (useChat client + UIMessage
record types); @openrouter/ai-sdk-provider is uninstalled.
- src/agent/models.ts: PiEngine {model, streamFn} via createModels +
openrouterProvider subpath; thinkingLevelFor maps config effort none->off
- src/agent/lh-tool.ts: neutral LhTool (zod authoring, optional execute,
clientQuestion flag); toPiTool converts via z.toJSONSchema and re-runs
zod inside execute so refinements ride the error-result retry path
- src/server/pi/record-adapter.ts: UIMessage records -> pi Messages
(dangling tool calls get synthesized interrupted results)
- src/server/pi/ui-stream.ts: SSE bridge emitting AI-SDK-shaped chunks so
the useChat client parses unchanged (continuation id rule, question
tools never emit output, tool-input-error for bad authoring)
- teacher-agent.ts: stateless per-turn pi Agents; shouldStopAfterTurn
stops only when every question-tool call resolved cleanly (errors retry)
- chat-handler.ts: buildAgent(outgoing) factory seam (stateless doctrine);
assertValidUiMessages owns the retired SDK validation gate
- sub-agents via shared runSubagent (pi-run.ts); extraction stages stop
after a clean submit; visual-maker keeps its hand-rolled loop over
direct streamSimple calls; aborted model calls bill no usage
- tests: all model seams ported to pi-ai faux provider (scripted
FauxResponseSteps, agent factories); handleChatRequest second param
is now an agent factory
Gates: tsc, lint, build, 305/305. Live GLM smoke (disposable driver,
deleted): prober streaming + persistence, question seam, turn-2
continuation same message id, per-turn real token usage - ALL PASS.
Data wiped at the pivot (chats/graph/jobs, .gitkeeps kept).
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import { z } from "zod";
|
||||
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
||||
|
||||
/**
|
||||
* lh's neutral tool definition + the converter to pi-agent-core's AgentTool
|
||||
* (docs/pi-engine-swap.md §3). The agent-band swap must not re-risk the
|
||||
* battle-tested zod authoring contracts (the quiz correctAnswer wording,
|
||||
* the nodeId superRefines) — so tools are AUTHORED exactly as before
|
||||
* (zod schemas, same execute signatures) in this shape, and converted
|
||||
* once, centrally, here.
|
||||
*
|
||||
* What the converter owns (the semantics the Vercel SDK gave implicitly):
|
||||
* - parameters: z.toJSONSchema feeds the model-facing declaration (pi
|
||||
* structurally validates plain JSON Schema itself).
|
||||
* - refinements: superRefine rules don't survive JSON Schema — the schema
|
||||
* is re-run inside execute, and a failure THROWS. pi's loop catches
|
||||
* execute throws and turns them into an error tool result the model
|
||||
* sees and retries on — the exact InvalidToolInputError semantics.
|
||||
* - client question tools (quiz / ask_user_question): execute returns a
|
||||
* synthetic "question posed" placeholder that is NEVER persisted — the
|
||||
* record keeps the part output-less until the client answers; the
|
||||
* learner's answer re-enters the next turn as the tool result. The
|
||||
* turn itself stops via shouldStopAfterTurn (teacher-agent.ts).
|
||||
*/
|
||||
|
||||
export interface LhTool<TInput = any, TResult = any> {
|
||||
name: string;
|
||||
description: string;
|
||||
/** The authoring contract (zod, refinements included). */
|
||||
schema: z.ZodType<TInput>;
|
||||
/**
|
||||
* Server-side execution. Absent + clientQuestion = the client answers.
|
||||
* Throws on validation failure — the loop converts to an error result.
|
||||
*/
|
||||
execute?: (input: TInput, ctx: { toolCallId: string }) => TResult | Promise<TResult>;
|
||||
/** Client-answered question tool (quiz/ask): a call ends the turn. */
|
||||
clientQuestion?: boolean;
|
||||
}
|
||||
|
||||
/** Author a tool (the `tool({...})` shape, engine-neutral). */
|
||||
export function lhTool<TInput, TResult = unknown>(def: LhTool<TInput, TResult>): LhTool<TInput, TResult> {
|
||||
return def;
|
||||
}
|
||||
|
||||
/** lh's port of the SDK's invalid-input message: what the model retried on. */
|
||||
export class ToolInputError extends Error {
|
||||
constructor(
|
||||
public readonly toolName: string,
|
||||
public readonly zodError: z.ZodError,
|
||||
) {
|
||||
super(
|
||||
`Invalid tool input for tool "${toolName}":\n${zodError.issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.length > 0 ? ` at ${issue.path.join(".")}` : "";
|
||||
return `- ${issue.message}${path}`;
|
||||
})
|
||||
.join("\n")}`,
|
||||
);
|
||||
this.name = "ToolInputError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert one lh tool to a pi AgentTool. */
|
||||
export function toPiTool(def: LhTool<any, any>): AgentTool<any, any> {
|
||||
return {
|
||||
name: def.name,
|
||||
label: def.name,
|
||||
description: def.description,
|
||||
parameters: z.toJSONSchema(def.schema as z.ZodType) as never,
|
||||
execute: async (toolCallId, args) => {
|
||||
// The authoring check: structural JSON Schema validation already
|
||||
// ran in the loop's prepare step; this re-run adds the refinements
|
||||
// (superRefine rules) the JSON Schema can't carry.
|
||||
const parsed = def.schema.safeParse(args);
|
||||
if (!parsed.success) throw new ToolInputError(def.name, parsed.error);
|
||||
if (def.clientQuestion || !def.execute) {
|
||||
// The synthetic placeholder — in-memory only; the record's tool
|
||||
// part keeps no output until the learner answers.
|
||||
return {
|
||||
content: [{ type: "text", text: "Question posed — the turn ends here; the learner's graded answer arrives as this call's result." }],
|
||||
details: null,
|
||||
};
|
||||
}
|
||||
const result = await def.execute(parsed.data, { toolCallId });
|
||||
return {
|
||||
// The model's view: today's parity — string results pass verbatim,
|
||||
// objects as JSON.
|
||||
content: [{ type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }],
|
||||
details: result,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Convert a tool registry (name → LhTool) to pi's AgentTool[]. */
|
||||
export function toPiTools(registry: Record<string, LhTool<any, any>>): AgentTool<any, any>[] {
|
||||
return Object.entries(registry).map(([name, def]) => toPiTool({ ...def, name }));
|
||||
}
|
||||
|
||||
/** Declaration-only pi Tools (name/description/parameters, no execute) —
|
||||
* for hand-rolled loops that own tool execution themselves. */
|
||||
export function piToolDeclarations(registry: Record<string, LhTool<any, any>>) {
|
||||
return Object.entries(registry).map(([name, def]) => ({
|
||||
name,
|
||||
label: name,
|
||||
description: def.description,
|
||||
parameters: z.toJSONSchema(def.schema as z.ZodType) as never,
|
||||
}));
|
||||
}
|
||||
Reference in New Issue
Block a user