NOETIC
Framework

Error Model

Noetic uses structured NoeticError objects with error codes and recovery hints.

Quick Example

import { AgentHarness, isNoeticError } from '@noetic-tools/core';

declare const harness: AgentHarness;

try {
  const result = await harness.execute('Do something');
} catch (e) {
  if (isNoeticError(e)) {
    console.log(e.noeticError.kind); // e.g. 'step_failed'
    console.log(e.message);         // Human-readable description
  }
}

Noetic wraps every framework-level failure in a structured NoeticError discriminated union. Instead of catching generic exceptions and guessing what went wrong, you can match on the kind field and access typed metadata for each error variant.

NoeticError Type

NoeticError is a discriminated union with a kind field. Each variant carries context-specific data:

kindKey FieldsWhen it occurs
step_failedstepId, cause: Error, retriesExhaustedA step throws after exhausting retries.
model_refusedstepId, refusal: stringThe model returns a refusal instead of content.
model_parse_errorstepId, raw: string, schema: StandardSchemaV1, zodError: ZodErrorStructured output fails schema validation. schema is the Standard Schema v1 validator; zodError stays populated for backward compatibility — the real ZodError for Zod schemas, a synthetic custom-issue ZodError adapted from the vendor's Standard Schema issues otherwise.
model_rate_limitstepId, retryAfter?: numberThe LLM provider returns a rate-limit response.
fork_partialstepId, succeeded[], failed[]Thrown by inParallel mode 'all' when any path fails, and by 'race' when every path fails. (Mode 'settle' never throws this — it reports failures in its SettleResult array instead.)
spawn_summary_failedstepId, childOutput: unknown, summaryCause: ErrorA spawn's child succeeded but a context layer's onReturn hook (such as a summarizer) failed. Never thrown: the spawn returns the child's unsummarized output, and this error is reported as a layer diagnostic and a spawn.summary_failed span event.
channel_timeoutchannelName, timeout: numberA recv() call — or a back-pressured send() parked on a full queue — exceeded its timeout.
channel_closedchannelNameAttempted to send on a closed external channel.
cancelledreason?: stringThe execution was cancelled via ctx.abort() or harness.cancel() — including a parent's cancellation cascading into an inParallel path or spawn child. Pending channel recv calls (and parked back-pressure send calls) on the aborted context reject with this kind immediately, and an in-flight model call or ACP agent turn is cut short. Under a graceful stop (cancel(ctx, { drainMs })), the steps, rounds and tool calls it held back throw this kind once the drain ends.
budget_exceededfield: 'cost' | 'steps' | 'duration', limit, actualA budget guard detected the run exceeded its limits.
steering_deniedguidance?: stringA context layer's steering hook (beforeToolCall / afterModelCall) returned a deny decision and the runtime blocked the action, or the harness approveToolCall approver refused an invokeTool call to a tool with needsApproval. guidance carries the corrective feedback, if any.
item_schema_mismatchcategory: 'items' | 'developerMessages' | 'toolCalls' | 'toolResults', itemType?: stringAn item failed extension-schema validation in the ItemSchemaRegistry: a category item (e.g. a tool result) matched none of the schemas registered for its category, or an unknown item type matched no registered schema under strict mode. itemType is set for the unknown-type case.
connection_unavailableconnection, reasonA connection is disconnected, its pool stayed full past waitMs, or an event stream a step was waiting on ended.
connection_failedconnection, op, errorThe server refused or failed the operation. error.code is one of the codes connection profiles define, such as malformed, guarded or ambiguous.
connection_ref_invalidconnection, reasonA ref was not issued by this harness, belongs to a different connection than the call's target, was used from another thread, or is stale.
connection_secret_refusedconnection, opA secret() was used outside a field the operation marks sensitive, or by the model.
connection_input_requiredconnection, requestA long-running operation asked a question nobody could answer.
connection_resume_refusedconnection, reasonA resumed run's connection was replaced and cannot safely be driven again.
verify_act_failedstepId, cause: Error, run?: BlobRefA step.verify's act threw. The verification run is recorded first, with the failure as evidence, and run points at it.

Full Type Definition

