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.
| Class | Status | code | Retriable |
|---|---|---|---|
AuthenticationError | 401 | authentication_error | No |
AuthorizationError | 403 | authorization_error | No |
InsufficientFundsError | 402 | InsufficientFunds | No |
ValidationError | 400 or 422 | validation_error | No |
NotFoundError | 404 | not_found | No |
ConflictError | 409 | conflict | No |
RateLimitError | 429 | rate_limit_error | Yes |
CoreSourceError | varies | varies | varies |
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.
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#
| Property | Type | Notes |
|---|---|---|
code | string | Stable machine-readable code |
status | number | undefined | HTTP status |
retriable | boolean | Whether retrying could plausibly help |
retryAfter | number | undefined | Seconds to wait, when the server said |
requestId | string | undefined | Read from a request-id response header when one is present |
docUrl | string | undefined | Link to relevant documentation |
details | Record<string, unknown> | undefined | Structured context |
type | string | undefined | Server-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.
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:
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.
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.
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.
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.
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.
