provider-events: raw provider stream recorder (LH_PROVIDER_EVENTS=1)

pi-ai 1.0's onProviderStreamEvent, at the layer our incident class lives
(GLM arg truncation, DeepSeek id resets) — env-gated (owner choice:
no app surface, off by default; raw events carry full prompts), one
JSONL per chat under data/debug/provider-events/ with a 20MB cap and a
run-start marker for record correlation.

Wired at every agent construction: teacher/prober turns (AgentBuildOptions.
chatId), jobs via LoopContext (researcher, demo-builder through
runSubagent; visual-maker + pdf-reader through their streamOnce), and
the extraction pipeline (extractor + auditor keyed by source id).
practice deliberately unwired (sunk-cost module).
This commit is contained in:
amosblomqvist
2026-10-04 20:21:18 +03:00
parent d2d159d8ae
commit a2c1287415
12 changed files with 330 additions and 10 deletions
+16 -5
View File
@@ -1,6 +1,7 @@
import { Agent } from "@earendil-works/pi-agent-core";
import type { AgentEvent, AgentMessage, AgentTool } from "@earendil-works/pi-agent-core";
import { questionResult, ToolInputError } from "./tool-execute.ts";
import { invalidInputResult, questionResult } from "./tool-execute.ts";
import { providerEventObserver } from "./provider-events.ts";
import type { Usage } from "@earendil-works/pi-ai";
import { PATHS } from "../../config.ts";
import { piEngineFor, type PiEngine } from "./models.ts";
@@ -111,6 +112,11 @@ export interface AgentBuildOptions {
* fresh per turn, honoring enabled overrides (switch on + file
* exists); absent → the cached pure-source composition. */
promptSwitches?: Record<string, boolean>;
/** Debug: when set AND LH_PROVIDER_EVENTS=1, raw provider stream events
* (pre-normalization) append to data/debug/provider-events/
* <chatId>.jsonl via providerEventObserver — the GLM-truncation /
* DeepSeek-id incident layer. Absent or env-off → no observer. */
chatId?: string;
}
/**
@@ -158,6 +164,10 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
const systemPrompt = opts.promptSwitches
? composeInstructions(kind, opts.promptSwitches)
: composedInstructions[kind]!;
// Debug recorder (owner, 2026-10-04): raw provider events when
// LH_PROVIDER_EVENTS=1 and the caller handed a chatId; undefined is
// the normal no-observer path (tests, env-off).
const providerEvents = opts.chatId ? providerEventObserver(opts.chatId, kind) : undefined;
const agent = new Agent({
initialState: {
systemPrompt,
@@ -167,6 +177,7 @@ function baseAgentOptions(kind: AgentKind, opts: AgentBuildOptions, tools: Agent
messages: opts.messages ?? [],
},
streamFn: engine.streamFn,
...(providerEvents ? { onProviderStreamEvent: providerEvents } : {}),
// 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
@@ -232,9 +243,9 @@ export function createTeacherAgent(
}
/** The bare teach question set (tests, bare agents): quiz with the shared
* authoring refinement (quizInputProblems, run in execute — throws
* ToolInputError, which pi's loop converts to the error tool result the
* model retries on) but NO per-session nodeId check (no record to read),
* authoring refinement (quizInputProblems, run in execute — returns an
* invalidInputResult isError result the model retries on, pi 1.0
* convention) but NO per-session nodeId check (no record to read),
* plus ask. */
export function bareQuestionTools(): Record<string, AgentTool<any>> {
// A typed const (not an inline literal): the specific AgentTool typing
@@ -248,7 +259,7 @@ export function bareQuestionTools(): Record<string, AgentTool<any>> {
executionMode: "sequential",
execute: async (_toolCallId, input) => {
const problems = quizInputProblems(input);
if (problems.length > 0) throw new ToolInputError("quiz", problems);
if (problems.length > 0) return invalidInputResult("quiz", problems);
return questionResult();
},
};