type NoeticError =
  | { kind: 'step_failed'; stepId: string; cause: Error; retriesExhausted: boolean }
  | { kind: 'model_refused'; stepId: string; refusal: string }
  | { kind: 'model_parse_error'; stepId: string; raw: string; schema: StandardSchemaV1; zodError: ZodError }
  | { kind: 'model_rate_limit'; stepId: string; retryAfter?: number }
  | { kind: 'fork_partial'; stepId: string; succeeded: Array<{ stepId: string; value: unknown }>; failed: Array<{ stepId: string; error: NoeticError }> }
  | { kind: 'spawn_summary_failed'; stepId: string; childOutput: unknown; summaryCause: Error }
  | { kind: 'channel_timeout'; channelName: string; timeout: number }
  | { kind: 'channel_closed'; channelName: string }
  | { kind: 'cancelled'; reason?: string }
  | { kind: 'budget_exceeded'; field: 'cost' | 'steps' | 'duration'; limit: number; actual: number }
  | { kind: 'steering_denied'; guidance?: string }
  | { kind: 'item_schema_mismatch'; category: 'items' | 'developerMessages' | 'toolCalls' | 'toolResults'; itemType?: string }
  | { kind: 'connection_unavailable'; connection: string; reason: string }
  | { kind: 'connection_failed'; connection: string; op: string; error: { code: ConnectionErrorCode; message: string; details?: Record<string, unknown> } }
  | { kind: 'connection_ref_invalid'; connection: string; reason: string }
  | { kind: 'connection_secret_refused'; connection: string; op: string }
  | { kind: 'connection_input_required'; connection: string; request: { id: string; message: string; schema?: JsonSchema } }
  | { kind: 'connection_resume_refused'; connection: string; reason: string }
  | { kind: 'verify_act_failed'; stepId: string; cause: Error; run?: BlobRef };

Model provider failures

A failing provider call in a callModel or decide step never reaches you as a raw Error. A 429 becomes model_rate_limit, a failure caused by cancellation becomes cancelled, and anything else becomes step_failed with the provider's error on cause.

NoeticErrorImpl Class

NoeticErrorImpl extends the standard Error class, so it works with try/catch and stack traces. The structured error data lives on the .noeticError property:

class NoeticErrorImpl extends Error {
  readonly noeticError: NoeticError;
  constructor(error: NoeticError);
}

The message string is auto-generated from the error kind and its fields. For example, a step_failed error produces:

Step 'fetch-data' failed: Connection refused

isNoeticError Type Guard

Use isNoeticError() to narrow an unknown catch value:

import { isNoeticError } from '@noetic-tools/core';

function isNoeticError(e: unknown): e is NoeticErrorImpl;

This is an instanceof check against NoeticErrorImpl with a structural fallback: any Error carrying a noeticError object whose kind is a string also passes. The fallback keeps the guard reliable when more than one copy of the framework is loaded (mixed src/dist resolution, duplicated node_modules), where a plain instanceof would fail across module copies. The inner kind is not validated against the closed union, so errors minted by a newer framework version still pass.

Handling Errors in Loops

Loops accept an onError callback that receives the structured error and returns a recovery action:

import { callModel, loop, until } from '@noetic-tools/core';

const resilientLoop = loop({
  id: 'retry-loop',
  steps: [callModel({ id: 'generate', model: 'openai/gpt-4o' })],
  until: until.noToolCalls(),
  onError(error, ctx) {
    if (error.kind === 'model_rate_limit') {
      return 'retry';  // Try the same iteration again
    }
    if (error.kind === 'step_failed' && !error.retriesExhausted) {
      return 'skip';   // Move to the next iteration
    }
    return 'abort';    // Re-throw the error
  },
});
Return valueBehavior
'retry'Re-execute the same iteration immediately.
'skip'Skip this iteration and continue with the previous output.
'abort'Re-throw the error, ending the loop.

Matching on Error Kind

Because NoeticError is a discriminated union, TypeScript narrows the type after checking kind:

declare const e: unknown;

if (isNoeticError(e)) {
  const err = e.noeticError;

  switch (err.kind) {
    case 'model_parse_error':
      // TypeScript knows: err.raw, err.schema, err.zodError
      console.log('Validation errors:', err.zodError.issues);
      break;

    case 'fork_partial':
      // TypeScript knows: err.succeeded, err.failed
      console.log(`${err.succeeded.length} paths ok, ${err.failed.length} failed`);
      break;

    case 'budget_exceeded':
      // TypeScript knows: err.field, err.limit, err.actual
      console.log(`${err.field} limit: ${err.limit}, used: ${err.actual}`);
      break;
  }
}

NoeticConfigError

