JuliusBrussee/caveman · error · Error

caveman agent: run ended without terminal evidence

Error message

caveman agent: run ended without terminal evidence

What it means

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.

Source

Thrown at packages/agent/src/runtime.ts:1001

  options: InternalRunOptions,
  executionContext: InternalExecutionContext,
): Promise<RunResult> {
  let final: RunResult | undefined;
  for await (const event of streamAgentWithOptions(
    definition,
    input,
    options,
    executionContext,
  )) {
    if (event.type === "run_end") final = event.result;
    if (event.type === "run_error") {
      // The ledger is not lost on the throwing path either: the partial receipt
      // rides on a typed `cause` so a caller that only awaits the promise can
      // still read what was spent before the failure.
      throw new CavemanRunError(event.code, event.message, event.receipt);
    }
  }
  if (!final) throw new Error("caveman agent: run ended without terminal evidence");
  return final;
}

export function streamAgent(
  definition: AgentDefinition,
  input: string,
  options: RunOptions = {},
): AsyncGenerator<CavemanRunEvent> {
  rejectInternalRunOptions(options);
  return streamAgentWithOptions(
    definition,
    input,
    options,
    rootExecutionContext(definition, options.maxCostUsd, options),
  );
}

function streamAgentWithOptions(

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Audit any custom streamFn: it must forward the terminal run_end/run_error event unchanged
  2. Temporarily remove the custom streamFn/onPayload options and re-run to confirm the stock path terminates correctly
  3. If it reproduces with no customized options, capture the partial receipt and report a framework issue

Example fix

// before — test double ends the stream without a terminal event
const fakeStream: StreamFn = async function* () { yield partialEvent; };
// after — always forward/emit the terminal event
const fakeStream: StreamFn = async function* (selected, context, streamOptions) {
  for await (const ev of realStream(selected, context, streamOptions)) yield ev;
};
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const result = await runAgent(def, input, opts);
} catch (err) {
  if (err instanceof Error && err.message === 'caveman agent: run ended without terminal evidence') {
    // Almost certainly your custom streamFn dropped the terminal event.
    // Log the options in use, remove custom streamFn/onPayload, and re-run.
    // If it reproduces with stock options, report it upstream with the run options (no secrets).
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/50b217e204af87cd. Report an issue: GitHub.