Errors and retries

Two kinds of failure are easy to conflate and should not be. A thrown error means a request did not succeed. A failed run status means the request succeeded and the work did not. agent.run() resolving with status: 'failed' is not an exception. Nothing threw, and the result still carries usage and any artifacts produced along the way.

The error hierarchy#

Everything the SDK throws for an API failure extends CoreSourceError.

ClassStatuscodeRetriable
AuthenticationError401authentication_errorNo
AuthorizationError403authorization_errorNo
InsufficientFundsError402InsufficientFundsNo
ValidationError400 or 422validation_errorNo
NotFoundError404not_foundNo
ConflictError409conflictNo
RateLimitError429rate_limit_errorYes
CoreSourceErrorvariesvariesvaries

InsufficientFundsError is the one to special-case in a product. Its code is InsufficientFunds (deliberately unlike the snake_case of the others) and it means the account needs topping up.

Catching#

isCoreSourceError narrows API, network, and request-timeout failures represented by CoreSourceError. The SDK also uses plain TypeError and Error for local configuration and lifecycle mistakes, so an error that does not match still needs to be inspected rather than assumed to come from application code.

TypeScript
import {
  createAgent,
  isCoreSourceError,
  InsufficientFundsError,
  RateLimitError
} from '@coresourceai/sdk';

const agent = createAgent({ type: 'agent' });

try {
  const result = await agent.run('Reconcile the ledger.');
  console.log(result.text);
} catch (error) {
  if (error instanceof InsufficientFundsError) {
    console.error('The account has insufficient funds for this request.');
  } else if (error instanceof RateLimitError) {
    console.error(`Rate limited; retry after ${error.retryAfter ?? 'a moment'}s`);
  } else if (isCoreSourceError(error)) {
    console.error(`${error.code} (${error.status}): ${error.message}`);
  } else {
    throw error;
  }
} finally {
  await agent.close();
}

What an error carries#

PropertyTypeNotes
codestringStable machine-readable code
statusnumber | undefinedHTTP status
retriablebooleanWhether retrying could plausibly help
retryAfternumber | undefinedSeconds to wait, when the server said
requestIdstring | undefinedRead from a request-id response header when one is present
docUrlstring | undefinedLink to relevant documentation
detailsRecord<string, unknown> | undefinedStructured context
typestring | undefinedServer-side error type

retriable is the field to branch on for generic handling. It saves you maintaining your own list of which statuses are worth another attempt.

TypeScript
import { isCoreSourceError } from '@coresourceai/sdk';

function shouldRetry(error: unknown) {
  return isCoreSourceError(error) && error.retriable;
}

The SDK redacts known credential patterns when constructing a CoreSourceError and when serializing its details. toJSON() gives you a structured form for your logger, but error data should still be treated as sensitive:

TypeScript
import { isCoreSourceError } from '@coresourceai/sdk';

function logFailure(error: unknown) {
  if (isCoreSourceError(error)) {
    console.error(JSON.stringify(error.toJSON()));
    return;
  }

  console.error(error);
}

Built-in retries#

Every request-taking method accepts a retry option. Eligible GET requests use up to two retries by default. Mutating requests, including a run start, are retried only when an idempotencyKey is present.

TypeScript
import { createAgent } from '@coresourceai/sdk';

const agent = createAgent({ type: 'agent' });

const result = await agent.run('Summarise the changelog.', {
  idempotencyKey: 'summarise-changelog-2026-07-29',
  retry: {
    maxRetries: 4,
    baseDelayMs: 500,
    maxDelayMs: 10_000
  }
});

await agent.close();

retry also accepts shorthand: true to enable defaults, false to disable, or a number as a retry count. Retry counts are capped at 10.

TypeScript
import type { AgentRetrySetting } from '@coresourceai/sdk';

const off: AgentRetrySetting = false;
const threeRetries: AgentRetrySetting = 3;
const tuned: AgentRetrySetting = { maxRetries: 5, baseDelayMs: 250 };

Idempotency#

Repeating a request that starts work is dangerous without a key. If a start() succeeds server-side and the response is lost, sending it again without the same key can start a second run. The SDK therefore does not automatically retry mutations unless a key is present.

TypeScript
import { createAgent } from '@coresourceai/sdk';

const agent = createAgent({ type: 'saga' });

const run = await agent.start('Process the nightly batch.', {
  idempotencyKey: 'nightly-batch-2026-07-29',
  retry: 3
});

await agent.close();

Choose a key that is stable for the logical unit of work: a job id, a date, an order number. A random UUID generated per attempt is not an idempotency key.

Use one whenever the work has a real-world effect and a natural unique name. Read-only calls do not need one.

Distinguishing failure modes#

Three outcomes need different handling, and only one of them throws.

TypeScript
import { createAgent, isCoreSourceError } from '@coresourceai/sdk';

const agent = createAgent({ type: 'saga', outputs: ['report.md'] });

try {
  const result = await agent.run('Compile the report.');

  switch (result.status) {
    case 'succeeded':
      console.log(result.text);
      break;

    case 'partial_success':
      // Real work happened. Keep the artifacts.
      console.warn(`Partial: ${result.artifacts.length} artifacts produced`);
      break;

    case 'awaiting_input':
      // Not a failure - see /docs/running/human-input.
      console.log('Waiting on:', result.requiredAction?.question);
      break;

    default:
      console.error(`Run ${result.id} ended as ${result.status}`);
  }
} catch (error) {
  // The request itself failed. No run outcome exists to inspect.
  if (isCoreSourceError(error)) {
    console.error(`${error.code}: ${error.message}`);
  } else {
    throw error;
  }
} finally {
  await agent.close();
}

Configuration errors#

Some mistakes are caught locally, before any request, and throw plain TypeError rather than a CoreSourceError:

  • A tool name that does not match ^[A-Za-z][A-Za-z0-9_-]{0,63}$.
  • Two tools registered under the same name.
  • A missing API key, which throws CoreSource apiKey is required. Pass apiKey or set CORESOURCE_API_KEY.
  • Saving an agent whose config contains function handlers or local skill bundles.

Calling registerRuntimeTools() or registerMcpServer() after the first run has started throws a plain Error, as does using an agent after close().

These are programming errors. They should fail your tests, not be caught at runtime.