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
+9
View File
@@ -106,6 +106,9 @@ interface LoopContext {
/** Fires when abortJob kills the run; abortSignal is passed into every
* model call so the spend stops mid-call, not at the next loop check. */
abortSignal: AbortSignal;
/** The job's chat (debug: raw provider-event recording under
* LH_PROVIDER_EVENTS=1 — forwarded into every subagent run). */
chatId?: string;
/** Fold one model call's usage into the job record (progress write). */
onUsage: (step: UsageStepLike) => void;
}
@@ -127,6 +130,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
const usageTotals = new Map<string, JobUsage>();
function makeLoopContext(jobId: string, engine: PiEngine): LoopContext {
const chatId = jobStore.getJob(jobId)?.chatId;
// Provider-qualified stamp: the engine's actual model — the only
// thing that ran (BYOK: no config-pin fallback stamps exist).
const model = `${engine.model.provider}:${engine.model.id}`;
@@ -137,6 +141,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
registerJobAbort(jobId, controller);
return {
abortSignal: controller.signal,
chatId,
onUsage: (step) => {
const next = accumulateJobUsage(usageTotals.get(jobId), model, step);
usageTotals.set(jobId, next);
@@ -355,6 +360,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
engine: model,
search: deps.searchProvider,
abortSignal: loop.abortSignal,
chatId: loop.chatId,
// Money channel: each agent step is one model call — fold its
// usage into the record as it completes (progress write).
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
@@ -403,6 +409,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{
engine: model,
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
onActivity: (line) => jobStore.updateJob(jobId, { activity: line }),
},
@@ -438,6 +445,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{
engine: model,
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
onActivity: (line) => jobStore.updateJob(jobId, { activity: line }),
},
@@ -476,6 +484,7 @@ export function createJobRunner(deps: JobRunnerDeps): JobRunner {
{
engine: model,
abortSignal: loop.abortSignal,
chatId: loop.chatId,
onUsage: (usage) => loop.onUsage(piUsageToStep(usage)),
onActivity: (line) => jobStore.updateJob(jobId, { activity: line }),
},