{"record":{"id":"50b217e204af87cd","repo":"JuliusBrussee/caveman","slug":"caveman-agent-run-ended-without-terminal-evidence","errorCode":null,"errorMessage":"caveman agent: run ended without terminal evidence","messagePattern":"caveman agent: run ended without terminal evidence","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":1001,"sourceCode":"  options: InternalRunOptions,\n  executionContext: InternalExecutionContext,\n): Promise<RunResult> {\n  let final: RunResult | undefined;\n  for await (const event of streamAgentWithOptions(\n    definition,\n    input,\n    options,\n    executionContext,\n  )) {\n    if (event.type === \"run_end\") final = event.result;\n    if (event.type === \"run_error\") {\n      // The ledger is not lost on the throwing path either: the partial receipt\n      // rides on a typed `cause` so a caller that only awaits the promise can\n      // still read what was spent before the failure.\n      throw new CavemanRunError(event.code, event.message, event.receipt);\n    }\n  }\n  if (!final) throw new Error(\"caveman agent: run ended without terminal evidence\");\n  return final;\n}\n\nexport function streamAgent(\n  definition: AgentDefinition,\n  input: string,\n  options: RunOptions = {},\n): AsyncGenerator<CavemanRunEvent> {\n  rejectInternalRunOptions(options);\n  return streamAgentWithOptions(\n    definition,\n    input,\n    options,\n    rootExecutionContext(definition, options.maxCostUsd, options),\n  );\n}\n\nfunction streamAgentWithOptions(","sourceCodeStart":983,"sourceCodeEnd":1019,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/766dce6b1394ebb56a3090748d5a0240a5aefb36/packages/agent/src/runtime.ts#L983-L1019","documentation":"Thrown on the promise-returning path (runtime.ts:1001) when the run's own event stream finishes without ever emitting run_end or run_error. Every well-formed run terminates with exactly one of those events; their absence means the stream was cut short — almost always a caller-supplied streamFn/transport that ends the stream without a terminal event, and otherwise a framework bug worth reporting.","triggerScenarios":"A custom RunOptions.streamFn wrapper (mocking, proxying, telemetry) that returns or completes the underlying stream without forwarding Pi's terminal event; an onPayload/stream adapter that swallows the final chunk; an abort path that closes the generator early. Bare runAgent with no custom options hitting this indicates a runtime bug — file it with the receipt.","commonSituations":"Writing a fake streamFn for tests that stops after N chunks and forgets the closing event; inserting a middleware that filters events too aggressively; version drift between the exact-pinned Pi core and a custom transport written against an older event shape.","solutions":["Audit any custom streamFn: it must forward the terminal run_end/run_error event unchanged","Temporarily remove the custom streamFn/onPayload options and re-run to confirm the stock path terminates correctly","If it reproduces with no customized options, capture the partial receipt and report a framework issue"],"exampleFix":"// before — test double ends the stream without a terminal event\nconst fakeStream: StreamFn = async function* () { yield partialEvent; };\n// after — always forward/emit the terminal event\nconst fakeStream: StreamFn = async function* (selected, context, streamOptions) {\n  for await (const ev of realStream(selected, context, streamOptions)) yield ev;\n};","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  const result = await runAgent(def, input, opts);\n} catch (err) {\n  if (err instanceof Error && err.message === 'caveman agent: run ended without terminal evidence') {\n    // Almost certainly your custom streamFn dropped the terminal event.\n    // Log the options in use, remove custom streamFn/onPayload, and re-run.\n    // If it reproduces with stock options, report it upstream with the run options (no secrets).\n  }\n  throw err;\n}","preventionTips":["Custom streamFn wrappers must forward every event, especially run_end/run_error — write them as pass-through for-await loops","In test doubles, always terminate the fake stream with the same terminal event shape the real transport emits","Pin the exact framework version when customizing transports; event shapes are versioned with the exact-pinned upstream"],"tags":["runtime-invariant","streaming","events"],"backgroundTag":"premature-stream-close","analyzedSha":"766dce6b1394ebb56a3090748d5a0240a5aefb36","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}