NoeticConfigError is a separate class from NoeticError. It reports a setup mistake — an invalid builder option, missing credentials, a workflow that references something unregistered — rather than a failure while running. Builders throw it when you construct a step; AgentHarness.run() and execute() check what they can before the first step runs.

class NoeticConfigError extends Error {
  readonly code: string;    // SCREAMING_SNAKE_CASE, e.g. 'MISSING_API_KEY'
  readonly hint: string;    // a sentence saying what to do next
  readonly docsUrl: string; // this page, at the section for `code`
}

function isNoeticConfigError(e: unknown): e is NoeticConfigError;

docsUrl defaults to https://noetic.tools/docs/framework/errors#<code in lower case> — the matching section below. For example, MISSING_API_KEY links to #missing_api_key.

import { isNoeticConfigError, loop, until } from '@noetic-tools/core';

try {
  loop({ id: '', steps: [], until: until.maxSteps(10) });
} catch (e) {
  if (isNoeticConfigError(e)) {
    console.error(`${e.code}: ${e.message}\n${e.hint}\nSee ${e.docsUrl}`);
  }
}

Configuration Error Codes

Every code a NoeticConfigError can carry, with its cause and fix.

Credentials and Harness Setup

NO_LLM_PROVIDER

Cause: A callModel or decide step needs a model, but the harness has no LLM credentials. AgentHarness.run() and execute() raise this before any step runs when the model step is visible in the step tree; a model step chosen at run time raises it when it is reached.

Fix: Pass callModelDefaults: { apiKey: '...' } to AgentHarness (it defaults to the Noetic platform) or set NOETIC_API_KEY. For OpenRouter directly, pass callModelDefaults: { provider: 'openrouter', apiKey: '...' } or set OPENROUTER_API_KEY.

MISSING_API_KEY

Cause: createOpenRouterEmbed() was called without an OpenRouter API key.

Fix: Pass the key explicitly, for example createOpenRouterEmbed(process.env.OPENROUTER_API_KEY).

MISSING_CALL_MODEL

Cause: A steering() rule is evaluated by an LLM, but no model call is available to the layer.

Fix: Give the harness LLM credentials (see NO_LLM_PROVIDER), or use a rule that does not need a model.

NO_STEP_CONFIGURED

Cause: harness.execute() (or the session API) was called on a harness built without an agentGraph.

Fix: Pass agentGraph to the AgentHarness constructor, or drive steps directly with harness.run(step, input, ctx).

MISSING_HARNESS_CONTEXT

Cause: A workflow() or dynamicWorkflow() step ran on a context with no harness attached.

Fix: Run the step through AgentHarness.run() (or execute()), which attaches the harness to the context.

STEP_LEDGER_RETENTION_INVALID

Cause: environment.storage.stepLedgerRetention has a cap that is not a positive number or Infinity.

Fix: Omit the field to take the default, pass a positive number, or pass Infinity to remove the cap.

CHECKPOINT_SCHEMA_MISMATCH

Cause: harness.restore() found a checkpoint written by a runtime version whose snapshot schema this version does not recognize.

Fix: Clear the snapshot with CheckpointStore.clear() and restart the execution.

Step Builders

EMPTY_STEP_ID

Cause: A step builder (runCode, callModel, invokeTool, loop, spawn, withContext, inParallel, conditional, schedule, decide, routeSwitch, workflow, step.acpAgent) or registerStep() got an empty id.

Fix: Give every step a unique, non-empty id, for example runCode({ id: 'fetch-data', ... }).

EMPTY_STEP_ARRAY

