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.
llm_refusedstepId, refusal: stringThe model returns a refusal instead of content.
llm_parse_errorstepId, raw: string, schema: ZodType, zodError: ZodErrorStructured output fails Zod validation.
llm_rate_limitstepId, retryAfter?: numberThe LLM provider returns a rate-limit response.
fork_partialstepId, succeeded[], failed[]Thrown by fork 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.)
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(). Pending channel recv calls (and parked back-pressure send calls) on the aborted context reject with this kind immediately.
budget_exceededfield: 'cost' | 'steps' | 'duration', limit, actualA budget guard detected the run exceeded its limits.
steering_deniedguidance?: stringA memory layer's steering hook (beforeToolCall / afterModelCall) returned a deny decision and the runtime blocked the action. guidance carries the corrective feedback, if any.
handle_evictedhandleId, stepId, gracePeriodMsDetachedHandle.await() found the subprocess adapter persistently returning null for a previously spawned handle (evicted by a restart without restore, or a storage inconsistency). Raised after a short grace period instead of polling forever.
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.

Full Type Definition

type NoeticError =
  | { kind: 'step_failed'; stepId: string; cause: Error; retriesExhausted: boolean }
  | { kind: 'llm_refused'; stepId: string; refusal: string }
  | { kind: 'llm_parse_error'; stepId: string; raw: string; schema: ZodType; zodError: ZodError }
  | { kind: 'llm_rate_limit'; stepId: string; retryAfter?: number }
  | { kind: 'fork_partial'; stepId: string; succeeded: Array<{ stepId: string; value: unknown }>; failed: Array<{ stepId: string; error: NoeticError }> }
  | { 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: 'handle_evicted'; handleId: string; stepId: string; gracePeriodMs: number }
  | { kind: 'item_schema_mismatch'; category: 'items' | 'developerMessages' | 'toolCalls' | 'toolResults'; itemType?: string };

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 { step, loop, until } from '@noetic-tools/core';

const resilientLoop = loop({
  id: 'retry-loop',
  steps: [step.llm({ id: 'generate', model: 'openai/gpt-4o' })],
  until: until.noToolCalls(),
  onError(error, ctx) {
    if (error.kind === 'llm_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 'llm_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;
  }
}

On this page