# Python SDK

> Python SDK for inference shell agents and apps.

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

---

**Python SDK for inference shell agents and apps.**

The Python SDK provides both sync and async clients for running apps and chatting with agents.

---

## Installation

```bash
pip install inferencesh
```

For async support:

```bash
pip install inferencesh[async]
```

---

## Client API

The `inference()` and `async_inference()` factories return sync `Inference` and async `AsyncInference` clients. Responses are unwrapped from the `{ "data", "messages" }` envelope and errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details.

| Property | Purpose |
|----------|---------|
| `client.tasks` | Run, get, cancel, delete, wait, and stream tasks; `files` and `delete_files` for a task's input and output files |
| `client.files` | Upload files |
| `client.agents` | Create agents (`agents.create()` is the same as `agent()` without per-chat context) |
| `client.sessions` | List, inspect, and end sessions; use `client.session()` for a context manager |
| `client.live` | Run a stream function and dial its socket (async only; requires `inferencesh[async]`) |
| `client.sockets` | HTTP helpers for stream-task sockets; `open()` redials with a fresh credential (async only) |

`api_key` is optional (**inferencesh ≥ v0.22.0**). Omit it and the client reads the `INFERENCE_API_KEY` environment variable; an explicit `api_key` wins. With neither, the constructor raises `ValueError`. The same applies to `Inference()` and `AsyncInference()`.

```python
from inferencesh import inference, async_inference

client = inference()                       # reads INFERENCE_API_KEY
client = inference(api_key="inf_your_key")  # explicit key wins
aclient = async_inference()                 # same rule for the async client
```

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

Prefer `client.tasks.run()` for new code.

