chore: pi family 0.85.1 -> 0.87.1 (finishTurn, transcript contexts, typebox 1.3.27)

Mechanical fallout only; semantic deltas flagged in the wave report:
- shouldStopAfterTurn -> finishTurn ({action:'end'} wrap; loop order verified
  equivalent for Wave C stranding)
- transcript rework: seeded leading SystemMessage filtered at both
  state.messages read seams (ui-stream settleWith, pi-run slice); prompt
  and tool declarations now ride the transcript
- streamOnce sites normalizeContext() (Context -> TranscriptContext)
- AgentToolResult details / ToolCall.arguments JsonValue narrowing (casts)
- AUTH_PROVIDER_KEYS regenerated (meta added); glm-5.3-flash price pin
  follows the 0.87.1 catalog (0.15/0.5/0.05)
- vanish probe: 0/12 vs 2/6 baseline; live smoke green on PORT=3004
This commit is contained in:
amosblomqvist
2026-09-26 21:45:51 +03:00
parent 9853075331
commit 876185eb6e
12 changed files with 329 additions and 850 deletions
+272 -830
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -17,9 +17,8 @@
"desktop:dist": "next build && node scripts/build-server.mjs && electron-builder --mac --arm64" "desktop:dist": "next build && node scripts/build-server.mjs && electron-builder --mac --arm64"
}, },
"dependencies": { "dependencies": {
"@earendil-works/pi-agent-core": "^0.85.1", "@earendil-works/pi-agent-core": "^0.87.1",
"@earendil-works/pi-ai": "^0.85.1", "@earendil-works/pi-ai": "^0.87.1",
"typebox": "1.3.7",
"@mozilla/readability": "^0.6.0", "@mozilla/readability": "^0.6.0",
"@radix-ui/react-use-controllable-state": "^1.2.6", "@radix-ui/react-use-controllable-state": "^1.2.6",
"@rive-app/react-webgl2": "^4.32.1", "@rive-app/react-webgl2": "^4.32.1",
@@ -58,6 +57,7 @@
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"tokenlens": "^1.3.1", "tokenlens": "^1.3.1",
"turndown": "^7.2.4", "turndown": "^7.2.4",
"typebox": "^1.3.27",
"use-stick-to-bottom": "^1.1.6", "use-stick-to-bottom": "^1.1.6",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
+5 -1
View File
@@ -1,4 +1,5 @@
import type { AssistantMessage, Message, ToolResultMessage, Usage, Tool } from "@earendil-works/pi-ai"; import type { AssistantMessage, Message, ToolResultMessage, Usage, Tool } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
import { Type } from "typebox"; import { Type } from "typebox";
import { piEngineFor, type PiEngine, type ThinkingLevel } from "../models.ts"; import { piEngineFor, type PiEngine, type ThinkingLevel } from "../models.ts";
import type { PdfResult, PdfToolInput } from "../../core/contracts.ts"; import type { PdfResult, PdfToolInput } from "../../core/contracts.ts";
@@ -392,7 +393,10 @@ async function streamOnce(
reasoning: ThinkingLevel | undefined, reasoning: ThinkingLevel | undefined,
abortSignal?: AbortSignal, abortSignal?: AbortSignal,
): Promise<AssistantMessage> { ): Promise<AssistantMessage> {
const stream = engine.streamFn(engine.model, { systemPrompt, messages, tools }, { // 0.87.1: provider-facing contexts are TranscriptContexts — the prompt
// and tool declarations fold into a leading system message.
// normalizeContext is the library's only Context→TranscriptContext entry.
const stream = engine.streamFn(engine.model, normalizeContext({ systemPrompt, messages, tools }), {
reasoning: reasoning && reasoning !== "off" ? reasoning : undefined, reasoning: reasoning && reasoning !== "off" ? reasoning : undefined,
apiKey: process.env.OPENROUTER_API_KEY || undefined, apiKey: process.env.OPENROUTER_API_KEY || undefined,
signal: abortSignal, signal: abortSignal,
+12 -3
View File
@@ -65,9 +65,13 @@ export async function runSubagent(tools: AgentTool<any, any>[], opts: SubagentRu
messages: opts.messages ?? [], messages: opts.messages ?? [],
}, },
streamFn: engine.streamFn, streamFn: engine.streamFn,
shouldStopAfterTurn: (context) => { // 0.87.1: shouldStopAfterTurn → finishTurn (same call point; "end"
// exits before the steering poll, exactly the old boolean-true).
finishTurn: (turn) => {
turns++; turns++;
return turns >= opts.maxTurns || opts.shouldStop?.(context.message, context.toolResults) === true; return turns >= opts.maxTurns || opts.shouldStop?.(turn.message, turn.toolResults) === true
? { action: "end" }
: undefined;
}, },
}); });
@@ -119,7 +123,12 @@ export async function runSubagent(tools: AgentTool<any, any>[], opts: SubagentRu
} }
if (errorMessage !== undefined) throw new Error(errorMessage); if (errorMessage !== undefined) throw new Error(errorMessage);
const newMessages = agent.state.messages.slice(before); // 0.87.1: drop the seeded leading SystemMessage (transcript rework —
// see ui-stream's settleWith) before slicing past the input count, or
// the seed displaces the slice and the last INPUT message leaks in.
const newMessages = agent.state.messages
.filter((m) => (m as { role?: string }).role !== "system")
.slice(before);
const assistants = newMessages.filter((m) => (m as { role?: string }).role === "assistant") as AssistantMessage[]; const assistants = newMessages.filter((m) => (m as { role?: string }).role === "assistant") as AssistantMessage[];
const last = assistants[assistants.length - 1]; const last = assistants[assistants.length - 1];
const text = last const text = last
+5 -1
View File
@@ -1,4 +1,5 @@
import type { AssistantMessage, Message, Tool, ToolResultMessage, Usage } from "@earendil-works/pi-ai"; import type { AssistantMessage, Message, Tool, ToolResultMessage, Usage } from "@earendil-works/pi-ai";
import { normalizeContext } from "@earendil-works/pi-ai";
import { Type } from "typebox"; import { Type } from "typebox";
import { piEngineFor, type PiEngine, type ThinkingLevel } from "../models.ts"; import { piEngineFor, type PiEngine, type ThinkingLevel } from "../models.ts";
import type { VisualResult } from "../../core/contracts.ts"; import type { VisualResult } from "../../core/contracts.ts";
@@ -307,7 +308,10 @@ async function streamOnce(
reasoning: ThinkingLevel | undefined, reasoning: ThinkingLevel | undefined,
abortSignal?: AbortSignal, abortSignal?: AbortSignal,
): Promise<AssistantMessage> { ): Promise<AssistantMessage> {
const stream = engine.streamFn(engine.model, { systemPrompt, messages, tools }, { // 0.87.1: provider-facing contexts are TranscriptContexts — the prompt
// and tool declarations fold into a leading system message.
// normalizeContext is the library's only Context→TranscriptContext entry.
const stream = engine.streamFn(engine.model, normalizeContext({ systemPrompt, messages, tools }), {
reasoning: reasoning && reasoning !== "off" ? reasoning : undefined, reasoning: reasoning && reasoning !== "off" ? reasoning : undefined,
apiKey: process.env.OPENROUTER_API_KEY || undefined, apiKey: process.env.OPENROUTER_API_KEY || undefined,
signal: abortSignal, signal: abortSignal,
+8 -2
View File
@@ -122,7 +122,8 @@ export interface AgentBuildOptions {
* *
* Question results also carry terminate: true (tool-execute's * Question results also carry terminate: true (tool-execute's
* questionResult), so single-question batches stop natively at the pi * questionResult), so single-question batches stop natively at the pi
* loop's own batch check; this shouldStopAfterTurn hook is the * loop's own batch check; this finishTurn hook (0.87.1 rename of
* shouldStopAfterTurn) is the
* belt-and-suspenders HALF for the mixed batch (a question + other calls * belt-and-suspenders HALF for the mixed batch (a question + other calls
* in one message) that terminate-only semantics would let run on — the * in one message) that terminate-only semantics would let run on — the
* learner cannot answer while a turn is in flight (per-chat 409), so a * learner cannot answer while a turn is in flight (per-chat 409), so a
@@ -164,7 +165,12 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
messages: opts.messages ?? [], messages: opts.messages ?? [],
}, },
streamFn: engine.streamFn, streamFn: engine.streamFn,
shouldStopAfterTurn: stopper, // 0.87.1: shouldStopAfterTurn → finishTurn. Same call point (after
// tool results attach, before turn_end) and `{ action: "end" }` still
// exits BEFORE the steering poll — the parked-marker stranding our
// Wave C registerLiveAgent-before-runPiTurn design depends on.
// undefined preserves normal scheduling (the old boolean false).
finishTurn: (turn) => (stopper(turn) ? { action: "end" } : undefined),
// Wave C (2026-09-26, owner correction same day): markers handed to a // Wave C (2026-09-26, owner correction same day): markers handed to a
// live run drain from pi's STEERING queue — the lesson loop is one // live run drain from pi's STEERING queue — the lesson loop is one
// continuous teach→quiz→teach run, so delivery must inject at turn // continuous teach→quiz→teach run, so delivery must inject at turn
+1
View File
@@ -95,6 +95,7 @@ export const AUTH_PROVIDER_KEYS: readonly AuthProviderInfo[] = [
{ id: "groq", envVar: "GROQ_API_KEY", label: "Groq" }, { id: "groq", envVar: "GROQ_API_KEY", label: "Groq" },
{ id: "huggingface", envVar: "HF_TOKEN", label: "Hugging Face" }, { id: "huggingface", envVar: "HF_TOKEN", label: "Hugging Face" },
{ id: "kimi-coding", envVar: "KIMI_API_KEY", label: "Kimi For Coding" }, { id: "kimi-coding", envVar: "KIMI_API_KEY", label: "Kimi For Coding" },
{ id: "meta", envVar: "META_API_KEY", label: "Meta" },
{ id: "minimax", envVar: "MINIMAX_API_KEY", label: "MiniMax" }, { id: "minimax", envVar: "MINIMAX_API_KEY", label: "MiniMax" },
{ id: "minimax-cn", envVar: "MINIMAX_CN_API_KEY", label: "MiniMax CN" }, { id: "minimax-cn", envVar: "MINIMAX_CN_API_KEY", label: "MiniMax CN" },
{ id: "mistral", envVar: "MISTRAL_API_KEY", label: "Mistral" }, { id: "mistral", envVar: "MISTRAL_API_KEY", label: "Mistral" },
+3 -1
View File
@@ -117,7 +117,9 @@ export function newAnswerResult(
toolCallId, toolCallId,
toolName, toolName,
content: [{ type: "text", text: JSON.stringify(answer) }], content: [{ type: "text", text: JSON.stringify(answer) }],
details: answer, // 0.87.1 narrowed ToolResultMessage.details to JsonRepresentation —
// our payloads are JSON-safe answer structures, so the cast is honest.
details: answer as ToolResultMessage["details"],
isError: false, isError: false,
timestamp: Date.now(), timestamp: Date.now(),
}; };
+11 -1
View File
@@ -115,8 +115,18 @@ export function runPiTurn(spec: PiTurnSpec): PiTurnHandle {
// The settle seam: the record write is the agent's OWN final state // The settle seam: the record write is the agent's OWN final state
// (native history — signatures, usage, provider stamps all real). // (native history — signatures, usage, provider stamps all real).
// Copy the array: the agent may outlive this call's read. // Copy the array: the agent may outlive this call's read.
// 0.87.1 (transcript rework): the Agent seeds a leading SystemMessage
// from initialState.systemPrompt (prompt + tool declarations) — it is
// synthesized per turn and must NEVER persist (the record would bloat
// with tool declarations AND replay would see messages[0] === system,
// suppressing the fresh per-turn re-seed). Our app mints no system
// messages of its own (the prompt is composed per turn), so dropping
// the role wholesale is exact; revisit if prompt-updates ever land.
const settleWith = () => { const settleWith = () => {
onFinish([...agent.state.messages], sawAbort); onFinish(
[...agent.state.messages].filter((m) => (m as { role?: string }).role !== "system"),
sawAbort,
);
}; };
const handle = (event: AgentEvent) => { const handle = (event: AgentEvent) => {
+2 -2
View File
@@ -29,7 +29,7 @@ import * as fs from "node:fs";
import * as path from "node:path"; import * as path from "node:path";
import { after, describe, it } from "node:test"; import { after, describe, it } from "node:test";
import { Value } from "typebox/value"; import { Value } from "typebox/value";
import { createFauxCore, fauxAssistantMessage, fauxToolCall, type FauxResponseStep } from "@earendil-works/pi-ai"; import { createFauxCore, fauxAssistantMessage, fauxToolCall, type FauxResponseStep, type ToolCall } from "@earendil-works/pi-ai";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { PATHS } from "../config.ts"; import { PATHS } from "../config.ts";
import { import {
@@ -122,7 +122,7 @@ function scriptedModel(turns: Array<{ toolCalls?: Array<{ toolCallId: string; to
const steps: FauxResponseStep[] = turns.map((step) => { const steps: FauxResponseStep[] = turns.map((step) => {
if (step.toolCalls) { if (step.toolCalls) {
return fauxAssistantMessage( return fauxAssistantMessage(
step.toolCalls.map((c) => fauxToolCall(c.toolName, c.input as Record<string, unknown>, { id: c.toolCallId })), step.toolCalls.map((c) => fauxToolCall(c.toolName, c.input as ToolCall["arguments"], { id: c.toolCallId })),
{ stopReason: "toolUse" }, { stopReason: "toolUse" },
); );
} }
+4 -4
View File
@@ -22,7 +22,7 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import * as fs from "node:fs"; import * as fs from "node:fs";
import { after, describe, it } from "node:test"; import { after, describe, it } from "node:test";
import { createFauxCore, fauxAssistantMessage, fauxToolCall, type FauxResponseStep } from "@earendil-works/pi-ai"; import { createFauxCore, fauxAssistantMessage, fauxToolCall, type FauxResponseStep, type ToolCall } from "@earendil-works/pi-ai";
import type { PiEngine } from "../src/agent/models.ts"; import type { PiEngine } from "../src/agent/models.ts";
import type { LhMessage } from "../src/core/native-messages.ts"; import type { LhMessage } from "../src/core/native-messages.ts";
import { newPlanBlockMessage } from "../src/core/native-messages.ts"; import { newPlanBlockMessage } from "../src/core/native-messages.ts";
@@ -268,7 +268,7 @@ function submitModel(name: string, input: unknown, onCall?: () => void): PiEngin
onCall?.(); onCall?.();
void context; void context;
return fauxAssistantMessage( return fauxAssistantMessage(
[fauxToolCall(name, input as Record<string, unknown>, { id: `${name}-call` })], [fauxToolCall(name, input as ToolCall["arguments"], { id: `${name}-call` })],
{ stopReason: "toolUse" }, { stopReason: "toolUse" },
); );
}, },
@@ -956,7 +956,7 @@ function findingsStep(capture?: (view: string) => void): FauxResponseStep {
return (context) => { return (context) => {
capture?.(capturedView(context)); capture?.(capturedView(context));
return fauxAssistantMessage( return fauxAssistantMessage(
[fauxToolCall("submit_findings", FINDINGS as unknown as Record<string, unknown>, { id: "findings-call" })], [fauxToolCall("submit_findings", FINDINGS as unknown as ToolCall["arguments"], { id: "findings-call" })],
{ stopReason: "toolUse" }, { stopReason: "toolUse" },
); );
}; };
@@ -974,7 +974,7 @@ function passThroughAuditStep(): FauxResponseStep {
edges: [], edges: [],
attribution: { quiz: {} }, attribution: { quiz: {} },
dropped: [{ ref: "p2", reason: "evidence not found" }], dropped: [{ ref: "p2", reason: "evidence not found" }],
} as unknown as Record<string, unknown>, } as unknown as ToolCall["arguments"],
{ id: "review-call" }, { id: "review-call" },
), ),
], ],
+3 -2
View File
@@ -66,8 +66,9 @@ describe("the model catalog projection", () => {
assert.ok(glm, "glm-5.3-flash in catalog"); assert.ok(glm, "glm-5.3-flash in catalog");
assert.deepEqual(glm!.supportedLevels, ["low", "high", "max"]); assert.deepEqual(glm!.supportedLevels, ["low", "high", "max"]);
assert.deepEqual(glm!.input, ["text", "image"]); assert.deepEqual(glm!.input, ["text", "image"]);
// pi's current glm price (our old table said 0.15/0.5 — stale) // pi's current glm price (0.87.1 raised it back to 0.15/0.5 — the
assert.deepEqual(glm!.cost, { input: 0.075, output: 0.25, cacheRead: 0.015, cacheWrite: 0 }); // 0.85.x catalog had temporarily listed 0.075/0.25/0.015)
assert.deepEqual(glm!.cost, { input: 0.15, output: 0.5, cacheRead: 0.05, cacheWrite: 0 });
// model slots wave: the providers surface + the resolved slot pins // model slots wave: the providers surface + the resolved slot pins
const openrouter = providers.find((p) => p.id === "openrouter"); const openrouter = providers.find((p) => p.id === "openrouter");
assert.ok(openrouter && openrouter.keyPresent, "openrouter federated (key pinned above)"); assert.ok(openrouter && openrouter.keyPresent, "openrouter federated (key pinned above)");