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
1npm install @inferencesh/sdkThe package is ES modules. Import it:
1import { inference } from '@inferencesh/sdk';2import 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). 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 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 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, the API answers 403 requires_sign_in (ErrorCodeRequiresSignIn).
→ Engines REST API — drain, safe binary updates, and cloud rental extensions
→ Flow Runs REST API — run saved flows programmatically
Apps API
Use client.apps to manage apps you own (not for running tasks — use client.tasks.run() for that):
1import {2 AppStatusActive,3 AppStatusDeprecated,4 AppStatusMaintenance,5 AppStatusRetired,6} from '@inferencesh/sdk';78const app = await client.apps.getByName('myteam/my-app');9const versions = await client.apps.listVersions(app.id);1011// Promote a staged deploy to current (same as the version picker in the web app)12await client.apps.setCurrentVersion(app.id, versions.items[0].id);1314// Mark deprecated while keeping runs working (see Apps REST API — lifecycle status)15await client.apps.updateStatus(app.id, AppStatusDeprecated, 'Use myteam/flux-v2 instead');1617// Temporarily block new runs18await client.apps.updateStatus(app.id, AppStatusMaintenance, 'Upgrading GPU drivers');1920// Permanently retire (excluded from store/search; historical tasks remain)21await 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; types ship in @inferencesh/sdk ≥ v0.21.0. Browseable tag slugs and display titles come from GET /store/tags (Store API), 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.
Store browse metadata — @inferencesh/sdk ≥ v0.21.0 exports StoreCategoryDTO and StoreTagDTO for Store API GET /store/categories and 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):
1import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';23const { data: categories } = await client.http.request<StoreCategoryDTO[]>(4 'get',5 '/store/categories',6);7const { data: tags } = await client.http.request<StoreTagDTO[]>('get', '/store/tags');→ Apps REST API — lifecycle status · Deploy staged versions · Instance types REST API
Flows API
Use client.flows for flow definitions and client.flowRuns to execute saved flows:
1const flow = await client.flows.create('image-pipeline');2await client.flows.updateVisibility(flow.id, 'public');34const 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 · Flow Runs REST API
Search API
Use client.search for programmatic discovery. It calls the same endpoints as belt suggest and the web app's search bar:
1const { results } = await client.search.suggest({2 query: 'flux image',3 context: 'user asked about image models', // optional POST /suggest context; query is embedded first, then context4 scope: ['git:github.com/acme/design', 'lang:typescript'], // optional; environment signals for ranking5 limit: 5,6});7// Result descriptions are clipped to DescriptionLimitListing (200 characters).89for (const hit of results) {10 console.log(`[${hit.type}] ${hit.name} — ${hit.command}`);11}1213// Full-text search in a single collection14const 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 · Search concept
Workspace APIs
Manage workspace context, secrets, API keys, and project folders programmatically:
1const { data: { user, team } } = await client.teams.me();2const { data: teams } = await client.teams.list();34await client.secrets.create({5 key: 'OPENAI_API_KEY',6 value: 'sk-...',7 description: 'OpenAI',8});910const { data: { items: keys } } = await client.apiKeys.list({ limit: 50 });11const { data: newKey } = await client.apiKeys.create({12 name: 'deploy-bot',13 scopes: ['tasks:read', 'tasks:write'],14 scope: 'workspace', // optional: 'user' (default) or 'workspace'; a workspace key needs manage_keys15 expires_at: '2026-12-31T00:00:00Z', // optional16});1718const project = await client.projects.create({ name: 'Production' });| Namespace | Key methods | REST reference |
|---|---|---|
client.teams | me, list, get, create, getMembers, addMember, invite helpers | Workspaces |
client.secrets | list, create, update, reveal, delete | Secrets |
client.apiKeys | list, create, delete | 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"):
1const entry = await client.knowledge.create({2 name: 'deployment-order',3 description: 'Services must deploy in dependency order',4 type: 'observation',5 version: {6 content: {7 path: 'content.md',8 content: '# Deployment Order\n\nDeploy common-go before api.',9 },10 scope: ['git:github.com/acme/api', 'lang:go'], // optional; see Knowledge API — Scoping11 },12});1314const skill = await client.skills.getByName('myteam', 'code-review');15const content = await client.skills.getContent('myteam', 'code-review');16const { items } = await client.skills.listStore({ limit: 20 });→ Knowledge REST API · Skills REST API · Skill 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.
1const caps = await client.credentials.getCapabilities();2const { auth_url } = await client.credentials.connect({3 provider: 'google',4 type: 'oauth',5 scopes: ['https://www.googleapis.com/auth/spreadsheets'],6});78await client.credentials.checkRequirements({9 secrets: [{ key: 'OPENAI_API_KEY' }],10 credentials: [{ key: 'google.sheets' }],11});1213const linear = await client.mcpServers.get('linear');14const tools = await client.mcpServers.listTools('linear');15const result = await client.mcpServers.callTool('linear', 'list_issues', { limit: 10 });→ Credentials REST API · MCP servers REST API · Connectors overview
Chats and task observability
Inspect agent runs and task execution outside the high-level client.agent() helper:
1const { data: chat } = await client.chats.get('chat_abc123');2const { data: { status } } = await client.chats.getStatus('chat_abc123');3const { data: trace } = await client.chats.getTrace('chat_abc123');4await client.chats.stop('chat_abc123');56const { data: logs } = await client.tasks.getLogs('task_abc123');7const { data: timings } = await client.tasks.getTimings('task_abc123');8const { data: samples } = await client.tasks.getTelemetry('task_abc123');→ Agents REST API · Tasks REST API · 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.
1import type { TaskFileDTO, DeleteTaskFilesResponse } from '@inferencesh/sdk';2import { TaskFileRoleInput, TaskFileRoleOutput } from '@inferencesh/sdk';34const { data: files } = await client.tasks.files('task_abc123');5const { data: inputs } = await client.tasks.files('task_abc123', TaskFileRoleInput);67const { data: result } = await client.tasks.deleteFiles('task_abc123', TaskFileRoleOutput);8// result.deleted, result.skipped910await 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 · Delete task files · Delete task
Private engines
For cloud engine rentals and private hardware:
1const { data: { items } } = await client.engines.list({ limit: 20 });2const { data: engine } = await client.engines.get('eng_abc123');3console.log(engine.engine_version); // installed inferencesh-engine binary, e.g. "v1.10.15"45await client.engines.drain('eng_abc123');6await client.engines.updateBinary('eng_abc123'); // POST /engines/{id}/update7await client.engines.extend('eng_abc123');89// Cloud engines include nested instance pricing (spot vs on-demand)10if (engine.instance?.rental_type === InstanceRentalTypeSpot) {11 console.log('Spot price (cents/hr):', engine.instance.hourly_price);12}@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.
→ Engines REST API · Managing engines · Instance types
Running Apps
Basic Usage
1import { inference } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Run and wait for completion (default)6const result = await client.run({7 app: 'infsh/echo',8 input: { message: 'Hello from JavaScript!' }9});1011console.log(result.output);With Progress Updates
1const result = await client.run(2 { app: 'infsh/flux-schnell', input: {...} },3 {4 onUpdate: (update) => {5 console.log(`Status: ${update.status}`);6 if (update.logs?.length) {7 console.log(update.logs[update.logs.length - 1]);8 }9 }10 }11);Fire and Forget
1const task = await client.run(2 { app: 'infsh/slow-task', input: {...} },3 { wait: false }4);5console.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:
1import { inference } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Use an agent you created in the web app6const agent = client.agent('my-org/my-agent@abc123'); // Format: namespace/name@version78const response = await agent.sendMessage('Hello!');9console.log(response);Template agents that declare context fields take values on the first message. The agent's tools use them as {{context.NAME}}:
1const agent = client.agent('my-team/pricing-agent@latest', {2 context: { version_id: 'v1', app_id: 'app_123' },3});With AgentChatProvider, pass context on agentConfig — see Template agents — 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.
1import { AgentChatProvider, useAgentActions } from '@inferencesh/sdk/agent';2import type { ChatSettingsRequest } from '@inferencesh/sdk';34function ChatControls() {5 const { updateChatSettings, switchAgent } = useAgentActions();67 const rename = () =>8 updateChatSettings({ name: 'Incident triage', visibility: 'team' });910 const clearStaleMemory = () =>11 updateChatSettings({ forget_memory: ['stale_goal'] });1213 const handOff = () => switchAgent('my-team/escalation-agent@latest');1415 // ...16}| 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 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 · 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).
1import { useAgentActions } from '@inferencesh/sdk/agent';23const {4 approveTool,5 rejectTool,6 getAlwaysAllowOptions,7 alwaysAllowTool,8 explainTool,9} = useAgentActions();1011async function onApprovalPrompt(toolInvocationId: string) {12 const options = await getAlwaysAllowOptions(toolInvocationId);13 if (!options?.options.length) {14 console.warn(options?.unavailable ?? 'No always-allow choices');15 return;16 }1718 // options.default is the narrowest key that would auto-approve this call again19 const key = options.default ?? options.options[0].key;20 try {21 const result = await alwaysAllowTool(toolInvocationId, { option: key });22 console.log('Saved chat rules:', result?.rules);23 } catch (err) {24 // 409: option stale — re-fetch getAlwaysAllowOptions and try again25 // 400: call no longer awaiting approval26 throw err;27 }28}2930async function onExplainClick(toolInvocationId: string) {31 const { risk_level, explanation, reasoning, risk } = await explainTool(toolInvocationId);32 console.log(risk_level, explanation, reasoning, risk);33}| 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 (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) |
1import type { AgentPermissions, PolicyRuleDTO } from '@inferencesh/sdk';2import { PolicyKindRemoteExec, PolicyKindTool } from '@inferencesh/sdk';34function describeRule(rule: PolicyRuleDTO): string {5 if (rule.kind === PolicyKindRemoteExec) {6 return `remote command: ${rule.specifier}`;7 }8 if (rule.kind === PolicyKindTool) {9 return `tool: ${rule.specifier}`;10 }11 return `${rule.kind} ${rule.specifier}`;12}1314// Agent template default for new chats (YAML `version.permissions` / AgentDTO)15const 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).
@inferencesh/sdk ≥ v0.18.0 removed ChatData.always_allowed_tools from generated types — the API stores always-allow as chat 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 · Explain tool call · Chat permissions rules · Approval settings
A2A agent card
Fetch the Agent2Agent (A2A) protocol card for marketplace listings (for example GCP AI Agents Producer Portal). Requires agents:read.
1const card = await client.agents.getA2ACard('agent_abc123');2// or by namespace/name ref via GET /agents/{namespace}/{name}/cardReturns raw A2A JSON (not wrapped in { data: … }). See Agents REST API — Get A2A agent card.
List internal tool definitions
Fetch the built-in internal tool categories for agent configuration UIs. Requires agents:read.
1const { data: categories } = await client.agents.getInternalTools();23for (const category of categories) {4 // default_enabled is false for opt-in categories5 console.log(category.id, category.default_enabled);6}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 and Agents REST API: List internal tool definitions.
Creating Ad-Hoc Agents
Create agents on-the-fly without saving to workspace:
1import { inference, tool, string, internalTools } from '@inferencesh/sdk';23const client = inference({ apiKey: 'inf_your_key' });45// Define tools6const calculator = tool('calculator')7 .describe('Performs math operations')8 .param('expression', string('Math expression'))9 .build();1011// Create ad-hoc agent12const agent = client.agent({13 core_app: { ref: 'infsh/claude-sonnet-4@latest' },14 name: 'Math Assistant',15 system_prompt: 'You help with math. Use the calculator tool.',16 tools: [calculator],17 internal_tools: internalTools().memory().build(),18});1920const 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.
1await agent.sendMessage('Explain quantum computing', {2 onDelta: (d) => {3 // d.delta is this batch, d.output everything so far for d.messageId4 process.stdout.write(d.delta.response ?? '');5 },6 onMessage: (msg) => {7 if (msg.content) {8 for (const c of msg.content) {9 if (c.type === 'text' && c.text) {10 process.stdout.write(c.text);11 }12 }13 }14 },15 onToolCall: async (call) => {16 console.log(`\n[Tool: ${call.name}]`);17 // Execute the tool and submit its result string18 const result = await executeMyTool(call.name, call.args);19 await agent.submitToolResult(call.id, result);2021 // A result can carry files uploaded first with client.files.upload:22 // const ref = await client.files.upload(pngBlob);23 // await agent.submitToolResult(call.id, 'Screenshot attached.', [ref]);24 25 // For widget interactions, you can also pass action + form_data:26 // await agent.submitToolResult(call.id, {27 // action: { type: 'confirm' },28 // form_data: { name: 'John', email: '[email protected]' }29 // });30 },31});File Attachments
1// From Blob2const imageBlob = new Blob([imageBuffer], { type: 'image/png' });3const response = await agent.sendMessage('What\'s in this image?', {4 files: [imageBlob]5});67// From data URI8const response = await agent.sendMessage('Analyze this', {9 files: ['data:image/png;base64,iVBORw0KGgo...']10});1112// Reuse an uploaded FileRef without re-uploading13const ref = await agent.uploadFile(imageBlob, { filename: 'photo.png' });14await 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.
Read the routing of a channel-originated chat from getChat():
1const chat = await agent.getChat();2console.log(chat?.channel_context?.channel_type); // "slack"See Agents REST API: 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:
1const output = await agent.run('Summarize this document in JSON');2console.log(output);For manual control (streaming, tool callbacks), use sendMessage() and read chat.output from getChat() instead.
Chat Management
1// Get current chat2const chat = await agent.getChat();3console.log(`Chat ID: ${chat?.id}`);4console.log(`Messages: ${chat?.messages.length}`);56// Stop generation7await agent.stopChat();89// Start fresh10agent.reset();1112// Cleanup streams13agent.disconnect();Interrupt gates
When an agent run pauses for tool approval, auth, widgets, or a lifecycle hook gate, list and resolve interrupts:
1import { InterruptResourceHookEvent, InterruptResourceToolInvocation } from '@inferencesh/sdk';23// List pending interrupts for a run (GET /agent-runs/{runId}/interrupts)4const { data: pending } = await client.agents.listRunInterrupts('ar_abc123');56for (const interrupt of pending ?? []) {7 if (interrupt.resource_type === InterruptResourceHookEvent) {8 console.log('Hook gate:', interrupt.meta);9 }10}1112// Resolve with allow or deny (POST /interrupts/{id}/resolve)13const { data: resolved } = await client.agents.resolveInterrupt('int_abc123', 'allow');1415// From the Agent SDK helper (same endpoints)16await 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.
| 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 · Python SDK — 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) 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.
1import type { ToolInvocationDTO } from '@inferencesh/sdk';2import {3 parseMCPInputState,4 elicitParams,5 isURLElicitation,6 useAgentActions,7} from '@inferencesh/sdk/agent';89function MCPInputForm({ invocation }: { invocation: ToolInvocationDTO }) {10 const { submitMCPInput } = useAgentActions();11 const state = parseMCPInputState(invocation.data);12 if (!state) return null;1314 async function submitLogin() {15 await submitMCPInput(invocation.id, {16 login: { action: 'accept' },17 });18 }1920 return (21 <>22 {Object.entries(state.input_requests).map(([key, request]) => {23 const params = elicitParams(request);24 if (!params) return null;25 if (isURLElicitation(params)) {26 return (27 <a key={key} href={params.url} target="_blank" rel="noreferrer">28 {params.message}29 </a>30 );31 }32 // Render a form from params.requestedSchema, then:33 // await submitMCPInput(invocation.id, { [key]: { action: 'accept', content: { field: value } } });34 return <p key={key}>{params.message}</p>;35 })}36 <button type="button" onClick={submitLogin}>37 Continue38 </button>39 </>40 );41}Standalone client.agent(...) callers use the same API:
1const agent = client.agent('my-org/support@latest');23// After you locate the awaiting_input MCP invocation on a message:4await agent.submitMCPInput('tinv_abc123', {5 step_a: { action: 'accept', content: { email: '[email protected]' } },6 step_b: { action: 'decline' },7});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} 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
→ Using tools — Interactive elicitation · Connector tools
Tool Builder
Define tools with the fluent API:
1import {2 tool, appTool, agentTool, callTool, httpTool, mcpTool, webhookTool, internalTools,3 string, number, integer, boolean, enumOf, array, object, optional,4 CredentialProviderGoogle,5} 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:
1import {2 CredentialProviderMCP,3 CredentialStatusConnected,4 EngineStatusDisconnected,5 CloudAWS,6 InstanceStatusRunning,7 InstanceRentalTypeOnDemand,8 InstanceRentalTypeSpot,9 ToolParamTypeString,10 ToolParamTypeObject,11 ToolAuthTypeNone,12 ToolAuthTypeCredential,13 ToolAuthTypeBearer,14 ToolAuthTypeAPIKey,15} 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:
1import {2 ResourceConcurrency,3 ResourceFeatureSeedance,4 EntitlementTypeLimit,5 EntitlementSourceTier,6 type EntitlementDTO,7 type EntitlementErrorMeta,8} from '@inferencesh/sdk';910const ent: EntitlementDTO = {11 resource: ResourceConcurrency,12 type: EntitlementTypeLimit,13 limit: 3,14 source: EntitlementSourceTier,15};Entitlement error metadata — EntitlementErrorMeta mirrors the meta object on limit_exceeded and feature_not_available responses. Parse it from InferenceError.responseBody:
1import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';23try {4 await client.tasks.run({ app: 'my-app', input: {} });5} catch (e) {6 if (e instanceof InferenceError) {7 const body = JSON.parse(e.responseBody);8 const meta: EntitlementErrorMeta = body.meta ?? body.error?.meta ?? {};9 if (meta.upgrade_available) {10 console.log(meta.resource_label, meta.addon_plan_name);11 }12 }13}See Entitlements API — 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 for the full list.
1// Client tool (runs in your code)2const search = tool('search_files')3 .describe('Search for files')4 .param('pattern', string('Glob pattern'))5 .param('recursive', optional(boolean('Search subdirs')))6 .build();78// App tool (calls another inference app)9const generate = appTool('generate', 'infsh/flux-schnell@latest')10 .describe('Generate an image')11 .param('prompt', string('Image description'))12 .build();1314// Agent tool (delegates to sub-agent)15const researcher = agentTool('research', 'my-org/researcher@v1')16 .describe('Research a topic')17 .param('topic', string('Topic'))18 .build();1920// Authenticated HTTP (callTool and httpTool are aliases)21const getData = httpTool('get_data', 'https://api.example.com/data')22 .describe('Fetch data from the API')23 .method('GET')24 .auth({ bearer: 'my_api_key' })25 .build();2627// OAuth: runtime injects the caller's own connected account's access token28const gmailSend = httpTool('gmail_send', 'https://gmail.googleapis.com/gmail/v1/users/me/messages/send')29 .describe('Send email via Gmail')30 .method('POST')31 .auth({ credential: CredentialProviderGoogle })32 .build();3334// Webhook tool (calls external API)35const notify = webhookTool('slack', 'https://hooks.slack.com/...')36 .describe('Send Slack message')37 .secret('SLACK_SECRET')38 .param('message', string('Message'))39 .build();4041// Internal tools config42const internals = internalTools().memory().plan().build();4344// Meta tools are opt-in (search_tools, list_tools, execute_tool)45const withMeta = internalTools().meta().build();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.
1import type {2 TaskDTO,3 ChatDTO,4 ChatMessageDTO,5 ChannelContext,6 AgentTool,7 AppStatus,8 TaskStatusCompleted,9 TaskStatusFailed,10} from '@inferencesh/sdk';11import {12 AppStatusActive,13 AppStatusDeprecated,14 AppStatusMaintenance,15 AppStatusRetired,16} from '@inferencesh/sdk';1718// Check task status19if (result.status === TaskStatusCompleted) {20 console.log('Done!');21} else if (result.status === TaskStatusFailed) {22 console.log('Failed:', result.error);23}2425// App lifecycle status (on AppDTO.status)26if (app.status === AppStatusDeprecated) {27 console.log(app.status_message);28}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 for parity.
Current user (GET /me) — import MeResponse for the envelope returned by GET /me. team is a full TeamDTO (not TeamRelationDTO from GET /teams). Settings gates use team_view.can and team_view.org.can:
1import type { MeResponse } from '@inferencesh/sdk';23const { data: me } = await client.teams.me(); // Response<MeResponse>4const caps = me.team_view?.can ?? [];Run acknowledgement (POST /apps/run) — import TaskResultDTO for the JSON body from POST /apps/run (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 theCredential*types andclient.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.
1import type { CredentialConfigDTO, CredentialDTO } from '@inferencesh/sdk';2import { CredentialScopeUser } from '@inferencesh/sdk';34function connectLabel(config: CredentialConfigDTO): string {5 if (config.connection_scope === CredentialScopeUser) {6 return `connect your ${config.name} account`;7 }8 return `connect ${config.name}`;9}1011function listSubtitle(credential: CredentialDTO): string {12 const accountId = credential.account_identifier ?? '';13 if (credential.scope === CredentialScopeUser) {14 return `${accountId} (your account)`;15 }16 return accountId;17}1819const perUserOAuth: CredentialConfigDTO = {20 slug: 'x',21 provider: 'x',22 type: 'oauth',23 name: 'X',24 short_name: 'X',25 description: '',26 allows_byok: false,27 available: true,28 has_managed: true,29 connection_scope: CredentialScopeUser,30};| 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.
Flow utility nodes — use with the Flows REST API when calling POST /flows/{id}/actions:
1import type { UtilityConfig } from '@inferencesh/sdk';23const gate: UtilityConfig = {4 preset: 'gate',5 gate: { field: 'status', operator: 'equals', value: 'approved' },6};78const selector: UtilityConfig = {9 preset: 'selector',10 selector: { field: 'items', mode: 'first' },11};1213// 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:
1import {2 GraphNodeStatusBlocked,3 GraphNodeStatusFailed,4 GraphNodeStatusRunning,5} from '@inferencesh/sdk';67const { data: run } = await client.flowRuns.get('flowrun_abc123');89// A gate whose condition is false is reported as failed; its dependents are blocked10if (run.node_statuses?.gate_1 === GraphNodeStatusFailed) {11 console.log('Gate condition was false');12}13if (run.node_statuses?.upscale === GraphNodeStatusBlocked) {14 console.log('Upscale did not run');15}16if (run.node_statuses?.generate === GraphNodeStatusRunning) {17 console.log('Still running:', run.node_tasks.generate?.task_id);18}1920console.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:
1import type {2 DecisionInput,3 DecisionOutput,4 DecisionVisionInput,5 DecisionChoiceQuestion,6 DecisionScoreQuestion,7 DecisionNoulQuestion,8} from '@inferencesh/sdk';910const input: DecisionInput = {11 state: { summary: 'User cannot log in after password reset' },12 choices: [13 {14 id: 'root_cause',15 instructions: 'Most likely cause',16 options: [17 { name: 'cache', description: 'Stale session cache' },18 { name: 'identity', description: 'IdP sync delay' },19 ],20 },21 ],22};2324// After client.tasks.create / poll — cast or validate result.output25const output = result.output as DecisionOutput;26console.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.
Interrupt gates — use with Agents REST API — Interrupt gates:
1import type { InterruptDTO } from '@inferencesh/sdk';2import {3 InterruptReasonToolApproval,4 InterruptStatusPending,5 InterruptResourceToolInvocation,6} from '@inferencesh/sdk';78const pending: InterruptDTO = {9 id: 'int_abc123',10 run_id: 'ar_abc123',11 chat_id: 'chat_abc123',12 reason: InterruptReasonToolApproval,13 status: InterruptStatusPending,14 resource_id: 'ti_abc123',15 resource_type: InterruptResourceToolInvocation,16};| 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 and chats opened via 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.
1import type { AgentDTO, ChatDTO } from '@inferencesh/sdk';23const { data: agent } = await client.agents.get('agent_abc123');4if (agent.harness !== 'inference') {5 console.log(agent.harness, agent.profile_id, agent.remote_id);6}78const { data: chat } = await client.chats.get('chat_abc123');9console.log(chat.work_dir, chat.harness_session_id);1011type LaunchHarnessSessionRequest = {12 profile_id: string;13 work_dir?: string;14 resume_session_id?: string;15};1617const launchBody: LaunchHarnessSessionRequest = {18 profile_id: 'prof_xyz',19 work_dir: '/home/you/project',20};2122const launched = (await fetch(`${baseUrl}/remotes/${remoteId}/sessions`, {23 method: 'POST',24 headers: {25 Authorization: `Bearer ${apiKey}`,26 'Content-Type': 'application/json',27 },28 body: JSON.stringify(launchBody),29}).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 |
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).
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:
1import type { AgentEvent } from '@inferencesh/sdk';2import {3 AgentEventRunStarted,4 AgentEventRunStateChanged,5 AgentEventContentDelta,6 AgentEventToolCompleted,7 AgentEventApprovalRequired,8} from '@inferencesh/sdk';910function isRunStarted(event: AgentEvent): boolean {11 return event.type === AgentEventRunStarted;12}| 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.
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 (inject, non-allow decision, or error; not every run). See Chat message roles.
App requirement and setup-action types
Use with Check requirements, client.credentials.checkRequirements(), and the credentials: / secrets: blocks of inf.yml:
1import type {2 CheckRequirementsRequest,3 CredentialRequirement,4 SecretRequirement,5 SetupAction,6} from '@inferencesh/sdk';7import {8 SetupActionAddSecret,9 SetupActionConnect,10 SetupActionAddScopes,11 RequirementTypeCredential,12} from '@inferencesh/sdk';1314const request: CheckRequirementsRequest = {15 secrets: [{ key: 'OPENAI_API_KEY', optional: false }],16 credentials: [17 { slug: 'google', capabilities: ['google.sheets'] },18 {19 slug: 'acme',20 secrets: ['ACME_API_KEY'],21 provider: { name: 'Acme CRM', website: 'acme.com' },22 },23 ],24};2526const { data } = await client.credentials.checkRequirements(request);27if (!data?.satisfied) {28 for (const err of data?.errors ?? []) {29 if (err.type === RequirementTypeCredential && err.action?.type === SetupActionConnect) {30 console.log('Connect', err.action.provider_name);31 }32 }33}| 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 |
Cost estimation — use with Estimate app cost (POST /store/apps/{appId}/estimate):
1import type { EstimateCostRequest, EstimateCostResponse } from '@inferencesh/sdk';23const req: EstimateCostRequest = {4 input: { n: 4 },5 function: 'generate', // optional for multi-function apps6};78const resp: EstimateCostResponse = {9 confidence: 'range',10 min: 150_000,11 max: 400_000,12 depends_on: ['output_tokens'],13 pricing_description: '$0.0015–$0.004 per run',14};1516const failed: EstimateCostResponse = {17 confidence: 'unknown',18 depends_on: ['output_tokens'],19 estimate_error: 'CEL evaluation failed: undefined variable "output_tokens"',20};| 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:
1import type { PlanDTO } from '@inferencesh/sdk';2import { PlanTypeBase, PlanTypeAddon } from '@inferencesh/sdk';34const base: PlanDTO = {5 name: 'pro',6 plan_type: PlanTypeBase,7 credits_monthly: 5_000_000_000,8 stackable: false,9 required_plan_ids: [],10 limits: {},11};1213const addon: PlanDTO = {14 name: 'extra concurrency',15 plan_type: PlanTypeAddon,16 stackable: true,17 required_plan_ids: ['plan_pro', 'plan_team'],18 required_plan_names: ['Pro', 'Team'],19 limits: {},20};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).
API key scopes — use with API keys — Scope catalog (GET /scopes):
1import type { ScopesResponse, ScopePreset } from '@inferencesh/sdk';23const catalog: ScopesResponse = {4 scopes: [{ value: 'agents:execute', label: 'execute', group: 'agents' }],5 groups: [{ id: 'agents', label: 'agents' }],6 presets: [7 {8 id: 'run',9 label: 'run',10 description: 'execute agents, apps, and flows',11 scopes: ['agents:read', 'apps:read', 'agents:execute', 'apps:execute'],12 summary: ['run apps and agents on your behalf'],13 hidden: false,14 },15 ],16};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 (@inferencesh/sdk ≥ v0.15.0):
1import {2 ApiKeyScopeWorkspace,3 type ApiKeyScope,4} from '@inferencesh/sdk';56const scope: ApiKeyScope = ApiKeyScopeWorkspace; // or ApiKeyScopeUser78const { data: key } = await client.apiKeys.create({9 name: 'ci',10 scope,11 scopes: ['apps:execute'],12});13console.log(key.key); // the only time the full value is returnedCreateApiKeyRequest.scope accepts user (default) or workspace. ApiKeyDTO carries scope, created_by, and, on list responses, creator.
Instance catalog — use with Instance types REST API for GPU pricing and availability (@inferencesh/sdk ≥ v0.14.2):
1import type {2 InstanceTypeAvailability,3 InstanceTypeConfiguration,4 InstanceTypeDTO,5} from '@inferencesh/sdk';6import { InstanceRentalTypeOnDemand, InstanceRentalTypeSpot } from '@inferencesh/sdk';78// GET /instances/types returns InstanceTypeDTO[]9const row: InstanceTypeDTO = {10 cloud: 'runpod',11 cloud_logo_url: 'https://cloud.inference.sh/logos/runpod.com.png',12 configuration: {13 gpu_type: 'H100',14 gpu_manufacturer: 'nvidia',15 num_gpus: 1,16 nvlink: false,17 },18 availability: [19 { available: true, region: 'us-east-1', rental_type: InstanceRentalTypeOnDemand },20 {21 available: true,22 region: 'us-east-1',23 rental_type: InstanceRentalTypeSpot,24 hourly_price: 199, // cents per hour — spot rows only25 },26 ],27};2829const 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.
Engines — use with the Engines REST API:
1import type { EngineDTO } from '@inferencesh/sdk';2import {3 EngineStatusRunning,4 InstanceRentalTypeSpot,5} from '@inferencesh/sdk';67const engine: EngineDTO = {8 id: 'eng_abc123',9 name: 'gpu-box-1',10 status: EngineStatusRunning,11 engine_version: 'v1.10.15', // inferencesh-engine binary; empty until first telemetry sync12 api_url: 'https://api.inference.sh',13 workers: [],14 instance: {15 rental_type: InstanceRentalTypeSpot,16 hourly_price: 199,17 },18};Spot cloud engines stop permanently when the provider reclaims the instance; provision a new engine or use on-demand — see Engines API — 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):
1import type { StoreCategoryDTO, StoreTagDTO } from '@inferencesh/sdk';23const categoriesPayload = await fetch('https://api.inference.sh/store/categories?type=app').then((r) =>4 r.json(),5);6const categories: StoreCategoryDTO[] = categoriesPayload.data;7const tagsPayload = await fetch('https://api.inference.sh/store/tags').then((r) => r.json());8const 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 and List browseable tags.
Per-app store listings — use with the Store REST API:
1import type { AppStoreListingDTO } from '@inferencesh/sdk';23const listing: AppStoreListingDTO = {4 id: 'app_abc123',5 allows_cloud_workers: true,6 allows_private_workers: true,7 max_concurrency: 0,8 max_concurrency_per_team: 5,9 min_concurrency: 2,10 required_feature: 'feature:seedance', // optional — explicit entitlement grant required11};min_concurrency protects cloud workers holding the app from displacement by other apps; it does not start workers. See Reserved cloud capacity. When required_feature is set, dispatch requires a boolean entitlement with enabled: true — see Feature-gated store apps.
| Constant | API value |
|---|---|
AppStatusActive | active |
AppStatusMaintenance | maintenance |
AppStatusDeprecated | deprecated |
AppStatusRetired | retired |
See Apps REST API — 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:
1const { inference, tool, string } = require('@inferencesh/sdk');23async function main() {4 const client = inference({ apiKey: 'inf_...' });5 const result = await client.run({...});6}78main();Error Handling
Responses are unwrapped from the { "data", "messages" } envelope (.data and .messages on every result) and errors are RFC 9457 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.
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.
1const http = createHttpClient({ apiKey: 'inf_...', onError: globalHandler });23try {4 await http.request('get', '/tasks/task_abc', { handleErrors: false });5} catch (error) {6 if (error instanceof InferenceError && error.statusCode === 403) {7 // Inspect the refusal without invoking onError8 }9 throw error;10}1import { inference, InferenceError, type FailedRequest } from '@inferencesh/sdk';23const client = inference({4 getToken: () => session.accessToken,5 onError: async (error, retry, request: FailedRequest) => {6 if (error instanceof InferenceError && error.statusCode === 401) {7 if (request.token !== session.accessToken) {8 // Credentials were already replaced; retry with the new token.9 return retry();10 }11 await refreshSession();12 return retry();13 }14 throw error;15 },16});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:
1import { createHttpClient, StreamableManager } from '@inferencesh/sdk';23const http = createHttpClient({ getToken: () => host.getAccessToken() });45const manager = new StreamableManager({6 request: (init) => http.fetch('/tasks/task_abc123/stream', init),7 onData: (update) => console.log(update.status),8});9await 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
1import {2 RequirementsNotMetException,3 InferenceError,4 RequirementTypeCredential,5 CredentialProviderGoogle,6} from '@inferencesh/sdk';78try {9 const result = await client.run({ app: 'my-app', input: {...} });10} catch (e) {11 if (e instanceof RequirementsNotMetException) {12 // HTTP 412 — requirements payload is not wrapped in v213 console.log('Missing requirements:');14 for (const err of e.errors) {15 console.log(` - ${err.type}: ${err.key}`);16 if (err.type === RequirementTypeCredential && err.action?.provider === CredentialProviderGoogle) {17 // User must connect Google — see /docs/extend/credentials18 }19 }20 } else if (e instanceof InferenceError) {21 console.log(`HTTP ${e.statusCode}: ${e.message}`); // e.responseBody has full problem+json22 }23}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:
1import { InferenceError, type EntitlementErrorMeta } from '@inferencesh/sdk';23function parseEntitlementMeta(error: InferenceError): EntitlementErrorMeta | null {4 if (!error.responseBody) return null;5 try {6 const body = JSON.parse(error.responseBody);7 return body.error?.meta ?? body.meta ?? null;8 } catch {9 return null;10 }11}1213try {14 await client.run({ app: 'my-app', input: {} });15} catch (e) {16 if (e instanceof InferenceError && (e.statusCode === 402 || e.statusCode === 403)) {17 const meta = parseEntitlementMeta(e);18 if (meta?.upgrade_available) {19 console.log(`Blocked on ${meta.resource_label ?? meta.resource}`);20 if (meta.addon_plan_id) {21 console.log(`Add-on available: ${meta.addon_plan_name} ($${(meta.addon_plan_price ?? 0) / 100}/mo)`);22 }23 }24 }25}→ Entitlements API — Error metadata — field reference and response examples
→ REST overview — response format — problem+json shape
File Upload
1// Upload separately2const file = await client.uploadFile(buffer, {3 filename: 'image.png',4 contentType: 'image/png'5});6console.log(`Uploaded: ${file.uri}`);78// Use in app input9const result = await client.run({10 app: 'image-processor',11 input: { image: file.uri }12});REST API (curl)
For simple integrations, you can use the REST API directly with streaming:
1# Single call with streaming2curl -N -X POST \3 -H "Authorization: Bearer YOUR_API_KEY" \4 -H "Content-Type: application/json" \5 -d '{6 "agent": "my-org/assistant@abc123",7 "input": {"text": "Hello!", "role": "user"},8 "stream": true9 }' \10 https://api.inference.sh/agents/runThe -N flag disables curl's buffering so you see events in real-time.
Without streaming (returns JSON with chat_id for subsequent messages):
1curl -X POST \2 -H "Authorization: Bearer YOUR_API_KEY" \3 -H "Content-Type: application/json" \4 -d '{5 "agent": "my-org/assistant@abc123",6 "input": {"text": "Hello!", "role": "user"}7 }' \8 https://api.inference.sh/agents/runWhat's Next?
- Python SDK — Python SDK reference
- Tool Builder — Full tool builder API
- REST API — Direct HTTP endpoints