# JavaScript SDK

> JavaScript/TypeScript SDK for inference.sh agents and apps.

**URL:** https://inference.sh/docs/api/sdk-javascript
**Last updated:** 2026-10-18

---

**JavaScript/TypeScript SDK for inference.sh agents and apps.**

The JavaScript SDK provides a fully-typed client for running apps and chatting with agents.

---

## Installation

```bash
npm install @inferencesh/sdk
```

The package is ES modules. Import it:

```typescript
import { inference } from '@inferencesh/sdk';
import type { Task, InferenceConfig } from '@inferencesh/sdk';
```

**@inferencesh/sdk ≥ v0.23.0** loads in plain Node with no bundler: `import` works from Node ESM, and `require()` works from CommonJS on Node 22.12+ (see [CommonJS support](#commonjs-support)). This covers every subpath, `@inferencesh/sdk/proxy/*` included. Earlier releases failed in plain Node with `ERR_MODULE_NOT_FOUND`.

| Import | Purpose |
|--------|---------|
| `@inferencesh/sdk` | Client, types and helpers |
| `@inferencesh/sdk/agent` | React agent chat provider and hooks |
| `@inferencesh/sdk/proxy` | Shared server-proxy code (`PROXY_ENDPOINTS`, options types) |
| `@inferencesh/sdk/proxy/nextjs` | Next.js App Router and Page Router handlers |
| `@inferencesh/sdk/proxy/express` | Express middleware |
| `@inferencesh/sdk/proxy/hono` | Hono handler |
| `@inferencesh/sdk/proxy/remix` | Remix loader/action handler |
| `@inferencesh/sdk/proxy/svelte` | SvelteKit handler |
| `@inferencesh/sdk/internal/upload` | Upload helper used by the SDK (internal) |

Type definitions ship with the package; there is no separate `@types` install.

**Node.js:** **@inferencesh/sdk ≥ v0.9.0** requires **Node.js 22.12.0 or later** on the server (see npm `engines`). The release depends on `eventsource` 5.x for SSE; that package is ESM-only and needs Node 22.12+. Upgrade Node before bumping past v0.8.x if you are still on Node 18–22.11.

The client runs in the browser, but an API key must never reach client-side code. Put it behind a [server proxy](/docs/api/sdk/server-proxy) and point the browser at that.

---

## Client API

The `inference()` client exposes namespaced APIs:

| Property | Purpose |
|----------|---------|
| `client.tasks` | Run, get, cancel, delete, list, and stream tasks; `files` and `deleteFiles` for a task's input and output files; `getLogs`, `getTimings`, and `getTelemetry` for observability |
| `client.files` | Upload, list, get, and delete files |
| `client.agents` | Chat via `client.agent()`; template CRUD, `getByName`, `getA2ACard`, `listRunInterrupts`, and `resolveInterrupt` |
| `client.sessions` | List, inspect, keepalive, and end sessions |
| `client.chats` | List, get, update, delete chats; `getStatus`, `stop`, `stream`, and `getTrace` |
| `client.apps` | List, create, and manage deployed apps (requires app scopes) |
| `client.flows` / `client.flowRuns` | Flow definitions and saved flow run lifecycle (`flows:read` / `flows:execute`) |
| `client.engines` | List, match resources, drain, `updateBinary`, stop, and restart private engines |
| `client.knowledge` / `client.skills` | Knowledge entries and skill CRUD |
| `client.teams` | Workspace membership, invites, and `me()` |
| `client.secrets` | Workspace secret CRUD and reveal |
| `client.apiKeys` | List, create, and delete API keys |
| `client.credentials` | Connect OAuth services, check requirements, MCP connections |
| `client.search` | Unified discovery (`/suggest`) and full-text search (`/search`) |
| `client.projects` | Workspace project folders |
| `client.mcpServers` | MCP directory browse, owned servers, and tool calls |

Platform namespaces ship in **@inferencesh/sdk ≥ v0.6.12**. See [SDK overview — client structure](/docs/api/sdk/overview#client-structure) for Python parity.

`client.run()` and `client.uploadFile()` remain supported as legacy aliases for `client.tasks.run()` and `client.files.upload()`.

### Partial updates (PATCH)

**@inferencesh/sdk ≥ v0.22.0:** `update()` on `client.projects`, `client.knowledge`, `client.skills`, `client.chats`, `client.engines` and `client.teams` sends **PATCH** instead of POST. A field left out of the body keeps its value. An API that does not answer PATCH on these routes returns **405**.

Typed write bodies (`ProjectWriteBody`, `KnowledgeUpdateBody`, `TeamUpdateBody` and the other `*UpdateBody` / `*WriteBody` types) name the fields each method sends. Spreading a loaded DTO into `update()` still type-checks; an explicit field the route does not write does not.

| Namespace | Partial-update helpers |
|-----------|------------------------|
| `client.apps` | `patch(appId, AppPatchBody)`: PATCH the app's copy fields. `update()` stays POST, since it also edits the version in place |
| `client.flows` | `patch(flowId, FlowPatchBody)`; `saveViewport(flowId, FlowViewportBody)` for the editor viewport |
| `client.chats` | `updateSettings(chatId, settings)`: name, visibility, tool approval, hooks, memory (`POST /chats/{id}/settings`) |

`client.flowRuns.update()` is deprecated: the API refuses it, because a flow run has no caller-writable fields. Use `client.flowRuns.updateVisibility()` to change who can see a run.

Tool approvals (`approveTool`, `rejectTool`, `alwaysAllowTool`, `resolveInterrupt`) and widening a chat's approval settings need the account holder's own sign-in. With an API key, an app's token or the [server proxy](/docs/api/sdk/server-proxy), the API answers **403** `requires_sign_in` (`ErrorCodeRequiresSignIn`).

→ [Engines REST API](/docs/api/rest/engines) — drain, safe binary updates, and cloud rental extensions  
→ [Flow Runs REST API](/docs/api/rest/flow-runs) — run saved flows programmatically

### Apps API

Use `client.apps` to manage apps you own (not for running tasks — use `client.tasks.run()` for that):

```typescript
import {
  AppStatusActive,
  AppStatusDeprecated,
  AppStatusMaintenance,
  AppStatusRetired,
} from '@inferencesh/sdk';

const app = await client.apps.getByName('myteam/my-app');
const versions = await client.apps.listVersions(app.id);

// Promote a staged deploy to current (same as the version picker in the web app)
await client.apps.setCurrentVersion(app.id, versions.items[0].id);

// Mark deprecated while keeping runs working (see Apps REST API — lifecycle status)
await client.apps.updateStatus(app.id, AppStatusDeprecated, 'Use myteam/flux-v2 instead');

// Temporarily block new runs
await client.apps.updateStatus(app.id, AppStatusMaintenance, 'Upgrading GPU drivers');

// Permanently retire (excluded from store/search; historical tasks remain)
await client.apps.updateStatus(app.id, AppStatusRetired, 'Replaced by myteam/flux-v2');
```

| Method | Description |
|--------|-------------|
| `list`, `get`, `getByName` | List and fetch apps |
| `create`, `update`, `patch`, `delete`, `duplicate` | Lifecycle (`patch` sends PATCH with copy fields only) |
| `listVersions`, `getByVersionId` | Version history |
| `setCurrentVersion(appId, versionId)` | `PUT /apps/{id}/versions/{versionId}/current` |
| `updateStatus(appId, status, message?)` | `POST /apps/{id}/status` — lifecycle status |
| `updateVisibility`, `transferOwnership` | Ownership and visibility |
| `getLicense`, `saveLicense` | License records |

**Categories and tags** — `AppDTO` includes `category` (`AppCategory*`, including `AppCategoryDecision` for decision models) and `tags` (`string[]`). Task payloads for decision apps use `DecisionInput` / `DecisionOutput` (and `DecisionVisionInput` when images are required) — same JSON shape as [App code — Decision models](/docs/extend/app-code#decision-models-contract); types ship in **@inferencesh/sdk ≥ v0.21.0**. Browseable tag slugs and display titles come from `GET /store/tags` ([Store API](/docs/api/rest/store#list-browseable-tags)), not from generated SDK constants — **v0.21.0** removes the `AppTag*` / `AppTagTitle*` constants that older releases exported. Pass `tags` on `client.apps.create` to replace the app's tag list; omit it on deploy to inherit from version metadata. Tag rules: [Apps API — App tags](/docs/api/rest/apps#app-tags).

**Store browse metadata** — **`@inferencesh/sdk` ≥ v0.21.0** exports `StoreCategoryDTO` and `StoreTagDTO` for [Store API](/docs/api/rest/store#list-store-categories) `GET /store/categories` and [List browseable tags](/docs/api/rest/store#list-browseable-tags) `GET /store/tags`. There is no `client.store` helper yet; call the public endpoints with `client.http.request` (no API key required):

```typescript
import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';

const { data: categories } = await client.http.request<StoreCategoryDTO[]>(
  'get',
  '/store/categories',
);
const { data: tags } = await client.http.request<StoreTagDTO[]>('get', '/store/tags');
```

→ [Apps REST API — lifecycle status](/docs/api/rest/apps#app-lifecycle-status) · [Deploy staged versions](/docs/extend/deploying) · [Instance types REST API](/docs/api/rest/instances)

### Flows API

Use `client.flows` for flow definitions and `client.flowRuns` to execute saved flows:

```typescript
const flow = await client.flows.create('image-pipeline');
await client.flows.updateVisibility(flow.id, 'public');

const run = await client.flowRuns.create(flow.id, { text: 'hello' });
```

| Method | Description |
|--------|-------------|
| `list`, `get`, `create`, `update`, `patch`, `delete` | Flow definition CRUD (`patch` writes only the fields sent) |
| `saveViewport(flowId, body)` | `POST /flows/{id}/viewport` |
| `listVersions`, `duplicate`, `createApp` | Versions, copy, deploy as app |
| `updateVisibility(flowId, visibility)` | `POST /flows/{id}/visibility` |
| `transferOwnership(flowId, teamId)` | `POST /flows/{id}/transfer` |
| `stream(flowId)` | SSE for editor graph updates |

→ [Flows REST API](/docs/api/rest/flows) · [Flow Runs REST API](/docs/api/rest/flow-runs)

### Search API

Use `client.search` for programmatic discovery. It calls the same endpoints as `belt suggest` and the web app's search bar:

```typescript
const { results } = await client.search.suggest({
  query: 'flux image',
  context: 'user asked about image models', // optional POST /suggest context; query is embedded first, then context
  scope: ['git:github.com/acme/design', 'lang:typescript'], // optional; environment signals for ranking
  limit: 5,
});
// Result descriptions are clipped to DescriptionLimitListing (200 characters).

for (const hit of results) {
  console.log(`[${hit.type}] ${hit.name} — ${hit.command}`);
}

// Full-text search in a single collection
const apps = await client.search.search({ q: 'transcribe', type: 'apps', limit: 10 });
```

| Method | Description |
|--------|-------------|
| `suggest(params)` | `POST /suggest` — ranked discovery across apps, skills, knowledge, and docs |
| `search(params)` | `POST /search` — full-text search with optional `type` filter |

→ [Search REST API](/docs/api/rest/search) · [Search concept](/docs/concepts/search)

### Workspace APIs

Manage workspace context, secrets, API keys, and project folders programmatically:

```typescript
const { data: { user, team } } = await client.teams.me();
const { data: teams } = await client.teams.list();

await client.secrets.create({
  key: 'OPENAI_API_KEY',
  value: 'sk-...',
  description: 'OpenAI',
});

const { data: { items: keys } } = await client.apiKeys.list({ limit: 50 });
const { data: newKey } = await client.apiKeys.create({
  name: 'deploy-bot',
  scopes: ['tasks:read', 'tasks:write'],
  scope: 'workspace', // optional: 'user' (default) or 'workspace'; a workspace key needs manage_keys
  expires_at: '2026-12-31T00:00:00Z', // optional
});

const project = await client.projects.create({ name: 'Production' });
```

| Namespace | Key methods | REST reference |
|-----------|-------------|----------------|
| `client.teams` | `me`, `list`, `get`, `create`, `getMembers`, `addMember`, invite helpers | [Workspaces](/docs/api/rest/teams) |
| `client.secrets` | `list`, `create`, `update`, `reveal`, `delete` | [Secrets](/docs/api/rest/secrets) |
| `client.apiKeys` | `list`, `create`, `delete` | [API Keys](/docs/api/rest/api-keys) |
| `client.projects` | `list`, `get`, `create`, `update`, `delete` | — |

### Knowledge and skills

Use `client.knowledge` for observations, references, and other knowledge types. Use `client.skills` for skill packages (prefer over knowledge entries with `type: "skill"`):

```typescript
const entry = await client.knowledge.create({
  name: 'deployment-order',
  description: 'Services must deploy in dependency order',
  type: 'observation',
  version: {
    content: {
      path: 'content.md',
      content: '# Deployment Order\n\nDeploy common-go before api.',
    },
    scope: ['git:github.com/acme/api', 'lang:go'], // optional; see Knowledge API — Scoping
  },
});

const skill = await client.skills.getByName('myteam', 'code-review');
const content = await client.skills.getContent('myteam', 'code-review');
const { items } = await client.skills.listStore({ limit: 20 });
```

→ [Knowledge REST API](/docs/api/rest/knowledge) · [Skills REST API](/docs/api/rest/skills) · [Skill registry](/docs/skills/registry)

### Credentials and MCP servers

Connect OAuth services and call MCP connector tools with `client.credentials` (`CredentialsAPI`). **v0.8.0** removed the integration-named SDK aliases (`client.integrations`, `IntegrationsAPI`, `Integration*` types). Upgrade from v0.7.x before bumping.

```typescript
const caps = await client.credentials.getCapabilities();
const { auth_url } = await client.credentials.connect({
  provider: 'google',
  type: 'oauth',
  scopes: ['https://www.googleapis.com/auth/spreadsheets'],
});

await client.credentials.checkRequirements({
  secrets: [{ key: 'OPENAI_API_KEY' }],
  credentials: [{ key: 'google.sheets' }],
});

const linear = await client.mcpServers.get('linear');
const tools = await client.mcpServers.listTools('linear');
const result = await client.mcpServers.callTool('linear', 'list_issues', { limit: 10 });
```

→ [Credentials REST API](/docs/api/rest/credentials) · [MCP servers REST API](/docs/api/rest/mcp-servers) · [Connectors overview](/docs/connectors/overview)

### Chats and task observability

Inspect agent runs and task execution outside the high-level `client.agent()` helper:

```typescript
const { data: chat } = await client.chats.get('chat_abc123');
const { data: { status } } = await client.chats.getStatus('chat_abc123');
const { data: trace } = await client.chats.getTrace('chat_abc123');
await client.chats.stop('chat_abc123');

const { data: logs } = await client.tasks.getLogs('task_abc123');
const { data: timings } = await client.tasks.getTimings('task_abc123');
const { data: samples } = await client.tasks.getTelemetry('task_abc123');
```

→ [Agents REST API](/docs/api/rest/agents) · [Tasks REST API](/docs/api/rest/tasks) · [Observability](/docs/runtime/observability)

### Task files

**@inferencesh/sdk ≥ v0.20.0** adds `client.tasks.files()`, `client.tasks.deleteFiles()`, and `client.tasks.delete(taskId, { files: true })`. Earlier releases delete the task record only.

```typescript
import type { TaskFileDTO, DeleteTaskFilesResponse } from '@inferencesh/sdk';
import { TaskFileRoleInput, TaskFileRoleOutput } from '@inferencesh/sdk';

const { data: files } = await client.tasks.files('task_abc123');
const { data: inputs } = await client.tasks.files('task_abc123', TaskFileRoleInput);

const { data: result } = await client.tasks.deleteFiles('task_abc123', TaskFileRoleOutput);
// result.deleted, result.skipped

await client.tasks.delete('task_abc123', { files: true });
```

| Method | REST | Notes |
|--------|------|-------|
| `files(taskId, role?)` | `GET /tasks/{id}/files` | `role` is `'input'` or `'output'` |
| `deleteFiles(taskId, role?)` | `DELETE /tasks/{id}/files` | Task must be finished |
| `delete(taskId, { files: true })` | `DELETE /tasks/{id}?files=true` | Deletes the files, then the task |

→ [List task files](/docs/api/rest/tasks#list-task-files) · [Delete task files](/docs/api/rest/tasks#delete-task-files) · [Delete task](/docs/api/rest/tasks#delete-task)

### Private engines

For cloud engine rentals and private hardware:

```typescript
const { data: { items } } = await client.engines.list({ limit: 20 });
const { data: engine } = await client.engines.get('eng_abc123');
console.log(engine.engine_version); // installed inferencesh-engine binary, e.g. "v1.10.15"

await client.engines.drain('eng_abc123');
await client.engines.updateBinary('eng_abc123'); // POST /engines/{id}/update
await client.engines.extend('eng_abc123');

// Cloud engines include nested instance pricing (spot vs on-demand)
if (engine.instance?.rental_type === InstanceRentalTypeSpot) {
  console.log('Spot price (cents/hr):', engine.instance.hourly_price);
}
```

**@inferencesh/sdk ≥ v0.14.2** adds `InstanceRentalType` (`on_demand`, `spot`) on catalog `availability` rows, engine-picker offers from `GET /engines/types`, and `engine.instance` on `EngineDTO`. Spot cloud engines can be reclaimed by the provider at any time — see [Instance types](/docs/api/rest/instances).

→ [Engines REST API](/docs/api/rest/engines) · [Managing engines](/docs/private/managing-engines) · [Instance types](/docs/api/rest/instances)

---

## Running Apps

### Basic Usage

```typescript
import { inference } from '@inferencesh/sdk';

const client = inference({ apiKey: 'inf_your_key' });

// Run and wait for completion (default)
const result = await client.run({
  app: 'infsh/echo',
  input: { message: 'Hello from JavaScript!' }
});

console.log(result.output);
```

### With Progress Updates

```typescript
const result = await client.run(
  { app: 'infsh/flux-schnell', input: {...} },
  {
    onUpdate: (update) => {
      console.log(`Status: ${update.status}`);
      if (update.logs?.length) {
        console.log(update.logs[update.logs.length - 1]);
      }
    }
  }
);
```

### Fire and Forget

```typescript
const task = await client.run(
  { app: 'infsh/slow-task', input: {...} },
  { wait: false }
);
console.log(`Task ID: ${task.id}`);
```

`POST /apps/run` returns a slim **task result** (`id`, `status`, `output`, and optional `socket` for streaming functions) — typed as `TaskResultDTO` on `client.tasks.create()`. **`@inferencesh/sdk` v0.14.0+:** `client.run(..., { wait: false })` (and `client.tasks.run`) performs `GET /tasks/{id}` before returning, so the value is a full `Task` (`input`, `logs`, `session_id`, and other fields when the server sends them). Use `create()` when you only need the run acknowledgement; call `client.tasks.get(id)` yourself if you want the full record without starting a watch.

---

## Agent SDK

Chat with AI agents programmatically.

### Using an Existing Agent

Reference agents created in the web app:

```typescript
import { inference } from '@inferencesh/sdk';

const client = inference({ apiKey: 'inf_your_key' });

// Use an agent you created in the web app
const agent = client.agent('my-org/my-agent@abc123'); // Format: namespace/name@version

const response = await agent.sendMessage('Hello!');
console.log(response);
```

Template agents that declare **context** fields take values on the first message. The agent's tools use them as `{{context.NAME}}`:

```typescript
const agent = client.agent('my-team/pricing-agent@latest', {
  context: { version_id: 'v1', app_id: 'app_123' },
});
```

With `AgentChatProvider`, pass `context` on `agentConfig` — see [Template agents — per-chat context](/docs/api/agent/template#per-chat-context).

### Chat settings and switching agents

When a chat is already bound (for example you passed `chatId` to `AgentChatProvider` or the provider created one on the first message), `useAgentActions()` can change per-chat settings or hand the conversation to another template agent without starting a new chat.

```typescript
import { AgentChatProvider, useAgentActions } from '@inferencesh/sdk/agent';
import type { ChatSettingsRequest } from '@inferencesh/sdk';

function ChatControls() {
  const { updateChatSettings, switchAgent } = useAgentActions();

  const rename = () =>
    updateChatSettings({ name: 'Incident triage', visibility: 'team' });

  const clearStaleMemory = () =>
    updateChatSettings({ forget_memory: ['stale_goal'] });

  const handOff = () => switchAgent('my-team/escalation-agent@latest');

  // ...
}
```

| Method | REST | Notes |
|--------|------|-------|
| `updateChatSettings(settings)` | `POST /chats/:chatId/settings` | Partial update — omit fields you do not want to change. Includes `forget_memory` to drop keys from `agent_data.memory`. Resolves to `ChatSettingsDTO`. |
| `switchAgent(agentRef)` | `POST /chats/:id/agent` | Next run uses the new agent's tools and system prompt. Only between agents on the platform loop — not onto/off [remote harness](/docs/api/rest/agents#remote-harness-runs) agents. Resolves to `ChatAgentDTO`. |

Both methods no-op without a bound `chatId` (they log to the console). On failure they dispatch `SET_ERROR` and rethrow so your `onError` callback can surface the message.

`updateChatSettings` and `switchAgent` ship in **@inferencesh/sdk ≥ v0.15.0**. **@inferencesh/sdk ≥ v0.17.0:** The API answers with `ChatSettingsDTO` and `ChatAgentDTO` (only the fields those endpoints change), not a full `ChatDTO`. `AgentChatProvider` merges them into `useAgentChat().chat` and leaves `messages` unchanged; responses for a different `chat_id` are ignored. Low-level `updateChatSettings` / `setAgent` in the agent module return the same DTOs. If you call the REST endpoints yourself, merge the same fields onto `agent_data` / agent refs instead of replacing your chat object or message list.

`ChatDTO.chat_messages` is optional in generated types because chat metadata endpoints never load messages — use `GET /chats/:id/messages` or `useAgentChat().messages`.

→ [Update chat settings](/docs/api/rest/agents#chat-settings) · [Set agent](/docs/api/rest/agents#set-agent)

### Scoped always allow and explain

Requires **@inferencesh/sdk ≥ v0.16.0**. When a platform loop tool pauses with `awaiting_approval`, the API suggests **scoped** always-allow choices (exact command, prefix, whole remote, whole tool, …). The React Agent SDK loads those options, saves the one the person picks, and approves the current call. Call `explainTool` only when the person asks for a plain-language summary (not on every render).

```typescript
import { useAgentActions } from '@inferencesh/sdk/agent';

const {
  approveTool,
  rejectTool,
  getAlwaysAllowOptions,
  alwaysAllowTool,
  explainTool,
} = useAgentActions();

async function onApprovalPrompt(toolInvocationId: string) {
  const options = await getAlwaysAllowOptions(toolInvocationId);
  if (!options?.options.length) {
    console.warn(options?.unavailable ?? 'No always-allow choices');
    return;
  }

  // options.default is the narrowest key that would auto-approve this call again
  const key = options.default ?? options.options[0].key;
  try {
    const result = await alwaysAllowTool(toolInvocationId, { option: key });
    console.log('Saved chat rules:', result?.rules);
  } catch (err) {
    // 409: option stale — re-fetch getAlwaysAllowOptions and try again
    // 400: call no longer awaiting approval
    throw err;
  }
}

async function onExplainClick(toolInvocationId: string) {
  const { risk_level, explanation, reasoning, risk } = await explainTool(toolInvocationId);
  console.log(risk_level, explanation, reasoning, risk);
}
```

| Method | REST endpoint |
|--------|---------------|
| `getAlwaysAllowOptions(toolInvocationId)` | `GET /chats/{chatId}/tools/{toolInvocationId}/always-allow/options` |
| `alwaysAllowTool(toolInvocationId, { option })` | `POST /chats/{chatId}/tools/{toolInvocationId}/always-allow` |
| `explainTool(toolInvocationId)` | `POST /chats/{chatId}/tools/{toolInvocationId}/explain` |
| `approveTool` / `rejectTool` | `POST /tools/{toolInvocationId}/invoke` · `POST /tools/{toolInvocationId}/reject` |

`alwaysAllowTool` resolves to `AlwaysAllowResultDTO` (saved `rules` and the chosen `option`). Passing a **string** as the second argument is still accepted from older integrations (it was the tool name) and means “use the API default option.” On **409** or **400**, the SDK rethrows without putting the chat in an error state so you can refresh options or show why always-allow is unavailable.

`getAlwaysAllowOptions` returns `null` when no chat is bound yet. `explainTool` is not available on [harness-backed chats](/docs/api/rest/agents#explain-tool-call) (no platform model on the harness).

Exported types: `AlwaysAllowOptionsDTO`, `AlwaysAllowOptionDTO`, `AlwaysAllowScope`, `AlwaysAllowRequest`, `AlwaysAllowResultDTO`, `AlwaysAllowChoice`, `PolicyRuleDTO`, `ToolExplanationDTO`, `ToolRiskLevel`.

**Policy kinds and agent permissions (@inferencesh/sdk ≥ v0.18.0)** — saved always-allow choices and workspace policy rows share `PolicyRuleDTO`. `kind` is a typed `PolicyKind` (import the `PolicyKind*` constants instead of raw strings):

| Constant | Governs |
|----------|---------|
| `PolicyKindRemoteExec` | Shell commands on a remote (`remote_exec`) |
| `PolicyKindWorkspace` | Folders on a remote |
| `PolicyKindHarness` | Harness tool approvals (for example `remote_session_resolve`) |
| `PolicyKindTool` | Platform-loop tool calls by tool name |
| `PolicyKindWebFetch` | Outbound HTTP fetch domains |
| `PolicyKindApp`, `PolicyKindAgent`, `PolicyKindKnowledge`, `PolicyKindMcp`, `PolicyKindFlow` | Usage policy (resolved resource ids) |

```typescript
import type { AgentPermissions, PolicyRuleDTO } from '@inferencesh/sdk';
import { PolicyKindRemoteExec, PolicyKindTool } from '@inferencesh/sdk';

function describeRule(rule: PolicyRuleDTO): string {
  if (rule.kind === PolicyKindRemoteExec) {
    return `remote command: ${rule.specifier}`;
  }
  if (rule.kind === PolicyKindTool) {
    return `tool: ${rule.specifier}`;
  }
  return `${rule.kind} ${rule.specifier}`;
}

// Agent template default for new chats (YAML `version.permissions` / AgentDTO)
const defaults: AgentPermissions = { allow_all_tools: true };
```

`AgentPermissions` (`allow_all_tools` today) is on agent create/update payloads as `permissions`. It seeds `agent_data.allow_all_tools` when a chat is created; team and org policy can still ask or deny over it. See [Allow all tools on new chats (agent template)](/docs/api/agent/approval#agent-template-defaults).

**@inferencesh/sdk ≥ v0.18.0** removed `ChatData.always_allowed_tools` from generated types — the API stores always-allow as [chat rules](/docs/api/rest/agents#chat-permissions-rules). Read `result.rules` from `alwaysAllowTool`, call `GET /chats/{id}/rules`, or use the workspace Permissions pane. Legacy chats may still return `always_allowed_tools` on the wire until migrated; the REST docs describe synthesized rule rows.

→ [List always-allow options](/docs/api/rest/agents#list-always-allow-options) · [Explain tool call](/docs/api/rest/agents#explain-tool-call) · [Chat permissions rules](/docs/api/rest/agents#chat-permissions-rules) · [Approval settings](/docs/api/agent/approval#scoped-always-allow-and-explain)

### A2A agent card

Fetch the [Agent2Agent (A2A)](https://a2a-protocol.org/latest/specification/) protocol card for marketplace listings (for example GCP AI Agents Producer Portal). Requires `agents:read`.

```typescript
const card = await client.agents.getA2ACard('agent_abc123');
// or by namespace/name ref via GET /agents/{namespace}/{name}/card
```

Returns raw A2A JSON (not wrapped in `{ data: … }`). See [Agents REST API — Get A2A agent card](/docs/api/rest/agents#get-a2a-agent-card).

### List internal tool definitions

Fetch the built-in internal tool categories for agent configuration UIs. Requires `agents:read`.

```typescript
const { data: categories } = await client.agents.getInternalTools();

for (const category of categories) {
  // default_enabled is false for opt-in categories
  console.log(category.id, category.default_enabled);
}
```

Each `InternalToolDefinition` includes `id`, `name`, `description`, `tools`, `scope`, and `default_enabled` (what the runtime applies when that category's flag is unset in `internal_tools`). See [Internal Tools: Default behavior](/docs/api/agent/internal-tools#default-behavior) and [Agents REST API: List internal tool definitions](/docs/api/rest/agents#list-internal-tool-definitions).

### Creating Ad-Hoc Agents

Create agents on-the-fly without saving to workspace:

```typescript
import { inference, tool, string, internalTools } from '@inferencesh/sdk';

const client = inference({ apiKey: 'inf_your_key' });

// Define tools
const calculator = tool('calculator')
  .describe('Performs math operations')
  .param('expression', string('Math expression'))
  .build();

// Create ad-hoc agent
const agent = client.agent({
  core_app: { ref: 'infsh/claude-sonnet-4@latest' },
  name: 'Math Assistant',
  system_prompt: 'You help with math. Use the calculator tool.',
  tools: [calculator],
  internal_tools: internalTools().memory().build(),
});

const response = await agent.sendMessage('What is 42 * 17?');
```

### Streaming Responses

Tokens arrive through `onDelta` while the assistant message is being written; `onMessage` delivers each message as a whole once it is stored. `onDelta` is streaming-only: with `stream: false` the message arrives whole through `onMessage`. Pass a `signal` (an `AbortSignal`) to stop waiting for the turn; the agent keeps running server-side, `stopChat()` cancels it.

```typescript
await agent.sendMessage('Explain quantum computing', {
  onDelta: (d) => {
    // d.delta is this batch, d.output everything so far for d.messageId
    process.stdout.write(d.delta.response ?? '');
  },
  onMessage: (msg) => {
    if (msg.content) {
      for (const c of msg.content) {
        if (c.type === 'text' && c.text) {
          process.stdout.write(c.text);
        }
      }
    }
  },
  onToolCall: async (call) => {
    console.log(`\n[Tool: ${call.name}]`);
    // Execute the tool and submit its result string
    const result = await executeMyTool(call.name, call.args);
    await agent.submitToolResult(call.id, result);

    // A result can carry files uploaded first with client.files.upload:
    // const ref = await client.files.upload(pngBlob);
    // await agent.submitToolResult(call.id, 'Screenshot attached.', [ref]);
    
    // For widget interactions, you can also pass action + form_data:
    // await agent.submitToolResult(call.id, {
    //   action: { type: 'confirm' },
    //   form_data: { name: 'John', email: 'john@example.com' }
    // });
  },
});
```

### File Attachments

```typescript
// From Blob
const imageBlob = new Blob([imageBuffer], { type: 'image/png' });
const response = await agent.sendMessage('What\'s in this image?', {
  files: [imageBlob]
});

// From data URI
const response = await agent.sendMessage('Analyze this', {
  files: ['data:image/png;base64,iVBORw0KGgo...']
});

// Reuse an uploaded FileRef without re-uploading
const ref = await agent.uploadFile(imageBlob, { filename: 'photo.png' });
await agent.sendMessage('Summarize this', { files: [ref] });
```

`client.agent().sendMessage()` uploads the files and sends their URIs on `POST /agents/run` as `input.images` / `input.files`. `AgentChatProvider` / `useAgentActions().sendMessage()` create the chat, then post the uploaded refs as `attachments` on `POST /chats/{id}/messages`. **@inferencesh/sdk ≥ v0.22.1** sends them; earlier releases uploaded the files but posted only the text, so the agent never saw them.

### Channel routing (`channel_context`)

`channel_context` on `agent.sendMessage()` and `agent.run()` is deprecated. **@inferencesh/sdk 0.23.0** accepts the option and does not send it, and the API ignores it on runs. A chat's reply routing (Slack thread, Telegram chat, …) comes only from messages that arrive through the platform's [channel integrations](/docs/api/rest/agents#messaging-channel-webhooks).

Read the routing of a channel-originated chat from `getChat()`:

```typescript
const chat = await agent.getChat();
console.log(chat?.channel_context?.channel_type); // "slack"
```

See [Agents REST API: Channel context](/docs/api/rest/agents#channel-context).

### Structured output (`agent.run`)

When the agent uses the `finish` tool, call `agent.run()` to send a message, wait for completion, and receive the parsed `chat.output` payload:

```typescript
const output = await agent.run('Summarize this document in JSON');
console.log(output);
```

For manual control (streaming, tool callbacks), use `sendMessage()` and read `chat.output` from `getChat()` instead.

→ [Structured output](/docs/api/agent/structured-output)

### Chat Management

```typescript
// Get current chat
const chat = await agent.getChat();
console.log(`Chat ID: ${chat?.id}`);
console.log(`Messages: ${chat?.messages.length}`);

// Stop generation
await agent.stopChat();

// Start fresh
agent.reset();

// Cleanup streams
agent.disconnect();
```

### Interrupt gates

When an agent run pauses for tool approval, auth, widgets, or a [lifecycle hook gate](/docs/api/agent/lifecycle-hooks#gate), list and resolve interrupts:

```typescript
import { InterruptResourceHookEvent, InterruptResourceToolInvocation } from '@inferencesh/sdk';

// List pending interrupts for a run (GET /agent-runs/{runId}/interrupts)
const { data: pending } = await client.agents.listRunInterrupts('ar_abc123');

for (const interrupt of pending ?? []) {
  if (interrupt.resource_type === InterruptResourceHookEvent) {
    console.log('Hook gate:', interrupt.meta);
  }
}

// Resolve with allow or deny (POST /interrupts/{id}/resolve)
const { data: resolved } = await client.agents.resolveInterrupt('int_abc123', 'allow');

// From the Agent SDK helper (same endpoints)
await agent.resolveInterrupt('int_abc123', 'deny');
```

When a run is paused, `client.chats.get()` includes `active_run.interrupt_reason`, `interrupt_tool_id`, and `interrupt_meta`. See [Agents REST API — Interrupt gates](/docs/api/rest/agents#interrupt-gates).

| Method | REST endpoint |
|--------|---------------|
| `client.agents.listRunInterrupts(runId)` | `GET /agent-runs/{runId}/interrupts` |
| `client.agents.resolveInterrupt(id, decision)` | `POST /interrupts/{id}/resolve` |
| `agent.resolveInterrupt(id, decision)` | Same as above |

→ [CLI — Interrupt](/docs/extend/cli-setup#interrupt-cli) · [Python SDK — Interrupt gates](/docs/api/sdk-python#interrupt-gates)

### MCP input requests (MRTR)

When an agent calls a remote MCP tool that needs extra user input mid-flight, the runtime parks the tool invocation in `awaiting_input` and stores an **MCPInputState** object in the invocation’s `data` field (`input_required`, `input_requests`, optional `request_state`, and `round`). The same [Multi Round-Trip Request (MRTR)](/docs/connectors/using-tools#interactive-elicitation-mrtr) flow applies whether you call MCP tools directly or through an agent.

**@inferencesh/sdk ≥ v0.12.0** adds helpers under `@inferencesh/sdk/agent` and `submitMCPInput` on the agent client and `useAgentActions()` hook. Import `ElicitResult` (and optional `ElicitActionAccept` / `ElicitActionDecline` / `ElicitActionCancel`) from `@inferencesh/sdk` for typed responses.

```typescript
import type { ToolInvocationDTO } from '@inferencesh/sdk';
import {
  parseMCPInputState,
  elicitParams,
  isURLElicitation,
  useAgentActions,
} from '@inferencesh/sdk/agent';

function MCPInputForm({ invocation }: { invocation: ToolInvocationDTO }) {
  const { submitMCPInput } = useAgentActions();
  const state = parseMCPInputState(invocation.data);
  if (!state) return null;

  async function submitLogin() {
    await submitMCPInput(invocation.id, {
      login: { action: 'accept' },
    });
  }

  return (
    <>
      {Object.entries(state.input_requests).map(([key, request]) => {
        const params = elicitParams(request);
        if (!params) return null;
        if (isURLElicitation(params)) {
          return (
            <a key={key} href={params.url} target="_blank" rel="noreferrer">
              {params.message}
            </a>
          );
        }
        // Render a form from params.requestedSchema, then:
        // await submitMCPInput(invocation.id, { [key]: { action: 'accept', content: { field: value } } });
        return <p key={key}>{params.message}</p>;
      })}
      <button type="button" onClick={submitLogin}>
        Continue
      </button>
    </>
  );
}
```

Standalone `client.agent(...)` callers use the same API:

```typescript
const agent = client.agent('my-org/support@latest');

// After you locate the awaiting_input MCP invocation on a message:
await agent.submitMCPInput('tinv_abc123', {
  step_a: { action: 'accept', content: { email: 'you@example.com' } },
  step_b: { action: 'decline' },
});
```

Under the hood, `submitMCPInput` posts the JSON string from `buildMCPInputResult(responses)` via the same REST path as `submitToolResult` (`POST /tools/{id}`). You must include one `ElicitResult` per key in `input_requests`. If the server rejects the payload (**HTTP 400**, for example a missing key), the SDK **rethrows** the error without marking the chat connection failed so your UI can show validation next to the form. Other failures behave like `submitToolResult` (connection error + `onError`).

| Helper | Purpose |
|--------|---------|
| `parseMCPInputState(data)` | Parse invocation `data` (object or JSON string) into `MCPInputState`, or `null` |
| `elicitParams(request)` | Read `elicitation/create` params (`mode`, `message`, `requestedSchema`, `url`) |
| `isURLElicitation(params)` | `true` for URL/OAuth steps (`mode: 'url'` or a `url` with no explicit form mode) |
| `buildMCPInputResult(responses)` | Serialize `{ [key]: ElicitResult }` for custom UIs that call `submitToolResult` directly |

### A2UI surfaces (`Widget`, `McpApp`)

Interactive agent UI is an **A2UI surface** on `ToolInvocationDTO.widget`. The SDK exports `Widget` as an alias of `A2UISurface`, plus component types such as `A2UIMcpApp` (`"McpApp"`) and `A2UIForm` (`"Form"`).

MCP Apps embeds use `A2UIComponent` fields `mcpPageHash`, `mcpHtml`, `mcpCsp`, `mcpServerSlug`, `mcpCredentialId`, `mcpToolName`, `mcpToolInput`, and `mcpToolResult`. When `mcpPageHash` is set, the host loads the page from [`GET /mcp-ui-pages/{hash}`](/docs/api/rest/mcp-servers#get-stored-page) so repeated tool runs in one chat share one stored copy. `mcpHtml` carries the document inline — for example artifact pages, messages written before hash storage, or pages that could not be stored. When `mcpArtifactId` is set, the host serves the page through artifact data endpoints instead of the MCP proxy.

→ [Connector tools — MCP Apps UI](/docs/connectors/agent-tools#mcp-apps-ui-interactive-tool-pages)

→ [Using tools — Interactive elicitation](/docs/connectors/using-tools#interactive-elicitation-mrtr) · [Connector tools](/docs/api/agent/connector-tools)

---

## Tool Builder

Define tools with the fluent API:

```typescript
import {
  tool, appTool, agentTool, callTool, httpTool, mcpTool, webhookTool, internalTools,
  string, number, integer, boolean, enumOf, array, object, optional,
  CredentialProviderGoogle,
} from '@inferencesh/sdk';
```

`httpTool` is an alias for `callTool` (authenticated HTTP). `webhookTool` is legacy for unsigned webhooks. Use `mcpTool` for connector (MCP) tools.

### Typed constants (v0.6.8+)

The SDK exports typed string constants for credentials, instances, and tool schemas:

```typescript
import {
  CredentialProviderMCP,
  CredentialStatusConnected,
  EngineStatusDisconnected,
  CloudAWS,
  InstanceStatusRunning,
  InstanceRentalTypeOnDemand,
  InstanceRentalTypeSpot,
  ToolParamTypeString,
  ToolParamTypeObject,
  ToolAuthTypeNone,
  ToolAuthTypeCredential,
  ToolAuthTypeBearer,
  ToolAuthTypeAPIKey,
} from '@inferencesh/sdk';
```

Use these instead of raw strings when building credential clients or validating tool parameter types.

**Entitlements** — use with the [Entitlements REST API](/docs/api/rest/entitlements):

```typescript
import {
  ResourceConcurrency,
  ResourceFeatureSeedance,
  EntitlementTypeLimit,
  EntitlementSourceTier,
  type EntitlementDTO,
  type EntitlementErrorMeta,
} from '@inferencesh/sdk';

const ent: EntitlementDTO = {
  resource: ResourceConcurrency,
  type: EntitlementTypeLimit,
  limit: 3,
  source: EntitlementSourceTier,
};
```

**Entitlement error metadata** — `EntitlementErrorMeta` mirrors the `meta` object on `limit_exceeded` and `feature_not_available` responses. Parse it from `InferenceError.responseBody`:

```typescript
import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';

try {
  await client.tasks.run({ app: 'my-app', input: {} });
} catch (e) {
  if (e instanceof InferenceError) {
    const body = JSON.parse(e.responseBody);
    const meta: EntitlementErrorMeta = body.meta ?? body.error?.meta ?? {};
    if (meta.upgrade_available) {
      console.log(meta.resource_label, meta.addon_plan_name);
    }
  }
}
```

See [Entitlements API — Error metadata](/docs/api/rest/entitlements#error-metadata) for field definitions.

| Constant (feature gates) | API value |
|--------------------------|-----------|
| `ResourceFeatureBYOK` | `feature:byok` |
| `ResourceFeatureSeedance` | `feature:seedance` |
| `ResourceFeatureWebhooks` | `feature:webhooks` |
| `ResourceFeaturePublishApps` | `feature:publish_apps` |

See [Entitlements API — Resources](/docs/api/rest/entitlements#resources) for the full list.

```typescript
// Client tool (runs in your code)
const search = tool('search_files')
  .describe('Search for files')
  .param('pattern', string('Glob pattern'))
  .param('recursive', optional(boolean('Search subdirs')))
  .build();

// App tool (calls another inference app)
const generate = appTool('generate', 'infsh/flux-schnell@latest')
  .describe('Generate an image')
  .param('prompt', string('Image description'))
  .build();

// Agent tool (delegates to sub-agent)
const researcher = agentTool('research', 'my-org/researcher@v1')
  .describe('Research a topic')
  .param('topic', string('Topic'))
  .build();

// Authenticated HTTP (callTool and httpTool are aliases)
const getData = httpTool('get_data', 'https://api.example.com/data')
  .describe('Fetch data from the API')
  .method('GET')
  .auth({ bearer: 'my_api_key' })
  .build();

// OAuth: runtime injects the caller's own connected account's access token
const gmailSend = httpTool('gmail_send', 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send')
  .describe('Send email via Gmail')
  .method('POST')
  .auth({ credential: CredentialProviderGoogle })
  .build();

// Webhook tool (calls external API)
const notify = webhookTool('slack', 'https://hooks.slack.com/...')
  .describe('Send Slack message')
  .secret('SLACK_SECRET')
  .param('message', string('Message'))
  .build();

// Internal tools config
const internals = internalTools().memory().plan().build();

// Meta tools are opt-in (search_tools, list_tools, execute_tool)
const withMeta = internalTools().meta().build();
```

→ [Full Tool Builder Reference](/docs/api/agent/tools)

---

## TypeScript Types

The SDK is fully typed. Import status constants and DTOs from `@inferencesh/sdk`.

`DescriptionLimitListing` (200) and `DescriptionLimitSkill` (1024) are generated constants for validating writes (**@inferencesh/sdk ≥ v0.20.0**); see [description length limits](/docs/api/rest/overview#description-length-limits).

```typescript
import type {
  TaskDTO,
  ChatDTO,
  ChatMessageDTO,
  ChannelContext,
  AgentTool,
  AppStatus,
  TaskStatusCompleted,
  TaskStatusFailed,
} from '@inferencesh/sdk';
import {
  AppStatusActive,
  AppStatusDeprecated,
  AppStatusMaintenance,
  AppStatusRetired,
} from '@inferencesh/sdk';

// Check task status
if (result.status === TaskStatusCompleted) {
  console.log('Done!');
} else if (result.status === TaskStatusFailed) {
  console.log('Failed:', result.error);
}

// App lifecycle status (on AppDTO.status)
if (app.status === AppStatusDeprecated) {
  console.log(app.status_message);
}
```

### Generated types for REST integrations

OpenAPI-aligned types ship in `@inferencesh/sdk` for endpoints you call directly with `fetch` or when building consent UIs. See the [Python SDK generated types](/docs/api/sdk-python#generated-types-inferenceshtypes) for parity.

**Current user (`GET /me`)** — import `MeResponse` for the envelope returned by [`GET /me`](/docs/api/rest/teams#get-current-user). `team` is a full `TeamDTO` (not `TeamRelationDTO` from `GET /teams`). Settings gates use `team_view.can` and `team_view.org.can`:

```typescript
import type { MeResponse } from '@inferencesh/sdk';

const { data: me } = await client.teams.me(); // Response<MeResponse>
const caps = me.team_view?.can ?? [];
```

**Run acknowledgement (`POST /apps/run`)** — import `TaskResultDTO` for the JSON body from [`POST /apps/run`](/docs/api/rest/tasks#run-task) (and from `client.tasks.create()`). It is not a full task document; fetch or watch with `client.tasks.get()` / `client.tasks.watch()` when you need `input`, `logs`, or `session_id`.

**Workspaces (`GET /teams`, `GET /teams/{id}`)** — **`@inferencesh/sdk` v0.14.0+:** `client.teams.list()`, `get()`, `create()`, and `update()` return `TeamDTO` (including your `role` and `usage_policy_id` on list/get). `GET /me` still wraps a full `TeamDTO` in `MeResponse.team` as above.

> Credential types were named `Integration*` until 2026-09; SDK 0.8.0 removed the old names. Use the `Credential*` types and `client.credentials`.

**Credential configs**: merged setup view from `GET /credentials/configs`. It returns the provider catalog, connection state, and connected MCP servers. One call is enough for connect/disconnect UIs. See [Credential configs](/docs/api/rest/credentials#credential-configs).

```typescript
import type { CredentialConfigDTO, CredentialDTO } from '@inferencesh/sdk';
import { CredentialScopeUser } from '@inferencesh/sdk';

function connectLabel(config: CredentialConfigDTO): string {
  if (config.connection_scope === CredentialScopeUser) {
    return `connect your ${config.name} account`;
  }
  return `connect ${config.name}`;
}

function listSubtitle(credential: CredentialDTO): string {
  const accountId = credential.account_identifier ?? '';
  if (credential.scope === CredentialScopeUser) {
    return `${accountId} (your account)`;
  }
  return accountId;
}

const perUserOAuth: CredentialConfigDTO = {
  slug: 'x',
  provider: 'x',
  type: 'oauth',
  name: 'X',
  short_name: 'X',
  description: '',
  allows_byok: false,
  available: true,
  has_managed: true,
  connection_scope: CredentialScopeUser,
};
```

| Constant | API value | Meaning |
|----------|-----------|---------|
| `CredentialGrantCredentials` | `credentials` | OAuth **application** row (client id/secret and related app keys), not a login |
| `CredentialGrantToken` | `token` | **Connection** row: OAuth login, API key, service account, or MCP authorization |
| `CredentialScopeUser` | `user` | Personal connection. Use it for scope-aware list labels, section headings, and disconnect copy. |
| `CredentialScopeTeam` | `team` | Shared workspace connection |
| `CredentialScopeOrg` | `org` | Shared across all workspaces of an org. Connecting one requires an org admin, from the organization workspace. |
| `CredentialScopePlatform` | `platform` | Platform-managed credentials |
| `CredentialScopeAgent` | `agent` | Agent-scoped credential |

`CredentialConfigDTO` exposes `connection_scope` and optional nested `app` (OAuth application) instead of `grant`. `grant` and `app_credential_id` appear on `CredentialDTO` — use `grant` to tell app rows (`credentials`) from logins (`token`). `scope` on `CredentialDTO` indicates who owns the connection. `CredentialConfigDTO.credential` holds the connected login; `CredentialConfigDTO.app` holds the OAuth app when configured. See [Managed credentials and grant type](/docs/api/rest/credentials#managed-credentials-and-grant-type).

**Flow utility nodes** — use with the [Flows REST API](/docs/api/rest/flows#primitive-utility-nodes) when calling `POST /flows/{id}/actions`:

```typescript
import type { UtilityConfig } from '@inferencesh/sdk';

const gate: UtilityConfig = {
  preset: 'gate',
  gate: { field: 'status', operator: 'equals', value: 'approved' },
};

const selector: UtilityConfig = {
  preset: 'selector',
  selector: { field: 'items', mode: 'first' },
};

// node.add payload: { id, type: 'gate', position, data: { utility: gate } }
```

| Type | Description |
|------|-------------|
| `FlowNodeData` | Per-node graph data. Has `utility` plus the legacy `gate_condition` and `selector_config` |
| `UtilityConfig` | `preset` (`gate`, `selector`, `merge`, `constant`), optional `expression`, `gate`, `selector`, `constant`, `random`, `random_min`, `random_max` |
| `GateCondition` | `field`, `operator` (`equals`, `not_equals`, `contains`, `gt`, `lt`, `is_empty`, `is_not_empty`), `value` |
| `SelectorConfig` | `field`, `mode` (`first`, `last`, `index`), optional `index` |

**Flow run progress** — use with [Flow Runs REST API: Per-node progress](/docs/api/rest/flow-runs#per-node-progress):

```typescript
import {
  GraphNodeStatusBlocked,
  GraphNodeStatusFailed,
  GraphNodeStatusRunning,
} from '@inferencesh/sdk';

const { data: run } = await client.flowRuns.get('flowrun_abc123');

// A gate whose condition is false is reported as failed; its dependents are blocked
if (run.node_statuses?.gate_1 === GraphNodeStatusFailed) {
  console.log('Gate condition was false');
}
if (run.node_statuses?.upscale === GraphNodeStatusBlocked) {
  console.log('Upscale did not run');
}
if (run.node_statuses?.generate === GraphNodeStatusRunning) {
  console.log('Still running:', run.node_tasks.generate?.task_id);
}

console.log(run.node_outputs?.selector_1?.value);
```

| Type / constant | Description |
|-----------------|-------------|
| `FlowRunDTO` | Flow run response. Includes `node_tasks`, optional `node_statuses`, and `node_outputs` |
| `GraphNodeStatus` | Per-node status string: `pending`, `ready`, `running`, `completed`, `failed`, `cancelled`, `skipped`, `blocked` |
| `GraphNodeStatusPending`, `GraphNodeStatusReady`, `GraphNodeStatusRunning`, `GraphNodeStatusCompleted`, `GraphNodeStatusFailed`, `GraphNodeStatusCancelled`, `GraphNodeStatusSkipped`, `GraphNodeStatusBlocked` | Constants for each status value |

### Decision app types

Apps with `category: decision` exchange the same JSON on task runs. Import the generated types when you build or parse payloads in TypeScript:

```typescript
import type {
  DecisionInput,
  DecisionOutput,
  DecisionVisionInput,
  DecisionChoiceQuestion,
  DecisionScoreQuestion,
  DecisionNoulQuestion,
} from '@inferencesh/sdk';

const input: DecisionInput = {
  state: { summary: 'User cannot log in after password reset' },
  choices: [
    {
      id: 'root_cause',
      instructions: 'Most likely cause',
      options: [
        { name: 'cache', description: 'Stale session cache' },
        { name: 'identity', description: 'IdP sync delay' },
      ],
    },
  ],
};

// After client.tasks.create / poll — cast or validate result.output
const output = result.output as DecisionOutput;
console.log(output.choices.root_cause.choice, output.choices.root_cause.probabilities);
```

| Type | Role |
|------|------|
| `DecisionInput` | `state` plus optional `choices`, `scores`, `nouls` question arrays |
| `DecisionVisionInput` | `DecisionInput` plus `images` (file URIs visible to every question) |
| `DecisionChoiceQuestion` / `DecisionChoiceOption` | Fixed options; answers keyed by option `name` |
| `DecisionScoreQuestion` | Ordered `levels` (low → high); level numbers are array indices |
| `DecisionNoulQuestion` / `DecisionNoulCriteria` | Yes/no probability; optional `criteria.true` / `criteria.false` labels |
| `DecisionOutput` | `choices`, `scores`, `nouls` maps keyed by question `id`, plus `model` and `input_tokens` |
| `DecisionChoiceAnswer` | `choice`, `confidence`, `probabilities` |
| `DecisionScoreAnswer` | `score`, `normalized`, `confidence`, `probabilities`, `legend` |
| `DecisionNoulAnswer` | `noul` (probability true) |

Conceptual overview and JSON examples: [Decision apps](/docs/concepts/apps#decision-apps).

**Interrupt gates** — use with [Agents REST API — Interrupt gates](/docs/api/rest/agents#interrupt-gates):

```typescript
import type { InterruptDTO } from '@inferencesh/sdk';
import {
  InterruptReasonToolApproval,
  InterruptStatusPending,
  InterruptResourceToolInvocation,
} from '@inferencesh/sdk';

const pending: InterruptDTO = {
  id: 'int_abc123',
  run_id: 'ar_abc123',
  chat_id: 'chat_abc123',
  reason: InterruptReasonToolApproval,
  status: InterruptStatusPending,
  resource_id: 'ti_abc123',
  resource_type: InterruptResourceToolInvocation,
};
```

| Type / constant | Values |
|-----------------|--------|
| `InterruptReason` | `tool_approval`, `client_tool`, `widget`, `auth`, `confirmation`, `hook_gate` — mirrors `active_run.interrupt_reason` |
| `InterruptStatus` | `pending`, `resolved`, `expired`, `cancelled` |
| `InterruptResolution` | `allow`, `deny` — set on `InterruptDTO` after resolve; also used as `default_resolution` on gate hooks |
| `InterruptResourceType` | `tool_invocation` (tool approval gates), `hook_event` (lifecycle hook gates) |
| `InterruptDTO` | Full interrupt record (`run_id`, `chat_id`, `source`, `resource_id`, `resource_type`, `meta`, `resolved_data`, `expires_at`, …) |

### Remote harness chats

**@inferencesh/sdk ≥ v0.8.3** exports OpenAPI fields for [harness-driven agents](/docs/api/rest/agents#remote-harness-runs) and chats opened via [Launch harness session](/docs/api/rest/remotes#launch-harness-session). There is no `client.remotes` namespace yet; call `POST /remotes/{id}/sessions` with `fetch` (or your HTTP client) and type the JSON with `ChatDTO`.

```typescript
import type { AgentDTO, ChatDTO } from '@inferencesh/sdk';

const { data: agent } = await client.agents.get('agent_abc123');
if (agent.harness !== 'inference') {
  console.log(agent.harness, agent.profile_id, agent.remote_id);
}

const { data: chat } = await client.chats.get('chat_abc123');
console.log(chat.work_dir, chat.harness_session_id);

type LaunchHarnessSessionRequest = {
  profile_id: string;
  work_dir?: string;
  resume_session_id?: string;
};

const launchBody: LaunchHarnessSessionRequest = {
  profile_id: 'prof_xyz',
  work_dir: '/home/you/project',
};

const launched = (await fetch(`${baseUrl}/remotes/${remoteId}/sessions`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(launchBody),
}).then((r) => r.json())) as ChatDTO;
```

| Field / type | Description |
|--------------|-------------|
| `AgentDTO.harness` | `"inference"` for the platform loop, or a harness registry id (`claude`, `codex`, …) when a remote profile drives the agent |
| `AgentDTO.profile_id` / `AgentDTO.remote_id` | Set on harness-backed agents (see REST docs) |
| `ChatDTO.work_dir` | Folder the harness uses for this chat |
| `ChatDTO.harness_session_id` | Harness-native session id (for example to resume with `claude --resume`) |
| `ChatDTO.channel_context` | External channel origin for reply routing — see [Channel context](/docs/api/rest/agents#channel-context) |

**Channel routing**: `ChannelContext`, `ChannelTypeSlack`, `ChannelTypeTelegram`, `ChannelTypeTeams`, and `ChannelTypeDiscord` type `channel_context` on `ChatDTO`. The `sendMessage()` option is deprecated and not sent (**0.23.0**). Wire keys are `channel_type` and `channel_metadata` (object). See [Channel routing (`channel_context`)](#channel-routing-channel_context).

**Agent run events** — harness and platform runs publish sequenced `AgentEvent` envelopes on the run and chat event bus. Custom clients (for example a dashboard that consumes `remote_session_event` from a remote daemon) can type them with the generated constants:

```typescript
import type { AgentEvent } from '@inferencesh/sdk';
import {
  AgentEventRunStarted,
  AgentEventRunStateChanged,
  AgentEventContentDelta,
  AgentEventToolCompleted,
  AgentEventApprovalRequired,
} from '@inferencesh/sdk';

function isRunStarted(event: AgentEvent): boolean {
  return event.type === AgentEventRunStarted;
}
```

| Constant | Wire `type` |
|----------|-------------|
| `AgentEventRunStarted` | `run.started` |
| `AgentEventRunStateChanged` | `run.state_changed` |
| `AgentEventTurnStarted` / `AgentEventTurnCompleted` | `turn.started` / `turn.completed` |
| `AgentEventContentDelta` | `content.delta` |
| `AgentEventToolStarted` / `AgentEventToolCompleted` | `tool.started` / `tool.completed` |
| `AgentEventApprovalRequired` / `AgentEventApprovalResolved` | `approval.required` / `approval.resolved` |
| `AgentEventHookExecuted` | `hook.executed` |
| `AgentEventUsageUpdated` | `usage.updated` |
| `AgentEventContextUpdated` | `context.updated` |
| `AgentEventLimitsUpdated` | `limits.updated` |
| `AgentEventAccountUpdated` | `account.updated` |
| `AgentEventContextCompacted` | `context.compacted` |
| `AgentEventSubagentStarted` / `AgentEventSubagentUpdated` / `AgentEventSubagentCompleted` | `subagent.started` / `subagent.updated` / `subagent.completed` |
| `AgentEventError` | `error` |

Each event has `id`, `run_id`, `chat_id`, `timestamp`, and a `payload` whose shape depends on `type` (`RunStartedPayload`, `ContentDeltaPayload`, …). See [Harness session frames](/docs/api/rest/remotes#harness-session-frames).

The context, limits, account and subagent constants and their payload types ship in **@inferencesh/sdk ≥ v0.23.0**. The events come from agentprotocol **v0.21.0** (usage cache fields, `context.updated`, `limits.updated`, `account.updated`) and **v0.22.0** (`subagent.*`). A remote daemon may send them in `remote_session_event`; the platform does not forward `context.updated`, `limits.updated`, `account.updated` or `subagent.*` to chat and run streams yet.

**`usage.updated`** is emitted once when a turn ends. `UsageUpdatedPayload`:

| Field | Meaning |
|-------|---------|
| `prompt_tokens` | Every input token of the turn, cache reads and writes included |
| `cached_input_tokens` | Input tokens read from a prompt cache (part of `prompt_tokens`) |
| `cache_creation_tokens` | Input tokens written to a prompt cache (part of `prompt_tokens`) |
| `completion_tokens` | Output tokens |
| `reasoning_tokens` | Reasoning tokens (part of `completion_tokens`) |
| `total_tokens` | Turn total |
| `cost_usd` | The agent's own cost figure |
| `status` | `complete` for a whole turn, `partial` when the turn ended early |

The cache, reasoning, cost and status fields are omitted when zero or empty.

| Event | Payload |
|-------|---------|
| `context.updated` | `used_tokens`, `max_tokens`: how full the context window is |
| `limits.updated` | `windows[]`: `id`, `kind` (`session`, `weekly`, `monthly`, `other`), `label`, `used_percent` (0 to 100), `resets_at`, `duration_minutes` |
| `account.updated` | `account`: `status` (`authenticated`, `unauthenticated`, `unknown`), `type` (`subscription`, `api_key`, `cloud`), `plan`, `plan_label`, `email`, `workspace` |

**Chat transcript types:** `ChatMessageRoleEvent`, `ChatEventTypeHook`, `ChatEvent`, and `ChatHookEvent` describe `role: "event"` messages for [notable lifecycle hook runs](/docs/api/agent/lifecycle-hooks#hook-runs-in-the-transcript) (inject, non-allow decision, or error; not every run). See [Chat message roles](/docs/api/rest/agents#chat-message-roles).

### App requirement and setup-action types

Use with [Check requirements](/docs/api/rest/credentials#check-requirements-pre-flight), `client.credentials.checkRequirements()`, and the `credentials:` / `secrets:` blocks of `inf.yml`:

```typescript
import type {
  CheckRequirementsRequest,
  CredentialRequirement,
  SecretRequirement,
  SetupAction,
} from '@inferencesh/sdk';
import {
  SetupActionAddSecret,
  SetupActionConnect,
  SetupActionAddScopes,
  RequirementTypeCredential,
} from '@inferencesh/sdk';

const request: CheckRequirementsRequest = {
  secrets: [{ key: 'OPENAI_API_KEY', optional: false }],
  credentials: [
    { slug: 'google', capabilities: ['google.sheets'] },
    {
      slug: 'acme',
      secrets: ['ACME_API_KEY'],
      provider: { name: 'Acme CRM', website: 'acme.com' },
    },
  ],
};

const { data } = await client.credentials.checkRequirements(request);
if (!data?.satisfied) {
  for (const err of data?.errors ?? []) {
    if (err.type === RequirementTypeCredential && err.action?.type === SetupActionConnect) {
      console.log('Connect', err.action.provider_name);
    }
  }
}
```

| Type | Fields |
|------|--------|
| `SecretRequirement` | `key`, `description`, `optional` |
| `CredentialRequirement` | `slug` (credential slug such as `google` or `acme`), `capabilities` (platform grants such as `google.sheets`), `secrets`, `scopes`, `description`, `optional`, `provider` (`CompanySeed`: `name`, `website`, … for a company the platform does not list; `company:` in inf.yml), `auth` (`AuthMethod`, for a slug no auth provider defines yet). **v0.26.0+**; before that, `key`, `provider` (a string), `name` and `website` sat on the entry. The API still reads those spellings. `CompanySeed` and `AuthMethod` were `ProviderSeed` and `AuthSchemeSpec` before INF-1079; `AuthSchemeDTO` is now `AuthProviderDTO`. |
| `SetupAction` | `type` (`SetupActionAddSecret`, `SetupActionConnect`, `SetupActionAddScopes`), `provider`, `provider_name`, `scopes`, `scope_descriptions`, `secrets`, `provider_website` |
| `CheckRequirementsResponse` | `satisfied`, `errors` (each `RequirementError` may include `action: SetupAction`) |
| `AppVersionDTO.required_credentials` | Same `CredentialRequirement` shape as in `inf.yml` |
| `AppFunction.credentials` | Credentials one function needs (`CredentialRequirement[]`), each covered by `required_credentials`. Empty means the function gets the app-wide list (**v0.23.0+**) |
| `AppFunction.annotations` | `ToolAnnotations` MCP hints (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) (**v0.23.0+**) |
| `AppVersionDTO.warnings` | `DeployWarning[]` (`code`, `function`, `message`), only on the version a deploy just created (**v0.23.0+**). See [Deploying: Warnings](/docs/extend/deploying#warnings) |

**Cost estimation** — use with [Estimate app cost](/docs/api/rest/store#estimate-app-cost) (`POST /store/apps/{appId}/estimate`):

```typescript
import type { EstimateCostRequest, EstimateCostResponse } from '@inferencesh/sdk';

const req: EstimateCostRequest = {
  input: { n: 4 },
  function: 'generate', // optional for multi-function apps
};

const resp: EstimateCostResponse = {
  confidence: 'range',
  min: 150_000,
  max: 400_000,
  depends_on: ['output_tokens'],
  pricing_description: '$0.0015–$0.004 per run',
};

const failed: EstimateCostResponse = {
  confidence: 'unknown',
  depends_on: ['output_tokens'],
  estimate_error: 'CEL evaluation failed: undefined variable "output_tokens"',
};
```

| Type | Fields |
|------|--------|
| `EstimateCostRequest` | `input`, `function` |
| `EstimateCostResponse` | `confidence`, `microcents`, `min`, `max`, `depends_on`, `estimate_error`, `pricing_description` |

**Plans and subscriptions** — use with the [Subscription REST API](/docs/api/rest/subscription):

```typescript
import type { PlanDTO } from '@inferencesh/sdk';
import { PlanTypeBase, PlanTypeAddon } from '@inferencesh/sdk';

const base: PlanDTO = {
  name: 'pro',
  plan_type: PlanTypeBase,
  credits_monthly: 5_000_000_000,
  stackable: false,
  required_plan_ids: [],
  limits: {},
};

const addon: PlanDTO = {
  name: 'extra concurrency',
  plan_type: PlanTypeAddon,
  stackable: true,
  required_plan_ids: ['plan_pro', 'plan_team'],
  required_plan_names: ['Pro', 'Team'],
  limits: {},
};
```

`stackable` controls whether a workspace can purchase multiple instances of the same add-on. `required_plan_ids` gates add-on checkout to workspaces on a qualifying base plan (see [Add-on subscriptions](/docs/api/rest/subscription#add-on-subscriptions)).

**API key scopes** — use with [API keys — Scope catalog](/docs/api/rest/api-keys#scope-catalog) (`GET /scopes`):

```typescript
import type { ScopesResponse, ScopePreset } from '@inferencesh/sdk';

const catalog: ScopesResponse = {
  scopes: [{ value: 'agents:execute', label: 'execute', group: 'agents' }],
  groups: [{ id: 'agents', label: 'agents' }],
  presets: [
    {
      id: 'run',
      label: 'run',
      description: 'execute agents, apps, and flows',
      scopes: ['agents:read', 'apps:read', 'agents:execute', 'apps:execute'],
      summary: ['run apps and agents on your behalf'],
      hidden: false,
    },
  ],
};
```

`ScopePreset.summary` is a human-readable permission list for consent UIs. `hidden` presets (for example `engine`) are omitted from non-admin `GET /scopes` responses.

**API key scope (who the key acts as)** — distinct from the permission `scopes` above. See [API Keys API — personal and workspace keys](/docs/api/rest/api-keys#personal-and-workspace-keys) (**@inferencesh/sdk ≥ v0.15.0**):

```typescript
import {
  ApiKeyScopeWorkspace,
  type ApiKeyScope,
} from '@inferencesh/sdk';

const scope: ApiKeyScope = ApiKeyScopeWorkspace; // or ApiKeyScopeUser

const { data: key } = await client.apiKeys.create({
  name: 'ci',
  scope,
  scopes: ['apps:execute'],
});
console.log(key.key); // the only time the full value is returned
```

`CreateApiKeyRequest.scope` accepts `user` (default) or `workspace`. `ApiKeyDTO` carries `scope`, `created_by`, and, on list responses, `creator`.

**Instance catalog** — use with [Instance types REST API](/docs/api/rest/instances) for GPU pricing and availability (**@inferencesh/sdk ≥ v0.14.2**):

```typescript
import type {
  InstanceTypeAvailability,
  InstanceTypeConfiguration,
  InstanceTypeDTO,
} from '@inferencesh/sdk';
import { InstanceRentalTypeOnDemand, InstanceRentalTypeSpot } from '@inferencesh/sdk';

// GET /instances/types returns InstanceTypeDTO[]
const row: InstanceTypeDTO = {
  cloud: 'runpod',
  cloud_logo_url: 'https://cloud.inference.sh/logos/runpod.com.png',
  configuration: {
    gpu_type: 'H100',
    gpu_manufacturer: 'nvidia',
    num_gpus: 1,
    nvlink: false,
  },
  availability: [
    { available: true, region: 'us-east-1', rental_type: InstanceRentalTypeOnDemand },
    {
      available: true,
      region: 'us-east-1',
      rental_type: InstanceRentalTypeSpot,
      hourly_price: 199, // cents per hour — spot rows only
    },
  ],
};

const spotRow: InstanceTypeAvailability = row.availability![1];
```

`InstanceTypeConfiguration` includes `gpu_manufacturer`, `nvlink`, `gpu_type`, `num_gpus`, and the other fields documented on the REST page. Each `availability` entry includes `rental_type` (`on_demand` or `spot`); spot rows also set `hourly_price` (cents). Engine-picker offers from `GET /engines/types` set top-level `rental_type`, `region`, and `hourly_price` on `InstanceTypeDTO`, and include an `options` array (every in-stock provider and per-region prices for that offer) — see [Engines API — List engine instance types](/docs/api/rest/engines#list-engine-instance-types).

**Engines** — use with the [Engines REST API](/docs/api/rest/engines):

```typescript
import type { EngineDTO } from '@inferencesh/sdk';
import {
  EngineStatusRunning,
  InstanceRentalTypeSpot,
} from '@inferencesh/sdk';

const engine: EngineDTO = {
  id: 'eng_abc123',
  name: 'gpu-box-1',
  status: EngineStatusRunning,
  engine_version: 'v1.10.15', // inferencesh-engine binary; empty until first telemetry sync
  api_url: 'https://api.inference.sh',
  workers: [],
  instance: {
    rental_type: InstanceRentalTypeSpot,
    hourly_price: 199,
  },
};
```

Spot cloud engines stop permanently when the provider reclaims the instance; provision a new engine or use on-demand — see [Engines API — Get engine](/docs/api/rest/engines#get-engine).

| Constant / type | Notes |
|-----------------|-------|
| `InstanceRentalType` | `on_demand`, `spot` — catalog rows, `EngineDTO.instance`, and nested `engine.instance` |
| `InstanceRentalTypeOnDemand` / `InstanceRentalTypeSpot` | Typed constants for `rental_type` |

**Store browse metadata** — category and tag catalogs from the public Store API (**`@inferencesh/sdk` ≥ v0.21.0** for `StoreCategoryDTO` / `StoreTagDTO`):

```typescript
import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';

const categoriesPayload = await fetch('https://api.inference.sh/store/categories?type=app').then((r) =>
  r.json(),
);
const categories: StoreCategoryDTO[] = categoriesPayload.data;
const tagsPayload = await fetch('https://api.inference.sh/store/tags').then((r) => r.json());
const tags: StoreTagDTO[] = tagsPayload.data;
```

| Type | REST endpoint | Notes |
|------|---------------|-------|
| `StoreCategoryDTO` | `GET /store/categories` | `slug`, `name`, `description`, `icon`, `rank`, `count` — pass `type` to scope `count` per resource kind |
| `StoreTagDTO` | `GET /store/tags` | `slug`, `title`, `count` — tags on ≥ 3 public apps |

There is no `client.store` namespace yet; call these endpoints with `fetch`. See [List store categories](/docs/api/rest/store#list-store-categories) and [List browseable tags](/docs/api/rest/store#list-browseable-tags).

**Per-app store listings** — use with the [Store REST API](/docs/api/rest/store#get-app-store-listing):

```typescript
import type { AppStoreListingDTO } from '@inferencesh/sdk';

const listing: AppStoreListingDTO = {
  id: 'app_abc123',
  allows_cloud_workers: true,
  allows_private_workers: true,
  max_concurrency: 0,
  max_concurrency_per_team: 5,
  min_concurrency: 2,
  required_feature: 'feature:seedance', // optional — explicit entitlement grant required
};
```

`min_concurrency` protects cloud workers holding the app from displacement by other apps; it does not start workers. See [Reserved cloud capacity](/docs/api/rest/store#reserved-cloud-capacity-min_concurrency). When `required_feature` is set, dispatch requires a boolean entitlement with `enabled: true` — see [Feature-gated store apps](/docs/api/rest/entitlements#feature-gated-store-apps).

| Constant | API value |
|----------|-----------|
| `AppStatusActive` | `active` |
| `AppStatusMaintenance` | `maintenance` |
| `AppStatusDeprecated` | `deprecated` |
| `AppStatusRetired` | `retired` |

See [Apps REST API — lifecycle status](/docs/api/rest/apps#app-lifecycle-status) for run and discovery behavior.

---

## CommonJS Support

**@inferencesh/sdk ≥ v0.23.0** can be loaded with `require()` on Node 22.12+, which loads ES modules from CommonJS:

```javascript
const { inference, tool, string } = require('@inferencesh/sdk');

async function main() {
  const client = inference({ apiKey: 'inf_...' });
  const result = await client.run({...});
}

main();
```

---

## Error Handling

Responses are unwrapped from the `{ "data", "messages" }` envelope (`.data` and `.messages` on every result) and errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json`.

`InferenceError.message` uses `detail` when present, otherwise `title`, then `message`, then the raw response body for non-JSON failures — the same extraction order as the [Python SDK](/docs/api/sdk-python#error-handling).

Pass `onError` when creating the client to intercept errors and optionally retry (token refresh, OTP, custom backoff). **@inferencesh/sdk ≥ v0.21.0** routes these failures through the same handler:

- REST calls via `request()`
- Raw `HttpClient.fetch()` calls (including stream bodies)
- Failed **initial** connections for SSE (`.stream()` / `createEventSource`) and NDJSON (`tasks.run()` / `tasks.watch()`, agent chat streams)

The handler receives `(error, retry, request)` where `request.token` is the bearer token the failed attempt used (`undefined` in proxy mode or when no token was sent). `retry()` sends the request again with the token from `getToken` or `apiKey` **at retry time** — it does not re-enter `onError`. Compare `request.token` with your in-memory credential when tokens can rotate while requests are in flight (for example an app embedded in an MCP host).

`getToken` may be async; the client awaits it before every request, stream handshake, and `fetch()`.

Return the promise from `retry()` to retry after refreshing credentials. For `fetch()` and the EventSource handshake, you can also resolve with a `Response` directly; any other return value leaves the original failure in place.

`HttpClient.request()` also accepts `handleErrors` (default `true`). With `handleErrors: false`, that call bypasses `onError`: there is no retry, no credential prompt, and the failure is thrown as-is (`InferenceError` for HTTP refusals, or the underlying network error). The flag is client-only and is not sent on the wire. Use it when a global handler would be wrong for one endpoint — for example handling an expected 403 locally before deciding whether to refresh credentials.

```typescript
const http = createHttpClient({ apiKey: 'inf_...', onError: globalHandler });

try {
  await http.request('get', '/tasks/task_abc', { handleErrors: false });
} catch (error) {
  if (error instanceof InferenceError && error.statusCode === 403) {
    // Inspect the refusal without invoking onError
  }
  throw error;
}
```

```typescript
import { inference, InferenceError, type FailedRequest } from '@inferencesh/sdk';

const client = inference({
  getToken: () => session.accessToken,
  onError: async (error, retry, request: FailedRequest) => {
    if (error instanceof InferenceError && error.statusCode === 401) {
      if (request.token !== session.accessToken) {
        // Credentials were already replaced; retry with the new token.
        return retry();
      }
      await refreshSession();
      return retry();
    }
    throw error;
  },
});
```

### Raw `fetch` on `HttpClient`

**@inferencesh/sdk ≥ v0.21.0** adds `HttpClient.fetch(endpoint, init?)` on `createHttpClient()` and on the internal client behind `inference()`. It applies the same base URL, proxy routing, auth headers, and `credentials` mode as `request()`, but returns the raw `Response` so you can read streams, text, or non-JSON bodies. HTTP refusals throw the same errors as `request()` (`InferenceError`, `RequirementsNotMetException`) after `onError` runs. Aborts via `init.signal` reject without calling `onError`.

Use it for custom NDJSON or SSE integrations:

```typescript
import { createHttpClient, StreamableManager } from '@inferencesh/sdk';

const http = createHttpClient({ getToken: () => host.getAccessToken() });

const manager = new StreamableManager({
  request: (init) => http.fetch('/tasks/task_abc123/stream', init),
  onData: (update) => console.log(update.status),
});
await manager.start();
```

`HttpClient.getStreamableConfig()` is deprecated: config built synchronously bypasses `onError` and freezes the token from when it was read. It throws when `getToken` is async — use `fetch()` or a `StreamRequest` as above instead.

**v0.21.0 breaking changes for custom HTTP wiring:** a hand-built `AgentClient` must expose `http.fetch` (not only `request()`). `StreamableManagerOptions` is a type alias, not an interface — extend it with an intersection (`type X = StreamableManagerOptions<T> & { … }`) instead of `interface X extends StreamableManagerOptions<T>`.

→ [Streaming — SSE and NDJSON transports](/docs/api/sdk/streaming#sse-eventsource-streams-javascript)

```typescript
import {
  RequirementsNotMetException,
  InferenceError,
  RequirementTypeCredential,
  CredentialProviderGoogle,
} from '@inferencesh/sdk';

try {
  const result = await client.run({ app: 'my-app', input: {...} });
} catch (e) {
  if (e instanceof RequirementsNotMetException) {
    // HTTP 412 — requirements payload is not wrapped in v2
    console.log('Missing requirements:');
    for (const err of e.errors) {
      console.log(`  - ${err.type}: ${err.key}`);
      if (err.type === RequirementTypeCredential && err.action?.provider === CredentialProviderGoogle) {
        // User must connect Google — see /docs/extend/credentials
      }
    }
  } else if (e instanceof InferenceError) {
    console.log(`HTTP ${e.statusCode}: ${e.message}`); // e.responseBody has full problem+json
  }
}
```

**Entitlement errors** (HTTP 402/403 with plan limits or feature gates) attach structured metadata on a top-level `meta` extension member of the problem details. Parse `InferenceError.responseBody` and type the result with `EntitlementErrorMeta`:

```typescript
import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';

function parseEntitlementMeta(error: InferenceError): EntitlementErrorMeta | null {
  if (!error.responseBody) return null;
  try {
    const body = JSON.parse(error.responseBody);
    return body.error?.meta ?? body.meta ?? null;
  } catch {
    return null;
  }
}

try {
  await client.run({ app: 'my-app', input: {} });
} catch (e) {
  if (e instanceof InferenceError && (e.statusCode === 402 || e.statusCode === 403)) {
    const meta = parseEntitlementMeta(e);
    if (meta?.upgrade_available) {
      console.log(`Blocked on ${meta.resource_label ?? meta.resource}`);
      if (meta.addon_plan_id) {
        console.log(`Add-on available: ${meta.addon_plan_name} ($${(meta.addon_plan_price ?? 0) / 100}/mo)`);
      }
    }
  }
}
```

→ [Entitlements API — Error metadata](/docs/api/rest/entitlements#error-metadata) — field reference and response examples

→ [REST overview — response format](/docs/api/rest/overview#response-format) — problem+json shape

---

## File Upload

```typescript
// Upload separately
const file = await client.uploadFile(buffer, {
  filename: 'image.png',
  contentType: 'image/png'
});
console.log(`Uploaded: ${file.uri}`);

// Use in app input
const result = await client.run({
  app: 'image-processor',
  input: { image: file.uri }
});
```

---

## REST API (curl)

For simple integrations, you can use the REST API directly with streaming:

```bash
# Single call with streaming
curl -N -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "my-org/assistant@abc123",
    "input": {"text": "Hello!", "role": "user"},
    "stream": true
  }' \
  https://api.inference.sh/agents/run
```

The `-N` flag disables curl's buffering so you see events in real-time.

**Without streaming** (returns JSON with `chat_id` for subsequent messages):

```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "my-org/assistant@abc123",
    "input": {"text": "Hello!", "role": "user"}
  }' \
  https://api.inference.sh/agents/run
```

---

## What's Next?

- [**Python SDK**](/docs/api/sdk-python) — Python SDK reference
- [**Tool Builder**](/docs/api/agent/tools) — Full tool builder API
- [**REST API**](/docs/api/sdk/running-apps) — Direct HTTP endpoints

