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:
@@ -1,7 +1,9 @@
|
||||
import { isToolUIPart, type LanguageModel, type UIMessage } from "ai";
|
||||
import { isToolUIPart, type UIMessage } from "ai";
|
||||
import type { PiEngine } from "../models.ts";
|
||||
import {
|
||||
accumulateJobUsage,
|
||||
buildSubagentSteer,
|
||||
piUsageToStep,
|
||||
type DemoResult,
|
||||
type DemoToolInput,
|
||||
type JobKind,
|
||||
@@ -18,7 +20,7 @@ import { registerJobAbort, unregisterJobAbort } from "./aborts.ts";
|
||||
import { MODELS } from "../../../config.ts";
|
||||
import type { ChatStore, JobStore } from "../../core/ports.ts";
|
||||
import { runDemoBuilder } from "./demo-builder.ts";
|
||||
import { createResearcherAgent } from "./researcher.ts";
|
||||
import { runResearcherAgent } from "./researcher.ts";
|
||||
import { runVisualMaker } from "./visual-maker.ts";
|
||||
|
||||
/**
|
||||
@@ -42,13 +44,13 @@ import { runVisualMaker } from "./visual-maker.ts";
|
||||
|
||||
/**
|
||||
* Test seam: the spawn wiring has no injection point at the tool layer, so
|
||||
* tests swap in a MockLanguageModelV4 here to keep job tests off the network.
|
||||
* tests swap in a scripted pi engine here to keep job tests off the network.
|
||||
* Undefined in production. (The jobs spawn through the module-level KIND
|
||||
* runners, which read these at call time.)
|
||||
*/
|
||||
export const researchModelOverride: { current?: LanguageModel } = {};
|
||||
export const demoModelOverride: { current?: LanguageModel } = {};
|
||||
export const visualModelOverride: { current?: LanguageModel } = {};
|
||||
export const researchModelOverride: { current?: PiEngine } = {};
|
||||
export const demoModelOverride: { current?: PiEngine } = {};
|
||||
export const visualModelOverride: { current?: PiEngine } = {};
|
||||
|
||||
/** The dispatch-time tool input (from src/agent/tools/jobs.ts schemas). */
|
||||
type JobToolInput = ResearchToolInput | DemoToolInput | VisualToolInput;
|
||||
@@ -232,7 +234,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
|
||||
async function runResearchJob(
|
||||
jobId: string,
|
||||
model: LanguageModel | undefined,
|
||||
model: PiEngine | undefined,
|
||||
passedInput: JobToolInput | undefined,
|
||||
loop: LoopContext,
|
||||
): Promise<void> {
|
||||
@@ -253,28 +255,25 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
.join("\n\n");
|
||||
|
||||
const fetchedUrls: string[] = [];
|
||||
const agent = createResearcherAgent(model);
|
||||
|
||||
try {
|
||||
jobStore.updateJob(jobId, { activity: "researching" });
|
||||
const result = await agent.generate({
|
||||
prompt,
|
||||
const brief = await runResearcherAgent(prompt, {
|
||||
engine: model,
|
||||
abortSignal: loop.abortSignal,
|
||||
// Money channel: each agent step is one model call — fold its
|
||||
// usage into the record as it completes (progress write).
|
||||
onStepFinish: ({ usage }) => loop.onUsage(usage),
|
||||
onToolExecutionStart: ({ toolCall }) => {
|
||||
const input = toolCall.input as { query?: string; url?: string } | undefined;
|
||||
if (toolCall.toolName === "web_fetch" && typeof input?.url === "string") {
|
||||
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
|
||||
onToolStart: (toolName, args) => {
|
||||
const input = args as { query?: string; url?: string } | undefined;
|
||||
if (toolName === "web_fetch" && typeof input?.url === "string") {
|
||||
fetchedUrls.push(input.url);
|
||||
}
|
||||
jobStore.updateJob(jobId, { activity: activityLine(toolCall.toolName, input) });
|
||||
jobStore.updateJob(jobId, { activity: activityLine(toolName, input) });
|
||||
},
|
||||
});
|
||||
|
||||
const brief = result.text.trim();
|
||||
if (!brief) throw new Error("the researcher produced no text");
|
||||
const research: ResearchResult = { brief, sources: parseSources(brief, fetchedUrls) };
|
||||
if (!brief.trim()) throw new Error("the researcher produced no text");
|
||||
const research: ResearchResult = { brief: brief.trim(), sources: parseSources(brief.trim(), fetchedUrls) };
|
||||
await finishJob(jobId, research);
|
||||
} catch (err) {
|
||||
// Real-kill abort (2026-09): the abort path flipped the record and
|
||||
@@ -291,7 +290,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
|
||||
async function runDemoJob(
|
||||
jobId: string,
|
||||
model: LanguageModel | undefined,
|
||||
model: PiEngine | undefined,
|
||||
passedInput: JobToolInput | undefined,
|
||||
loop: LoopContext,
|
||||
): Promise<void> {
|
||||
@@ -308,9 +307,9 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
const result = await runDemoBuilder(
|
||||
{ title, brief },
|
||||
{
|
||||
model,
|
||||
engine: model,
|
||||
abortSignal: loop.abortSignal,
|
||||
onUsage: loop.onUsage,
|
||||
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
|
||||
onActivity: (line) => jobStore.updateJob(jobId, { activity: line }),
|
||||
},
|
||||
);
|
||||
@@ -327,7 +326,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
|
||||
async function runVisualJob(
|
||||
jobId: string,
|
||||
model: LanguageModel | undefined,
|
||||
model: PiEngine | undefined,
|
||||
passedInput: JobToolInput | undefined,
|
||||
loop: LoopContext,
|
||||
): Promise<void> {
|
||||
@@ -343,9 +342,9 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
|
||||
const result = await runVisualMaker(
|
||||
{ brief },
|
||||
{
|
||||
model,
|
||||
engine: model,
|
||||
abortSignal: loop.abortSignal,
|
||||
onUsage: loop.onUsage,
|
||||
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
|
||||
onActivity: (line) => jobStore.updateJob(jobId, { activity: line }),
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user