Cause: An empty array was given where a step goes (harness.run, harness.detachedSpawn, agentGraph, a spawn or withContext child, a schedule body, an inParallel path, a conditional route's pick, or a when/otherwise clause). An array of steps runs in sequence and needs at least one step.

Fix: Pass the steps to run, or a single step.

INVALID_STEP_IN_ARRAY

Cause: An element of an array of steps is not a step: a nested array, null or another non-object, or an object without a known step kind and a string id. The array is checked where it becomes a step (the builder, when/otherwise, harness.run, detachedSpawn, the harness constructor for agentGraph, or when an inParallel path or a conditional route's pick is resolved), so it fails there instead of as a TypeError mid-run. Nested arrays are rejected, not flattened. TypeScript rejects these arrays at compile time; the check covers untyped callers.

Fix: Put only steps in the array. Spread a nested array into its parent ([a, ...rest]) rather than nesting it.

MISSING_EXECUTE_FUNCTION

Cause: runCode() or tool() was called without an execute function.

Fix: Pass an async execute function — execute: async (input, ctx) => result for runCode, execute: async (args, toolCtx) => result for tool.

EMPTY_TOOL_NAME

Cause: tool() got an empty name.

Fix: Pass a unique name, for example tool({ name: 'greet', ... }).

MISSING_TOOL

Cause: invokeTool() was called without a tool.

Fix: Pass a tool built with tool().

MISSING_MODEL

Cause: callModel() got an empty model, a lazy model getter returned an empty string at run time, or a decide() step has neither its own model nor a harness decisionModel.

Fix: Pass a non-empty model id such as 'anthropic/claude-sonnet-4-20250514'. For decide, set model on the step or decisionModel on the harness.

UNKNOWN_TOOL_CHOICE

Cause: A callModel step's toolChoice: { tool } names a tool the step cannot call. callModel() (and the JSON hydrator) throws it at build time when tools is an eager array without that client tool; the harness throws it at request time, before anything is sent, when the tool is outside the unified tool set or the step's restriction.

Fix: Force a client tool the step offers — add it to tools, or register it through a context layer — or use toolChoice: 'required' to make the model call any allowed tool. Server tools cannot be forced.

MISSING_JSON_SCHEMA

Cause: A tool input schema or a structured-output schema cannot be turned into JSON Schema for the model.

Fix: Use a schema that implements StandardJSONSchemaV1 (Zod does), or pass inputJsonSchema on tool() / outputJsonSchema on the model step.

MISSING_LOOP_BODY

Cause: loop() got an empty steps array.

Fix: Add at least one step to steps.

MISSING_UNTIL_PREDICATE

Cause: loop() was called without an until predicate.

Fix: Pass one, for example until: until.maxSteps(10). Use until.never() for a loop that only an error or cancellation ends.

MISSING_CHILD_STEP

Cause: spawn() or withContext() was called without a child step.

Fix: Pass the step to run inside the child context as child.

MISSING_CONTEXT_LAYERS

Cause: withContext() was called without context layers.

Fix: Pass the layers to provide, for example context: [scratchpad()].

MISSING_MERGE_FUNCTION

Cause: inParallel() in 'all' or 'settle' mode was called without merge.

Fix: Pass a merge function that combines the path results into one output.

MISSING_ROUTE_FUNCTION

Cause: conditional() was called without a route function.

Fix: Pass a route function that picks the next step at run time.

MISSING_BODY_STEP

Cause: schedule() was called without a body step.

Fix: Pass the step to run on each scheduled iteration.

INVALID_INTERVAL_MS

Cause: schedule() got an interval that is negative, NaN, or infinite.

Fix: Pass a non-negative number of milliseconds.

INVALID_JITTER

Cause: schedule() got a jitter that is negative, NaN, or infinite.

Fix: Pass a non-negative number of milliseconds, or omit jitter.

Channels

INVALID_CHANNEL_TIMEOUT

Cause: A channel send() or recv() got a negative or NaN timeout. The call rejects with this error instead of waiting.

Fix: Pass a positive timeout in milliseconds, or 0 to wait with no timeout.

Decide and routeSwitch

INVALID_DECIDE_QUESTIONS

Cause: decide() got an invalid question map: no questions, a boolean question with neither instructions nor criteria, a choice question with fewer than 1 or more than 255 values or duplicates, or a score question with fewer than 2 or more than 10 levels or duplicates.

Fix: Correct the question named in the error message.

INVALID_UNTIL_DECIDER

Cause: A decide step used as a loop's until (directly, or inside any/all) does not ask exactly one question, or its question is not boolean. Raised where the loop or combinator is built. Also raised if a decider predicate is called outside a loop.

Fix: Give the decider one boolean question whose yes means stop, e.g. questions: { done: { type: 'boolean', instructions: 'Is the task finished?' } }.

INVALID_DECIDE_SAMPLES

Cause: decide() got a samples value that is not a positive integer.

Fix: Omit samples, or pass a whole number of runs to average, for example samples: 3.

UNKNOWN_ROUTE_QUESTION

Cause: routeSwitch() routes on a question its decide step does not ask.

Fix: Set on to one of the decide step's question ids.

INCOMPLETE_ROUTE_CASES

Cause: routeSwitch() has no case for one or more outcomes of the question it routes on.

Fix: Give a case for every outcome named in the error message.

INVALID_MIN_CONFIDENCE

Cause: routeSwitch() got a minConfidence outside [0, 1].

Fix: Pass a confidence between 0 and 1, for example minConfidence: 0.6.

MISSING_UNCERTAIN_STEP

Cause: routeSwitch() was called without an uncertain step.

Fix: Pass uncertain: the step to run when the model refuses or is not confident enough.

Context Layers

INVALID_HISTORY_MAX_ITEMS

Cause: history() got a maxItems that is not an integer in the allowed range.

Fix: Pass a sensible cap such as { maxItems: 40 }, or leave the layer out to keep history uncapped.

INVALID_BUDGET_INPUT

Cause: allocateBudgets() got NaN for a budget input (a layer budget, the total budget, or a reserve).

Fix: Pass a real number. Infinity is allowed and means uncapped.

JSON Workflows

WORKFLOW_VALIDATION_FAILED

Cause: A workflow document does not match WorkflowDocumentSchema, one of its expressions does not parse as JSONata, or dynamicWorkflow()'s planner failed to produce a valid document within its revision budget.

Fix: Correct the document against the published JSON Schema, and fix any expression syntax the message names (it gives the JSONata error code and position). For dynamicWorkflow, give the planner clearer instructions or a more capable model, or raise maxRevisions.

INVALID_WORKFLOW_SOURCE

Cause: workflow() got both or neither of document and ref.

Fix: Pass exactly one: document: { version: 1, root: ... } for an inline workflow, or ref: '<name>' with a workflows registry.

UNKNOWN_WORKFLOW_REFERENCE

Cause: A workflow() step's ref, or a subflow node, names a workflow that is not registered.

Fix: Register the workflow under that name in the workflows option (or HydrationContext.workflows).

WORKFLOW_RECURSION_LIMIT

Cause: Named sub-workflows recursed deeper than maxRecursionDepth (default 100) on one execution path. Raised at execution time, when the ref one level too deep would resolve.

Fix: Give the recursion a base case, such as a conditional that stops calling the subflow, or raise maxRecursionDepth (Infinity removes the limit).

INVALID_MAX_RECURSION_DEPTH

Cause: maxRecursionDepth is not a positive integer or Infinity. Raised when a named ref first resolves.

Fix: Pass a positive integer such as 100, or Infinity.

EXPRESSION_FAILED

Cause: An expression failed at execution time: JSONata raised an error (such as arithmetic on a string), a compute expression gave no value or gave a function, or a test gave something other than a boolean. The message names the node and quotes JSONata's error code.

Fix: Fix the expression or the input it reads. A path that matches nothing gives no value, so check field names against the node's input.

EXPRESSION_LIMIT_EXCEEDED

Cause: An expression ran past one of its limits: its step budget, its time budget, its nesting depth, or the size of a sequence it built.

Fix: Bound the recursion or data size in the expression, or raise the limits through expressionLimits on parseAndRunWorkflow, dynamicWorkflow, workflow(), or HydrationContext.

INVALID_EXPRESSION_LIMITS

Cause: An expressionLimits value is not a positive integer or Infinity.

Fix: Pass a positive integer for each limit you set, Infinity to lift one, or leave it unset to keep its default.

UNKNOWN_NODE_KIND

Cause: A workflow node has a kind the hydrator does not know.

Fix: Use one of the node kinds listed in the error's hint.

UNKNOWN_UNTIL_PREDICATE

Cause: A workflow loop's until predicate has an unknown kind.

Fix: Use one of maxSteps, maxCost, maxDuration, noToolCalls, never, outputContains, outputEquals, converged, expr, any, all, or decide.

UNKNOWN_TOOL_REFERENCE

Cause: A workflow node names a tool that is not in HydrationContext.tools.

Fix: Register the tool under that name in HydrationContext.tools before hydrating. The hint lists the tools that are registered.

UNKNOWN_LAYER_REFERENCE

Cause: A workflow node names a context layer that is not in HydrationContext.layers.

Fix: Register the layer under that name in HydrationContext.layers.

UNKNOWN_UI_LIBRARY_REFERENCE

Cause: A workflow node's output names a UI library that is not in HydrationContext.uiLibraries.

Fix: Register an output codec under that name, for example uiLibraries: new Map([['my-lib', openUi(myLibrary)]]) with openUi from @noetic-tools/openui.

UNKNOWN_ACP_AGENT_REFERENCE

Cause: An acp-agent node names an agent that is not in HydrationContext.acpAgents.

Fix: Pass agent adapters via HydrationContext.acpAgents, for example createAcpAgentRegistry(claudeCode()) from @noetic-tools/acp.

UNKNOWN_SUBPROCESS_REFERENCE

Cause: A run node names a subprocess adapter that HydrationContext.resolveSubprocess cannot resolve.

Fix: Map the reference in resolveSubprocess(ref), or omit subprocess to use the harness default.

INVALID_FORK_INPUT

Cause: A dynamic inParallel node (one with each) got input that is not JSON, or JSON that does not hold an array (at the over key, when set).

Fix: Feed the node a JSON array, or a JSON object with an array at the over key.

INVALID_REPLAY_KEY_INPUT

Cause: A node with "replayKey": { "from": [...] } got input that is not a JSON object, so the named properties cannot be read. You see it as a warning, not a thrown error: the node runs without replay or recording.

Fix: Feed the node a JSON object holding the from properties, or use "replayKey": true to key on the whole input.

INVALID_CUSTOM_ITEM_TYPE

Cause: A custom item was declared with a type that has no prefix: part.

Fix: Name it prefix:name after the Open Responses convention for extension items, e.g. myapp:progress.

INVALID_CUSTOM_ITEM_SHAPE

Cause: A custom item's JSON Schema does not load, or its output does not describe an object, or declares id or type.

Fix: Give an object schema for output and leave out id and type: the runtime adds both to every final item.

CUSTOM_ITEM_MISMATCH

Cause: workflow({ customItems }) and its document declare one item name with different types.

Fix: Give both declarations the same type, or rename one of them.

UNKNOWN_CUSTOM_ITEM

Cause: A step's emitItems.item names a custom item that is not in its scope: not on the step and, for a JSON node, not at its document's root.

Fix: Declare the item on the step (customItems) or at the workflow root.

CUSTOM_ITEM_HAS_NO_EVENTS

Cause: A hook or a generator runCode yielded an event, but the step's emitItems names no item, or names one declared without an event schema.

Fix: Name an item with an event schema in emitItems: { item }, or return the value instead of yielding it.

CUSTOM_ITEM_ALREADY_FINAL

Cause: One step dispatch gave a custom item a second final item, for example both onStart and onDone returned one.

Fix: Return the final fields from one hook only.

INVALID_CUSTOM_ITEM

Cause: An event or a final item does not match the schema its custom item declares. The message names the item type, the part (event or output), and the schema's issues.

Fix: Make the value match the declared event or output schema.

ACP Agents

MISSING_ACP_AGENT

Cause: step.acpAgent() or acpAgentTool() has no agent adapter, or its lazy agent resolved to nothing at run time.

Fix: Pass an agent factory result, for example agent: claudeCode() from @noetic-tools/acp.

MISSING_PROMPT

Cause: step.acpAgent() was built with neither prompt nor content, or its prompt resolved to an empty string at run time.

Fix: Pass a non-empty prompt for plain text, content for image, audio, or resource blocks, or give the step a string input.

ACP_REUSE_WITHOUT_KEEPALIVE

Cause: A step sets session.reuse but leaves keepAlive at 'step', so the connection closes before another step can share it.

Fix: Add keepAlive: 'run' to share the connection for the rest of the run, or keepAlive: 'harness' to keep it past the run and close it yourself with harness.closeAcpSessions().

ACP_SESSION_AGENT_CONFLICT

Cause: A step reuses a session key that another step opened with a different agent.

Fix: Give every step that shares a session.reuse key the same agent, or use separate keys.

ACP_SESSION_CAPABILITY_CONFLICT

Cause: A step reuses a session but asks for different clientCapabilities than the connection was opened with. ACP negotiates capabilities once per connection.

Fix: Give every step that shares a session.reuse key the same clientCapabilities, or use a separate session.

ACP_SESSION_SETTING_CONFLICT

Cause: A step reuses a session but asks for a different cwd or different MCP servers than the session was created with, or serves a tool the session's tool list lacks (the error names it).

Fix: Give every step that shares a session.reuse key the same cwd and tools, or use a separate session.

ACP_TRANSPORT_CANNOT_REACH

Cause: step.acpAgent() gives tools to an agent attached through a custom transport. Noetic did not start that agent, and the transport has no reach() to carry the tool bridge to wherever it runs. With tools left out, such an agent simply gets none.

Fix: Give the agent a command and a machine instead — localMachine() for this machine, or webSocketMachine() / commandMachine() from @noetic-tools/acp/remote for a remote runner (noetic-acp serve) — or leave tools off the step.

ACP_TRANSPORT_CANNOT_LAUNCH

Cause: As ACP_TRANSPORT_CANNOT_REACH, for an agent whose wiring has launch settings (Claude Code, Codex, opencode, pi): attached to an agent someone else started, those settings cannot be applied, so its tools would not match callModel.

Fix: Give the preset a machine instead of a transport; the machine's launch() starts the agent with those settings.

ACP_AGENT_CANNOT_TAKE_TOOLS

Cause: step.acpAgent() gives tools to gemini() without trustWorkspace: true. Gemini CLI starts no MCP server, the tool bridge included, in a folder it does not trust, so the agent would run with none of the tools callModel would offer.

Fix: Pass gemini({ trustWorkspace: true }) to trust the session's folder for the agent process. Trusting it also loads the folder's own .gemini/settings.json, GEMINI.md and .env, so do this only for a repository you wrote; otherwise leave tools off the step.

ACP_SERVER_TOOL

Cause: step.acpAgent() was given a server tool such as openrouter:web_search. Only a model provider can run server tools.

Fix: Pass only client tools built with tool().

ACP_TOOL_NAME_COLLISION

Cause: Two of a step's tools reduce to the same wire name (names keep only letters, digits, _, and -), so the agent could not tell them apart.

Fix: Rename one of the tools.

ACP_SERVE_KIND_RULES_WITHOUT_TOOLS

Cause: serveAcp() got a kind-based permission rule (such as { kind: 'execute' }) but no tools to look kinds up from.

Fix: Pass the harness's tools in the serve options: serveAcp(harness, { tools, permissions }).

Connections

CONNECTION_UNKNOWN

Cause: A handle, workflow node, trigger or allowlist names a connection the harness does not have, or a connectionTools('<name>') pick (or a JSON { "connection" } pick) names one neither the harness nor the turn attaches, found when the turn's tool set is built.

Fix: Add the connection under that name in new AgentHarness({ connections }), or correct the name. The hint lists the connections the harness has.

CONNECTION_UNSATISFIED

Cause: A connection does not offer what is required of it. The error's missing field says which: a profile or extension version, an operation, an event, or a sense a verify step needs.

Fix: Point the handle or manifest entry at a connection whose server offers what is missing, or relax the requirement (a wider version range, fewer operations).

CONNECTION_NOT_ALLOWED

Cause: A JSON workflow asks for more than its connections allowlist grants, or sets a setup field the profile reserves to the host (such as the command or path of an app). Or an acp-agent node asks for more than the acp allowlist grants its agent (permissions, mode, model, cwd, clientCapabilities, session), or names an MCP server, header or environment value the mcp allowlist does not grant, or names one header or variable twice.

Fix: Widen the allowlist passed to parseAndRunWorkflow or dynamicWorkflow, or move the host-only field into the pool's own setup in the harness configuration. For an MCP server URL, write it as the message shows.

CONNECTION_INVALID

Cause: A connection manifest entry, handle or provider configuration is malformed: for example a bad version range, a fresh handle without a pool, a secret() or an MCP server $secret used with no environment.secrets resolver, an mcp allowlist URL pattern with * in the host other than a leading *. followed by at least two labels, a listed entry with no name, a record key that disagrees with mcp({ name }), a turn or step entry that sets pool, a function-form tools that returns a connection the turn never attached, an mcp() config that is not ACP's McpServer shape (a command line string, protocol, headers or environment as a record), a header or environment variable given twice, auth together with an Authorization header, a connectionTools() name that is not a connection name, or a document { "mcp" } connection whose key disagrees with its name or that sets from, fresh or setup.

Fix: Correct the entry the message names. The hint says which field is wrong.

CONNECTION_TOOL_COLLISION

Cause: Two connection operations would reach the model under the same tool name.

Fix: Rename one of the connections, or set expose: 'call' or 'none' on one of them.

CONNECTION_NAME_TAKEN

Cause: An entry names a connection that its scope or an outer one already attaches with a different provider or options. The scopes are the harness, then the turn (execute(input, { connections })), then a step's tools. Two entries in one list or record with the same name and different providers fail the same way.

Fix: Pass the same entry object the outer scope uses, or give this one another name.

Verification

VERIFY_NOT_CONFIGURED

Cause: A step.verify (or a verify workflow node) ran, but the harness has no verifier.

Fix: Register one: new AgentHarness({ verification: verifier() }), with verifier from @noetic-tools/verify.

VERIFY_INVALID

Cause: A verify step is malformed: its act contains nothing that acts, a matcher cannot be parsed by its sense's grammar, or a witness is asked to run an operation with effects.

Fix: Give act at least one call with effects on the subject, fix the matcher text the message quotes, and let witnesses only run operations without effects.

On this page

Quick ExampleNoeticError TypeFull Type DefinitionModel provider failuresNoeticErrorImpl ClassisNoeticError Type GuardHandling Errors in LoopsMatching on Error KindNoeticConfigErrorConfiguration Error CodesCredentials and Harness SetupNO_LLM_PROVIDERMISSING_API_KEYMISSING_CALL_MODELNO_STEP_CONFIGUREDMISSING_HARNESS_CONTEXTSTEP_LEDGER_RETENTION_INVALIDCHECKPOINT_SCHEMA_MISMATCHStep BuildersEMPTY_STEP_IDEMPTY_STEP_ARRAYINVALID_STEP_IN_ARRAYMISSING_EXECUTE_FUNCTIONEMPTY_TOOL_NAMEMISSING_TOOLMISSING_MODELUNKNOWN_TOOL_CHOICEMISSING_JSON_SCHEMAMISSING_LOOP_BODYMISSING_UNTIL_PREDICATEMISSING_CHILD_STEPMISSING_CONTEXT_LAYERSMISSING_MERGE_FUNCTIONMISSING_ROUTE_FUNCTIONMISSING_BODY_STEPINVALID_INTERVAL_MSINVALID_JITTERChannelsINVALID_CHANNEL_TIMEOUTDecide and routeSwitchINVALID_DECIDE_QUESTIONSINVALID_UNTIL_DECIDERINVALID_DECIDE_SAMPLESUNKNOWN_ROUTE_QUESTIONINCOMPLETE_ROUTE_CASESINVALID_MIN_CONFIDENCEMISSING_UNCERTAIN_STEPContext LayersINVALID_HISTORY_MAX_ITEMSINVALID_BUDGET_INPUTJSON WorkflowsWORKFLOW_VALIDATION_FAILEDINVALID_WORKFLOW_SOURCEUNKNOWN_WORKFLOW_REFERENCEWORKFLOW_RECURSION_LIMITINVALID_MAX_RECURSION_DEPTHEXPRESSION_FAILEDEXPRESSION_LIMIT_EXCEEDEDINVALID_EXPRESSION_LIMITSUNKNOWN_NODE_KINDUNKNOWN_UNTIL_PREDICATEUNKNOWN_TOOL_REFERENCEUNKNOWN_LAYER_REFERENCEUNKNOWN_UI_LIBRARY_REFERENCEUNKNOWN_ACP_AGENT_REFERENCEUNKNOWN_SUBPROCESS_REFERENCEINVALID_FORK_INPUTINVALID_REPLAY_KEY_INPUTINVALID_CUSTOM_ITEM_TYPEINVALID_CUSTOM_ITEM_SHAPECUSTOM_ITEM_MISMATCHUNKNOWN_CUSTOM_ITEMCUSTOM_ITEM_HAS_NO_EVENTSCUSTOM_ITEM_ALREADY_FINALINVALID_CUSTOM_ITEMACP AgentsMISSING_ACP_AGENTMISSING_PROMPTACP_REUSE_WITHOUT_KEEPALIVEACP_SESSION_AGENT_CONFLICTACP_SESSION_CAPABILITY_CONFLICTACP_SESSION_SETTING_CONFLICTACP_TRANSPORT_CANNOT_REACHACP_TRANSPORT_CANNOT_LAUNCHACP_AGENT_CANNOT_TAKE_TOOLSACP_SERVER_TOOLACP_TOOL_NAME_COLLISIONACP_SERVE_KIND_RULES_WITHOUT_TOOLSConnectionsCONNECTION_UNKNOWNCONNECTION_UNSATISFIEDCONNECTION_NOT_ALLOWEDCONNECTION_INVALIDCONNECTION_TOOL_COLLISIONCONNECTION_NAME_TAKENVerificationVERIFY_NOT_CONFIGUREDVERIFY_INVALIDRelated Pages