JuliusBrussee/caveman · error
cave_incomplete_evidence
cave_incomplete_evidence
Error message
cave_incomplete_evidence: Pi emitted no final assistant message
What it means
Thrown when the run produced no final assistant message AND no stopReason was recorded — Pi emitted nothing and the runtime did not deliberately stop. Per the inline comment this is 'the honest outcome rather than missing evidence': the caller gets an error instead of a fabricated empty answer.
Source
Thrown at packages/agent/src/runtime.ts:2107
}
if (ladderFailure) throw ladderFailure;
if (spendFailure) throw spendFailure;
if (usageFailure !== undefined &&
(efficiencyPlan !== undefined ||
executionContext.spendLedgers.length > 0 ||
usageFailure.message === "cave_provider_model_identity_mismatch" ||
usageFailure.message === "cave_provider_identity_missing")) {
throw usageFailure;
}
if (pendingSpendReservations.length > 0) {
markSpendIncomplete(executionContext.spendLedgers);
throw new Error("cave_subagent_spend_evidence_incomplete");
}
// A run stopped before its first call has no assistant message, and that is
// the honest outcome rather than missing evidence: the caller gets an empty
// answer, zero usage, and the reason the runtime declined to spend.
if (!finalMessage && stopReason === undefined) {
throw new Error("cave_incomplete_evidence: Pi emitted no final assistant message");
}
if (finalMessage &&
(finalMessage.stopReason === "error" || finalMessage.stopReason === "aborted")) {
throw new Error(`cave_provider_terminal_${finalMessage.stopReason}`);
}
const text = finalMessage === undefined ? "" : assistantText(finalMessage);
if (definition.output?.schema && finalMessage !== undefined) {
let parsed: unknown;
try {
parsed = JSON.parse(text);
} catch {
throw new Error("cave_output_schema_invalid_json");
}
if (!Value.Check(definition.output.schema, parsed)) {
throw new Error("cave_output_schema_mismatch");
}
}
if (appliedPlan.appliedTransformIDs.length > 0 && !cacheBoundaryKnown) {View on GitHub (pinned to 27d5a3981a)
Solutions
- Retry the run — transient empty streams from providers are the most common cause
- Log the provider request/response cycle to confirm the provider truly returned no message
- Verify your streamFn / provider adapter forwards assistant_message events correctly
- If it reproduces deterministically, capture the provider's raw response and file it against the adapter
Defensive patterns
Strategy: retry
Type guard
function isNoFinalMessage(e: unknown): e is Error {
return e instanceof Error && e.message.startsWith("cave_incomplete_evidence");
} Try / catch
for (let attempt = 0; attempt < 2; attempt++) {
try { return await agent.run(input, opts); }
catch (e) { if (!isNoFinalMessage(e) || attempt === 1) throw e; }
} Prevention
- Log raw provider responses during development to spot empty streams early
- Keep stream adapters simple — throwing inside streamFn after the call starts produces this
- Retry once on empty-stream errors before escalating
When it happens
Trigger: The provider stream ends without any assistant message and without the budget ladder stopping the run — e.g. the run is interrupted before its first call completes, or the provider closes the stream cleanly but empty.
Common situations: Provider returning an empty completion; network interruption before the first token; misconfigured stream that resolves immediately; abort racing the first call.
Related errors
- cave_harness_transform_not_evaluated
- cave_harness_unknown_transform
- cave_fixture_terminal_evidence_missing
- caveman build: invalid .caveman/provider.json
- caveman build: set CAVE_MODEL when zero or multiple provider
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/f29c68d53a609123.
Report an issue: GitHub.