The JavaScript SDK exposes additional namespaces (`apps`, `chats`, `engines`, `search`, and more). For features not listed above, use the [REST API](/docs/api/rest/overview) or see the [SDK overview — client structure](/docs/api/overview#client-structure) parity table.

## Reference strings (`Ref`)

CLI commands, task inputs, and agent tools accept refs in `[type/]namespace/name[@version][:function]` form. The same grammar is used across the platform (Go `apitypes.Ref.Parse` is the source of truth). Parse and format refs in Python with `Ref` from `inferencesh` (**inferencesh ≥ v0.7.27**):

```python
from inferencesh import Ref

ref = Ref.parse("app/acme/voice-loop@v3:stream")
assert ref.type == "app"
assert ref.namespace == "acme"
assert ref.name == "voice-loop"
assert ref.version_id == "v3"
assert ref.function == "stream"
assert ref.is_typed() is True
assert str(ref) == "app/acme/voice-loop@v3:stream"

plain = Ref.parse("acme/widget@latest")
assert plain.version_id == ""  # @latest is treated as unversioned

ref, has_namespace = Ref.try_parse("acme/widget")
assert has_namespace is True
```

| Piece | Meaning |
|-------|---------|
| `type/` | Optional resource kind: `app`, `agent`, `skill`, or `knowledge` |
| `namespace/name` | Workspace slug and resource slug (`acme/widget`) |
| `@version` | Version pin (`abc123`, `v2`, …). `@latest` is ignored (unversioned) |
| `:function` | Function name on multi-function apps (only after `@version` when both are present) |

Unknown first path segments are **not** treated as a type prefix. For example, `not-a-type/acme/widget` is a single opaque name. Colons inside the name before `@` stay in the name (`acme/weird:name@ver` → name `weird:name`, version `ver`).

Helpers: `full_name()` → `namespace/name`, `qualified_name()` → `type/namespace/name` when typed, otherwise `namespace/name`.

---

## Live apps and stream sockets

Stream functions keep a duplex socket open for the life of the task. Use the async client (`pip install inferencesh[async]`):

```python
from inferencesh import async_inference

client = async_inference(api_key="inf_...")

task, session = await client.live(
    {"app": "xai/grok-voice", "function": "talk", "input": {"voice": "eve"}},
    on_clear=lambda field: player.flush(field),  # app sent {"$clear": field}
)

await session.send(mic_frame)  # bytes for binary live fields
async for frame in session:
    handle(frame)
await session.close()

# After a reload, redial with a fresh credential (task id or run response)
session = await client.sockets.open(task["id"], on_clear=lambda f: player.flush(f))
```

The sync `client.sockets` namespace supports `get`, `list`, `for_task`, `access`, and `delete` over HTTP. Calling `open()` on the sync client raises `NotImplementedError` — use `AsyncInference` instead.

When the app cuts a live output short it may send a `$clear` control frame. Pass `on_clear` to `client.live()` or `client.sockets.open()` (or `AsyncLiveSession` callbacks) to drop buffered audio or UI for that field; without `on_clear`, the frame arrives as an ordinary JSON patch.

→ [Streaming apps](/docs/extend/streaming) — app-side `Live`, PCM fields, and local testing with `belt app test --stream`

---

## Running Apps

### Basic Usage

```python
from inferencesh import inference

client = inference(api_key="inf_your_key")

# Run and wait for completion (default)
result = client.tasks.run({
    "app": "infsh/echo",
    "input": {"message": "Hello from Python!"}
})

print(result["output"])
```

### Streaming Progress

```python
for update in client.tasks.run({"app": "infsh/flux-schnell", "input": {...}}, stream=True):
    print(f"Status: {update['status']}")
    if update.get("logs"):
        print(update["logs"][-1])
```

Partial `{data, fields}` envelopes are unwrapped automatically. If the stream ends without a terminal event, the SDK reconciles via `GET /tasks/:id` and populates `stream.result`. See [Streaming — partial updates and reconciliation](/docs/api/sdk/streaming#partial-updates).

### Fire and Forget

```python
task = client.tasks.run({"app": "infsh/slow-task", "input": {...}}, wait=False)
print(f"Task ID: {task['id']}")
```

### Task Management

```python
from inferencesh import TaskStatus

# Get current task state
task = client.tasks.get(task_id)

# Cancel a running task
client.tasks.cancel(task_id)

# Wait for a task started with wait=False (optional timeout in seconds)
result = client.tasks.wait_for_completion(task_id, timeout=300)

# Stream updates for an existing task
with client.tasks.stream(task_id) as stream:
    for update in stream:
        if update["status"] == TaskStatus.COMPLETED:
            print(update["output"])
            break
```

### Task files and deletion

**inferencesh ≥ v0.19.0** adds `delete()`, `files()`, and `delete_files()` on `client.tasks` (and the same names on the async client). They call the [Tasks REST API](/docs/api/rest/tasks#delete-task) endpoints for removing finished tasks, listing input/output storage files, and deleting files while keeping the task record.

```python
# Remove the task record (files stay in storage)
client.tasks.delete(task_id)

# Delete the task and its input/output files (task must be terminal)
client.tasks.delete(task_id, files=True)

# List platform files the task used or produced
files = client.tasks.files(task_id).data
outputs = client.tasks.files(task_id, role="output").data

# Delete files but keep the task (terminal state required)
result = client.tasks.delete_files(task_id).data
print(result["deleted"], result["skipped"])
client.tasks.delete_files(task_id, role="input")
```

`role` must be `"input"` or `"output"` when set. `delete_files()` returns a `Response` whose `data` matches `DeleteTaskFilesResponse` (`deleted` file IDs and `skipped` entries with `reason`). See [List task files](/docs/api/rest/tasks#list-task-files) and [Delete task files](/docs/api/rest/tasks#delete-task-files) for resolution rules and error codes.

### Inspecting tasks (logs, timings, telemetry)

The Python SDK does not yet expose `getLogs`, `getTimings`, or `getTelemetry` helpers. Call the REST endpoints directly:

```python
import httpx

API = "https://api.inference.sh"
headers = {
    "Authorization": "Bearer inf_your_key",
}

logs = httpx.get(f"{API}/tasks/{task_id}/logs", headers=headers).json()
timings = httpx.get(f"{API}/tasks/{task_id}/timings", headers=headers).json()
telemetry = httpx.get(f"{API}/tasks/{task_id}/telemetry", headers=headers).json()
```

From the terminal: `belt task logs <id>`, `belt task timings <id>`. For telemetry, use the REST endpoint above or `client.tasks.getTelemetry(id)` in the [JavaScript SDK](/docs/api/sdk-javascript).

→ [Tasks REST API](/docs/api/rest/tasks) · [Observability](/docs/runtime/observability) · [SDK overview — inspect and observability](/docs/api/overview#inspect-and-observability-javascript)

---

## Agent SDK

Chat with AI agents programmatically.

### Using an Existing Agent

Reference agents by namespace/name and version:

```python
from inferencesh import inference

client = inference(api_key="inf_your_key")

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

response = agent.send_message("Hello!")
print(response)
```

Template agents that declare **context** fields take values when you create the agent (the agent's tools use them as `{{context.NAME}}`). Pass the `context` keyword argument (not available on `client.agents.create()`):

```python
agent = client.agent(
    "my-team/pricing-agent@latest",
    context={"version_id": "v1", "app_id": "app_123"},
)
```

→ [Template agents — per-chat context](/docs/api/agent/template#per-chat-context)

### Messaging channel routing

The API ignores `channel_context` on agent runs. In **inferencesh ≥ v0.24.0**, `send_message()` still accepts a `channel_context` argument for compatibility, but it is deprecated: passing it emits a `DeprecationWarning` and the value is not sent. Older versions sent it, and the run went ahead as if it were absent. Don't pass it. A chat's reply routing (Slack thread, Telegram chat, …) comes only from messages that arrive through the platform's channel integrations. Read it from `agent.get_chat()` on a channel-originated chat; see [Get chat: `channel_context`](/docs/api/rest/agents#get-chat).

→ [Messaging channel webhooks](/docs/api/rest/agents#messaging-channel-webhooks)

### Creating Ad-Hoc Agents

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

```python
from inferencesh import inference, AdHocAgentOptions, tool, string

client = inference(api_key="inf_your_key")

# Define tools
calculator = (
    tool("calculator")
    .describe("Performs math operations")
    .param("expression", string("Math expression"))
    .build()
)

# Create ad-hoc agent
agent = client.agent(AdHocAgentOptions(
    core_app={"ref": "infsh/claude-sonnet-4@latest"},
    name="Math Assistant",
    system_prompt="You help with math. Use the calculator tool.",
    tools=[calculator],
))

response = agent.send_message("What is 42 * 17?")
```

### Streaming Responses

```python
def handle_message(msg):
    if msg.get("content"):
        print(msg["content"], end="", flush=True)

def handle_tool(call):
    print(f"\n[Tool: {call.name}]")
    # Execute tool and return result string
    result = execute_my_tool(call.name, call.args)
    agent.submit_tool_result(call.id, result)

    # Return files with the result (inferencesh >= v0.27.3):
    # agent.submit_tool_result(call.id, result, attachments=[ref])
    
    # For widget interactions, you can also pass action + form_data:
    # agent.submit_tool_result(call.id, {
    #     "action": {"type": "confirm"},
    #     "form_data": {"name": "John", "email": "john@example.com"}
    # })

response = agent.send_message(
    "Explain quantum computing",
    on_message=handle_message,
    on_tool_call=handle_tool,
)
```

### File Attachments

```python
# From file path
response = agent.send_message(
    "What's in this image?",
    files=[open("image.png", "rb").read()]
)

# From base64
response = agent.send_message(
    "Analyze this",
    files=["data:image/png;base64,iVBORw0KGgo..."]
)
```

`send_message()` uploads each `files` entry automatically. Use `agent.upload_file()` when you need the `FileRef` to reuse the same URI across messages:

```python
ref = agent.upload_file(open("image.png", "rb").read(), filename="image.png")
# ref["uri"] — use client.files or REST APIs if you need the URI outside send_message
response = agent.send_message("Summarize this file", files=[open("image.png", "rb").read()])
```

### Tool result attachments

**inferencesh ≥ v0.27.3**: `submit_tool_result(tool_invocation_id, result, attachments=None)` takes a list of `FileRef`s (`uri`, `filename`, `content_type`, optional `id` and `size`) and sends them on [`POST /tools/{id}`](/docs/api/rest/agents#submit-tool-result). Upload the bytes first with `agent.upload_file()`. Each ref needs a `uri`; the API takes at most 20. `AsyncAgent.submit_tool_result` has the same signature.

```python
shot = agent.upload_file(open("screenshot.png", "rb").read(), filename="screenshot.png")
agent.submit_tool_result(call.id, "Captured the visible tab", attachments=[shot])
```

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

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

```python
result = agent.run("Summarize this document in JSON")
print(result)
```

Equivalent to `send_message()` followed by reading `chat["output"]`. For streaming or tool callbacks, use `send_message()` instead.

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

### Chat Management

```python
# Get current chat (metadata — status, output, active_run)
chat = agent.get_chat()
print(f"Chat ID: {chat['id']}")
print(f"Status: {chat['status']}")

# Message history: GET /chats/{id}/messages — see Agents REST API

# Stop generation
agent.stop_chat()

# Start fresh
agent.reset()
```

---

## Tool Builder

Define tools with the fluent API:

```python
from inferencesh import (
    tool, app_tool, agent_tool, webhook_tool, internal_tools,
    string, number, integer, boolean, enum_of, array, obj, optional
)

# Client tool (runs in your code)
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)
generate = (
    app_tool("generate", "infsh/flux-schnell@latest")
    .describe("Generate an image")
    .param("prompt", string("Image description"))
    .build()
)

# Agent tool (delegates to sub-agent)
researcher = (
    agent_tool("research", "my-org/researcher@v1")
    .describe("Research a topic")
    .param("topic", string("Topic"))
    .build()
)

# Webhook tool (calls external API)
notify = (
    webhook_tool("slack", "https://hooks.slack.com/...")
    .describe("Send Slack message")
    .secret("SLACK_SECRET")
    .param("message", string("Message"))
    .build()
)

# Call tool — authenticated HTTP (runtime injects vault secrets or OAuth tokens)
from inferencesh import call_tool

calendar = (
    call_tool("list_events", "https://www.googleapis.com/calendar/v3/calendars/primary/events")
    .method("GET")
    .auth(credential="google")
    .describe("List calendar events")
    .build()
)

# MCP connector tool — the server by slug; it runs as the caller's own login
from inferencesh import mcp_tool

list_issues = (
    mcp_tool("list_issues", server="linear", tool_name="list_issues")
    .describe("List Linear issues")
    .build()
)

# Internal tools config (default-on categories: plan, memory, widget, finish, skills)
internals = internal_tools().memory().plan().build()

# Opt-in categories: chain builders (inferencesh >= v0.14.1)
internals = (
    internal_tools()
    .meta()
    .artifact()
    .agent()
    .remote()
    .knowledge()
    .build()
)

# Disable skill_get while enabling knowledge tools
internals = internal_tools().skills(False).knowledge().build()
```

`internal_tools().all()` enables plan, memory, widget, and finish only; it does not turn on opt-in categories (`meta`, `artifact`, `agent`, `remote`, `knowledge`). The serialized field is `agent`, not `spawn`; older agent versions may still store `"spawn": true`, which the API treats as `agent`.

→ [Internal tools](/docs/api/agent/internal-tools): defaults, tool names, and category reference

`call_tool().auth(credential=...)` writes `type: "credential"` with `provider` in the serialized tool; each call signs in with the caller's own login to that provider. **`inferencesh` v0.32.0** removed `credential_id=` from `.auth()` and `mcp_tool()` (see [Call tools](/docs/api/agent/call-tools)).

→ [Call tools](/docs/api/agent/call-tools) · [Connector tools](/docs/api/agent/connector-tools) · [Full Tool Builder Reference](/docs/api/agent/tools)

---

## App authoring (LLM inputs)

When you build Python model apps, import typed chat inputs from `inferencesh.models`. As of **v0.7.9**, use `ChatInput` with a nested `ModelSettings` object for sampling parameters instead of flat top-level fields.

```python
from inferencesh.models import ChatInput, ModelSettings, LLMOutput, BaseApp

class MyInput(ChatInput):
    pass

class App(BaseApp):
    async def run(self, input_data: MyInput, metadata) -> LLMOutput:
        # input_data.text, input_data.context, input_data.system_prompt
        # input_data.model_settings.temperature, .max_tokens, .stop, etc.
        ...
```

`ModelSettings` groups provider-agnostic generation knobs. Every field is optional; omitted fields are not sent to the provider and the app’s defaults apply.

| Field | Description |
|-------|-------------|
| `temperature` | Sampling temperature (0–2) |
| `top_p` | Nucleus sampling probability mass |
| `top_k` | Top-k sampling (-1 disables) |
| `min_p` | Min-p sampling cutoff |
| `frequency_penalty` | Penalize tokens by how often they appear |
| `presence_penalty` | Penalize tokens that already appeared |
| `repetition_penalty` | Multiplicative repetition penalty (vLLM/HuggingFace style) |
| `seed` | Random seed for reproducible output |
| `stop` | Stop sequences |
| `max_tokens` | Maximum tokens to generate |

Example task input:

```json
{
  "text": "Write a haiku about clouds",
  "model_settings": {
    "temperature": 0.8,
    "max_tokens": 256,
    "stop": ["\n\n"]
  }
}
```

**Backwards compatibility:** `LLMInput` (subclass of `BaseLLMInput`) keeps flat `temperature`, `top_p`, and `max_tokens` for existing apps. New apps should prefer `ChatInput`.

Add only the nested settings field to a custom input with `ModelSettingsCapabilityMixin`. Capability mixins (`ImageCapabilityMixin`, `ReasoningCapabilityMixin`, `ToolsCapabilityMixin`, and others) live in `inferencesh.models.llm`.

Helpers for OpenAI-style message lists and streaming: `build_messages`, `stream_generate`, `timing_context` (import from `inferencesh` or `inferencesh.models`).

`build_messages` is an alias of `build_openai_messages(input_data, ..., image_limits=None, tool_media_as_user_message=False)` (in `inferencesh.models.llm`):

- `image_limits`: an `ImageLimits` for the provider. Base64 images are downsized to fit.
- `tool_media_as_user_message`: by default a tool's images and files go in its role `"tool"` message. Set `True` for a backend that accepts only text there; they then go in a user message after the tool messages.
- `reasoning_state_key` (≥ v0.27.1): the key your app writes `reasoning_details` under in `reasoning_state`. An assistant message with an entry under that key sends it back as `reasoning_details`; entries under other keys are left out.
- `reasoning_field` (≥ v0.27.4): the field an assistant message sends its own `reasoning` text back on, for a provider that takes it as text (DeepSeek and the backends serving it use `reasoning_content`). Applies when the message has no entry under `reasoning_state_key`. Without it, `reasoning` from `context` is not sent.
- `include_reasoning=True` is deprecated; it is the same as `reasoning_field="reasoning"`.

**Reasoning state (inferencesh ≥ v0.27.0).** `LLMOutput` and `ContextMessage` have an optional `reasoning_state`: a map of opaque JSON a provider needs on the next request (Anthropic thinking signatures, Gemini `thought_signature`, OpenAI encrypted reasoning). Set it on the final `LLMOutput` under a key your app owns; `LLMDelta` has no such field. The agent stores it on the assistant message and sends it back on that message's `context` entry on later turns, including in forked chats. It is not on chat messages from the REST API or SSE. Place only your own entry in a provider request: another provider's signature is refused.

```python
from inferencesh.models import LLMOutput, build_messages
from inferencesh.models.llm import merge_reasoning_details

# End of a turn: fold streamed reasoning_details fragments into whole blocks
output = LLMOutput(
    response=text,
    reasoning_state={"openrouter": merge_reasoning_details(streamed_details)},
)

# Next turn: each prior assistant message sends its "openrouter" entry back
messages = build_messages(input_data, reasoning_state_key="openrouter")

# DeepSeek: each assistant message carries its own reasoning text
messages = build_messages(input_data, reasoning_field="reasoning_content")
```

`merge_reasoning_details` (≥ v0.27.1) joins consecutive fragments with the same `index` and `type`, concatenating `text`, `summary` and `data` and keeping the other fields.

**inferencesh ≥ v0.26.0** adds `inferencesh.images` for fitting an image to a provider's limits:

```python
from inferencesh.images import ImageLimits, fit_image

data, mime = fit_image(
    "/tmp/screenshot.png",
    ImageLimits(max_long_edge=2048, max_pixels=1_150_000, max_bytes=5_000_000, mime_types=("image/png", "image/jpeg")),
)
```

`fit_image` returns `(bytes, mime)`, downsizing only; it never upscales. Each limit is optional (`None` means no limit of that kind). It needs Pillow: `pip install "inferencesh[images]"`. Without Pillow it logs a warning and returns the original bytes.

**inferencesh ≥ v0.13.0** merges streaming deltas with the same rules as the JavaScript SDK and belt: fields tagged in `_field_tags` on `LLMDelta` (and related delta types) use `concat`, `replace`, `indexed`, or `nested`; untagged strings concatenate and other untagged values replace. Custom pydantic delta types should set `_field_tags` explicitly when you stream partial objects outside `stream_generate()`.

→ [App code: LLM inputs](/docs/extend/app-code#llm-chat-inputs-python)

## App authoring (decision models)

Decision apps (`AppCategory.DECISION`) use a fixed request/response shape so callers can swap models without changing integration code. Pydantic helpers ship in **`inferencesh` ≥ v0.20.0** (`inferencesh.models`, backed by the generated API contract in `llm_types_gen.py`):

```python
from inferencesh import BaseApp
from inferencesh.models import DecisionInput, DecisionOutput, DecisionVisionInput, DecisionVideoInput
from inferencesh.models.decision import (
    ChoiceQuestion,
    ScoreQuestion,
    NoulQuestion,
    ChoiceAnswer,
    ScoreAnswer,
    NoulAnswer,
)

class App(BaseApp):
    async def run(self, input_data: DecisionInput, metadata) -> DecisionOutput:
        ...
```

| Type | Purpose |
|------|---------|
| `DecisionInput` | `state` plus optional `choices`, `scores`, `nouls` question lists |
| `DecisionVisionInput` | `DecisionInput` plus `images` (file URIs shared by every question) |
| `DecisionVideoInput` | `DecisionVisionInput` plus `videos` (file URIs every question sees after the images; **`inferencesh` ≥ v0.23.0**) |
| `DecisionOutput` | Maps of answers under `choices`, `scores`, `nouls` keyed by question `id`; plus `model`, `input_tokens` |
| `ChoiceQuestion` / `ScoreQuestion` / `NoulQuestion` | Question builders on `DecisionInput` (import from `inferencesh.models.decision`) |
| `ChoiceAnswer` | `choice`, `confidence`, `probabilities` |
| `ScoreAnswer` | `score`, `normalized`, `confidence`, `probabilities`, `legend` |
| `NoulAnswer` | `noul` (probability yes) |

App authors that wrap a shared decision model can build outputs with `DecisionOutput.from_answers(answers, input_data, model=..., input_tokens=...)` where `answers` matches the wire shape keyed by question `id`.

Field semantics, JSON examples, and authoring notes: [App code — Decision models](/docs/extend/app-code#decision-models-contract).

## App authoring (function metadata)

`function` declares a function's credentials and tool annotations. It stores them on the method and returns the method unchanged.

```python
from inferencesh import BaseApp, function

class App(BaseApp):
    @function(
        credentials=[{"key": "google.calendar.events"}],
        annotations={"readOnlyHint": False, "destructiveHint": False},
    )
    async def create_event(self, input_data: CreateEventInput) -> CreateEventOutput:
        ...
```

Both arguments are keyword-only and optional. `credentials` takes entries shaped like `inf.yml` `credentials:` entries (`CredentialRequirement`); `annotations` takes MCP tool annotations (`ToolAnnotations`). Both types are in `inferencesh.types`.

→ [Function metadata](/docs/extend/multi-function-apps#function-metadata) · [Per-function credentials](/docs/extend/credentials#per-function-credentials)

---

## Async Support

The async client mirrors the sync namespaced API (`tasks`, `files`, `agents`, `sessions`).

```python
from inferencesh import async_inference, TaskStatus
import asyncio

async def main():
    client = async_inference(api_key="inf_...")

    # Run and wait (default)
    result = await client.tasks.run({"app": "infsh/echo", "input": {...}})

    # Fire and forget, then wait or stream
    task = await client.tasks.run({"app": "infsh/slow-task", "input": {...}}, wait=False)
    result = await client.tasks.wait_for_completion(task["id"], timeout=300)

    async with client.tasks.stream(task["id"]) as stream:
        async for update in stream:
            if update["status"] == TaskStatus.COMPLETED:
                break

    await client.tasks.delete(task["id"], files=True)
    await client.tasks.files(task["id"])
    await client.tasks.delete_files(task["id"])

    # Upload via files namespace
    file_obj = await client.files.upload(b"PNGDATA")

    # Async agent
    agent = client.agent("my-org/assistant@abc123")
    response = await agent.send_message("Hello!")

    async for msg in agent.stream_messages():
        print(msg)

asyncio.run(main())
```

`client.run()` and `client.upload_file()` work as legacy aliases on the async client as well.

---

## Error Handling

With API v2, HTTP failures return [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) `application/problem+json`. The sync and async clients parse the same fields: `APIError.message` prefers `detail`, then `title`, then `message` (for non-standard problem payloads), and falls back to the raw body for non-JSON responses.

```python
from inferencesh import APIError, RequirementsNotMetError

try:
    result = client.tasks.run({"app": "my-app", "input": {...}})
except RequirementsNotMetError as e:
    # HTTP 412 — requirements payload is not wrapped in v2
    print(f"Missing requirements: {e.errors}")
    for err in e.errors:
        # err["type"] is "credential" (RequirementType.CREDENTIAL); "integration" still appears on older payloads
        print(f"  - {err['type']}: {err['key']}")
except APIError as e:
    print(f"HTTP {e.status_code}: {e.message}")  # e.response_body has full problem+json
except RuntimeError as e:
    print(f"Error: {e}")
```

`async_inference()` raises the same exception types with the same message extraction. Session helpers may raise `SessionNotFoundError`, `SessionExpiredError`, or `SessionEndedError` (subclasses of `APIError`).

**inferencesh ≥ v0.13.0** exports `ErrorCode` and typed `meta` objects for common problem responses. The problem body has no `code` field: the code is the last segment of `type` (`https://api.inference.sh/errors/payment_method_required`). **inferencesh ≥ v0.21.0** reads it for you as `APIError.code`:

```python
import json
from inferencesh import APIError
from inferencesh.types import (
    ErrorCode,
    EntitlementErrorMeta,
    PaymentMethodRequiredMeta,
    TeamRoleRequiredMeta,
)

try:
    client.tasks.run({"app": "my-app", "input": {...}})
except APIError as e:
    problem = json.loads(e.response_body)
    code = e.code  # before v0.21.0: problem["type"].rsplit("/", 1)[-1]
    meta = problem.get("meta") or {}

    if code == ErrorCode.TEAM_ROLE_REQUIRED.value:
        role_meta: TeamRoleRequiredMeta = meta
        print(role_meta.get("capability"), role_meta.get("required_role"))
    elif code == ErrorCode.PAYMENT_METHOD_REQUIRED.value:
        pay_meta: PaymentMethodRequiredMeta = meta
        print(pay_meta.get("billing_page"))
    elif code in (ErrorCode.LIMIT_EXCEEDED.value, ErrorCode.FEATURE_NOT_AVAILABLE.value):
        ent_meta: EntitlementErrorMeta = meta
        if ent_meta.get("upgrade_available"):
            print(ent_meta.get("addon_plan_name"))
```

A call that needs the account holder's own sign-in (tool approvals, for example) refuses an API key with 403 `requires_sign_in` (`ErrorCode.REQUIRES_SIGN_IN`, **inferencesh ≥ v0.21.0**).

Remote harness refusals (`remote_offline`, `harness_not_drivable`, `harness_too_old`, …) use the same `ErrorCode` values — see [Agents REST — Remote harness runs](/docs/api/rest/agents#remote-harness-runs).

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

---

## Generated types (`inferencesh.types`)

The package ships OpenAPI-aligned types for REST integrations. Import from `inferencesh.types` (not the top-level `inferencesh` package) when you call endpoints that do not yet have a Python client namespace.

**MCP tasks extension** — Custom MCP clients that call inference.sh over Streamable HTTP should declare the tasks extension and interpret `resultType` on `tools/call` responses. **inferencesh ≥ v0.13.0** adds `ClientCapabilities.extensions` and `ResultType.TASK` for async task handles (for example long-running `app_run`):

```python
from inferencesh.types import ClientCapabilities, ResultType, ToolCallResponse

client_caps: ClientCapabilities = {
    "extensions": {"io.modelcontextprotocol/tasks": {}},
}

response: ToolCallResponse = {...}  # from tools/call
if response.get("resultType") == ResultType.TASK:
    ...  # CreateTaskResult includes taskId — poll tasks/get until completed
elif response.get("resultType") == ResultType.INPUT_REQUIRED:
    ...  # MRTR — retry with inputResponses (see using-tools MRTR section)
```

Wire protocol steps (capabilities JSON, `tasks/get`, `tasks/cancel`) are in [MCP Server — Tasks extension and `app_run`](/docs/connectors/mcp-server#tasks-extension-and-app_run). MRTR elicitation uses `ResultType.INPUT_REQUIRED` on the same `ToolCallResponse` shape.

**Tool parameter schema (`ToolParameterProperty`)** — Agent and app tools serialize parameters with the same shape the platform sends to model providers. **inferencesh ≥ v0.14.0** adds optional `anyOf` and `enum` on `ToolParameterProperty` in `inferencesh.types` and on the pydantic mirror in `inferencesh.llm_types_gen` (aligned with agentprotocol v0.16.0):

```python
from inferencesh.types import ToolParameterProperty, ToolParamType

# Union parameter: value may be a string or an integer
status_code: ToolParameterProperty = {
    "title": "status_code",
    "description": "HTTP status as text or number",
    "anyOf": [
        {"type": ToolParamType.STRING, "title": "status_code"},
        {"type": ToolParamType.INTEGER, "title": "status_code"},
    ],
}

# Closed set on a scalar (also duplicated in description text for legacy readers)
sort: ToolParameterProperty = {
    "type": ToolParamType.STRING,
    "title": "sort",
    "enum": ["count", "first_seen"],
}
```

When `anyOf` is set, omit `type` on the parent property — a single `type` would misrepresent a multi-shape field. MCP connector tools populate these fields when converting cached MCP `inputSchema` JSON; see [Connector tools — unions and enums](/docs/connectors/agent-tools#json-schema-dialect).

**Agent run wire types**: **inferencesh ≥ v0.22.0** types `POST /agents/run` as `ApiAgentRunRequest`, with the user turn in `input: MessageInput` (`text`, `attachments`; the deprecated `images` / `files` are read only when `attachments` is empty). `CreateAgentMessageRequest` is no longer generated. `ContextField.in_prompt: true` shows a context value to the model, quoted in the system prompt; otherwise the model only learns the field's name and writes `{{context.NAME}}` in tool arguments.

```python
from inferencesh.types import ApiAgentRunRequest, MessageInput, ContextField

turn: MessageInput = {"text": "Summarize this", "attachments": []}
run: ApiAgentRunRequest = {
    "agent": "my-org/support-agent",
    "input": turn,
    "context": {"project_id": "proj_456"},
}

plan_field: ContextField = {
    "name": "plan",
    "description": "Billing plan label for the caller",
    "in_prompt": True,
}
```

`agent.send_message()` builds the same body. See [Adding tools: Declaring context fields](/docs/agents/adding-tools#declaring-context-fields).

**Messaging channels**: `ChannelContext` on `ChatDTO` when a chat originated from Slack, Telegram, or another platform:

```python
from inferencesh.types import ChannelContext, ChannelType

ctx: ChannelContext = {
    "channel_type": ChannelType.SLACK,
    "channel_metadata": {"channel": "C01234567", "thread_ts": "1234567890.123456"},
}
```

`channel_metadata` is provider-specific opaque JSON for reply routing. See [Messaging channel routing](#messaging-channel-routing) and [Get chat — `channel_context`](/docs/api/rest/agents#get-chat).

**Chat settings and agent switch** — `POST /chats/{chatId}/settings` and `POST /chats/{chatId}/agent` return narrow DTOs (`ChatSettingsDTO`, `ChatAgentDTO`) that describe what changed — not a full `ChatDTO`. Merge them into the chat you hold. There is no Python client helper yet; call the REST endpoints and type requests and responses with `inferencesh.types` (**inferencesh ≥ v0.16.0** for the response DTOs; `ChatSettingsRequest` ships from **v0.14.3**):

```python
import httpx
from inferencesh.types import (
    ChatSettingsRequest,
    ChatSettingsDTO,
    ChatAgentDTO,
)

settings_body: ChatSettingsRequest = {
    "name": "Incident triage",
    "visibility": "team",
    "allow_all_tools": False,
    "disable_hooks": True,
    "forget_memory": ["stale_goal"],
}
settings_resp = httpx.post(
    f"{API}/chats/{chat_id}/settings",
    headers={"Authorization": f"Bearer {api_key}"},
    json=settings_body,
)
updated_settings: ChatSettingsDTO = settings_resp.json()["data"]

switch_resp = httpx.post(
    f"{API}/chats/{chat_id}/agent",
    headers={"Authorization": f"Bearer {api_key}"},
    json={"agent": "myteam/escalation-agent@latest"},
)
agent_change: ChatAgentDTO = switch_resp.json()["data"]
```

Omit keys you do not want to change. `disable_hooks` stops [lifecycle hooks](/docs/api/agent/lifecycle-hooks) in that chat (review branches are opened with it set). Responses use the standard envelope, so read `data`. The settings response is a `ChatSettingsDTO` (`chat_id`, `name`, `visibility`, `allow_all_tools`, `disable_hooks`, `memory`) — merge those fields onto your local chat (map the three `agent_data` fields) instead of replacing stored messages. `POST /chats/{id}/agent` returns `ChatAgentDTO` with the resolved agent refs. See [Update chat settings](/docs/api/rest/agents#chat-settings) and [Set agent](/docs/api/rest/agents#set-agent).

**Scoped always allow and explain** — when a platform-loop tool pauses with `awaiting_approval`, list scoped always-allow choices, save one (which also approves the current call), or fetch a one-time plain-language summary. There is no Python client helper yet (the [JavaScript Agent SDK](/docs/api/sdk-javascript#scoped-always-allow-and-explain) wraps these). Call the REST endpoints and type payloads with `inferencesh.types` (**inferencesh ≥ v0.16.0**):

```python
import httpx
from inferencesh.types import (
    AlwaysAllowOptionsDTO,
    AlwaysAllowRequest,
    AlwaysAllowResultDTO,
    ToolExplanationDTO,
)

options: AlwaysAllowOptionsDTO = httpx.get(
    f"{API}/chats/{chat_id}/tools/{tool_invocation_id}/always-allow/options",
    headers={"Authorization": f"Bearer {api_key}"},
).json()["data"]

key = options.get("default") or options["options"][0]["key"]
body: AlwaysAllowRequest = {"option": key}
saved: AlwaysAllowResultDTO = httpx.post(
    f"{API}/chats/{chat_id}/tools/{tool_invocation_id}/always-allow",
    headers={"Authorization": f"Bearer {api_key}"},
    json=body,
).json()["data"]

explanation: ToolExplanationDTO = httpx.post(
    f"{API}/chats/{chat_id}/tools/{tool_invocation_id}/explain",
    headers={"Authorization": f"Bearer {api_key}"},
).json()["data"]
```

| REST endpoint | Types |
|---------------|-------|
| `GET .../always-allow/options` | `AlwaysAllowOptionsDTO`, `AlwaysAllowOptionDTO`, `PolicyRuleDTO` |
| `POST .../always-allow` | `AlwaysAllowRequest` → `AlwaysAllowResultDTO` |
| `POST .../explain` | `ToolExplanationDTO` |

Enums: `AlwaysAllowScope` (`exact`, `prefix`, `folder`, `remote`, `tool`), `ToolRiskLevel` (`low`, `medium`, `high`), `PolicyEffect` (`allow`, `ask`, `deny`). Re-fetch options after HTTP **409** (stale option key). `explain` is not available on [harness-backed chats](/docs/api/rest/agents#explain-tool-call).

**Policy kinds (inferencesh ≥ v0.17.0)** — saved always-allow choices and workspace policy rows share `PolicyRuleDTO`. `kind` is typed as `PolicyKind` (use the enum instead of raw strings):

| `PolicyKind` member | API value | Governs |
|---------------------|-----------|---------|
| `REMOTE_EXEC` | `RemoteExec` | Shell commands on a remote (`remote_exec`) |
| `WORKSPACE` | `Workspace` | Folders on a remote |
| `HARNESS` | `Harness` | Harness tool approvals (for example `remote_session_resolve`) |
| `TOOL` | `Tool` | Platform-loop tool calls by tool name |
| `WEB_FETCH` | `WebFetch` | Outbound HTTP fetch domains |
| `APP`, `AGENT`, `KNOWLEDGE`, `MCP`, `FLOW` | `App`, `Agent`, … | [Usage policy](/docs/org/usage-policy) (resolved resource ids) |

```python
from inferencesh.types import AgentPermissions, PolicyKind, PolicyRuleDTO

def describe_rule(rule: PolicyRuleDTO) -> str:
    if rule.get("kind") == PolicyKind.REMOTE_EXEC:
        return f"remote command: {rule.get('specifier')}"
    if rule.get("kind") == PolicyKind.TOOL:
        return f"tool: {rule.get('specifier')}"
    return f"{rule.get('kind')} {rule.get('specifier')}"

defaults: AgentPermissions = {"allow_all_tools": True}
```

**Agent permissions** — `AgentPermissions` (`allow_all_tools` optional) types `version.permissions` on agent templates. It seeds `agent_data.allow_all_tools` when a **new chat is created in the agent's own workspace**; chats other workspaces start with the agent get none of these defaults (they run on their own remotes and tools). 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 ≥ v0.17.0** removed `ChatData.always_allowed_tools` from generated types — always-allow is stored as [chat permission rules](/docs/api/rest/agents#chat-permissions-rules). Read `rules` from `AlwaysAllowResultDTO`, call `GET /chats/{id}/rules`, or use chat settings. `ChatData` still includes `allow_all_tools` and `disable_hooks`. Legacy chats may 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)

**Knowledge** — use with the [Knowledge REST API](/docs/api/rest/knowledge) until `client.knowledge` ships in Python (available today in the [JavaScript SDK](/docs/api/sdk-javascript#client-api)):

```python
from inferencesh.types import KnowledgeType, KnowledgeLifecycle, KnowledgeDTO

# Enum values match POST /knowledge `type` and `lifecycle` fields
KnowledgeType.OBSERVATION   # "observation"
KnowledgeType.SKILL         # "skill" — prefer /skills endpoints when applicable
KnowledgeLifecycle.DECAY    # "decay"
KnowledgeLifecycle.PERMANENT  # "permanent"
```

| Enum | Members (API string values) |
|------|-----------------------------|
| `KnowledgeType` | `concept`, `skill`, `observation`, `preference`, `reference`, `person`, `project`, `agent-config` |
| `KnowledgeLifecycle` | `permanent`, `decay` |

`KnowledgeDTO`, `KnowledgeCreateRequest`, and related request types mirror the REST schemas for typed `httpx` or `requests` calls. Set `scope` on `KnowledgeVersionInput` (or `version.scope` on create) to tag environment signals — see [Knowledge API — Scoping](/docs/api/rest/knowledge#project-scope).

**Graph edges** — edge `type` values on knowledge references, skill lineage, and [Supersede Skill](/docs/api/rest/skills#supersede-skill):

```python
from inferencesh.types import GraphEdgeType

GraphEdgeType.REFERENCES  # "references" — content mentions in markdown
GraphEdgeType.SUPERSEDES  # "supersedes" — replacement skill hides the old one from suggest
```

| Enum | Members (API string values) |
|------|-----------------------------|
| `GraphEdgeType` | `dependency`, `flow`, `conditional`, `execution`, `parent`, `ancestor`, `duplicate`, `references`, `supersedes` |

**Search / suggest** — use with the [Search REST API](/docs/api/rest/search) until `client.search` ships in Python (available today in the [JavaScript SDK](/docs/api/sdk-javascript#client-api)):

```python
from inferencesh.types import SuggestRequest, SuggestResponse, SuggestResult

request: SuggestRequest = {
    "query": "image generation",
    "context": "User asked for a fast model for social thumbnails",
    "limit": 5,
    "category": "apps",
    "scope": ["git:github.com/acme/design", "lang:typescript"],
}
result: SuggestResult = {
    "type": "app",
    "name": "okaris/flux",
    "description": "Image generation",
    "command": "inference run okaris/flux@abc1",
    "score": 0.92,
}
knowledge_hit: SuggestResult = {
    "type": "knowledge",
    "tag": "reference",
    "name": "myteam/stripe-webhooks",
    "description": "Stripe webhook handling patterns",
    "command": "belt knowledge get myteam/stripe-webhooks",
    "score": 0.81,
}
response: SuggestResponse = {
    "query": "image generation",
    "results": [result, knowledge_hit],
}
```

For hybrid ranking, the API embeds `query`, then optional `context`, then `scope` tags. Keyword matching uses `query` only — see [Search API — Suggest](/docs/api/rest/search#suggest).

| TypedDict | Fields |
|-----------|--------|
| `SuggestRequest` | `query`, `context`, `limit`, `category`, `scope` (`agent` is deprecated — ignored) |
| `SuggestResponse` | `query`, `results` (`List[SuggestResult]`) |
| `SuggestResult` | `type`, `tag`, `name`, `description`, `command`, `score` (descriptions clipped to 200 characters) |

**inferencesh ≥ v0.19.0** exports `DescriptionLimit` (`IntEnum`) with `LISTING` (200) and `SKILL` (1024) for client-side validation before writes — see [description length limits](/docs/api/rest/overview#description-length-limits):

```python
from inferencesh.types import DescriptionLimit

assert DescriptionLimit.LISTING == 200
assert DescriptionLimit.SKILL == 1024
```

Suggest result `description` fields are clipped to `DescriptionLimit.LISTING` characters on read.

**Instance catalog** — use with [Instance types REST API](/docs/api/rest/instances) for GPU pricing and availability:

```python
from inferencesh.types import (
    InstanceRentalType,
    InstanceTypeAvailability,
    InstanceTypeDTO,
    InstanceTypeConfiguration,
)

# GET /instances/types returns a list of InstanceTypeDTO
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": InstanceRentalType.ON_DEMAND},
        {
            "available": True,
            "region": "us-east-1",
            "rental_type": InstanceRentalType.SPOT,
            "hourly_price": 199,  # cents per hour — spot rows only
        },
    ],
}
spot_row: 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 [Instance types](/docs/api/rest/instances) and [Engines API — List engine instance types](/docs/api/rest/engines#list-engine-instance-types).

**Store browse metadata** — category and tag catalogs from the public Store API (`StoreCategoryDTO` / `StoreTagDTO` ship in the next `inferencesh` release after v0.20.0; until then, read the JSON as plain dicts):

```python
import httpx
from inferencesh.types import StoreCategoryDTO, StoreTagDTO

categories: list[StoreCategoryDTO] = httpx.get(
    "https://api.inference.sh/store/categories",
    params={"type": "app"},
).json()["data"]

tags: list[StoreTagDTO] = httpx.get("https://api.inference.sh/store/tags").json()["data"]
```

| TypedDict | 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 `httpx` or `requests`. See [List store categories](/docs/api/rest/store#list-store-categories) and [List browseable tags](/docs/api/rest/store#list-browseable-tags).

**Apps** — type `GET /apps/{namespace}/{name}` responses with `AppDTO` when you call the [Apps REST API](/docs/api/rest/apps#get-app) directly. Ref-route aliases return the **target** app's `namespace`, `name`, and `id`; when the requested name pins a [multi-function](/docs/extend/multi-function-apps) entry point, `resolved_function` names that function (omitted when the caller should use the version default). `resolved_function` is on `AppDTO` from **inferencesh ≥ v0.19.0** (JavaScript: **@inferencesh/sdk ≥ v0.20.0**):

```python
import httpx
from inferencesh.types import AppDTO

resp = httpx.get(
    "https://api.inference.sh/apps/elevenlabs/text-to-dialogue",
    headers={"Authorization": "Bearer inf_..."},
)
app: AppDTO = resp.json()["data"]
entry = app.get("resolved_function") or "default"
```

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

```python
from inferencesh.types import AppStoreListingDTO

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).

### App metadata

App listing fields on `GET /apps/{id}` and create/deploy payloads (**inferencesh ≥ v0.19.1** for `AppCategory.DECISION` and `tags` on `AppDTO` / `CreateAppRequest`):

```python
from inferencesh.types import AppCategory, AppDTO, CreateAppRequest

app: AppDTO = {
    "namespace": "myteam",
    "name": "router",
    "category": AppCategory.DECISION,
    "tags": ["routing", "reasoning"],
}

create: CreateAppRequest = {
    "namespace": "myteam",
    "name": "flux-schnell",
    "category": AppCategory.IMAGE,
    "tags": ["text-to-image", "open-weights"],
}

# Browseable slugs and display titles (public, no API key)
import httpx

tags = httpx.get("https://api.inference.sh/store/tags").json()["data"]
titles = {row["slug"]: row["title"] for row in tags}
label = titles.get("text-to-image", "text-to-image")
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `AppCategory` | `image`, `video`, `audio`, `text`, `chat`, `3d`, `flow`, `decision`, `other` — `decision` is for probability-over-answers models, not generative output |
| `AppDTO` | Response from `GET /apps/...`; includes `tags` when set and optional `resolved_function` on ref lookups (see [Apps API — Get app](/docs/api/rest/apps#get-app)) |
| `CreateAppRequest` | `POST /apps` body; optional `tags` replaces the app's tags when provided |

Browseable tag slugs and titles come from `GET /store/tags` ([Store API](/docs/api/rest/store#list-browseable-tags)). **v0.19.1** briefly exported `AppTag` / `AppTagTitle` enums; current types use `string[]` tags only so the tag set can change without a new SDK release.

Tag rules (slug format, max count, deploy vs `metadata.tags`) are documented on the [Apps API](/docs/api/rest/apps#app-tags).

**Bounty types**: use with [Workspaces API: bounties](/docs/api/rest/teams#bounties) (`GET /bounties`, `POST /me/bounty`). There is no `client.bounties` namespace yet; import the generated types when calling these endpoints with `httpx` or `requests`:

```python
import httpx
from inferencesh.types import (
    BountyProgramDTO,
    SubmitBountyRequest,
    SubmitBountyResponse,
)

programs: list[BountyProgramDTO] = httpx.get(
    f"{base_url}/bounties",
    headers={"Authorization": f"Bearer {api_key}"},
).json()

claim: SubmitBountyRequest = {
    "bounty_id": "app-builder",
    "proof_id": "01abc123def456",  # app id or namespace/name when proof_type is app
    "source": "cli",
}

# Form programs: submit the form first, then claim with proof_id = submission.id
form_claim: SubmitBountyRequest = {
    "bounty_id": "agent-survey",
    "proof_id": "fsm_abc123",
    "source": "cli",
}

result: SubmitBountyResponse = httpx.post(
    f"{base_url}/me/bounty",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    json=claim,
).json()
```

| TypedDict | Use |
|-----------|-----|
| `BountyProgramDTO` | Items returned by `GET /bounties` — includes `proof_type` (`app`, `form`, `text`, `none`) and `proof_form` (`namespace/name` when `proof_type` is `form`) |
| `SubmitBountyRequest` | Body for `POST /me/bounty` — `proof_id` is an app ref/id, a form submission `id`, or a text key depending on the program |
| `SubmitBountyResponse` | Response from `POST /me/bounty` |
| `BountySubmissionDTO` | Nested in `SubmitBountyResponse.submission` — `resource_id` is the app or form ID when applicable; `proof_id` is the submission id for form proofs |

Duplicate claims return HTTP `409` with code `already_claimed` (`ErrorCode.ALREADY_CLAIMED` in generated types). When `requires_payment_method` is set, a missing card returns `402` and **no** claim row is stored (safe to retry after billing is set up).

**Forms** — use with the [Forms REST API](/docs/api/rest/forms) (`GET /forms`, `POST /forms/{id}/submissions`). There is no `client.forms` namespace yet; the types ship in **inferencesh ≥ v0.19.0**:

```python
from inferencesh.types import (
    FormDTO,
    SubmitFormRequest,
    SubmitFormResponse,
    FormSubmissionDTO,
)

submit: SubmitFormRequest = {
    "data": {"discovery": "github"},
    "source": "script",
    "agent": "cursor",
}

recorded: SubmitFormResponse = httpx.post(
    f"{base_url}/forms/{form_id}/submissions",
    headers={
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    },
    json=submit,
).json()
# recorded["submission"] is a FormSubmissionDTO — credits are not granted here
```

`FormDTO` has no `bounty_name` field; link programs through `BountyProgramDTO.proof_form` instead. `SubmitSurveyResponse` still includes `granted_amount` and `reward_blocked_reason` for older clients, but servers return `0` and `""` — claim survey bounties with `POST /me/bounty` after `POST /me/survey`.

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

```python
from inferencesh.types import EstimateCostRequest, EstimateCostResponse

req: EstimateCostRequest = {
    "input": {"n": 4},
    "function": "generate",  # optional for multi-function apps
}
resp: EstimateCostResponse = {
    "confidence": "range",
    "min": 150000,
    "max": 400000,
    "depends_on": ["output_tokens"],
    "pricing_description": "$0.0015–$0.004 per run",
}
failed: EstimateCostResponse = {
    "confidence": "unknown",
    "depends_on": ["output_tokens"],
    "estimate_error": "CEL evaluation failed: undefined variable 'output_tokens'",
}
```

| TypedDict | 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):

```python
from inferencesh.types import PlanDTO, PlanType, EntitlementResource

base: PlanDTO = {
    "name": "pro",
    "plan_type": PlanType.BASE,
    "credits_monthly": 5_000_000_000,
    "stackable": False,
    "required_plan_ids": [],
    "limits": {},
}
addon: PlanDTO = {
    "name": "extra concurrency",
    "plan_type": PlanType.ADDON,
    "stackable": True,
    "required_plan_ids": ["plan_pro", "plan_team"],
    "required_plan_names": ["Pro", "Team"],
    "limits": {
        EntitlementResource.RESOURCE_CONCURRENCY: {"limit": 5},
    },
}
```

`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`):

```python
from inferencesh.types import ScopesResponse, ScopePreset, ScopeDefinition

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.

**Current user (`GET /me`)** — **inferencesh ≥ v0.13.0** exports `MeResponse` and the nested DTOs the REST page documents (`TeamDTO`, `OrgDTO`, `TeamViewDTO`, `DiagnosticsConfig`, `TeamKind`). Use them with `client` helpers or direct HTTP:

```python
import httpx
from inferencesh.types import MeResponse, TeamDTO, UserMetadataDTO

me: MeResponse = httpx.get(
    f"{base_url}/me",
    headers={"Authorization": f"Bearer {api_key}"},
).json()["data"]

team: TeamDTO | None = me.get("team")
metadata: UserMetadataDTO = (me.get("user") or {}).get("metadata") or {}
```

`team` is a full workspace record (`TeamDTO`), not the list-item shape from `GET /teams`. Capability checks belong on `me["team_view"]["can"]` (and `me["team_view"]["org"]["can"]` for org-workspace actions) — see [Get current user](/docs/api/rest/teams#get-current-user).

**User metadata** — nested on `UserDTO` inside `MeResponse`:

```python
from inferencesh.types import UserMetadataDTO

metadata: UserMetadataDTO = {
    "completed_onboarding": True,
    "terms_accepted_at": "2026-01-15T10:30:00Z",
    "terms_version": "2026-01",
}
```

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

```python
from inferencesh.types import EntitlementDTO, EntitlementSource, EntitlementType, EntitlementResource

ent: EntitlementDTO = {
    "resource": EntitlementResource.RESOURCE_CONCURRENCY,
    "type": EntitlementType.LIMIT,
    "limit": 3,
    "source": EntitlementSource.TIER,
}
```

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

```python
import json
from inferencesh import APIError
from inferencesh.types import EntitlementErrorMeta

try:
    client.tasks.run({"app": "my-app", "input": {...}})
except APIError as e:
    body = json.loads(e.response_body)
    meta: EntitlementErrorMeta = body.get("meta") or body.get("error", {}).get("meta", {})
    if meta.get("upgrade_available"):
        print(meta.get("resource_label"), meta.get("addon_plan_name"))
```

See [Entitlements API — Error metadata](/docs/api/rest/entitlements#error-metadata) for field definitions (`addon_plan_id`, `addon_plan_price`, `upgrade_available`, etc.).

**Team settings capabilities** — **inferencesh ≥ v0.13.0** adds `TeamCapability` for the `can` arrays on [`GET /teams/{id}/view`](/docs/api/rest/teams#workspace-settings-view) and `team_view` on `GET /me`:

```python
import httpx
from inferencesh.types import TeamCapability

view = httpx.get(
    f"{base_url}/teams/{team_id}/view",
    headers={"Authorization": f"Bearer {api_key}", "X-Team-ID": team_id},
).json()["data"]

if TeamCapability.MANAGE_BILLING.value in (view.get("can") or []):
    ...
if TeamCapability.MANAGE_ORG.value in ((view.get("org") or {}).get("can") or []):
    ...
```

| Enum | Members (API string values) |
|------|-----------------------------|
| `TeamCapability` | `edit_profile`, `manage_members`, `manage_admins`, `view_members`, `manage_keys`, `create_keys`, `manage_vault`, `view_billing`, `manage_billing`, `view_policy`, `manage_policy`, `manage_org`, `manage_sso`, `archive`, `create_team`, `create_org`, `connect_org_credential`, `connect_platform_credential` |

Capability meanings and org vs member-workspace split are documented on [Workspace settings view — Capability strings](/docs/api/rest/teams#capability-strings).

**Problem `code` values** — `ErrorCode` mirrors RFC 9457 `code` strings (`team_role_required`, `payment_method_required`, `blocked_by_usage_policy`, entitlement codes, remote harness errors, …). Use it with `TeamRoleRequiredMeta` and `PaymentMethodRequiredMeta` as in [Error handling](#error-handling).

| Enum | Members (API string values) |
|------|-----------------------------|
| `ErrorCode` | `invalid_request`, `validation_error`, `unauthorized`, `forbidden`, `not_found`, `team_role_required`, `blocked_by_usage_policy`, `limit_exceeded`, `feature_not_available`, `payment_method_required`, `remote_offline`, `harness_not_drivable`, … — full set in `inferencesh.types` |
| `EntitlementSource` | `tier`, `override`, `whitelist`, `trial` |
| `EntitlementResource` (feature gates) | `RESOURCE_FEATURE_BYOK` (`feature:byok`), `RESOURCE_FEATURE_SEEDANCE` (`feature:seedance`), … — see [Entitlements API](/docs/api/rest/entitlements#resources) |
| `WorkerStatus` | `reserved`, `busy`, `idle`, `inactive` |
| `EngineStatus` | `running`, `pending`, `draining`, `restarting`, `disconnected`, `stopping`, `stopped` |
| `InstanceRentalType` | `on_demand`, `spot` — cloud catalog rows, `InstanceDTO`, and nested `engine.instance` |

**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).

> Credential types were named `Integration*` until 2026-09; import the `Credential*` names from `inferencesh.types`.

```python
from inferencesh.types import (
    CredentialConfigDTO,
    CredentialDTO,
    CredentialGrant,
    CredentialScope,
)

def connect_label(config: CredentialConfigDTO) -> str:
    if config.get("connection_scope") == CredentialScope.USER:
        return f"connect your {config['name']} account"
    return f"connect {config['name']}"

def list_subtitle(credential: CredentialDTO) -> str:
    account_id = credential.get("account_identifier") or ""
    if credential.get("scope") == CredentialScope.USER:
        return f"{account_id} (your account)"
    return account_id

per_user_oauth: CredentialConfigDTO = {
    "available": True,
    "has_managed": True,
    "allows_byok": False,
    "connection_scope": CredentialScope.USER,
    "name": "X",
}
```

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

**inferencesh ≥ v0.7.35** models config rows with `connection_scope` and optional nested `app` (OAuth application) instead of `grant` on `CredentialConfigDTO`. `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).

**inferencesh ≥ v0.28.0** adds `CredentialDTO["secrets"]`: the vault keys of a key-based credential, as `CredentialSecretRef` (`id`, `key`, `updated_at`), never the values. **inferencesh ≥ v0.29.0** adds `terms_url` and `privacy_url` to `CompanySummary` (`CredentialConfigDTO["company"]`), next to `slug`, `name`, `icon_url`, `website` and `docs_url`. `CompanySummary` was `ProviderSummary` and the field was `provider_info` before INF-1079. The next minor release drops `provider_info` and renames `auth_scheme_id` to `auth_provider_id`, matching the API.

**Secrets and connection scope**: use with the [Secrets REST API](/docs/api/rest/secrets) and [`connection_scope` on connect](/docs/api/rest/credentials#connection-scope). The Python SDK has no `client.secrets` or `client.credentials` namespace. Import the generated types when calling these endpoints with `httpx` or `requests`:

```python
import httpx
from inferencesh.types import CredentialConnectRequest, CredentialScope, SecretCreateRequest

base_url = "https://api.inference.sh"
headers = {"Authorization": "Bearer inf_your_key"}

# POST /secrets: BYOK key linked to a workspace-scoped credential
byok: SecretCreateRequest = {
    "key": "OPENAI_API_KEY",
    "value": "sk-your-key-here",
    "provider": "openai",
    "connection_scope": CredentialScope.TEAM,
    "description": "Team OpenAI BYOK key",
}
httpx.post(f"{base_url}/secrets", headers=headers, json=byok)

# POST /credentials: OAuth connect with an explicit scope
connect: CredentialConnectRequest = {
    "provider": "github",
    "type": "oauth",
    "connection_scope": CredentialScope.USER,
}
httpx.post(f"{base_url}/credentials", headers=headers, json=connect)
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `CredentialScope` | `user`, `team`, `org`, `platform`, `agent`. Who owns the credential. Setting it explicitly requires the matching admin role. |
| `SecretCreateRequest` | Body for `POST /secrets`. Optional `provider` and `connection_scope` link a BYOK key to a credential at the chosen scope. |
| `CredentialConnectRequest` | Body for `POST /credentials`. Optional `connection_scope` is separate from OAuth `scopes`. |
| `CredentialCompleteOAuthRequest` | Body for `POST /credentials/oauth/complete`. Optional `params` maps extra redirect query keys (for example QuickBooks `realmId`, Shopify `shop`) for auth method templates as `{{callback.*}}`. **inferencesh ≥ v0.11.1** adds `params` to the generated type (REST behavior unchanged). See [Callback query parameters on OAuth complete](/docs/api/rest/auth-providers#callback-query-parameters-on-oauth-complete). |

Change who owns an existing connection with `PUT /credentials/{provider}/scope` (body: `{ "scope": "team" }`; MCP uses `?identifier=<server_url>`). See [Change connection scope](/docs/api/rest/credentials#change-connection-scope).

**MCP tool responses** — type [MCP tool proxy](/docs/api/rest/credentials#mcp-tool-proxy) results (`POST /mcps/{slug}/tools/{tool}`) and MRTR retries with `ToolCallRequest.inputResponses` / `requestState`. **inferencesh ≥ v0.13.0** adds `ResultType.TASK` for the MCP tasks extension and `ClientCapabilities.extensions` for extension negotiation during MCP initialize:

```python
import httpx
from inferencesh.types import ResultType, ToolCallRequest, ToolCallResponse

body: ToolCallRequest = {"name": "list_issues", "arguments": {"limit": 10}}
response: ToolCallResponse = httpx.post(
    f"{base_url}/mcps/linear/tools/list_issues",
    headers=headers,
    json=body,
).json()["data"]

kind = response.get("resultType") or ResultType.COMPLETE.value
if kind == ResultType.INPUT_REQUIRED.value:
    retry: ToolCallRequest = {
        **body,
        "inputResponses": {...},  # keys match inputRequests from the interim response
        "requestState": response.get("requestState", ""),
    }
    response = httpx.post(
        f"{base_url}/mcps/linear/tools/list_issues",
        headers=headers,
        json=retry,
    ).json()["data"]
elif kind == ResultType.TASK.value:
    # Server accepted the call as an async MCP task; poll tasks/get for the final ToolCallResponse.
    ...
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `ResultType` | `complete` (default when omitted), `input_required` (MRTR), `task` (MCP tasks extension) |
| `ToolCallResponse` | `resultType`, `content`, `structuredContent`, `isError`, `_meta`; MRTR adds `inputRequests`, `requestState` |
| `ToolCallRequest` | `name`, `arguments`; MRTR retries add `inputResponses`, `requestState` |
| `ClientCapabilities` | MCP client capabilities on initialize; `extensions` maps extension identifiers (for example tasks) to extension settings |

→ [Using tools — Interactive elicitation (MRTR)](/docs/connectors/using-tools#interactive-elicitation-mrtr)

**Engines** — use with the [Engines REST API](/docs/api/rest/engines) until `client.engines` ships in Python (available today in the [JavaScript SDK](/docs/api/sdk-javascript#private-engines)):

```python
from inferencesh.types import EngineDTO, EngineStatus, InstanceRentalType

engine: EngineDTO = {
    "id": "eng_abc123",
    "name": "gpu-box-1",
    "status": EngineStatus.RUNNING,
    "engine_version": "v1.10.15",  # inferencesh-engine binary; empty until first telemetry sync
    "api_url": "https://api.inference.sh",
    "workers": [],
    "instance": {
        "rental_type": InstanceRentalType.SPOT,
        "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).

**Pages**: use with the [Pages REST API](/docs/api/rest/pages) and [workspace username checks](/docs/api/rest/teams#check-username-availability) until dedicated client namespaces ship:

```python
from inferencesh.types import (
    AvailabilityResponse,
    PageDTO,
    PageMetadata,
    PageStatus,
    PageType,
)

# GET /pages/slug/{slug}
page: PageDTO = {
    "title": "Scheduled launch post",
    "slug": "launch-day",
    "status": PageStatus.SCHEDULED,
    "type": PageType.BLOG,
    "metadata": {
        "publish_at": "2026-08-01T09:00:00Z",  # write here on create/update
    },
    "publish_at": "2026-08-01T09:00:00Z",  # mirrored on read — do not require metadata drill-down
}

slug_check: AvailabilityResponse = {
    "value": "my-new-post",  # normalized slug from the server
    "available": False,
    "reason": "taken",  # or "reserved" — only set when available is false
}
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `PageStatus` | `UNKNOWN` (0), `DRAFT` (1), `PUBLISHED` (2), `ARCHIVED` (3), `SCHEDULED` (4) |
| `PageType` | `doc`, `blog`, `page` |
| `PageDTO` | Includes top-level `publish_at` (RFC 3339) alongside `metadata.publish_at` |
| `AvailabilityResponse` | Shared by page slug and workspace username availability endpoints. Replaces the older `SlugAvailabilityResponse` shape (`slug` → `value`, plus optional `reason`) |

### Interrupt gates

Use with the [Agents REST API — Interrupt gates](/docs/api/rest/agents#interrupt-gates) (`GET /agent-runs/{runId}/interrupts`, `POST /interrupts/{id}/resolve`). The JavaScript SDK exposes `client.agents.listRunInterrupts()` and `resolveInterrupt()`; in Python, call the REST endpoints directly and type responses with `inferencesh.types` (v0.7.23+).

```python
from inferencesh.types import (
    AgentRunDTO,
    InterruptDTO,
    InterruptReason,
    InterruptStatus,
    InterruptResolution,
    InterruptResourceType,
)

# active_run on GET /chats/:id when the run is paused
run: AgentRunDTO = chat["active_run"]
if run and run.get("interrupt_reason") == InterruptReason.HOOK_GATE:
  ...

# GET /agent-runs/{runId}/interrupts
pending: list[InterruptDTO] = [
    {
        "id": "int_abc123",
        "run_id": "ar_abc123",
        "chat_id": "chat_abc123",
        "reason": InterruptReason.TOOL_APPROVAL,
        "status": InterruptStatus.PENDING,
        "resource_id": "ti_abc123",
        "resource_type": InterruptResourceType.INTERRUPT_RESOURCE_TOOL_INVOCATION,
    }
]

# POST /interrupts/{id}/resolve response
resolved: InterruptDTO = {
    "id": "int_abc123",
    "status": InterruptStatus.RESOLVED,
    "resolution": InterruptResolution.ALLOW,
    "resolved_at": "2026-08-11T15:00:00Z",
    "resolved_data": {"reviewer": "security-bot"},
}
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `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`, …) |

`AgentRunDTO` also includes `interrupt_reason`, `interrupt_tool_id`, and `interrupt_meta` when the run is in `input_required` or `auth_required` state.

**Remote placement:** when an agent runs on a remote harness profile, typed REST responses include optional `profile_id` and `remote_id` on `AgentRunDTO` and `AgentDTO`, plus `harness_session_id` and `forked_from_message_id` on `ChatDTO`. See [Agents REST API — Remote placement](/docs/api/rest/agents#remote-placement-on-chats-and-runs) and [Remote harness runs](/docs/api/rest/agents#remote-harness-runs).

### Remote harness chats

**inferencesh ≥ v0.10.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 `httpx` or `requests` and type responses with `ChatDTO` and `AgentDTO` from `inferencesh.types`.

```python
import httpx
from typing import TypedDict, NotRequired

from inferencesh.types import AgentDTO, ChatDTO

API = "https://api.inference.sh"
headers = {"Authorization": "Bearer inf_your_key"}

agent: AgentDTO = httpx.get(f"{API}/agents/agent_abc123", headers=headers).json()["data"]
if agent.get("harness") != "inference":
    print(agent["harness"], agent.get("profile_id"), agent.get("remote_id"))

chat: ChatDTO = httpx.get(f"{API}/chats/chat_abc123", headers=headers).json()["data"]
print(chat.get("work_dir"), chat.get("harness_session_id"))

class LaunchHarnessSessionRequest(TypedDict):
    profile_id: str
    work_dir: NotRequired[str]
    resume_session_id: NotRequired[str]

launch_body: LaunchHarnessSessionRequest = {
    "profile_id": "prof_xyz",
    "work_dir": "/home/you/project",
}

launched: ChatDTO = httpx.post(
    f"{API}/remotes/remote_abc123/sessions",
    headers=headers,
    json=launch_body,
).json()["data"]
```

| 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` | When set, external channel origin (`channel_type`, `channel_metadata`) for reply routing — see [Channel context](/docs/api/rest/agents#channel-context) |

### App requirement and setup-action types

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

```python
from inferencesh.types import (
    CheckRequirementsRequest,
    CredentialRequirement,
    SecretRequirement,
    SetupAction,
    SetupActionType,
)

req: CheckRequirementsRequest = {
    "secrets": [{"key": "OPENAI_API_KEY", "optional": False}],
    "credentials": [
        {"slug": "google", "capabilities": ["google.sheets"]},     # platform capability
        {"slug": "acme", "secrets": ["ACME_API_KEY"],              # provider keys
         "provider": {"name": "Acme CRM", "website": "acme.com"}},
    ],
}
```

**inferencesh ≥ v0.26.0** types `CredentialRequirement` by `slug` and `capabilities`, matching the slug-keyed `credentials:` in `inf.yml`. The API still reads the older list spellings (`key` for a capability, `provider` as a slug string, `name` / `website` on the entry).

| Type | Fields |
|------|--------|
| `SecretRequirement` | `key`, `description`, `optional` |
| `CredentialRequirement` | `slug` (credential slug such as `google` or `acme`; derived from a capability-only entry), `capabilities` (platform grants such as `google.sheets`), `secrets` (keys to inject from the credential), `scopes` (OAuth scopes to request), `description`, `optional`, `provider` (`CompanySeed`, for a company the platform does not list; `company:` in inf.yml), `auth` (`AuthMethod`, for a slug no auth provider defines yet). `CompanySeed` and `AuthMethod` were `ProviderSeed` and `AuthSchemeSpec` before INF-1079; `AuthSchemeDTO` is now `AuthProviderDTO` |
| `CompanySeed` | `slug` (defaults to the credential slug), `name`, `description`, `website`, `logo_uri`, `docs_uri`, `tos_uri`, `policy_uri` |
| `SetupAction` | `type` (`SetupActionType`: `add_secret`, `connect`, `add_scopes`), `provider`, `provider_name`, `scopes`, `scope_descriptions`, `secrets` (keys to supply for an `add_secret` action), `provider_website` |
| `CheckRequirementsResponse` | `satisfied`, `errors` (list of `RequirementError`, each carrying an optional `action: SetupAction`) |

### Lifecycle hook types

Use with [Lifecycle hooks](/docs/api/agent/lifecycle-hooks) and `GET /agents/hook-events`:

```python
from inferencesh.types import (
    BuiltinHook,
    HookEvent,
    HookHandlerType,
    HookDecision,
    HookEventDefinition,
    InterruptResolution,
    LifecycleHookConfig,
)

# GET /agents/hook-events
events: list[HookEventDefinition] = [
    {
        "event": HookEvent.TOOL_CALL,
        "description": "Fires before a tool is executed",
        "can_gate": True,
    }
]

# Gate hook on agent.tool_call (no builder helper yet — set type directly)
gate_hook: LifecycleHookConfig = {
    "event": HookEvent.TOOL_CALL,
    "type": HookHandlerType.HOOK_HANDLER_GATE,
    "timeout": 600,
    "default_resolution": InterruptResolution.DENY,
}

# Builtin belt hooks (inferencesh >= v0.14.2)
from inferencesh import learning_hooks, lifecycle_hook

belt_hooks = learning_hooks(suggest=True, learn=True)
single = lifecycle_hook(HookEvent.TURN_START).builtin(BuiltinHook.BELT_SUGGEST).build()
```

| Enum / TypedDict | Notes |
|------------------|-------|
| `HookEvent` | `agent.start`, `agent.turn_start`, `agent.tool_call`, `agent.tool_result`, … |
| `HookHandlerType` | `webhook`, `task`, `gate`, `builtin` |
| `BuiltinHook` | `belt:suggest` (`agent.turn_start`), `belt:extract` (`agent.complete`, `agent.pre_compact`); see [Lifecycle hooks — Builtin](/docs/api/agent/lifecycle-hooks#builtin) |
| `HookDecision` | `allow`, `deny`, `stop`, `suspend` — `suspend` is returned by gate handlers |
| `HookEventDefinition` | Response shape from `GET /agents/hook-events` (`event`, `description`, `can_gate`) |
| `BuiltinHookDefinition` | Builtin hook catalog entry (`name`, `description`, allowed `events`) — **inferencesh ≥ v0.13.0** |
| `LifecycleHookConfig` | Agent version `hooks[]` entry; gate hooks add `default_resolution` |
| `ContextInjection.items` | Resource refs or paths a hook injection placed in context (dedup across turns) — **inferencesh ≥ v0.13.0** |

### Flow utility nodes

Use with the [Flows REST API](/docs/api/rest/flows#primitive-utility-nodes) when changing flow graphs with `httpx` or `requests`.

```python
from inferencesh.types import FlowNodeData

# Preferred: unified utility config on node_data
gate_data: FlowNodeData = {
    "utility": {
        "preset": "gate",
        "gate": {"field": "status", "operator": "equals", "value": "ready"},
    },
}

selector_data: FlowNodeData = {
    "utility": {
        "preset": "selector",
        "selector": {"field": "items", "mode": "first"},
    },
}

constant_data: FlowNodeData = {
    "utility": {"preset": "constant", "constant": 42},
}

# Legacy fields, used when "utility" is not set
legacy_gate: FlowNodeData = {
    "gate_condition": {"field": "status", "operator": "equals", "value": "ready"},
}
legacy_selector: FlowNodeData = {
    "selector_config": {"field": "items", "mode": "index", "index": 0},
}
```

| TypedDict | Fields |
|-----------|--------|
| `FlowNodeData` | Graph node payload. Has `utility` plus the legacy `gate_condition` and `selector_config` |
| `UtilityConfig` | `preset` (`gate`, `selector`, `merge`, `constant`), `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`), `index` (when `mode` is `index`) |

**Task files** — use with [List task files](/docs/api/rest/tasks#list-task-files) and [Delete task files](/docs/api/rest/tasks#delete-task-files). **inferencesh ≥ v0.19.0** ships `TaskFileDTO`, `DeleteTaskFilesResponse`, and `TaskFileRole` alongside `client.tasks.files()` / `delete_files()`:

```python
from inferencesh.types import DeleteTaskFilesResponse, TaskFileDTO, TaskFileRole

file_row: TaskFileDTO = {
    "id": "file_abc123",
    "role": TaskFileRole.OUTPUT,
    "uri": "https://cloud.inference.sh/t/abc123/image.png",
    "filename": "image.png",
    "content_type": "image/png",
    "size": 1048576,
}

purge: DeleteTaskFilesResponse = {
    "deleted": ["file_abc123"],
    "skipped": [{"id": "file_other", "reason": "forbidden"}],
}
```

| TypedDict / enum | Notes |
|------------------|-------|
| `TaskFileDTO` | One file from `GET /tasks/{id}/files` |
| `DeleteTaskFilesResponse` | Body from `DELETE /tasks/{id}/files` |
| `TaskFileRole` | `input`, `output` — optional `role` filter on list/delete |

### Flow run progress

Use with [Flow Runs REST API: Per-node progress](/docs/api/rest/flow-runs#per-node-progress). The Python SDK has no flow runs client yet. Call `GET /flowruns/{id}` and type the response:

```python
import httpx
from inferencesh.types import FlowRunDTO, GraphNodeStatus

headers = {"Authorization": "Bearer inf_your_key"}
run: FlowRunDTO = httpx.get(
    "https://api.inference.sh/flowruns/flowrun_abc123", headers=headers
).json()["data"]

statuses = run.get("node_statuses") or {}

# A gate whose condition is false is reported as failed; its dependents are blocked
if statuses.get("gate_1") == GraphNodeStatus.FAILED:
    print("Gate condition was false")

if statuses.get("upscale") == GraphNodeStatus.BLOCKED:
    print("Upscale did not run")

if statuses.get("generate") == GraphNodeStatus.RUNNING:
    print("Still running:", run["node_tasks"]["generate"]["task_id"])

outputs = run.get("node_outputs") or {}
print(outputs.get("selector_1", {}).get("value"))
```

| Type / enum | Description |
|-------------|-------------|
| `FlowRunDTO` | Flow run response. Includes `node_tasks`, `node_statuses`, and `node_outputs` |
| `GraphNodeStatus` | `PENDING`, `READY`, `RUNNING`, `COMPLETED`, `FAILED`, `CANCELLED`, `SKIPPED`, `BLOCKED` |

---

## File Upload

```python
# Upload separately
file = client.files.upload("/path/to/image.png")
print(f"Uploaded: {file['uri']}")

# Use in app input
result = client.tasks.run({
    "app": "image-processor",
    "input": {"image": file["uri"]}
})

# Or auto-upload from paths in task input
result = client.tasks.run({
    "app": "image-processor",
    "input": {"image": "/path/to/local/image.png"}  # Auto-uploaded
})
```

---

## 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?

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

