| description | Every deliberate SDK error is an SDKError with a stable LOUSHO_* code, a hint and a link to its section here; this page lists each code with its cause and fix. |
|---|
Nearly every error the SDK throws on purpose is an SDKError (or a subclass
such as ConfigurationError, ValidationError or MissingPeerDependencyError)
with:
code: a stable string,LOUSHO_<AREA>_<NAME>. Branch on it, not on the message text, which may get clearer over time.hint: one sentence on how to fix it.docs: a link to the code's section on this page.
The message ends with the same information on its own line, so an uncaught error tells you what to do:
ConfigurationError: createAgent: no model configured. Do one of the following: (1) pass a model: ...
[LOUSHO_CONFIG_MISSING_PROVIDER] Pass a model string such as createAgent({ model: 'openai/gpt-4.1-mini' }), a provider instance, or set LOUSHO_MODEL or a provider API key. (https://github.com/LinuxDevil/agent-sdk/blob/main/docs/errors.md#lousho_config_missing_provider)
A run that rejects (send(), stream()'s result, a session turn,
AgentExecutor.execute()) puts what it had spent on the error as
error.usage, a RunUsage like result.usage (its sub-agents included), so
a failed run's tokens and cost are not lost. It is set on any error the run
rejects with - an SDKError, a PropagatingToolError or your own - unless the
error has a usage property of its own, and it is not enumerable (logging or
serialising the error is unchanged). It is absent when the run failed before
it started.
import { createAgent, type RunUsage } from '@lousho/build-ai-agent';
try {
await createAgent({ provider }).send('Go');
} catch (error) {
const spent = (error as { usage?: RunUsage }).usage;
if (spent) console.error(`failed after ${spent.modelCalls} model calls, $${spent.costUsd ?? '?'}`);
throw error;
}error.detail is the message without that line. Tool and provider errors
(ToolExecutionError, LLMProviderError, TimeoutError, RateLimitError)
keep their message as it was, because the model sees it as a tool result or a
compacted provider error; their toString() still adds the line.
The exceptions are a few named Error subclasses with no code, kept plain
on purpose: PropagatingToolError and its subclasses (a tool throws one to
stop the run), WorkspaceError (a workspace operation refused or failed -
the model sees it as a tool error), McpToolError (a loaded MCP tool whose
server reports isError; also a tool error), AuthError (an
@lousho/build-ai-agent/auth entry rejects the request with its 401 or 403),
CronExpressionError (parseCronExpression(), which
defineSchedule() wraps as LOUSHO_SCHEDULE_INVALID) and
DecryptionError (EncryptionUtils, DTOEncryptionFilter).
CassetteMismatchError (recordReplay replay; see
Testing) is not one of these: it is an
SDKError with code LOUSHO_CASSETTE_INVALID
and .cassette / .callNumber fields.
import { createAgent, SDKError } from '@lousho/build-ai-agent';
try {
await createAgent({ provider, instructions: 'Be brief.' }).send('hi');
} catch (error) {
if (error instanceof SDKError && error.code === 'LOUSHO_SESSION_AWAITING_APPROVAL') {
console.log(error.hint, error.docs);
} else {
throw error;
}
}ERROR_CODES (exported) maps every code to its hint. A test keeps it, the codes
used in the source and the sections below in sync. A second test fails when new
SDK code throws a literal new Error(...) instead of an SDKError; the few
sites left are internal and listed in src/utils/plainErrors.test.ts with the
reason for each.
To start from a symptom instead of a code, see Troubleshooting.
Find a code by area:
Means: an option or argument has a value the SDK cannot use. This is the
default code of ConfigurationError; error.field names the option when known.
Fix: change the option the message names.
Example: withFallback([]) throws "withFallback() needs at least one provider".
new NodeWorkspace({ root }) with a missing or non-directory root, a duplicate tool
name in a ToolRegistry, a bad toolConcurrency, serveMcp() without a name
and lousho studio without Agent Forge's files are the same code.
Means: there is no model to run: createAgent() got no model or
provider and found nothing in the environment, or AgentExecutor.execute() /
stream() got no provider.
Fix: pass model: 'openai/gpt-4.1-mini' (any <provider>/<model>), pass a
provider instance, or set LOUSHO_MODEL or a provider key such as
OPENAI_API_KEY. The vendor/ model prefix chooses the provider; with
OpenRouter use openrouter/<vendor>/<model> (e.g.
openrouter/openai/gpt-4.1-mini). See Providers.
Example: createAgent({ instructions: 'x' }) with no provider env var set.
Means: AgentExecutor.execute() / stream() was called without agent.
Fix: pass the agent, e.g. AgentBuilder.create().setName('a').build(), or
use createAgent(), which needs no separate agent object.
Example: AgentExecutor.execute({ input: 'hi', provider }).
Means: AgentExecutor.execute() / stream() was called without input.
Fix: pass the user message as a string or a Message[].
Example: AgentExecutor.execute({ agent, provider }).
Means: two options that mean the same thing were both given.
Fix: keep one. For createAgent(), prompt is a deprecated alias of instructions:
keep instructions.
Example: createAgent({ provider, instructions: 'a', prompt: 'b' }).
Means: send() or stream() got a sessionId, which makes the run
durable, but the agent has no checkpoint store.
Fix: pass createAgent({ store: memoryStore() }) (or a SqliteStore, or a
store with checkpoints), or drop sessionId. See
Durable execution.
Example: createAgent({ provider }).send('hi', { sessionId: 'job-1' }).
Means: a createAgent() option given as a function of the run (model,
instructions / prompt or tools) threw while the run's config was being
resolved. The run never started: send() rejects, stream() ends with an
error event, and a session's transcript is left as it was. error.field
names the option and error.cause is what the function threw.
Fix: fix the function named in the message. See Dynamic config.
Example: createAgent({ provider, model: ({ metadata }) => plans[metadata.plan].model }) with an unknown plan.
Means: a model string is not <provider>/<model>.
Fix: write both parts, e.g. 'openai/gpt-4.1-mini' or
'anthropic/claude-sonnet-4-5'.
Example: resolveProvider('gpt-4o').
Means: the provider prefix of a model string is not one the SDK knows. The message lists the supported prefixes and suggests the closest one.
Fix: use a supported prefix (openai, anthropic, google, openrouter,
ollama, pi, mock), or pass your own provider instance. For a vendor with no
prefix, the message lists the other routes: openrouter/<vendor>/<model>,
pi/<vendor>/<model> when pi serves the vendor, or the vendor's AI SDK package
through fromAiSdk() (see Providers).
Example: createAgent({ model: 'opnai/gpt-4o' }) says "Did you mean 'openai/gpt-4o'?".
Means: the provider's credential env var (OPENAI_API_KEY,
ANTHROPIC_API_KEY, OPENROUTER_API_KEY) is not set.
Fix: set it, or pass a configured provider instance.
Example: createAgent({ model: 'openai/gpt-4.1-mini' }) without OPENAI_API_KEY.
Means: a model call failed (LLMProviderError, and
CompactedLLMProviderError, whose compacted.category says why:
timeout, context-length-exceeded, quota-exceeded, auth-failure,
unknown; a rate-limit failure has its own code,
LOUSHO_PROVIDER_RATE_LIMITED).
An OpenAI-compatible response that reports a failure inside a 200 (OpenRouter's
finish_reason: "error" or an error chunk mid-stream) fails as a retryable
502 instead of returning the cut-off text.
A context overflow is context-length-exceeded (not retryable) whatever the
status, including llama.cpp / LM Studio's "exceeds the available context size"
and "Context size has been exceeded", and whether it arrives from the request
or as a stream error. A connection failure is timeout and names the cause,
e.g. connection refused (ECONNREFUSED) at http://localhost:1234/v1/responses - is the server running?. When the provider's error body could not be parsed
(the message is only Bad Request), a short snippet of the body is appended.
Fix: for quota-exceeded (OpenAI's insufficient_quota 429, a 402 such as
OpenRouter's "Insufficient credits"; not retryable, retrying cannot add credit),
top up the account or fall back to another provider; for auth-failure, fix the API key; for context-length-exceeded,
shorten the conversation (see Context compaction); for
transient failures, use withRetry() / fallbackModels
(see Providers).
Example: a 401 from the provider with a revoked key.
Means: the provider throttled the caller: a model call failed with
compacted.category: 'rate-limit' (a CompactedLLMProviderError, still an
LLMProviderError), or a RateLimitError.
Fix: retry after compacted.retryAfterMs (withRetry() does this, up to
backoff.maxMs; a longer Retry-After fails at once with
compacted.retryAfterMs set), or send fewer requests.
Example: a 429 response (OpenAI's out-of-credit insufficient_quota 429
is quota-exceeded, so LOUSHO_PROVIDER_REQUEST_FAILED).
Means: a feature needs an optional package that is not installed
(MissingPeerDependencyError, or a provider's SDK such as @ai-sdk/openai).
A model call whose provider package is missing fails with this code (a
CompactedLLMProviderError whose cause is the MissingPeerDependencyError),
not LOUSHO_PROVIDER_REQUEST_FAILED, and the server routes send its message
(the install command) to the client instead of the generic one.
Fix: run the npm install command in the message (also on
error.installCommand). See Installation.
Example: SubprocessSandbox without dockerode:
npm install dockerode@^5.0.1.
Means: an agent has a hosted tool (webSearch(), codeInterpreter(),
fileSearch() or hostedTool()) that its provider cannot send: the provider
has no hosted tools (Ollama, a custom LLMProvider without
supportsHostedTool()), ai 4 is installed (hosted tools need ai 6 or 7), or
the installed provider package has no such tool. The message names the
provider, the tool and what would work.
Fix: run the tool on a provider and package pairing that supports it (see
Hosted provider tools), or leave it out of tools for
this provider.
Example: createAgent({ model: 'ollama/llama3.2', tools: [webSearch()] })
rejects its first send().
Means: a user message has a file part ({ type: 'file', data, mimeType })
that the provider cannot send: its media type is not one the provider sends on
your ai major (see Multimodal input). The
call is rejected before anything is sent, so the model never answers without
the file. Under withFallback() (or fallbackModels) the next provider gets
the call. agent.send() and agent.stream() reject with this code.
Fix: send the file to a provider and ai major that takes its type (or
list the type in fromAiSdk(model, { fileMediaTypes })), put the file's text
in the message, or set the provider's unsupportedFiles: 'text-note' to send
[file <name> (<type>) not sent] in its place, as before.
Example: a PDF sent to new OllamaProvider({ name: 'ollama' }).
Means: loadSpec() was given a path that does not exist.
Fix: check the path in the message - a typo, a wrong working directory, or a spec that was never scaffolded.
Example: loadSpec('agent.yaml') when no agent.yaml is in the cwd.
Means: loadSpec() found fields that fail validation. Each problem is
listed as '<path>': <problem>, plus any top-level field that looks like a
typo, with a suggestion.
Fix: fix each listed field. See Configuration.
Example:
loadSpec: 'agent.yaml' failed validation - 'prompt': missing required field 'prompt'; unknown field 'promt' (did you mean 'prompt'?)
Means: the spec is otherwise valid, but a top-level field is a likely typo of a spec field and would be ignored. Other unknown fields are still ignored.
Fix: rename it to the suggested field, or remove it.
Example: tool: [http] gives "unknown field 'tool' (did you mean 'tools'?)".
Means: the spec file's extension is not .yaml, .yml or .json.
Fix: rename the file, or convert it.
Example: loadSpec('agent.toml').
Means: a spec's tools entry names a tool that is not a built-in tool.
Fix: use one of the tools the message lists, or build the agent with
createAgent({ tools: [...] }) and your own tool. See Tools.
Example: tools: [not-a-real-tool] in a spec.
Means: a spec names a tool (github, jira) that needs credentials an
agent spec has no field for.
Fix: build the agent with createAgent() and pass the configured tool, e.g.
createGitHubTools(config) from @lousho/build-ai-agent/integrations.
Example: tools: [github] in a spec.
Means: a ToolExecutionError (or subclass, such as
ToolArgumentsValidationError). Inside a run the model gets it as a tool
result and the run carries on.
Fix: look at error.toolName and error.cause, and fix the tool or the
input it was given.
Example: a tool's execute threw. The built-in tools (http, email,
github, jira, slack, ask_question) throw an SDKError with this code for
a failed call; their message is the tool's result, so it carries no appended
[code] hint (docs) line.
Means: the model called a tool with arguments that do not match its input
schema (a ToolArgumentsValidationError). Inside a run the model gets the
issues as a tool result and can retry, so a run seldom ends on it.
Arguments that are not valid JSON (truncated, say) get the same error, with one
(root) issue: arguments are not valid JSON: <parse error>; received: <the text, truncated>. The tool does not run. A small repair is tried first: a
surrounding markdown code fence, trailing commas and double-encoded JSON are
fixed when the result is a JSON object. Empty arguments ("") mean {}. The
call's tool.start event carries rawArgs, the text the model sent, whenever
it was repaired or failed to parse.
Fix: if you called the tool yourself, fix the arguments named in the message;
if the model keeps getting them wrong, make the field .describe()
text clearer. error.issues lists each path and problem. See Tools.
Example: the model sends { to: 42 } to a tool whose to is a string.
Means: a stdio MCP server (mcpServers entry with command) did not
start: the command was not found, the process exited during the initialize
handshake, or it did not answer within connectTimeoutMs (default 60
seconds). The error (McpStartError) carries exitCode (or signal) and
stderr, the last 20 lines the process wrote there, and the message shows
both. connectMcp() and agent.ready() (or the first send()) fail with it.
Fix: read the stderr lines in the message, or run the command with its
args yourself. Check the package name and version for an npx server,
the env it needs, and cwd. With stderr: 'inherit' or 'ignore' there
are no lines to show. See MCP.
Example: npx -y some-mcp-server for a package that does not exist exits
with an npm error 404 line on stderr.
Means: a call to a loaded MCP tool failed at the protocol level: the
server exited or closed the connection (MCP error -32000: Connection closed), the request timed out, or the server answered with a JSON-RPC
error. The message names the tool as the model sees it (<server>__<tool>);
cause is the MCP SDK's own error, whose numeric code is the JSON-RPC one.
A tool that ran and reported isError: true is not this error (the model gets
its text as an McpToolError), nor is a sign-in request
(LOUSHO_MCP_AUTH_REQUIRED) or an abort. During
a run the result is a tool error and the run goes on; a flow step with
retry: { maxAttempts } runs again, and a server connected with lazy
(default) reconnects on the next call.
Fix: check the server named in the message is running and answers; for a
stdio server, run its command yourself and read its stderr. Raise the
server's timeoutMs if the request timed out.
Example: a stdio server crashes while handling files__read.
Means: a tool that needsApproval was called in an
AgentExecutor.execute() run that has no approvalStore to pause in.
Fix: pass approvalStore: new InMemoryApprovalStore() (or a persistent
store), or use createAgent(), which has one by default. See
Approvals.
Example: AgentExecutor.execute({ agent, input, provider, toolRegistry })
with a needsApproval tool.
Means: resumeAfterApproval() or agent.approvals.resolve() got an id that
is not pending: unknown, or already resolved.
Fix: resolve an id from agent.approvals.list() (or the approvalId of the
paused result); each approval resolves once.
Example: calling agent.approvals.resolve({ id, approved: true }) twice.
The approvals route (POST /chat/:sessionId/approvals/:id) answers this code
with a 404 also when the approval belongs to another session than the one
in the URL.
Means: agent.approvals.resolve() (or streamResolve(), or
resumeAfterApproval()) got an approval whose paused run no longer waits on
it: the run's checkpoint (a session turn's <id>.turn-<n>, or a
send(msg, { sessionId }) run's) was deleted (by checkpoints.delete(id),
discardPending(), a TTL, or by hand) or replaced by a newer run under the same
id. The tool was not run, and the approval is withdrawn: it is no longer listed,
and resolving it again fails with LOUSHO_APPROVAL_NOT_FOUND.
Fix: start a new turn in the session (or a new run) instead; there is
nothing left to continue. SqliteStore.prune() keeps paused turns, so this
needs the checkpoint to be removed some other way.
Example: deleting cust-7.turn-2 from the checkpoint store, then
approving the refund that turn paused on.
Means: a 409 from the approvals route: another request decided the same
approval first, and its continuation is still running (on this server
process).
Fix: do not decide again; read the session (GET /chat/:sessionId) to see
how the turn went on.
Example: two supervisors click "Approve" on the same refund at once.
Means: a 403 from the approvals route: the route's authorizeApproval
refused this caller. By default only the caller the paused run acts for may
decide it (see Route auth).
Fix: decide as a caller authorizeApproval accepts, or change
authorizeApproval.
Example: user bob posts a decision on an approval of alice's run.
Means: a 400 from the approvals route: the body has neither
approved (a boolean) nor answer (a string).
Fix: send { "approved": true } (or false, with an optional note),
or { "answer": "..." } for an ask_question pause.
Example: posting { "approve": "yes" } to
/chat/alice-1/approvals/ap-1.
Means: a 400 from the approvals route: answer was sent to a pause
that is not a question. A question pause takes answer — or approved with
the answer as note (and approved: false to decline); a tool-call pause
takes approved; a sign-in pause completes itself once the user signs in.
Fix: send the field for the pause's kind; GET /chat/:sessionId shows a
pending pause's approvalKind.
Example: posting { "approved": true } to a pause that waits for the
model's ask_question answer.
Means: a 403 from a session route: the route's authorizeSession
refused this caller the action (read, chat or approve) on this session.
Fix: use a session this caller owns, or change authorizeSession.
Example: user bob reads GET /chat/alice-1, which alice used first
(the default when no authorizeSession is set), or authorizeSession only
allows ids that start with the caller's id.
Means: a 403 from a session route before it ran: a POST came from a page
on another origin (its Origin is not the server's own and not in
allowedOrigins, or the browser marked it Sec-Fetch-Site: cross-site), or
the Host header is not one the server allows (allowedHosts, or loopback
names on lousho dev).
Fix: call from the server's own origin, or add the page's origin to
allowedOrigins (see Route auth); for a
Host refusal, add the name to allowedHosts (--allowed-hosts on
lousho dev).
Example: a page on https://evil.example posts to /api/agent/chat with
the user's session cookie.
Means: a 400 from a session route: the body is not valid JSON, or it
lacks a field the route needs (POST /chat without sessionId and input,
an empty input, an input with no user message).
Fix: send the fields the error message names, e.g.
{ "sessionId": "chat-1", "input": "Hello" } (see
HTTP API).
Example: POST /chat with { "sessionId": "chat-1", "input": [] }.
Means: a 415 from a session route: a POST body was not sent as JSON.
Form and text/plain bodies are refused because another site can send them
without a CORS preflight.
Fix: send Content-Type: application/json (curl -H 'Content-Type: application/json'), or pass requireJsonBody: false.
Example: curl -d '{"sessionId":"a","input":"hi"}' /chat without -H
sends application/x-www-form-urlencoded.
Means: a SessionAwaitingApprovalError: the session or sessionId run is
paused on an approval (error.approvalId), so it cannot take new input yet.
Fix: resolve the approval with agent.approvals.resolve() (or
resumeAfterApproval() with the same checkpointStore), then send again. See
Durable execution.
Over HTTP, POST /chat to such a session is a 409 with this code and the
approvals to decide (approvalId, approvalIds).
Example: session.send('next') while the previous turn waits on an approval.
Means: a session id is not 1-128 characters of letters, digits, _ and
- (ids become file names, so ../ and / are refused). Or a run id
(agent.send() / agent.stream()'s sessionId, a checkpoint store's id) is
not 1-200 characters of letters, digits, _, - and . (not starting or
ending with .), the same rule on every store; or it ends with .turn-<n> or
.paused, which are a session's own checkpoint keys (a run under
'victim.turn-2' would write into session victim's turn), or .fire (a
schedule's fire claim). The message says which of the two rules applies: an id
with a . or over 128 characters is a valid run id but not a valid session id.
Fix: use an id such as 'chat-9f2c4e7a1b', or omit it to get a generated one. On a
route that serves several users, do not make ids guessable from a user's name or number
(see Who may use a session).
Example: agent.session({ id: '../etc' }), or
agent.send(msg, { sessionId: 'chat.turn-2' }).
Means: a stored transcript (a FileSessionStore file, or a SqliteStore
row) is not valid JSON or not a JSON array of messages, e.g. a write cut short.
Fix: restore the file from a backup, or delete it to start the session over:
session.clear() removes it.
Example: sessions/user-42.json containing {}.
Means: session.compact(), session.clear() or session.fork() was called while a turn of
that session is running or queued. From send() / stream(): another writer the in-process
queue cannot see (another process, or a second store object over the same data) committed a
turn while this one ran, so this turn was not committed (atomically with a store that has
saveIf: memory, file and SQLite; best effort with KV). Tool calls of the turn that ran are
kept: the turn up to their results is added after the other writer's turn. Also from send(),
resume(), agent.send(msg, { sessionId }), agent.resume() and approvals.resolve(): the
checkpointed run (or session turn) is running in another process, replica or store object right
now, which holds its run lease (see
Durable execution); nothing ran here, and a resolved
approval stays pending. Over HTTP, a 409 from POST /chat: a request with the same
Idempotency-Key is still running its turn of that session on this server (see
Resending a turn).
Fix: await the turn's send() (or abort it), then call again. After a send() that failed
this way, send again: the session reads the current transcript at the start of every call. A
lease left by a process that died is taken over once it expires (30 s, plus up to 30 s of
clock-skew grace on the file and SQLite stores), or at once when that process provably exited
on the same host and PID namespace. A run that stalled past its lease fails with this code at
its next checkpoint instead of overwriting the run that took over.
Means: session.compact() was called while a durable session has an
interrupted turn, whose checkpoint is keyed by the transcript length.
Fix: finish it with session.resume() or drop it with
session.discardPending(), then call again.
Means: a durable session's send() / stream() found the session's
unfinished turn, whose last attempt failed with a provider error that retrying
will not fix (a context-length or auth failure, or another 4xx). The turn's
checkpoint records it as lastError and attempts; the turn is not replayed.
Fix: drop the turn with session.discardPending() and send again, or fix
the cause and retry it with session.resume().
Means: stream() was called on an AgentSession built by hand without a
streaming runner.
Fix: get the session from agent.session(), which can stream, or call
send().
Example: new AgentSession(run).stream('hi').
Means: session.fork({ fromStep }) was given a step that is not an integer
from 0 to the number of steps in session.history(). The error is a
ConfigurationError (field: 'fromStep') whose message lists the valid range.
Fix: pick a step from session.history(), or 0 to keep nothing.
Example: session.fork({ fromStep: 9 }) on a session with three steps.
Means: session.fork({ id }) was given an id that already has a transcript
in the session's store (or a checkpointed turn the fork would pick up), or the
session's own id. The error is a ConfigurationError (field: 'id').
Fix: pick an unused id, or leave id out to get <id>-fork-<n>.
Example: session.fork({ fromStep: 1, id: session.id }).
Means: fork() was called on an AgentSession built by hand without the
function that creates sibling sessions.
Fix: get the session from agent.session(), which can fork.
Example: new AgentSession(run).fork({ fromStep: 0 }).
Means: lousho eval --url, remoteTarget() or a remoteAgent() sub-agent
got 401 from the deployed agent: the bearer token is missing or wrong. An eval
case fails (the run goes on); the lead model gets a structured tool error. The
token is never part of the message.
Fix: pass the deployment's LOUSHO_API_TOKEN with --token or the
LOUSHO_EVAL_TOKEN environment variable. See
Run evals against a deployment.
Example: lousho eval --url https://agent.example.com against a deployment with a token set.
Means: a remote eval case (lousho eval --url, remoteTarget()) or a
remoteAgent() task could not run: the deployment was unreachable, answered
with a non-2xx status, was aborted, or its event stream broke or was truncated
(no run.done). A remoteAgent() task whose remote run ends in an error also
fails with it, and the message says "the remote run ended in an error". The
lead model gets it as a structured tool error; the bearer token is never part
of it.
Fix: read the message (it names the url and, for a sub-agent, the remote
session id); check the URL, GET <url>/health and the deployment's logs.
Example: lousho eval --url http://localhost:1 with nothing listening.
Means: a task call asked to resume or fork a taskId that this lead
session has no conversation for (never started, started in another lead
session or run, or not finished), or that belongs to another sub-agent. The
lead model gets it as a structured tool error.
Fix: use a taskId from an earlier task result of the same lead
session, with the same agent; or omit taskId to start a new task. See
Sub-agents.
Example: task({ agent: 'researcher', taskId: 'task_7', prompt }) when the
session has only task_1.
Means: a task call asked to resume or fork a task whose sub-agent is
still running, for example a background task that has not ended.
Fix: wait for it with agent_await (or stop it with agent_cancel),
then continue it.
Example: task({ agent: 'researcher', taskId: 'task_1', prompt }) right
after starting task_1 with background: true.
Means: AgentExecutor.fork() or agent.fork() was asked for a step the
session's checkpoint history does not have: the session is unknown, the step
was never reached, or its entries were dropped past the store's historyLimit.
The message lists the steps that are kept.
Fix: fork at one of the listed steps, or raise historyLimit on the store.
See Durable execution.
Example: agent.fork('job-1', { fromStep: 9 }) after a 3-step run.
Means: a stored checkpoint cannot be read: it is not valid JSON or not a
checkpoint, typically a write cut short by a crash or power loss on a store
without durable writes. send(), resume() and agent.resume() fail on it,
and agent.pending() lists the run as 'in-progress' with this code in its
lastError (retryable: false).
Fix: drop it: session.discardPending() (or session.clear()) for a
session's turn, checkpointStore.delete(id) for a run. The interrupted run
cannot be resumed.
Example: a 0-byte checkpoints/chat.turn-2.json left after a power loss.
Means: a paused or interrupted run was resumed by an agent that differs
from the one that saved it, and onAgentDrift is 'error'. The message names
what changed: the model, tools added, removed or with a changed input schema,
or the instructions. It is thrown before any model call or tool runs; the
checkpoint (and, for an approval, the pending record) is left as it was.
Fix: resume with the agent that paused the run, or set onAgentDrift to
'warn' (the default) or 'ignore' to continue anyway. See
Durable execution.
Example: createAgent({ store, onAgentDrift: 'error' }) after a deploy that
renamed a tool, then agent.resume('job-1').
Means: a resumed run is waiting on a tool call (an approved call, or a call
of the model's last turn that has no result yet) whose tool the resuming agent
no longer has. This is an error whatever onAgentDrift is, because the call
cannot run.
Fix: give the tool back under the same name, or drop the paused run (delete its checkpoint, reject its approval). See Durable execution.
Example: a run paused on charge_card, then a deploy removes that tool and
agent.approvals.resolve({ id, approved: true }) is called.
Means: an AgentRun from session.stream() was iterated a second time.
Fix: collect the events in the first for await loop, or call stream()
again for a new run. See Streaming.
Example: two for await (const event of run) loops over the same run.
Means: defineSchedule() was given an invalid definition: a cron expression
that does not parse (the message names the field), or not exactly one of
prompt and run. Agent directories hit this while loading schedules/.
Fix: correct the expression or give the schedule one of prompt / run.
See Schedules.
Example: defineSchedule({ cron: '61 * * * *', prompt: 'hi' }).
Means: a prompt schedule's turn ended with a finishReason other than
'stop'; the message names it. startSchedules() passes this to onError,
fireSchedule() rejects with it, and a Worker's scheduled() logs it. With
'awaiting-approval' the message names the approval, which stays pending.
Fix: resolve the pending approval with agent.approvals.resolve(), or
change the prompt, tools, output schema or limits so an unattended turn can
finish. See Schedules.
Example: a schedule whose prompt makes the agent call a needsApproval tool.
Means: a file in an agent directory's channels/ folder does not default-export
a channel (an object with parse and reply). The message names the file.
Fix: default-export a channel made with defineChannel(), httpChannel(),
webhookChannel(), slackChannel(), discordChannel(), telegramChannel(), githubChannel() or teamsChannel(). See Channels.
Example: export default { cron: 'x' } in channels/sms.ts.
Means: a file in an agent directory's memory/ folder does not default-export
a memory slot (an object with a scope and a provider). The message names the file.
Fix: default-export defineMemory({ ... }), or the same options without a name
(the file name is used). See Memory and Agent directories.
Example: export default { cron: 'x' } in memory/notes.ts.
Means: lousho add could not read a registry document: the URL did not answer
in time or returned an error, the local file is missing, the scheme is not http(s)
or the document is larger than the cap.
Fix: check the --registry value (or LOUSHO_REGISTRY) and your connection.
See Registry.
Example: lousho add x --registry https://example.invalid/index.json.
Means: the registry's index has no item with that name. The message suggests the closest name when there is one.
Fix: run lousho add --list and use one of the names.
Example: lousho add web-serach when the item is web-search.
Means: a registry index or item document is not valid JSON or does not match the format (a missing field, an unknown item type, an item whose document names another item).
Fix: fix the document the message names. See Registry.
Example: an item document without files.
Means: an item asks to write a file that is absolute, has .., backslashes or a
drive letter, is outside the folder its type may write to, resolves outside the agent
directory through a symlink, or is larger than the size caps. Nothing was written.
Fix: do not install the item; tell whoever hosts the registry. See Registry.
Example: a tool item with a file ../../.bashrc.
Means: a file the item would write already exists. Nothing was written.
Fix: pass --overwrite, or move your file away first.
Example: lousho add web-search twice.
Means: an item's code reaches for something its permission manifest does not
declare: it imports child_process without exec: true, writes files without
filesystem: "write", calls fetch() or names a URL host that is not in network,
reads an environment variable that is not in env, or uses eval, new Function,
a computed process.env[...] or a dynamic import() / require(), which are never
allowed. The message lists every finding as <file>:<line>: <what> (<fix>). Nothing
was written. --dry-run reports it too.
Fix: do not install the item; tell whoever hosts the registry. Its author fixes the code or declares the permission. See Registry.
Example: a tool item whose file imports node:child_process while its manifest
leaves exec out.
Means: a SubprocessSandbox with network: { allow } and a broker cannot make
the credential broker the container's only route out on this Docker daemon, so it
started no container instead of granting open egress. The message names the reason:
Docker Desktop (containers run in a VM, so the host has no address on the internal
network), rootless Docker, a daemon on another machine (the broker cannot listen on
the network's gateway), a reused network that is not internal, or an Engine older
than 25.0.5, which forwards DNS from internal networks.
Fix: run the agent on the Linux host of a Docker Engine 25.0.5 or later, or use
network: 'none'. See Workspace tools.
Example: new SubprocessSandbox({ network: { allow: ['api.github.com'] }, broker }) with Docker Desktop.
Means: SubprocessSandbox.writeFile() was given a path outside the sandbox's
root (by .., an absolute path elsewhere, or a symlink that leads out), or the
root itself. Nothing was written.
Fix: write under the root, or construct the sandbox with the root that should
contain the file (new SubprocessSandbox({ root }); the default is the working
directory at construction). See Guardrails.
Example: sandbox.writeFile('/etc/profile.d/x.sh', ...) with the default root.
Means: loadAgentDir() (or lousho dev, lousho build, which use it) could
not load a directory: it is missing or unreadable, a file is empty, a tools/
file has no usable export, a config file does not parse, or a sub-agent folder is
malformed. The message names the file.
Fix: correct the file the message names. See Agent directories.
Example: loadAgentDir('./agents/support') where instructions.md is empty.
Means: a skill is malformed (defineSkill(), a skills/ folder, including
loadSkills() on a file with bad frontmatter) or withSkills() was given
duplicate names, or a skill name that collides with the load_skill or
read_skill_file tool.
Fix: give each skill a unique name, a description and content; rename a tool called
load_skill. See Skills.
Example: defineSkill({ name: 'x', description: '', content: '...' }).
Means: a flow definition is wrong: a missing or duplicate input name, a missing flow name or code, or a node of an unknown type.
Fix: fix the part of the flow the message names. See Flows.
Example: two .input('city') calls on one FlowBuilder.
Means: a flow's toolCall step was not run. A permission rule or the tool's
needsApproval policy denied it, or the call needs approval and the flow
context's approve callback rejected it, returned 'defer' (a flow cannot
pause), or was not passed at all.
Fix: pass approve in the flow context to decide calls that need approval,
or change the rule that denied the call. See Flows.
Example: a flow step that calls a needsApproval: true payment tool, run
with no approve callback.
Means: FlowExecutor.resume() was called with a flow whose steps changed
(added, removed, moved, or a step's type, id, tool or agent changed) since the
run was saved. Completed steps are saved by their place in the flow, so the
saved results would land on the wrong steps. Prompts and other settings may
change freely.
Fix: resume with the flow version that started the run, or start a new run
under another runId. See Flows.
Example: a run saved after charge in [charge, notify], resumed with
[audit, charge, notify].
Means: storage failed: a SQLite database could not be opened (the cause
has the driver's error), was used after close(), has a newer schema than this
SDK knows, or node:sqlite is missing; or a file exceeded the storage size limit;
or a fileStore() / FileSessionStore file could not be read or replaced
because another handle kept it open (Windows EPERM / EACCES / EBUSY
for longer than the ~1.3 s the store retries; the cause is the last error);
or a fileMemory change waited more than 10 s for another writer's
<key file>.lock lock file.
Fix: check the path and permissions, use Node >= 22.13 for SqliteStore (or a
file-based store), and create a new store after closing one.
Example: new SqliteStore('/read-only/agent.db').
Means: a SqliteStore write could not get the database's write lock because another connection or process (a second replica, a backup, a long transaction) holds it. The driver waits only briefly (50 ms) so the event loop is not frozen; the stores' transactional writes (checkpoint and approval saves, resolve(), saveIf(), OAuth sign-in claims, SQLite memory updates) then retry with backoff for about 5 seconds before they give up with this error. prune() does not retry. The original driver error is the cause.
Fix: retry the call. If it keeps happening, find what holds the lock for long.
Example: two processes saving to the same agent.db while one runs a multi-second transaction.
Means: a trigger adapter got invalid options: a cron adapter without exactly
one of intervalMs / cron, a webhook auth block with an empty secret or an
unknown type, or a Slack trigger that cannot verify requests.
Fix: use the example in the message. See Channels and Schedules.
Example: webhookChannel({ auth: { type: 'hmac', secret: '' } }).
Means: a call to a chat platform's API (Slack, Discord, Telegram, GitHub) failed; the message names the call and the HTTP status or the platform's error.
Fix: check the bot token and its permissions, and the platform status. See Channels.
Example: Slack chat.postMessage answering channel_not_found, Telegram sendMessage answering 400 for an unknown chat, or GitHub POST /repos/{owner}/{repo}/issues/{number}/comments answering 403 for a token without write access.
Means: lousho build / lousho dev / the node-server runtime could not
bundle or start the agent: a missing --agent, an agent path that is not found
or not an agent directory, a bundling failure (an import that cannot be resolved; install it, or set LOUSHO_BUILD_VERBOSE=1 for the full log), a bad LOUSHO_STORE value, missing runtime sources, or a tool the
Cloudflare Worker target does not have.
Fix: follow the message. See Deployment.
Example: lousho build --target node-server without --agent.
Means: an eval helper was used wrongly: defineEval() outside vitest,
t.judge() before t.send() or without a judge, or llmJudge() outside the
judge runner.
Fix: follow the message. See Evals.
Example: calling t.judge('polite') before t.send('hi').
Means: a check a test helper makes did not hold: an eval did not pass, or a
mockModel() script still had unused turns at the end.
Fix: read the message; fix the agent or remove the extra scripted turns. See Testing.
Example: mockModel([...three turns]) where the agent stopped after two.
Means: a record/replay cassette is missing, is not valid JSON or does not
match the recorded request. The message names the file. A mismatch inside a
run reaches send() as the CassetteMismatchError itself (from
@lousho/build-ai-agent/testing, with .cassette and .callNumber) - it is
a test-fixture failure, so it is never compacted into a
CompactedLLMProviderError.
Fix: record it again (lousho eval --record <file>, or recordReplay() with
mode: 'record'). See Testing.
Example: lousho eval --replay for an eval that was never recorded.
Means: an SDKError created without a code.
Fix: read the message; it says what failed.
Example: new SDKError('Something failed').
Means: an AgentExecutionError: running an agent failed.
Fix: look at error.cause for the underlying failure.
Example: new AgentExecutionError('Agent failed', agentId, cause).
Means: a FlowExecutionError: a flow step failed.
Fix: look at error.step and error.cause. See Flows.
Example: a flow step whose agent threw.
Means: a ValidationError: input failed validation.
Fix: fix the fields listed in error.errors.
Example: new ValidationError('Validation failed', { email: ['Invalid email'] }).
Means: a TimeoutError: an operation (error.operation) did not finish
within error.timeoutMs.
Fix: raise the timeout, or make the operation faster.
Example: a flow step with timeoutMs whose attempt takes longer than that (error.operation is flow.node <type>).
Means: reserved. An invalid structured-output reply is not thrown today: the
run ends with finishReason: 'output-invalid' and outputError.
Fix: see Structured output.
Example: a reply that does not match output: zodSchema after the repair step.
Means: a run's or a session's limits budget (maxTokens, maxCostUsd,
maxDurationMs, ...) tripped under onExceeded: 'throw'. BudgetExceededError
carries budget: { limit, value, max, scope }. With the default
onExceeded: 'stop' nothing is thrown: the run ends with
finishReason: 'budget-exceeded'.
Fix: raise the limit named in the message, or drop onExceeded: 'throw'.
See Budgets.
Example: createAgent({ provider, limits: { maxCostUsd: 0.01, onExceeded: 'throw' } }) whose run costs more than a cent.
Means: an input, output or tool guardrail blocked a run under
onTripped: 'throw'. GuardrailError carries
guardrail: { name, kind, reason, toolName? }. With the default
onTripped: 'stop' nothing is thrown: the run ends with
finishReason: 'guardrail'.
Fix: look at error.guardrail for which guardrail blocked and why, or drop
onTripped: 'throw'. See Input and output guardrails.
Example: createAgent({ provider, guardrails: { input: [maxLengthGuardrail({ maxChars: 10 })], onTripped: 'throw' } }) sent a longer message.
Means: a route auth helper from @lousho/build-ai-agent/auth (jwt(),
oidc(), basic(), apiToken()) got options it cannot verify with, and
refused them when it was created, not at the first request. The message names
the option: no key source or two of them, an HMAC secret shorter than 32
bytes, an algorithm that does not fit the key (secret with RS256, a public
key with HS256), no audience (and no allowAnyAudience: true), a
clockToleranceSec above 300, a key-set or issuer URL that is not https, an
empty user list or token (also createRouteHandler({ auth: '' }) and
serveFetch(request, ctx, '')).
Fix: change the option the message names. See Route auth and principals.
Example: jwt({ secret: process.env.JWT_SECRET! }) without audience.
Means: a file, SQLite or KV store was asked to store an OAuth token, a
pending sign-in or a registered client (or to read one that exists), and it has
no token key: neither its tokenKey option nor the LOUSHO_TOKEN_KEY
environment variable is set. Tokens are only ever stored encrypted, and there
is no default key. Reads of records that do not exist need no key.
Fix: generate a key once with generateTokenKey() (32 random bytes as
base64), keep it as a secret, and pass it as tokenKey or set
LOUSHO_TOKEN_KEY. On Cloudflare Workers, add it with
wrangler secret put LOUSHO_TOKEN_KEY. See Token storage.
Example: new SqliteStore('./agent.db').tokens.set('github', { owner: 'app' }, token)
with LOUSHO_TOKEN_KEY unset.
Means: a stored OAuth record could not be decrypted: the store's key is not the one it was written with, or the record was changed or copied to another owner. The message names the provider, never the token.
Fix: use the key the tokens were written with. While rotating, list the old
key after the new one (tokenKey: [newKey, oldKey], or
LOUSHO_TOKEN_KEY="<new>,<old>"). If the old key is lost, delete the record
(tokens.delete(provider, owner)) and sign in again. See
Token storage.
Example: a SqliteStore file written with one tokenKey and opened with
another.
Means: a tool called ctx.getToken() (or requireAuth()) for a provider
whose credential belongs to each user (credentialOwner: 'user', the default),
but the run has no principal, so there is no user to own the token. The call
fails with this error; the run does not pause and no sign-in link is made.
Fix: serve the agent behind route auth or a channel, so each run has a
principal (Auth), or define the provider with
credentialOwner: 'app' when one credential is meant for everyone. See
Tools that need sign-in.
Example: agent.send('List my repositories') without { principal }, with
a tool that calls ctx.getToken(github).
Means: a tool asked for the app's own credential (credentialOwner: 'app')
and none is stored (or it was revoked). A chat user is never sent the app's
sign-in link, so the call fails instead of pausing.
Fix: sign the app in once: open the URL from
agent.oauth.signInUrl(provider) and let the callback store the token. See
App credentials.
Example: a post_update tool calling ctx.getToken(slackBot) before the
operator connected Slack.
Means: a tool called ctx.getToken(), or agent.oauth was used, on an
agent without a token store, so there is nowhere to read or keep a token. The
run stops with this error. An HTTP MCP server with oauth needs the store
too: connecting it (ready(), connectMcp() without tokens) fails with
this error.
Fix: pass createAgent({ store }) with tokens: memoryStore(),
fileStore(), SqliteStore or KVStore (the persistent ones need a
tokenKey). See Token storage.
Example: createAgent({ provider, tools: [listRepos] }) with no store.
Means: agent.oauth.complete() (the /oauth/callback route) got a state
that is unknown, was already used, or is older than 10 minutes, or (with an
authenticated callback request) a sign-in started for another user. The route
answers 400.
Fix: start again from a fresh link: approve nothing, let the tool ask
again (a new pause has a new link), or call agent.oauth.signInUrl() again for
an app sign-in. See The callback route.
Example: opening the same provider redirect twice, or after 10 minutes.
Means: agent.approvals.resolve({ id, approved: true }) was called on a
sign-in pause (kind: 'sign-in') before the user's token was stored. The pause
stays as it was. On POST /chat/:id/approvals/:approvalId this is HTTP 409.
Fix: open the sign-in link first and let the provider redirect to the
callback, then approve again; or approve with false to cancel the call. See
What the user sees.
Example: clicking "I've signed in" before finishing the provider's page.
Means: the provider's token endpoint refused to exchange the authorization
code (or a request to it failed), or the callback came without a code, or no
provider with the pending sign-in's name is defined in this process. The
message names the provider, the HTTP status and the OAuth error code, never
the code or the response body. The callback route answers 502. For an MCP
server with oauth the message names the server and the OAuth error code.
Fix: check tokenUrl, clientId, clientSecret, clientAuth and that
redirectUri matches the one registered with the provider exactly, and that
the module defining the provider is loaded by the process serving the
callback. Then sign in again. See The callback route.
Example: invalid_client from GitHub after the client secret was rotated.
Means: an HTTP MCP server with oauth refused the connection or a tool
call because the app has no usable token for it: nobody has signed the app in
yet, the grant was revoked, or the server's url changed since the sign-in (a
token is only sent to the url it was obtained for). The server's status becomes
'needs-auth'. When connecting, ready() (and connectMcp()) fails with this
error; during a run, the tool call's result is this error and the run goes on.
A chat user is never asked to sign in for an MCP server, and the message never
contains the authorization URL.
Fix: sign the app in once: open the URL from
agent.oauth.mcpSignInUrl('<server>') and let the provider redirect to the
callback route. The next connection or tool call uses the stored token. See
MCP servers with OAuth.
Example: createAgent({ mcpServers: { linear: { url, oauth: { redirectUri } } }, store })
and await agent.ready() before the operator signed in.