Metadata-Version: 2.1
Name: nineth
Version: 0.15.4
Summary:  model sdk built by the 9th ditrict at tooig
Project-URL: Homepage, https://github.com/districtt/rooster
Project-URL: Bug Tracker, https://github.com/districtt/rooster/issues
Author-email: "Tooig, Inc" <tooighq@gmail.com>, Oyebamijo <boy@oyebamijo.com>
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.10
Requires-Dist: httpx<1.0,>=0.27.2
Description-Content-Type: text/markdown

# nineth

`nineth` is the public Python, JavaScript, and TypeScript SDK for the district's model API.

This guide is caller-facing and SDK-specific.

Updated October 7, 2026, for **0.15.3** and the current server runtime. Existing
**0.15.0–0.15.2** clients retain the same wire contract; 0.15.3 adds exception diagnostics.
Existing 0.13.x clients should upgrade to use completion caps, visitor IDs,
request correlation and final JSON Schema validation. The context repair is
server-side and requires no additional SDK version beyond 0.15.0.

## Capabilities at a glance

| Need | SDK capability |
| --- | --- |
| Generate or analyze text | Synchronous/asynchronous Python requests; JavaScript/TypeScript requests |
| Receive progress | SSE events, a final result, and typed stream errors |
| Return application data | JSON mode plus optional final `response_schema` validation |
| Execute tools | Built-in service allowlists, validated custom-service schemas, managed callbacks or caller-managed pending actions |
| Retain context | Hot sessions and durable caller-owned `vcache`, with automatic visitor-scoped remembered IDs |
| Control generation | Completion-token caps, iteration limits and supported sampling controls |
| Handle dependency failures | Guard/capacity outcome codes, retryability, retry delay and request IDs |
| Continue during paid-provider outages | Server-side provider fallback, including bounded text-only `1984-AIR` |

Tool availability, model capabilities and provider capacity depend on the server
deployment. A proposed tool call is not proof that an external action succeeded;
use the actual service result or your application's authoritative record.

## Guard outcomes, budgets, and caller identities

Model requests now expose `max_tokens` (default 4096, range 1–32768), `user_id`,
`request_id`, and optional `response_schema`. Supply a stable pseudonymous
`user_id` from your authenticated backend; never trust a browser override or
expose the API key. The server scopes guard identities to the API caller.
Authentication, disabled-key checks, ownership, and funding remain caller scoped.
One reused client keeps automatic sessions and remembered cache IDs separate for each `user_id`. Use an explicit cache ID for visitor cache lifecycle operations; supplying the same explicit cache ID intentionally shares that knowledge cache.
Rotating visitor IDs grants no exemption from input or callback screening.

Successful outcomes have `status="completed"`. Guard rejection returns
`code="guard_rejected"`, `retryable=False`; failed required review returns
`status="guard_unavailable"`, `retryable=True`, and `retry_after`. Provider
exhaustion returns `code="quota_exhausted"`; other dependency failures return
`model_unavailable`. `NinethAPIError` retains these fields and `request_id` in
buffered and streamed paths. Inspect this outcome before parsing application JSON.
Do not treat a deterministic block as a formatting error or retry immediately.

Fallback capacity is finite and shared. A workflow may require multiple requests.
On exhaustion, retain the full returned cooldown and request ID; do not retry it
as a JSON formatting problem. Contact your service operator if capacity remains
unavailable. SDK settings do not increase the service's allocation or bypass review.

`response_schema` requires `response_format="json"` and validates the final
answer against JSON Schema Draft 2020-12. Remote references are rejected. Strict
streaming withholds generated text until validated, then emits `validated_output`
and the final `result`; invalid output returns `structured_output_invalid`.
Draft model-response and completion events are withheld too; `result` remains
the authoritative completed outcome.
The final answer must meet your schema before you use it. Schema validation does
not guarantee that a requested action is authorized or has executed. Pending
caller actions and callback-schema failure payloads retain their distinct statuses.
Using durable memory can make memory services available even when other built-in
services are disabled. An explicit final schema remains enforced during those
workflows. Service-call validation and authorization still apply before execution;
only the validated final answer is exposed as completed application data.
If a worker exhausts its iterations without a final response, it returns
`structured_output_invalid`, HTTP 422, `retryable=false`; this is a contract
failure and must not be retried as a provider outage.

See [Outcome contract](#outcome-contract) for statuses and handling rules,
[strict JSON](#recipe-4b-validated-application-json) for a schema example, and
[visitor isolation](#recipe-6g-isolate-visitors-on-one-client) for session usage.

## AIR fallback and context limits

`1984-AIR` (also accepted as `1984A`) selects the text fallback directly.
Other public aliases may use fallback automatically when capacity is unavailable.
It has a **32,768-token total context window**
and **1,024-token maximum completion**. A larger `max_tokens` request is reduced
to 1,024 on this route. That budget applies per model turn, not to the whole
workflow; guard reviews and additional iterations have separate costs.

The window includes runtime/caller policies, service definitions, the current
task, injected history and the reserved completion. The runtime now counts the
actual model tokens. The old 64,000-character rejection has been fixed. A
262,144-character transport safeguard remains; characters and tokens are
different, so this is not a promise that a caller can submit that much text.

When needed, the service reduces older **server-injected** history, recall and
service summaries to fit the selected context window. It preserves the current
task, your policies, tool definitions and latest tool results. Stored memory is
not deleted, but older excerpts may no longer appear in the current prompt;
use an explicit recall step when they are needed. Callers cannot mark their own
input or instructions as removable.

Reduced history cannot preserve every fact from a large prompt. Request explicit
`memory_read`, journal or scratch recall for details outside the selected view.
Required security checks still apply to these requests.

If protected content still cannot fit, expect non-retryable
`input_capacity_exceeded`. Reduce/split the task or select a suitable larger
model; retrying the identical request will not change its capacity. AIR does
not support image/audio fallback. It is a small-model fallback, with different
quality and capabilities from the larger primary models.

Cold starts and finite shared capacity apply. A response can take several minutes.
Python's default buffered timeout is 300 seconds; choose application, proxy and
request-lease deadlines that accommodate the full workflow. Increasing
a client timeout does not increase the server's deadlines or token window.
Required security review remains active. Neither model selection nor
`base_system=False` bypasses it.

If you maintain server internals, use [README.md](../README.md).

## Table of Contents

- [Capabilities at a glance](#capabilities-at-a-glance)
- [Guard outcomes, budgets, and caller identities](#guard-outcomes-budgets-and-caller-identities)
- [AIR fallback and context limits](#air-fallback-and-context-limits)
- [Install](#install)
- [Quick Start](#quick-start)
- [JavaScript and TypeScript](#javascript-and-typescript)
- [Public Surface](#public-surface)
- [Client Construction](#client-construction)
- [Provider Notes](#provider-notes)
- [Model Catalog](#model-catalog)
- [Request Arguments (Complete)](#request-arguments-complete)
- [Payload Mapping Reference](#payload-mapping-reference)
- [VCache Lifecycle](#vcache-lifecycle)
- [Callback Lifecycle Matrix](#callback-lifecycle-matrix)
- [Full End-to-End Request (All Parameters)](#full-end-to-end-request-all-parameters)
- [Cookbook](#cookbook)
  - [Recipe 1: Health check](#recipe-1-health-check)
  - [Recipe 2: Per-request model override](#recipe-2-per-request-model-override)
  - [Recipe 3: Reasoning and sampling](#recipe-3-reasoning-and-sampling)
  - [Recipe 4: Request JSON output](#recipe-4-request-json-output)
  - [Recipe 4b: Validated application JSON](#recipe-4b-validated-application-json)
  - [Recipe 4c: Bound completion budgets](#recipe-4c-bound-completion-budgets)
  - [Recipe 5: Compute totals](#recipe-5-compute-totals)
  - [Recipe 6: Session continuity](#recipe-6-session-continuity-session)
  - [Recipe 6b–6d: VCache persistent memory, override, rename/delete](#recipe-6b-persistent-memory-partitions-with-vcache)
  - [Recipe 6f: Seed a vcache buffer directly with `vcache.upsert`](#recipe-6f-seed-a-vcache-buffer-directly-with-vcacheupsert)
  - [Recipe 6e: Async session and vcache lifecycle](#recipe-6e-async-session-and-vcache-lifecycle)
  - [Recipe 6g: Isolate visitors on one client](#recipe-6g-isolate-visitors-on-one-client)
  - [Recipe 7: Built-in services with `default_service` (alias table)](#recipe-7-built-in-services-with-default_service)
  - [Recipe 7b: Ridge strategy loop](#recipe-7b-ridge-strategy-loop)
  - [Recipe 8: Streaming with service progress](#recipe-8-streaming-with-service-progress)
  - [Recipe 9: Caller-managed include services (manual resume)](#recipe-9-caller-managed-include-services-manual-resume)
  - [Recipe 10: SDK-managed callback runtime](#recipe-10-sdk-managed-callback-runtime-include_service-object)
  - [Recipe 10b: Callback endpoint contract](#recipe-10b-callback-endpoint-contract-requestresponse)
  - [Recipe 10c: `callback: false` mode](#recipe-10c-caller-managed-include-service-payload-callback-false)
  - [Recipe 11: Local schema.py references](#recipe-11-local-schemapy-include-service-references)
  - [Recipe 12: Messaging (email + telegram)](#recipe-12-messaging-email--telegram)
  - [Recipe 13–13c: Email templates (inbound, outbound, shape lists, images)](#recipe-13-inboundoutbound-email-templates-with-global-callback-url)
  - [Recipe 15: Policy and guardrail](#recipe-15-policy-and-guardrail)
  - [Recipe 16: Async streaming](#recipe-16-async-streaming)
  - [Recipe 17: Parallel deputies with `use_deputy`](#recipe-17-parallel-deputies-with-use_deputy)
- [Response Shapes](#response-shapes)
- [Outcome contract](#outcome-contract)
- [Error Handling](#error-handling)
- [Practical Patterns](#practical-patterns)
- [Troubleshooting](#troubleshooting)
- [Versioning and Compatibility](#versioning-and-compatibility)

## Install

```bash
pip install --upgrade "nineth>=0.15.0"
export NINETH_API_KEY="your-api-key"
```

For Node.js 18 or newer:

```ini
# .npmrc
@districtt:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
```

```console
npm install @districtt/nineth@^0.15.0
```

For local development, set `GITHUB_TOKEN` to a GitHub personal access token with
`read:packages`. GitHub Actions in this repository uses its workflow `GITHUB_TOKEN`.
The Python distribution is `nineth`; the GitHub npm package is
`@districtt/nineth`. They use the same version. Python uses snake_case request
arguments; JavaScript uses camelCase and maps them to the same HTTP payload.

## Quick Start

### 1) Basic synchronous request

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Explain why plants need sunlight in two sentences.",
        base_system=False,
        default_service=False,
        max_tokens=128,
        max_iterations=1,
    )
    if response["status"] == "completed":
        print(response["final_response"])
```

Typical response shape:

```json
{
  "status": "completed",
  "code": null,
  "retryable": false,
  "retry_after": null,
  "request_id": "server-generated-request-id",
  "final_response": "Plants use sunlight to make food through photosynthesis. ...",
  "iterations": 1,
  "usage": {
    "prompt_tokens": 1200,
    "completion_tokens": 40,
    "total_tokens": 1240
  },
  "service_calls": [],
  "service_responses": [],
  "events": []
}
```

### 2) Basic asynchronous request

```python
import asyncio
from nineth import AsyncNinethClient

async def main() -> None:
    async with AsyncNinethClient(default_model="1984-m3-0424") as client:
        response = await client.model.request("Summarize crude oil in 5 bullets.")
        print(response["final_response"])

asyncio.run(main())
```

### 3) Streaming request

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    for event in client.model.request("Analyze ETH setup.", stream=True):
        if event["type"] == "model_delta":
            print(event["data"]["text"], end="", flush=True)
        elif event["type"] == "result":
            print("\n---")
            print(event["data"]["final_response"])
```

Stream event types you should handle:

- `accepted`
- `model_delta`
- `validated_output` (when a final JSON Schema is supplied)
- `service_call`
- `service_response`
- `awaiting_client_services` (manual callback mode)
- `result`
- `error` (converted to `NinethAPIError` during SDK iteration)

Catch `NinethAPIError` around iteration as well as request creation. An SSE
connection can be accepted before a guard/provider failure becomes known.

## JavaScript and TypeScript

The npm package is a zero-dependency ESM client with bundled TypeScript declarations.
It supports buffered requests, SSE streaming, automatic session and vcache continuity,
vcache lifecycle methods, callback payloads, messaging configuration, and built-in
service selection.

```javascript
import { NinethClient } from "@districtt/nineth";

const client = new NinethClient({
  apiKey: process.env.NINETH_API_KEY,
  defaultModel: "1984-m3-0424",
});

const result = await client.model.request("Give me a concise BTC market brief.", {
  session: true,
  userId: "backend-visitor-a",
  maxTokens: 256,
  vcache: { name: "market-brief" },
  defaultService: ["search_news", "data_get_current_price"],
});

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

Streaming returns an async iterable:

```javascript
for await (const event of client.model.request("Research ETH liquidity.", {
  stream: true,
  defaultService: ["search_web", "read"],
})) {
  if (event.type === "model_delta") {
    process.stdout.write(event.data?.text ?? "");
  }
}
```

The first session response supplies the process and vcache identifiers. Reuse the same
client, visitor ID and vcache name and the npm client sends those identifiers automatically. Use
`client.vcache.delete(...)`, `rename(...)`, and `upsert(...)` for explicit lifecycle
operations. The npm-specific README is in [`npm/nineth/README.md`](npm/nineth/README.md).

These current controls map to the same HTTP fields in both SDKs:

| Python argument | JavaScript/TypeScript option | HTTP field |
| --- | --- | --- |
| `max_tokens` | `maxTokens` | `max_tokens` |
| `user_id` | `userId` | `user_id` |
| `request_id` | `requestId` | `request_id` |
| `response_schema` | `responseSchema` | `response_schema` |
| `response_format` | `responseFormat` | `response_format` |

Response objects retain snake_case fields such as `request_id` and
`retry_after`; JavaScript exceptions expose `requestId` and `retryAfter`.
For JavaScript exceptions, `status` is the HTTP status and `outcome` is the
server outcome string. Python uses `status_code` for HTTP and `status` for the
outcome. An error after SSE acceptance may have no HTTP error status; use its
`code` and retryability.

The npm client supports caller-managed schemas and `clientServiceResults`;
your application executes pending calls and resumes them. Loading local
`schema.py` implementations and automatically dispatching HTTP callbacks are
Python SDK features. Do not assume the npm client runs a Python service file
or automatically executes a pending action.

## Public Surface

Most applications only need:

- `NinethClient`
- `AsyncNinethClient`
- `client.health()`
- `client.model.request(...)`
- `client.vcache.delete(...)`
- `client.vcache.rename(...)`
- `client.vcache.upsert(...)`

`AVAILABLE_MODELS` is exported for convenience.

## Client Construction

```python
import httpx
from nineth import NinethClient

client = NinethClient(
    base_url="https://weirdpablo--rooster-api.modal.run",
    api_key="...",
    default_model="1984-m3-0424",
    timeout=httpx.Timeout(300.0, connect=10.0),
    stream_timeout=httpx.Timeout(connect=10.0, read=None, write=60.0, pool=60.0),
    headers={"X-Caller": "research-worker-1"},
)
```

Environment fallbacks:

- `NINETH_API_KEY`
- `NINETH_BASE_URL`
- `NINETH_DEFAULT_MODEL` (or `NINETH_MODEL`)

## Provider Notes

Provider routing happens server-side. Public aliases such as `1984-*` and
`amari-*` use the service's available capacity. In automatic
mode, a provider failure before streamed output can move the request to the
next configured provider; a partial response never switches providers. Raw
identifiers outside this list are not a stable public compatibility contract.
The SDK passes the `model` argument unchanged.

## Model Catalog

Current SDK `AVAILABLE_MODELS`:

- `1984-AIR`
- `1984-m0-brute`
- `1984-m0-sm`
- `1984-m1-unified`
- `1984-m2-light`
- `1984-m2-preview`
- `1984-m3-0317`
- `1984-m3-0404`
- `1984-m3-0421`
- `1984-m3-0424`
- `1984-m3-0503`
- `1984-m3-0505`
- `1984-m3-0507`
- `1984-m3-0614`
- `1984-c0-0427`
- `1984-c1-0503`
- `1984-c1-0505`
- `1984-c1-0507`
- `1984-c1-0614`
- `1984-c1-mini`
- `amari-0524`

Note:

- `AVAILABLE_MODELS` is the SDK's baked-in convenience list.
- `1984A` is accepted as an alias of `1984-AIR`, although it is not a separate entry in the convenience list.
- Server deployments can expose additional raw provider names or future aliases that remain valid when passed as `model=` or `default_model=`.

## Request Arguments (Complete)

`client.model.request(...)` accepts:

| Parameter | Type / Values | Description |
|---|---|---|
| **Task identity** | | |
| `task_input` | `str` | Prompt / task text. Required. |
| `model` | `str` | Model name. Optional if `default_model` was set on the client. |
| `user_id` | `str`, optional | Stable backend-supplied pseudonymous visitor identity, scoped to the authenticated API caller. Omitting it shares the caller-level guard identity. |
| `request_id` | `str`, optional | Correlation ID, 1–128 characters from `A-Z a-z 0-9 _ . : -`. Generated by the server if omitted. Correlation alone does not deduplicate external writes. |
| **Generation controls** | | |
| `reasoning` | `disabled` \| `low` \| `medium` \| `high` | Reasoning effort level. |
| `show_reasoning` | `bool` | Include model reasoning in the response. |
| `temperature` | `float` | Sampling temperature. |
| `top_p` | `float` | Nucleus sampling probability. |
| `min_p` | `float` | Minimum probability filter. |
| `top_k` | `int` | Top-K filter. |
| `repetition_penalty` | `float` | Repetition penalty. |
| `presence_penalty` | `float` | Presence penalty. |
| `frequency_penalty` | `float` | Frequency penalty. |
| `seed` | `int` | Seed for deterministic sampling. |
| `max_tokens` | `int`, 1–32768 | Completion budget per model turn; default 4096. Reaches fallback providers; AIR caps it at 1024. Separate from input context and iteration count. |
| **Loop controls** | | |
| `max_iterations` | `int` | Cap on agentic loop iterations; default 10. A small task can use 1. Additional iterations and reviews consume capacity. |
| `continuous` | `bool` | Run the loop continuously until a stop signal. |
| **Inputs** | | |
| `images` | `list[str]` | Base64-encoded image strings. |
| `audio` | `list[str \| dict]` | Base64 strings or `{data, mime_type?, filename?}` objects. |
| **Runtime controls** | | |
| `policy` | `str` | Caller-supplied instructions for the task. Security screening applies before execution. |
| `guardrail` | `str` | Additional security requirements for your application. These cannot weaken the service's security checks. |
| `base_system` | `bool` | Legacy provider-path compatibility control. |
| **Memory continuity** | | |
| `session` | `bool` | Keep using the active hot conversational session for the same visitor and memory scope on this client. |
| `vcache` | `dict` | Caller-owned persistent memory scope identified by `name` and `cache_id`. |
| `vcache.name` | `str` | Scope name; required when `vcache` is provided. |
| `vcache.cache_id` | `str` | Optional; omitted IDs are generated server-side and returned in the response. Later explicit IDs override a previously remembered generated ID for the same `vcache.name` on that client instance. |
| **Service controls** | | |
| `default_service` | `False` \| `True` \| `list[str]` | Disable built-ins, enable all built-ins, or provide an explicit allowlist. Default `False`. Custom included services are configured separately; security checks remain active. |
| `include_service` | `list \| dict` | Caller-managed services, or callback object via `callback: true` / `callback: false` / `callback: "https://..."`. |
| `client_service_results` | `list[dict]` | Manual callback resume payloads. |
| `callback_url` | `str` | Shared callback URL inherited by managed include-service flows and email templates that omit their own URL. |
| `use_deputy` | `bool` | Enable the `deputy` service for this request, allowing the model to spin up parallel analysis deputies. Default `False`. Set to `True` only when the task is complex enough to benefit from parallel analysis; each deputy consumes additional tokens. |
| **Output controls** | | |
| `stream` | `bool` | Enable SSE streaming. |
| `response_format` | `text` \| `json` | Output format. |
| `response_schema` | `dict`, optional | Final JSON Schema Draft 2020-12 contract; requires `response_format="json"`. Remote references are rejected. Validated final output is withheld from streaming until it passes. Does not validate tool parameters. |
| `compute` | `bool` | Include simplified total token consumption in the response. |
| `verbose` | `bool` | Verbose output (legacy alias: `debug`). |
| `system_prompt` | `str` | Legacy alias for `policy`; `policy` wins when both are supplied. |
| `debug` | `bool` | Legacy alias that enables `verbose`. |
| **Messaging transport** | | |
| `messaging.email` | `dict` | Email delivery configuration. |
| `messaging.email.use_cache` | `bool` | When `true`, the request's vcache is attached to this mailbox so the inbound email processor uses it for session memory. |
| `messaging.telegram` | `dict` | Telegram delivery configuration. Use `name` for a logical server-registered bot or `botId` for an already-known internal bot ID. |
| `messaging.telegram.use_cache` | `bool` | When `true`, the request's vcache is attached to this bot so the inbound Telegram processor uses it for session memory. |

`messaging.*.use_cache=True` requires `vcache`. The resolved binding is owned by the authenticating API key; another key cannot take over the same mailbox, bot binding, process id, context id, or vcache identity.

## Payload Mapping Reference

`_build_payload(...)` in the SDK maps request arguments into the API payload with the following rules.

Core fields always emitted:

- `task_input`
- `model`
- `max_iterations`
- `max_tokens` (default 4096, per model turn)
- `show_reasoning`
- `continuous` (`continuous` arg or derived as `max_iterations > 10`)
- `session`
- `base_system` (legacy compatibility)
- `default_service` (boolean or expanded/merged service list)
- `use_deputy` (always emitted; `False` by default)
- `verbose`

Conditionally emitted fields:

- `reasoning` -> `reasoning_effort`
- `user_id` -> `user_id`
- `request_id` -> `request_id` (the server generates one if omitted)
- `response_schema` -> `response_schema`, including an explicitly empty schema `{}`
- `temperature` -> `temperature`
- `top_p` -> `top_p`
- `min_p` -> `min_p`
- `top_k` -> `top_k`
- `repetition_penalty` -> `repetition_penalty`
- `presence_penalty` -> `presence_penalty`
- `frequency_penalty` -> `frequency_penalty`
- `seed` -> `seed`
- `include_service` -> `include_service` (deduplicated list/object form; `callback: false` is preserved)
- `client_service_results` -> `client_service_results`
- `images` -> `images`
- `audio` -> `audio`
- `policy` -> `policy`
- `guardrail` -> `guardrail`
- `messaging` -> normalized `messaging`
- `callback_url` -> normalized `callback_url`
- `response_format="json"` -> `response_format: "json"`
- `compute=True` -> `compute: true`
- `vcache` -> `vcache` (name-only requests are allowed; the server fills `cache_id` when omitted)
- remembered SDK session id -> `process_id` (internal resume wire field when `session=True`)

Vcache continuity rules:

- if the first request uses `vcache={"name": "workspace"}`, the server returns a generated `cache_id`
- the SDK remembers that generated id per visitor + `vcache.name` for later name-only requests on the same client instance
- a later explicit `vcache={"name": "workspace", "cache_id": "chosen-id"}` takes precedence over the remembered generated id
- once that explicit request succeeds, the SDK remembers the explicit `cache_id` for subsequent name-only requests on the same client instance

Validation rule:

- `client_service_results` requires `session=True` and an already established session for that client + visitor + vcache scope. Keep the same visitor ID when resuming.

Service auto-enable rule:

- Email messaging can auto-enable `send_email`/`send_reply` when outbound behavior is needed.
- Telegram messaging auto-enables plain and Bot API 10.1 rich send/edit services.
- When `vcache` is set, memory management services are automatically added to `default_service` so the model has persistent memory access without the caller needing to enumerate service names. This covers journal (read/write/search), knowledge archive (`search_knowledge`, `memory_read`), scratch (ephemeral working notes), documents, and media. The auto-enable follows the same merge rules as messaging: if `default_service` was `False`, it becomes an explicit allowlist; if it was already a list, the memory service names are merged in without duplicates; if it was `True` (all services), it stays `True`.
- When `use_deputy=True`, `deputy` is merged into the effective `default_service` list using the same rules. When `use_deputy=False` (default), any `deputy` entry is stripped from the list even if provided explicitly.

Callback propagation rule:

- Top-level `callback_url` is normalized and propagated into email templates (inbound or outbound) missing `url`.
- If `include_service` is list-form and `callback_url` exists, payload is promoted to object-form with callback + schema.
- If `include_service.callback` is `false`, the SDK preserves that flag in the emitted object even when a global `callback_url` is present.
- If `include_service.callback` is `true`, the SDK treats it as a mode flag and uses the top-level `callback_url`.

## VCache Lifecycle

Use the `client.vcache` namespace when you want to manage a provisioned durable memory scope directly instead of only referencing it from `client.model.request(...)`.

Delete a vcache instance:

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    client.model.request(
        "Remember that this workspace only covers power markets.",
        session=True,
        vcache={"name": "research-team"},
    )

    deleted = client.vcache.delete(name="research-team")
    print(deleted["found"])
```

Rename a vcache instance while keeping the same `cache_id`:

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    client.model.request(
        "Remember that this workspace only covers power markets.",
        session=True,
        vcache={"name": "research-team"},
    )

    renamed = client.vcache.rename(name="research-team", new_name="power-team")
    print(renamed["vcache"])
```

Lifecycle rules:

- all three lifecycle methods require an API key, like normal model requests
- all three methods accept `cache_id=` explicitly; when it is omitted, the SDK tries to reuse the remembered `cache_id` for that `name`
- `client.vcache.delete(...)` removes the selected cache and its stored memory
- `client.vcache.rename(...)` changes the selected cache's name while retaining its stored memory
- `client.vcache.upsert(...)` appends state-entry dicts to the vcache buffer without affecting the durable memory tree structure
- every lifecycle call is authorized against the API key that first claimed the vcache identity; cross-key access returns `403`
- delete, rename, and upsert require access to the selected cache scope
- successful `delete` clears the client's remembered `cache_id` and session id for that vcache scope
- successful `rename` transfers the client's remembered `cache_id` and session id from the old name to the new name
- Legacy lifecycle path fields are deprecated and empty. Use `vcache.name`, `vcache.cache_id`, `success` and `found` to track the selected cache and operation result.
- `upsert` does not mutate the client's remembered state

## Callback Lifecycle Matrix

| Surface | Callback source | Runtime events | Purpose |
| --- | --- | --- | --- |
| `include_service` (managed mode) | `include_service.callback.url` or global `callback_url` | `service_call`, `interlude`, `final_response` | Execute caller-owned tools and request missing service parameters. |
| inbound email templates | template `url` or global `callback_url` | `inbound_email_received` (pre-model), `inbound_email_interlude`, `inbound_email_model_response` (post-model) | Acknowledge inbound payloads, request template variables, and publish final model output trace. |
| outbound email templates | template `url` or global `callback_url` fallback (`messaging.email.callback_url`) | `outbound_email_interlude` when model requests template variables | Request caller-owned variables before template rendering/sending. |

Interlude payloads include:

- `template_name`
- `template_type` (`inbound` or `outbound`)
- `required_fields`
- `known_parameters`
- `received` and `resend_email` context when available

Callback responses for model-resume surfaces may also include:

- `status`: caller-defined state such as `ok`, `warn`, `failed`, or an application-specific value
- `message`: caller-authored guidance that is surfaced back to the model on the resumed turn

The same response envelope is recognized across all three callback URL surfaces:

1. `include_service.callback.url`
2. inbound email template `url`
3. top-level `callback_url` when the SDK or API inherits it into `include_service` or email templates

Model-visible behavior:

- `include_service` `service_call` and `interlude` callbacks preserve `status` and `message` into the resumed `client_service_results`
- inbound email `inbound_email_received` pre-model callbacks preserve `status` and `message` into the email runtime context before the model replies
- email template interludes (`inbound_email_interlude`, `outbound_email_interlude`) preserve `status` and `message` alongside returned parameters
- post-model callbacks such as `final_response` and `inbound_email_model_response` accept the same envelope, but any returned `status` and `message` are recorded only for observability because the model turn is already complete

For caller-managed schemas, `strict: true` is enforced before a call becomes
externally visible. Validation covers nested required fields, scalar/object/array
types, enums, constants, bounds, `additionalProperties`, and supported
`anyOf`/`oneOf`/nullable forms. Invalid proposals are repaired internally within
the bounded model loop. Exhaustion returns:

```json
{
  "status": "service_schema_validation_failed",
  "service_name": "create_briefing",
  "violations": [
    {
      "path": "case_files[0].size",
      "message": "Expected one of: small, medium, large"
    }
  ]
}
```

No invalid call appears in `pending_client_calls`. Nineth assigns `call_id` only
after validation succeeds; the model and application never create it.

## Full End-to-End Request (All Parameters)

This section shows the same request at three levels:

- the SDK call your application writes
- the JSON payload the SDK sends to `/model`
- the buffered response shape you should expect back

### Complete SDK call

```python
from nineth import NinethClient

weather_schema = {
        "name": "get_weather",
        "description": "Resolve weather by city",
        "parameters": {
                "type": "object",
                "properties": {
                        "location": {"type": "string"},
                        "units": {"type": "string"},
                },
                "required": ["location"],
        },
}

with NinethClient(default_model="1984-m3-0424") as client:
        response = client.model.request(
                task_input="Draft an outbound response after checking weather and inbox context.",
                model="1984-m3-0424",
                reasoning="medium",
                show_reasoning=False,
                temperature=0.5,
                top_p=0.9,
                min_p=0.05,
                top_k=40,
                repetition_penalty=1.05,
                presence_penalty=0.1,
                frequency_penalty=0.1,
                seed=7,
                max_iterations=8,
                max_tokens=512,
                user_id="backend-visitor-ops-alice",
                request_id="ops-request-001",
                continuous=False,
                images=["<base64-image>"],
                audio=[{"data": "<base64-audio>", "mime_type": "audio/mpeg", "filename": "brief.mp3"}],
                policy="Use concise ops language.",
                guardrail="Never reveal credentials.",
                session=True,
                vcache={"name": "ops-desk", "cache_id": "alice"},
                base_system=True,
                default_service=["browser", "read"],
                include_service={"callback": True, "schema": [weather_schema]},
                client_service_results=None,  # supply results only when resuming a paused session
                callback_url="https://app.example.com/unified-callback",
                stream=False,
                response_format="json",
                response_schema={
                        "type": "object",
                        "properties": {"summary": {"type": "string"}, "risk": {"type": "string"}},
                        "required": ["summary", "risk"],
                        "additionalProperties": False,
                },
                compute=True,
                verbose=True,
                messaging={
                        "email": {
                                "address": "helpdesk@example.com",
                                "name": "Helpdesk Bot",
                                "instruction": "Classify ticket urgency.",
                                "templates": [
                                        {
                                                "type": "inbound",
                                                "shape": [
                                                        {
                                                                "name": "ticket_reply_primary",
                                                                "subject": "Ticket {{ticket_id}} Update",
                                                                "html": "<p>{{summary}}</p>",
                                                        },
                                                        {
                                                                "name": "ticket_reply_escalation",
                                                                "subject": "Escalation {{ticket_id}}",
                                                                "html": "<p>{{action_required}}</p>",
                                                        },
                                                ],
                                        },
                                        {
                                                "type": "outbound",
                                                "name": "ticket_outbound_notice",
                                                "recipients": ["owner@example.com"],
                                                "shape": [
                                                        {
                                                                "name": "ticket_notice_primary",
                                                                "subject": "Notice {{ticket_id}}",
                                                                "html": "<p>{{body}}</p>",
                                                        }
                                                ],
                                        },
                                ],
                        },
                        "telegram": {
                                "botId": "ops-bot",
                                "chatId": "12345",
                        },
                },
        )
```

### Representative emitted HTTP payload

The control fields below use their wire names. Service/messaging lists are
abbreviated; vcache and transport configuration also merge the corresponding
memory and messaging services into the effective allowlist.

```json
{
    "task_input": "Draft an outbound response after checking weather and inbox context.",
    "model": "1984-m3-0424",
    "reasoning_effort": "medium",
    "show_reasoning": false,
    "temperature": 0.5,
    "top_p": 0.9,
    "min_p": 0.05,
    "top_k": 40,
    "repetition_penalty": 1.05,
    "presence_penalty": 0.1,
    "frequency_penalty": 0.1,
    "seed": 7,
    "max_iterations": 8,
    "max_tokens": 512,
    "user_id": "backend-visitor-ops-alice",
    "request_id": "ops-request-001",
    "continuous": false,
    "images": ["<base64-image>"],
    "audio": [
        {
            "data": "<base64-audio>",
            "mime_type": "audio/mpeg",
            "filename": "brief.mp3"
        }
    ],
    "policy": "Use concise ops language.",
    "guardrail": "Never reveal credentials.",
    "session": true,
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "base_system": true,
    "default_service": ["search_web", "read"],
    "include_service": {
        "callback": {"url": "https://app.example.com/unified-callback"},
        "schema": [
            {
                "name": "get_weather",
                "description": "Resolve weather by city",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {"type": "string"},
                        "units": {"type": "string"}
                    },
                    "required": ["location"]
                }
            }
        ]
    },
    "callback_url": "https://app.example.com/unified-callback",
    "response_format": "json",
    "response_schema": {
        "type": "object",
        "properties": {"summary": {"type": "string"}, "risk": {"type": "string"}},
        "required": ["summary", "risk"],
        "additionalProperties": false
    },
    "compute": true,
    "verbose": true,
    "messaging": {
        "email": {
            "address": "helpdesk@example.com",
            "name": "Helpdesk Bot",
            "instruction": "Classify ticket urgency.",
            "templates": [
                {
                    "type": "inbound",
                    "shape": [
                        {
                            "name": "ticket_reply_primary",
                            "subject": "Ticket {{ticket_id}} Update",
                            "html": "<p>{{summary}}</p>"
                        },
                        {
                            "name": "ticket_reply_escalation",
                            "subject": "Escalation {{ticket_id}}",
                            "html": "<p>{{action_required}}</p>"
                        }
                    ]
                },
                {
                    "type": "outbound",
                    "name": "ticket_outbound_notice",
                    "recipients": ["owner@example.com"],
                    "shape": [
                        {
                            "name": "ticket_notice_primary",
                            "subject": "Notice {{ticket_id}}",
                            "html": "<p>{{body}}</p>"
                        }
                    ]
                }
            ]
        },
        "telegram": {
            "botId": "ops-bot",
            "chatId": "12345"
        }
    }
}
```

Important request notes:

- on a first request you may omit `vcache.cache_id`; the server will generate one UUID and return it in `response["vcache"]`
- on later `session=True` requests the SDK may inject `process_id` automatically behind the scenes to resume the active hot session for that same vcache scope
- if you later send an explicit `vcache.cache_id`, that explicit id wins over any previously remembered generated id and becomes the remembered id for future name-only calls on that client
- this is an illustrative configuration combining media, tools and messaging; replace placeholders and enable only the capabilities your task needs. AIR cannot handle the media inputs in this example.
- `client_service_results=None` is omitted from the first HTTP request. Supply actual results only after `awaiting_client_services`, using its issued call IDs and the same visitor/session; see Recipe 9.
- `response_schema` validates the final application answer. Tool parameter schemas and the control/error envelope are separate contracts.

### Exhaustive buffered response shape

```json
{
    "status": "completed",
    "code": null,
    "retryable": false,
    "retry_after": null,
    "request_id": "ops-request-001",
    "final_response": {
        "summary": "Ticket classified and customer updated",
        "risk": "low"
    },
    "raw_response": "{\"summary\":\"Ticket classified and customer updated\",\"risk\":\"low\"}",
    "usage": {
        "prompt_tokens": 2140,
        "completion_tokens": 490,
        "total_tokens": 2630
    },
    "compute": 2630,
    "thinking": [
        "Checked inbound context.",
        "Resolved tool output.",
        "Drafted outbound reply."
    ],
    "service_calls": [
        {
            "service_name": "get_weather",
            "call_id": "client_1_1",
            "params": {"location": "Nairobi"}
        }
    ],
    "service_responses": [
        {
            "service_name": "get_weather",
            "success": true,
            "result": {"location": "Nairobi", "forecast": "Partly cloudy"}
        }
    ],
    "artifacts": [
        {
            "type": "email_template",
            "name": "ticket_outbound_notice",
            "status": "configured"
        }
    ],
    "iterations": 3,
    "events": [
        {
            "type": "mailbox_configured",
            "data": {
                "address": "helpdesk@example.com",
                "inbound_uuid": "inb_abc123"
            }
        },
        {
            "type": "service_response",
            "data": {
                "service_name": "send_reply",
                "success": true
            }
        }
    ],
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "session_id": "proc_ops_desk_alice_01"
}
```

Response field notes:

- inspect `status` before consuming application data; errors raise `NinethAPIError`, while pending actions and callback-validation failures remain distinct responses
- `request_id` correlates the operation; `code`, `retryable` and `retry_after` carry control information separately from `final_response`
- `final_response` is the caller-facing result; in `response_format="json"` mode it is parsed into a dict/list when possible
- `raw_response` is present only when JSON mode preserved the original raw string alongside parsed output
- `usage` always reports token accounting when the server provides it
- `compute` is present only when `compute=True`
- `thinking` is present only when reasoning output is surfaced
- `service_calls` and `service_responses` are ordered requests/results; a call entry alone does not prove that its external action succeeded
- `artifacts` contains durable side effects the runtime chose to expose
- `events` is the broader execution trace, including messaging and callback milestones
- `vcache` is present when a durable vcache scope is active
- `session_id` is present when `session=True` is active; it is the caller-facing alias of the underlying resume `process_id`

Expected callback payload families for the same request:

| Phase | Event type | Key fields |
| --- | --- | --- |
| include-service execution | `service_call` | `service_name`, `params`, `available_services`, `process_id` |
| include-service missing params | `interlude` | `service_name`, `required_fields`, `known_parameters`, `reason` |
| include-service final | `final_response` | `final_response`, `process_id` |
| inbound pre-model | `inbound_email_received` | `phase=pre_model`, `received`, `resend_email`, `available_templates` |
| template variable fetch | `inbound_email_interlude` / `outbound_email_interlude` | `template_name`, `template_type`, `required_fields`, `known_parameters` |
| inbound post-model | `inbound_email_model_response` | `phase=post_model`, `model_response`, `service_calls`, `service_responses` |

## Cookbook

### Recipe 1: Health check

```python
from nineth import NinethClient

with NinethClient() as client:
    print(client.health())
```

Notes:

- `client.health()` is API-key protected like other runtime endpoints.
- If authentication is missing/invalid, the server returns auth errors rather than a public health payload.
- Server schema/docs endpoints are typically hidden in production unless the server is started with `ROOSTER_EXPOSE_OPENAPI=true`.

Typical response:

```json
{"status": "ok", "timestamp": "2026-05-24T00:00:00+00:00"}
```

### Recipe 2: Per-request model override

```python
with NinethClient(default_model="1984-m2-preview") as client:
    a = client.model.request("fast summary")
    b = client.model.request("deeper review", model="1984-m3-0424")
```

### Recipe 3: Reasoning and sampling

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Build a scenario tree for BTC next week.",
        reasoning="high",
        temperature=0.4,
        top_p=0.9,
        seed=7,
    )
```

### Recipe 4: Request JSON output

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Return a JSON object with keys trend, risk, levels.",
        response_format="json",
    )

print(type(response["final_response"]))  # dict if JSON parse succeeded
print(response.get("raw_response"))      # original string is preserved
```

Typical JSON-mode response:

```json
{
  "final_response": {
    "trend": "neutral",
    "risk": "medium",
    "levels": ["68000", "70000"]
  },
  "raw_response": "{\"trend\":\"neutral\",...}",
  "iterations": 1
}
```

### Recipe 4b: Validated application JSON

Use a schema when your application requires specific fields/types. JSON mode
alone requests/parses JSON; it does not enforce your application's shape.

```python
from uuid import uuid4
from nineth import NinethClient

summary_schema = {
    "type": "object",
    "properties": {
        "summary": {"type": "string"},
        "next_step": {"type": ["string", "null"]},
    },
    "required": ["summary", "next_step"],
    "additionalProperties": False,
}

with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Summarize: review is Friday; prepare the agenda. Return summary and next_step as JSON.",
        response_format="json",
        response_schema=summary_schema,
        max_tokens=256,
        max_iterations=1,
        base_system=False,
        default_service=False,
        user_id="backend-visitor-a",
        request_id=uuid4().hex,
    )
    if response["status"] == "completed":
        print(response["final_response"]["summary"])
```

For strict streaming, pass the same schema with `stream=True`. Generated
answer text is withheld until validation; expect `validated_output` followed
by a final `result`. Tool/progress events can still occur. Do not wait only
for `model_delta` to obtain a strict final answer. Invalid final JSON/schema
raises `NinethAPIError` with `code="structured_output_invalid"`.

JavaScript/TypeScript uses the same contract:

```javascript
import { randomUUID } from "node:crypto";
import { NinethClient } from "@districtt/nineth";

const client = new NinethClient({
  apiKey: process.env.NINETH_API_KEY,
  defaultModel: "1984-m3-0424",
});
const response = await client.model.request(
  "Summarize: review is Friday; prepare the agenda. Return summary and next_step as JSON.",
  {
    responseFormat: "json",
    responseSchema: {
      type: "object",
      properties: {
        summary: { type: "string" },
        next_step: { type: ["string", "null"] },
      },
      required: ["summary", "next_step"],
      additionalProperties: false,
    },
    maxTokens: 256,
    maxIterations: 1,
    baseSystem: false,
    defaultService: false,
    userId: "backend-visitor-a",
    requestId: randomUUID(),
  },
);
if (response.status === "completed") console.log(response.final_response.summary);
```

Final schemas do not validate pending tool parameters or replace callback
authorization. Declare tool parameters separately, enable `strict=True` on
custom schemas, and execute only valid authorized calls. A pending action is
not a completed application response.

### Recipe 4c: Bound completion budgets

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Give three short names for a neighborhood reading club.",
        max_tokens=128,
        max_iterations=1,
        base_system=False,
        default_service=False,
    )
```

`max_tokens` reserves output capacity per model turn. It does not enlarge the
input window or cap the full workflow's total token usage. Use small caps for
short answers or JSON decisions; leave sufficient room for the complete
answer. Too-small caps can truncate output and fail schema validation.
Provider fallback receives the cap, subject to model-specific limits such as
AIR's 1,024-token maximum. Required security reviews remain separate and cannot be
disabled by lowering this value.

### Recipe 5: Compute totals

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request("Explain carry trade risk.", compute=True)
    print(response.get("compute"))
```

### Recipe 6: Session continuity (`session`)

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    first = client.model.request("Remember: my risk budget is medium.", session=True)

    second = client.model.request(
        "What risk budget did I set?",
        session=True,
    )
    print(second["final_response"])
```

Important rule:

- keep using the same `NinethClient` instance when you want `session=True` to auto-reuse the latest session id.

### Recipe 6b: Persistent memory partitions with `vcache`

_Use when you need durable memory that survives session resets, identified by a caller-controlled scope name and cache ID._

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    r1 = client.model.request(
        "Remember that this workspace tracks only energy equities.",
        session=True,
        vcache={"name": "research-team"},
    )

    resolved_vcache = r1["vcache"]

    r2 = client.model.request(
        "What domain did I say this workspace tracks?",
        session=True,
        vcache={"name": "research-team"},
    )

    print(resolved_vcache["cache_id"])
    print(r1["session_id"])
    print(r2["final_response"])
```

`vcache` behavior:

- creates/uses the selected persistent cache across sessions
- omitting `cache_id` on the first request creates one UUID and returns it as `response["vcache"]["cache_id"]`
- subsequent requests from the same `NinethClient` may keep passing only `{"name": ...}`; the client reuses the resolved `cache_id`
- a later explicit `{"name": ..., "cache_id": ...}` overrides the previously remembered generated id for that same `name`
- the filesystem scope persists independently of individual sessions
- `session=True` reuses only the hot conversational session for that vcache scope
- starting a new session in the same vcache scope clears hot conversational state while keeping durable memory artifacts in that vcache path

### Recipe 6c: Override a generated `cache_id`

_Use when you want to replace the server-generated `cache_id` with a deterministic caller-supplied ID mid-conversation — for example, to pin a workspace to a known identifier after initial discovery._

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    first = client.model.request(
        "Remember that this workspace is for North Sea gas only.",
        session=True,
        vcache={"name": "energy-desk"},
    )

    second = client.model.request(
        "Keep using the same durable workspace, but pin my own id now.",
        session=True,
        vcache={"name": "energy-desk", "cache_id": "desk-alpha"},
    )

    third = client.model.request(
        "Which market did I say this desk covers?",
        session=True,
        vcache={"name": "energy-desk"},
    )

    print(first["vcache"])
    print(second["vcache"])
    print(third["vcache"])
```

Expected behavior:

- the first response returns a server-generated `cache_id`
- the second request's explicit `cache_id="desk-alpha"` overrides that generated id for this client instance
- the third name-only request reuses `desk-alpha`

### Recipe 6d: Rename or delete a durable vcache

_Use to rename a vcache scope (e.g. after project handoff) or to fully tear down the durable memory tree when it is no longer needed._

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    client.model.request(
        "Remember that this workspace covers only LNG shipping.",
        session=True,
        vcache={"name": "shipping-desk"},
    )

    renamed = client.vcache.rename(name="shipping-desk", new_name="lng-desk")
    print(renamed["vcache"])

    deleted = client.vcache.delete(name="lng-desk")
    print(deleted["found"])
```

Operational semantics:

- `rename` keeps the same `cache_id` and moves the durable files to the new `name`
- `delete` removes the full provisioned memory tree for that `name` + `cache_id`
- after a successful lifecycle operation, the SDK updates or clears its remembered local state for that vcache scope

### Recipe 6f: Seed a vcache buffer directly with `vcache.upsert`

_Use when you want to pre-populate a vcache with state entries before running a model request, or to append entries at any time without going through a model turn._

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    # First request to provision the vcache and remember its cache_id
    client.model.request(
        "Start tracking this research session.",
        session=True,
        vcache={"name": "research-desk"},
    )

    # Directly write state entries into the vcache buffer
    result = client.vcache.upsert(
        name="research-desk",
        data=[
            {
                "id": "00000000-0000-0000-0000-000000000001",
                "statetag": "task_input",
                "content": {"text": "Background: LNG shipping market context."},
                "importance": 0.8,
                "timestamp": "2025-01-01T00:00:00+00:00",
            },
            {
                "id": "00000000-0000-0000-0000-000000000002",
                "statetag": "memory_summary",
                "content": {"text": "User is researching LNG tanker rates."},
                "importance": 0.9,
                "timestamp": "2025-01-01T00:00:01+00:00",
            },
        ],
    )
    print(result["written"])  # 2
```

`upsert` rules:

- appends entries to the vcache buffer file; does **not** overwrite existing content
- requires a known `cache_id` (either remembered from a prior request or provided explicitly via `cache_id=`)
- each entry in `data` must be a dict; non-dict items are silently skipped
- valid `statetag` values: `task_input`, `model_output`, `service_call`, `service_response`, `interrupt`, `memory_summary`
- does **not** update the SDK's local session or vcache id state

### Recipe 6e: Async session and vcache lifecycle

_Drop-in async version of the full vcache lifecycle (Recipes 6b–6d). Use in async-native applications or when all model operations must be non-blocking._

All vcache and model operations work identically in async mode:

```python
import asyncio
from nineth import AsyncNinethClient

async def main() -> None:
    async with AsyncNinethClient(default_model="1984-m3-0424") as client:
        # Initial request — server generates cache_id
        r1 = await client.model.request(
            "Remember that this workspace covers only energy equities.",
            session=True,
            vcache={"name": "async-research"},
        )
        print(r1["vcache"]["cache_id"])   # server-generated UUID
        print(r1["session_id"])

        # Continue same session
        r2 = await client.model.request(
            "What domain did I say this workspace covers?",
            session=True,
            vcache={"name": "async-research"},
        )
        print(r2["final_response"])

        # Rename the durable scope
        renamed = await client.vcache.rename(
            name="async-research",
            new_name="async-energy",
        )
        print(renamed["vcache"])

        # Delete the durable scope
        deleted = await client.vcache.delete(name="async-energy")
        print(deleted["found"])

asyncio.run(main())
```

### Recipe 6g: Isolate visitors on one client

Supply a stable pseudonymous ID derived on your authenticated backend for
each visitor. Keep it unchanged across turns and callback resumes. A shared
client can remember separate sessions and generated cache IDs for each ID:

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    for visitor in ("backend-visitor-a", "backend-visitor-b"):
        client.model.request(
            "Remember that my preferred report format is a short paragraph.",
            user_id=visitor,
            session=True,
            vcache={"name": "report-preferences"},
            max_tokens=128,
            base_system=False,
        )

    response = client.model.request(
        "What report format did I choose?",
        user_id="backend-visitor-a",
        session=True,
        vcache={"name": "report-preferences"},
        max_tokens=128,
        base_system=False,
    )
```

JavaScript uses `userId` with the same `session`/`vcache` options. Reusing the
same **explicit** cache ID deliberately shares that knowledge cache; visitor
IDs do not make a shared explicit cache private. Vcache lifecycle calls do
not take a visitor argument: use the explicit cache ID returned for the
intended visitor. Guard identity isolation does not attest an end-user login;
your backend must bind the pseudonym to its own verified session. Keep API
keys private and retain application-level authorization/funding controls.

### Recipe 7: Built-in services with `default_service`

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Search web and summarize today's semiconductor headlines.",
        default_service=["browser", "read"],
    )
```

Notes:

- `default_service=True` enables all built-ins.
- `default_service=False` disables built-ins.
- List mode accepts group aliases such as `browser`, `knowledge`, `computer`, `workspace`, `voice`, `trading`, `ridge`.
- `"search"` is a recognized alias for `"browser"` — both expand to the same set.
- Alias expansion is automatic, and duplicate service names are deduplicated.
- Individual service names (e.g. `"search_web"`, `"voice_generate"`) can be listed directly alongside aliases.

#### Complete `default_service` alias expansion reference

| Alias | Expanded service names |
|---|---|
| `browser` (or `search`) | `search_web`, `search_news`, `search_discussions`, `search_unified`, `search_context`, `search_places`, `search_local_pois`, `search_poi_descriptions`, `search_rich`, `search_videos`, `search_images`, `search_answers`, `read`, `deepsearch` |
| `knowledge` | `list_documents`, `read_document_metadata`, `search_documents`, `read_document`, `read_document_markdown`, `search_knowledge`, `journal_read`, `journal_list`, `journal_search`, `memory_read`, `media_list`, `media_recall`, `media_search`, `media_read_manifest` |
| `computer` | `create_sandbox`, `sandbox_status`, `run`, `destroy_sandbox`, `sandbox`, `volume_write_file`, `volume_read_file`, `volume_search_replace`, `volume_list`, `create_scratch`, `write_scratch`, `read_scratch`, `list_scratches`, `search_scratches`, `delete_scratch`, `index_document`, `delete_document`, `journal_write`, `journal_search_replace`, `journal_delete`, `media_write_manifest`, `media_write_transcript`, `media_update`, `media_decompress`, `media_delete`, `set_alarm`, `schedule_at`, `get_current_time`, `cancel_alarm`, `set_plan`, `get_plan`, `update_plan`, `clear_plan` |
| `workspace` | `gh_clone`, `gh_new`, `gh_run`, `gh_commit`, `gh_push`, `gh_pull`, `gh_branch`, `gh_status`, `gh_pr`, `gh_list` |
| `voice` | `voice_list`, `voice_generate`, `voice_transcribe` |
| `trading` | `fund_balances`, `data_get_current_price`, `data_get_historical_ohlc`, `data_get_market_buffer`, `data_get_live_ticks`, `data_get_available_symbols`, `portfolio_list`, `portfolio_add`, `portfolio_update`, `portfolio_remove`, `performance`, `ridge_status`, `ridge_heartbeat`, `ridge_run`, `ridge_seed`, `ridge_telemetry`, `ridge_broker_manifest`, `ridge_paper_account`, `ridge_price_data`, `ridge_alt_data`, `ridge_tradable_assets`, `ridge_paper_execution`, `initialize_client`, `get_terminal_status`, `get_account_snapshot`, `get_open_positions`, `get_pending_orders`, `get_closed_orders`, `get_available_symbols`, `get_current_price`, `get_historical_ohlc`, `get_live_ticks`, `get_market_buffer`, `get_available_symbols`, `trade_market_buy`, `trade_market_sell`, `trade_buy_limit`, `trade_sell_limit`, `trade_buy_stop`, `trade_sell_stop`, `trade_buy_stop_limit`, `trade_sell_stop_limit`, `trade_modify_position`, `trade_close_position_partial`, `trade_close_position_full`, `trade_close_position_by_opposite`, `trade_cancel_order` |
| `ridge` | `ridge_status`, `ridge_heartbeat`, `ridge_run`, `ridge_seed`, `ridge_telemetry`, `ridge_broker_manifest`, `ridge_paper_account`, `ridge_price_data`, `ridge_alt_data`, `ridge_tradable_assets`, `ridge_paper_execution` |

Mix aliases and individual names freely — the SDK deduplicates:

```python
# aliases expand then single names are appended without repeating
response = client.model.request(
    "...",
    default_service=["browser", "search_web", "voice", "journal_write"],
    # search_web already in browser group — silently skipped
    # journal_write added explicitly after computer group would also include it
)
```

### Recipe 7b: Ridge strategy loop

`ridge` exposes strategy research, paper evaluation and status tools. Use the documented service results to inspect progress and evaluated candidates. Research results do not authorize live trading.

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0614") as client:
    response = client.model.request(
        """
        Inspect Ridge heartbeat, status, and broker manifest, then run one paper
        cycle. Summarize the selected generation, evaluation results,
        cold/warm/hot telemetry, and any branch-at-red guard that should stop
        live execution.
        """,
        session=True,
        continuous=True,
        vcache={"name": "trading", "cache_id": "eurusd-momentum"},
        default_service=["ridge", "knowledge"],
        max_iterations=20,
    )
    print(response["final_response"])
```

Operational notes:

- Ridge execution is managed by the service. SDK callers use the documented tools and inspect their actual results.
- The default Ridge model alias is `1984-m3-0614`.
- `ridge_status` reports active selection, latest telemetry, broker readiness, heartbeat and evaluated strategy state.
- `ridge_heartbeat` reports filesystem, Capital.com and barn/farm broker readiness, pending warm/hot alerts, and checkpoint recovery state.
- `ridge_paper_account`, `ridge_price_data`, `ridge_alt_data`, `ridge_tradable_assets`, and `ridge_paper_execution` expose the paper account/data/execution contract.
- `ridge_telemetry` reads recent cold/warm/hot reports.
- `ridge_run(mode="smoke")` is the safest full-loop regression because it disables model calls and uses deterministic fallback seeds. Normal `once` and `continuous` runs fail closed if the model is unavailable during seed/evolve.
- `ridge_run(execution_mode="live")` requires service-operator authorization. SDK intent alone cannot enable live trading.
- Use `stream=True` for long-running SDK calls when you want to display service progress events while the model inspects status, runs Ridge, and reads telemetry.

### Recipe 8: Streaming with service progress

```python
with NinethClient(default_model="1984-m3-0424") as client:
    for event in client.model.request(
        "Research AI gateway patterns and summarize.",
        stream=True,
        default_service=["browser", "read", "deepsearch"],
    ):
        if event["type"] == "model_delta":
            print(event["data"]["text"], end="")
        elif event["type"] == "service_call":
            print("\n[service]", event["data"]["service_name"])
        elif event["type"] == "service_response":
            print("\n[service done]", event["data"].get("service_name"))
```

### Recipe 9: Caller-managed include services (manual resume)

_Use when your caller code must execute the tool calls itself. The model issues call parameters; you run the tool, supply the results, and resume the conversation — no callback server required._

```python
weather_schema = {
    "name": "get_weather",
    "description": "Return weather for a city.",
    "strict": True,
    "parameters": {
        "type": "object",
        "properties": {"location": {"type": "string"}},
        "required": ["location"],
        "additionalProperties": False,
    },
}

with NinethClient(default_model="1984-m3-0424") as client:
    first = client.model.request(
        "Get weather for Lagos and summarize.",
        include_service=[weather_schema],
        session=True,
    )

    if first.get("status") == "awaiting_client_services":
        pending = first["pending_client_calls"]
        # Execute pending client tools yourself.
        manual_results = [
            {
                "call_id": pending[0]["call_id"],
                "service_name": "get_weather",
                "success": True,
                "result": {"location": "Lagos", "forecast": "sunny"},
            }
        ]
        final = client.model.request(
            "continue",
            include_service=[weather_schema],
            client_service_results=manual_results,
            session=True,
        )
```

With `strict=True`, every item in `pending_client_calls` already satisfies the
declared schema. Resume with the exact runtime-issued `call_id` and
`service_name`. Do not synthesize completion after transport or validation
failure.

### Recipe 10: SDK-managed callback runtime (`include_service` object)

_In Python, use this when the SDK should dispatch pending tool calls to your HTTP callback endpoint, collect its real results, and resume the model. The request returns the final model response after that loop, or a distinct validation/control outcome. The model proposes calls; your callback performs and authorizes the action._

```python
weather_schema = {
    "name": "get_weather",
    "description": "Return weather for a city.",
    "parameters": {
        "type": "object",
        "properties": {"location": {"type": "string"}},
        "required": ["location"],
    },
}

with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Get weather for Nairobi and summarize risk impact.",
        include_service={
            "callback": {"url": "https://app.example.com/api/callback"},
            "schema": [weather_schema],
        },
    )
```

### Recipe 10c: Caller-managed include-service payload (`callback: false`)

_Use when you want to register tool schemas but route all callback traffic yourself via a global `callback_url`, bypassing the SDK-managed interlude._

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Use the tool schema, but let the caller handle the calls.",
        include_service={
            "callback": False,
            "schema": [weather_schema],
        },
        callback_url="https://app.example.com/global-callback",
    )
```

In this mode the SDK preserves `callback: false` in the emitted object and does
not add the managed callback interlude schema.

Callback runtime events sent to your callback URL include:

- `service_call`
- `interlude` (missing caller-side parameters)
- `final_response`

### Recipe 10b: Callback endpoint contract (request/response)

_Reference when implementing the HTTP handler that receives tool-call requests from the server. Documents the request envelope, expected response shape, and all lifecycle event types._

When `include_service` callback mode is active, your callback endpoint receives
JSON requests with idempotency metadata:

```json
{
    "event_type": "service_call",
    "listener": "nineth_include_service_callback",
    "idempotency_key": "<sha1>",
    "process_id": "proc_abc123",
    "call_id": "call_1",
    "service_name": "get_weather",
    "params": {"location": "Nairobi"},
    "service": {
        "name": "get_weather",
        "description": "Return weather for a city.",
        "parameters": {"type": "object", "properties": {"location": {"type": "string"}}, "required": ["location"]}
    },
    "available_services": [{"name": "get_weather", "description": "..."}]
}
```

Your callback should return HTTP 200 with a JSON object.

Recommended callback response envelope for any callback that can influence the next model turn:

```json
{
    "status": "warn",
    "message": "Applicant is already blocked; reply with appeal instructions.",
    "success": true
}
```

Successful service response example:

```json
{
    "status": "ok",
    "message": "Weather lookup completed from the caller-side cache.",
    "success": true,
    "result": {
        "location": "Nairobi",
        "forecast": "Partly cloudy",
        "temperature_c": 22
    }
}
```

Interlude response example (ask caller for missing fields):

```json
{
    "status": "warn",
    "message": "Applicant is already blocked; use the appeal template.",
    "success": true,
    "parameters": {
        "location": "Nairobi",
        "units": "metric"
    }
}
```

Failure response example:

```json
{
    "status": "failed",
    "message": "Rate limit from upstream weather provider.",
    "success": false,
    "error": "Rate limit from upstream weather provider"
}
```

Notes:

- The SDK includes `X-Idempotency-Key` on callback HTTP requests.
- Callback responses must be JSON objects; non-object JSON is treated as a callback error.
- `status` and `message` are recognized the same way whether the callback URL was set directly on `include_service`, directly on an inbound template, or inherited from top-level `callback_url`.
- The reserved interlude service name is `request_include_service_interlude`.

### Recipe 11: Local schema.py include-service references

`include_service` supports legacy local references:

- absolute or relative `schema.py` path
- directory containing `schema.py`
- shorthand token discoverable from local `services/**/schema.py`
- manager class/object references from loaded schema modules

Example:

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Run local weather service.",
        include_service=["./services/weather/schema.py"],
    )
```

### Recipe 12: Messaging (email + telegram)

_Use when the model should trigger real email or Telegram deliveries as a side effect of the request. Messaging config auto-enables the relevant transport service names._

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Draft and send the update.",
        messaging={
            "email": {
                "address": "ops@example.com",
                "name": "Ops Bot",
                "instruction": "Reply with concise operational summaries.",
            },
            "telegram": {
                "botId": "bot-1",
                "chatId": "12345",
            },
        },
    )
```

A logical Telegram registration keeps the bot token and webhook configuration
on the Nineth server:

```python
with NinethClient() as client:
    registration = client.messaging.telegram.register(
        name="resident",
        bot="amir",  # select "amir" or "lati"
        callback_url="https://app.example.com/nineth/telegram",
        callback_secret="store-this-outside-source-control",
        delivery_mode="callback_only",
        use_cache=True,
        vcache={"name": "resident-chat", "cache_id": "person_123"},
        policy_key="daily_assistant_v2",
    )

    metadata = client.messaging.telegram.get(name="resident")
```

The async client has the same logical surface; each operation is awaited:

```python
async with AsyncNinethClient() as client:
    registration = await client.messaging.telegram.register(
        name="resident", bot="amir",
        callback_url="https://app.example.com/nineth/telegram",
        delivery_mode="callback_only",
    )
    page = await client.messaging.telegram.list_updates(name="resident", limit=50)
```

Both clients also expose `get`, `disconnect`, `rotate_secret`, `bind`, and
`unlink`. A single API key owns each logical registration. `X-API-Key` or
Bearer authentication is required; disabled keys are rejected. `/admin/*`
requires a separately allowlisted operator key. Callback URLs must be public
HTTPS destinations; redirects and private address literals are not accepted.
The server also rejects private DNS answers and pins each callback connection
to a checked public address while verifying the original HTTPS hostname.

`name` is an application-level identifier; it need not equal the Telegram bot
ID or username. Registration and reads return non-secret metadata such as
`channel`, `name`, `bot`, `bot_id`, `username`, `callback_url`, `status`,
`delivery_mode`, `vcache`, and `policy_key`. Bot tokens are never returned.
When `callback_secret` is omitted, a generated secret is returned once on first
registration. `rotate_secret(name=...)` returns a replacement once. Store
either value securely; subsequent `get(...)` calls omit it.

`amir` and `lati` are the two physical inbound transports. Each accepts one
active logical registration at a time so an inbound update cannot be delivered
across tenants. Disconnect the current registration before assigning that
transport to another logical application identity.

Nineth normalizes each inbound update before delivering it:

```json
{
  "event_type": "telegram.inbound_message",
  "event_id": "evt_...",
  "delivery_id": "dlv_...",
  "idempotency_key": "telegram:resident:update:10001",
  "received_at": "2026-07-29T12:00:01Z",
  "occurred_at": "2026-07-29T12:00:00Z",
  "bot": {
    "name": "resident",
    "bot_id": "tg_amir",
    "username": "amir",
    "transport": "amir"
  },
  "chat": {"chat_id": "12345", "type": "private"},
  "sender": {
    "telegram_user_id": "67890",
    "username": "example_user",
    "first_name": "Example",
    "last_name": null
  },
  "message": {
    "message_id": "991",
    "text": "Hello",
    "reply_to_message_id": null,
    "attachments": []
  },
  "routing": {
    "vcache": {"name": "resident-chat", "cache_id": "person_123"},
    "policy_key": "daily_assistant_v2"
  }
}
```

Callback requests carry:

- `X-Nineth-Delivery-Id`
- `X-Nineth-Event-Type`
- `X-Nineth-Idempotency-Key`
- `X-Nineth-Timestamp`
- `X-Nineth-Signature: v1=<hex HMAC-SHA256>`

The signed input is `timestamp + "." + raw_request_body`. Validate the timestamp
(within five minutes) and signature using the registration's callback secret,
then deduplicate by idempotency key. Edited Telegram messages have their own
update ID and therefore a distinct key. Any `2xx` response acknowledges
delivery; an optional JSON object supplies routing context. Nineth stages the
event before acknowledging Telegram and retries transient delivery failures.
Callback handlers should return quickly and tolerate at-least-once delivery.
Failed delivery can be reconciled with:

```python
events = client.messaging.telegram.list_updates(
    name="resident",
    after=cursor,
)
cursor = events["cursor"]
```

`events` are ordered by ingress time. Treat `cursor` as opaque and pass it back
unchanged; old event-ID cursors remain accepted during migration. Internal
staging payloads are omitted from the listing. Invalid or unknown cursors yield
an API validation error.

The callback may also return explicit routing context:

```json
{
  "success": true,
  "person_id": "person_123",
  "vcache": {"name": "resident-chat", "cache_id": "person_123"},
  "policy_key": "daily_assistant_v2",
  "continue_model": false
}
```

`callback_only` is the default and never starts the model automatically.
`delivery_mode="model"` or `continue_model: true` enables a server-side worker
using only the selected vcache and policy key. Server-side policy text is read
from the `NINETH_TELEGRAM_POLICIES_JSON` mapping. A policy from another SDK
request is never inherited. Model-mode registration is rejected when its policy
key is unresolved, and callback-requested continuation remains callback-only
when no registered policy resolves. The selected registration, vcache, policy
key, and tenant-scoped state identity are retained in runtime metadata.
Model continuation waits until the application callback succeeds. A failed
callback remains pending and does not start the model.

Account linking remains application-owned. Bind only after a signed web/deep-link
flow has resolved the application person:

```python
client.messaging.telegram.bind(
    name="resident",
    chat_id="12345",
    person_id="person_123",
    vcache={"name": "resident-chat", "cache_id": "person_123"},
    policy_key="daily_assistant_v2",
)
client.messaging.telegram.unlink(name="resident", chat_id="12345")
```

Never match Telegram usernames or display names to application users. Raw
Telegram updates are omitted unless `include_raw_update=True` was explicitly
selected.

For outbound delivery to a captured chat, reference the logical name:

```python
client.model.request(
    "Reply to the linked Telegram chat.",
    messaging={
        "telegram": {
            "name": "resident",
            "chatId": "12345",
            "use_cache": True,
        }
    },
    vcache={"name": "resident-chat", "cache_id": "person_123"},
)
```

The server resolves the logical name to the bot ID and token internally.
Named registrations reject caller-supplied `token`, `botToken`, `apiBase`,
`api_url`, and equivalent overrides. A custom Telegram API base is available
only with an explicitly caller-owned direct bot token. Transport failures are
returned as service errors. The physical bot webhook setup operation is an
authenticated POST managed by the server operator; applications do not need
to call `setWebhook` themselves.

Auto-enable behavior:

- Telegram messaging config auto-enables Telegram delivery service names, including `send_rich_message` and `edit_rich_message`.
- Email messaging auto-enables email send services except inbound-only setup flows.
- `generate_receipt` creates a compressed Lati transfer receipt PNG. The model supplies
  only `amount`, `currency`, and `recipient_name`; the service generates the date,
  24-hour time, transaction reference, logo, and disclaimer. A Telegram worker such as
  Lati then calls `send_photo` with the returned `send_with.arguments`; receipt
  generation does not choose credentials or recipients itself.

For structured Telegram text, the model can call `send_rich_message` with an `InputRichMessage`:

```json
{
  "rich_message": {
    "markdown": "## Deployment status\n\n- API: healthy\n- Worker: healthy\n- Queue: 3 pending"
  }
}
```

The server defaults to `uniform_text=true` inside `rich_message`: ordinary headings, paragraphs, bold, italic, links, lists, and code use Telegram's normal body renderer through `sendMessage` HTML. This avoids native rich-message body sizing differences on mobile. Edits and private drafts use the same conversion. Advanced blocks, tables, media, formulas, references, RTL, entity-detection settings, and long content retain native rich messages. Set `"uniform_text": false` to explicitly retain native rendering. This is a Rooster option and is removed before sending to Telegram. Native client font sizing cannot be set through the Bot API. Private drafts remain temporary; a final persistent delivery is still required.

`set_alarm` and `schedule_at` schedule later continuation in the same conversation. Persistent Telegram alarms are limited to 23 hours. Missed alarms and failed dispatches are recorded as aborted and are not retried automatically. Treat alarms as non-blocking, best-effort checkpoints and inspect their result when delivery matters.

### Recipe 12b: Messaging payload and result events

Example request emphasizing template payloads:

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Handle inbound request and reply.",
        callback_url="https://app.example.com/mailbox-hook",
        messaging={
            "email": {
                "address": "support@example.com",
                "name": "Support Bot",
                "instruction": "Classify and route inbound email.",
                "templates": [
                    {
                        "type": "inbound",
                        "shape": {
                            "subject": "string",
                            "html": "<p>{{body}}</p>",
                            "text": "string",
                            "from": "string"
                        }
                    },
                    {
                        "type": "outbound",
                        "recipients": ["customer@example.com"],
                        "message": {
                            "subject": "Ticket update",
                            "body": "Issue resolved.",
                            "html": "<p>Issue resolved.</p>"
                        }
                    }
                ]
            },
            "telegram": {
                "botId": "ops-bot",
                "chatId": "12345"
            }
        },
    )
```

Typical transport-side effect event fragments inside `events`:

```json
[
    {
        "type": "mailbox_configured",
        "data": {
            "address": "support@example.com",
            "inbound_uuid": "inb_abc123"
        }
    },
    {
        "type": "service_response",
        "data": {
            "service_name": "send_reply",
            "success": true
        }
    }
]
```

Inbound UUID behavior:

- The SDK caches `mailbox_configured` inbound IDs by sender address per client instance.
- Subsequent requests can automatically reuse the remembered `inbound_uuid` for the same address.

### Recipe 13: Inbound/outbound email templates with global callback URL

_Use when you need to define reusable inbound and outbound email template pairs and have all deliveries inherit a shared top-level callback URL._

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Configure mailbox templates.",
        callback_url="https://app.example.com/mailbox-hook",
        messaging={
            "email": {
                "address": "helpdesk@example.com",
                "name": "Helpdesk Bot",
                "templates": [
                    {
                        "type": "inbound",
                        "shape": {
                            "subject": "string",
                            "html": "<p>{{body}}</p>",
                            "text": "string",
                            "from": "string"
                        },
                        # url omitted -> inherits top-level callback_url
                    },
                    {
                        "type": "outbound",
                        "recipients": ["customer@example.com"],
                        "messages": [
                            {
                                "subject": "We received your request",
                                "body": "Thanks, we are on it.",
                                "html": "<p>Thanks, we are on it.</p>"
                            },
                            {
                                "subject": "Follow-up",
                                "body": "We will update you again soon."
                            }
                        ]
                    },
                ],
            }
        },
    )
```

### Recipe 13b: Multiple named Resend templates per type via `shape` list

_Use when you need distinct named template variants per direction — for example, separate triage and escalation shapes for inbound, or multiple outbound notice templates — all provisioned in a single request._

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Register reusable inbound/outbound template packs.",
        callback_url="https://app.example.com/unified-callback",
        messaging={
            "email": {
                "address": "helpdesk@example.com",
                "templates": [
                    {
                        "type": "inbound",
                        "shape": [
                            {
                                "name": "inbound_triage_primary",
                                "subject": "Ticket {{ticket_id}}",
                                "html": "<p>{{summary}}</p>"
                            },
                            {
                                "name": "inbound_triage_escalation",
                                "subject": "Escalation {{ticket_id}}",
                                "html": "<p>{{action_required}}</p>"
                            }
                        ]
                    },
                    {
                        "type": "outbound",
                        "name": "outbound_pack",
                        "recipients": ["owner@example.com"],
                        "shape": [
                            {
                                "name": "outbound_notice_primary",
                                "subject": "Notice {{ticket_id}}",
                                "html": "<p>{{body}}</p>"
                            }
                        ]
                    }
                ]
            }
        },
    )
```

Runtime behavior for shape lists:

- each shape entry must define its own `name`
- each entry is provisioned/cached as its own Resend template ID
- later requests can reuse provisioned IDs by template name without re-creating templates

### Recipe 13c: Complete email template shape — all design parameters

_Full parameter reference for the email template shape contract. Use this when building or debugging a complex Resend template — covers subject, HTML, fallback variable defaults, and image injection._

The full shape contract exposes five caller-controlled parameters:

| Parameter  | Required | Description |
|------------|----------|-------------|
| `name`     | no       | Internal Resend template name. Used for caching and idempotency. |
| `subject`  | **yes**  | Email subject line. May contain `{{{VAR_NAME}}}` handlebars placeholders. |
| `html`     | **yes**  | HTML body of the email. May contain `{{{VAR_NAME}}}` handlebars placeholders. |
| `fallback` | no       | Dict (or path to a `.json` file) mapping variable names to default values. Resend uses these when the model does not supply a value for a given placeholder at send time, preventing validation errors. |
| `images`   | no       | List of image objects injected as `<img>` tags into the template HTML at creation time. Each entry: `{"url": "…", "alt"?: "…", "width"?: N, "height"?: N, "position"?: "append"\|"prepend"}`. Local file paths are resolved to base64 data URIs by the SDK before the payload is sent; remote URLs (`http://`, `https://`) and data URIs are passed through unchanged. |

Variables are extracted automatically from `{{{TRIPLE_BRACE}}}` patterns in `subject`, `html`, and `text`. If a `fallback` dict is provided, each matched variable name is looked up in it; any match is registered as Resend's `fallbackValue` for that variable.

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Send an order confirmation email.",
        callback_url="https://app.example.com/hook",
        messaging={
            "email": {
                "address": "orders@example.com",
                "name": "Order Bot",
                "templates": [
                    {
                        "type": "outbound",
                        "recipients": ["customer@example.com"],
                        "shape": {
                            # 1. Template name
                            "name": "order-confirmation",

                            # 2. Subject with handlebars placeholder
                            "subject": "Your order for {{{PRODUCT}}} is confirmed!",

                            # 3. HTML body — the main visual design
                            "html": (
                                "<h1>Thanks for your order!</h1>"
                                "<p>Item: <strong>{{{PRODUCT}}}</strong></p>"
                                "<p>Total: <strong>{{{PRICE}}}</strong></p>"
                                "<p>Expected delivery: {{{DELIVERY_DATE}}}</p>"
                            ),

                            # 4. Fallback values — prevent send-time errors when the
                            #    model omits a variable. Can be an inline dict or a
                            #    path to a local .json file (resolved by the SDK before
                            #    the payload is sent to the server):
                            "fallback": {
                                "PRODUCT": "your item",
                                "PRICE": "N/A",
                                "DELIVERY_DATE": "TBD",
                            },
                            # — or equivalently —
                            # "fallback": "./templates/order_fallbacks.json",
                        },
                    }
                ],
            }
        },
    )
```

**Fallback as a JSON file** — when `fallback` is a string the SDK treats it as a local filesystem path, reads it, and inlines the parsed object before the payload is transmitted:

```json
{
    "PRODUCT": "your item",
    "PRICE": "N/A",
    "DELIVERY_DATE": "TBD"
}
```

```python
shape = {
    "name": "order-confirmation",
    "subject": "Your order for {{{PRODUCT}}} is confirmed!",
    "html": "<p>Item: {{{PRODUCT}}}</p><p>Total: {{{PRICE}}}</p>",
    "fallback": "./templates/order_fallbacks.json",   # resolved at call time
}
```

**Precedence rules** for fallback values:

1. An explicit `fallbackValue` (or `fallback_value`) on an item in the `variables` list wins unconditionally.
2. If no per-variable explicit value exists, the `fallback` dict is consulted by variable key.
3. If neither source supplies a value, the variable is registered without a fallback — Resend will require the caller to supply it at send time or return a validation error.

**Adding images to a template** — the `images` list is processed at template creation time. Each image is injected as an `<img>` tag directly into the HTML before the template is registered with Resend. Use `"position": "prepend"` to place the image at the top of the body; the default is `"append"` (bottom of the body).

```python
shape = {
    "name": "order-confirmation",
    "subject": "Your order for {{{PRODUCT}}} is confirmed!",
    "html": (
        "<h1>Thanks for your order!</h1>"
        "<p>Item: <strong>{{{PRODUCT}}}</strong></p>"
    ),
    "fallback": {"PRODUCT": "your item"},

    # Images are injected into the HTML at template-creation time.
    # Remote URLs are used as-is; local paths are base64-encoded by the SDK.
    "images": [
        # Brand logo from a CDN — prepended at the top of the email body
        {
            "url": "https://cdn.example.com/logo.png",
            "alt": "Acme Corp",
            "width": 160,
            "height": 40,
            "position": "prepend",
        },
        # A local banner image — the SDK reads the file and converts it
        # to a base64 data URI before sending the payload to the server
        {
            "url": "./assets/order-banner.png",
            "alt": "Order confirmed",
            "width": 600,
        },
    ],
}
```

Each image entry supports the following fields:

| Field      | Required | Description |
|------------|----------|-------------|
| `url`      | **yes**  | Remote URL (`https://…`), data URI (`data:image/…;base64,…`), or local file path. Local paths are resolved to base64 data URIs by the SDK. |
| `alt`      | no       | `alt` attribute value for accessibility. |
| `width`    | no       | `width` attribute in pixels. |
| `height`   | no       | `height` attribute in pixels. |
| `position` | no       | `"append"` (default) — inserted before `</body>` or at the end of the HTML. `"prepend"` — inserted after `<body…>` or at the start of the HTML. |



```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Transcribe and summarize this call.",
        audio=[
            {
                "data": "<base64-audio>",
                "mime_type": "audio/mpeg",
                "filename": "call.mp3",
            }
        ],
    )
```

### Recipe 15: Policy and guardrail

```python
with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Assess this strategy.",
        policy="Keep output in bullet points with risk-first framing.",
        guardrail="Refuse prohibited trading instructions.",
    )
```

Interpretation:

- `policy` supplies instructions for your task and is subject to security screening.
- `guardrail` adds application security requirements and cannot weaken the service's checks.
- A rejected request produces a typed guard outcome; inspect its code and retryability before reading application output.

### Recipe 16: Async streaming

_Use in async-native applications that need to consume a streaming model response token-by-token without blocking the event loop._

```python
import asyncio
from nineth import AsyncNinethClient

async def main() -> None:
    async with AsyncNinethClient(default_model="1984-m3-0424") as client:
        stream = await client.model.request(
            "Stream a quick macro brief.",
            stream=True,
        )
        async for event in stream:
            if event["type"] == "model_delta":
                print(event["data"]["text"], end="")

asyncio.run(main())
```

---

### Recipe 17: Parallel deputies with `use_deputy`

_Use when the task is complex enough that independent subtasks can be parallelised. The model governs the team and synthesises outputs into one final answer._

Set `use_deputy=True` to allow the model to call the `deputy` service. It is `False` by default to prevent unexpected token spend on simple tasks. Each deputy gets a separate temporary task context and only services permitted by your request's allowlist.

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request(
        "Analyse this acquisition target: strengths, risks, and market context.",
        use_deputy=True,
        default_service=["search_web"],
        reasoning="medium",
        max_iterations=8,
    )
    print(response["final_response"])
```

When the model decides the task benefits from parallel work, it emits a `deputy` service call such as:

```json
{
  "total_deputies": 3,
  "policy": "Cite specific evidence and flag uncertainty.",
  "deputies": [
    {
      "id": "strengths",
      "role": "strategic researcher",
      "task": "Identify the top 3 durable competitive advantages of the target company.",
      "temperature": 0.3,
      "max_tokens": 1200
    },
    {
      "id": "risks",
      "role": "risk analyst",
      "task": "Identify the top 3 material downside risks, including regulatory and market risks.",
      "temperature": 0.2,
      "max_tokens": 1200
    },
    {
      "id": "context",
      "role": "market analyst",
      "task": "Provide current market context for the target's industry segment.",
      "model": "1984-m2-light",
      "temperature": 0.4,
      "max_tokens": 800,
      "reasoning_effort": "low"
    }
  ]
}
```

All three deputies run concurrently. Their outputs, success flags, token usage and service traces are available to the parent task for its final response.

**Key constraints:**

- Deputies have no cross-deputy shared memory, no ability to contact the human, and cannot create further deputies.
- Each batch receives fresh deputy context IDs; no deputy hot-memory file is written or reused by a later batch.
- `task` is the objective. `policy` is a separate behavioral overlay inherited on top of the fixed `deputyPolicy.py` base policy.
- Deputy service requests outside your allowlist are rejected; deputies cannot send outbound messages.
- `total_deputies` must exactly match the number of entries in `deputies`.
- Total `max_tokens` across all deputies is capped at 20 000 by default.
- If `use_deputy=False` and the model attempts a `deputy` call, the call is rejected with an error observation; the model can still continue with other services.
- You can narrow a deputy's service surface with `allowed_services`, and you can override the per-deputy iteration cap with `max_iterations` or `continuous`.
- An explicit `allowed_services=[]` gives that deputy no task services; only the internal `done` lifecycle service remains available.

**When to enable `use_deputy`:**

| Task complexity | Recommended setting |
| --- | --- |
| Simple Q&A, summaries, formatting | `use_deputy=False` (default) |
| Multi-angle research or independent sub-questions | `use_deputy=True` |
| Fact-checking + risk + recommendation in one step | `use_deputy=True` |

---

## Response Shapes

### Buffered response

Common caller-facing shape:

```json
{
    "status": "completed",
    "code": null,
    "retryable": false,
    "retry_after": null,
    "request_id": "summary-2026-10-06-01",
    "final_response": "... or parsed JSON value ...",
    "raw_response": "... optional original JSON string ...",
    "usage": {
        "prompt_tokens": 123,
        "completion_tokens": 45,
        "total_tokens": 168
    },
    "compute": 168,
    "thinking": ["... optional reasoning trace ..."],
    "service_calls": [{"service_name": "search_web", "call_id": "call_12"}],
    "service_responses": [{"service_name": "search_web", "success": true}],
    "artifacts": [{"type": "email_template", "name": "ticket_notice_primary"}],
    "iterations": 3,
    "events": [{"type": "service_response", "data": {"service_name": "search_web", "success": true}}],
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "session_id": "proc_ops_desk_alice_01"
}
```

Field guide:

- Check `status` before reading `final_response`; paused and validation outcomes are not completed application answers.
- `request_id` identifies a request for support and troubleshooting; it is not a write-idempotency key.
- `raw_response` is conditional and mainly appears when JSON mode preserves the original string
- `usage` is the token ledger returned by the server
- `compute` is conditional on `compute=True`
- `thinking` is conditional on reasoning visibility
- `service_calls`, `service_responses`, `artifacts`, and `events` are trace arrays and may be empty
- `vcache` is present when the request used a durable vcache scope
- `session_id` is present when `session=True` is active

### Callback wait response (manual mode)

When waiting for caller-managed services, the SDK returns a paused response instead of a final model answer:

```json
{
    "status": "awaiting_client_services",
    "code": null,
    "retryable": false,
    "request_id": "weather-2026-10-06-01",
    "session_id": "proc_123",
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "pending_client_calls": [
        {
            "call_id": "call_1",
            "service_name": "get_weather",
            "params": {"location": "Lagos"}
        }
    ],
    "thinking": [],
    "service_calls": [],
    "service_responses": [],
    "artifacts": [],
    "events": []
}
```

Notes:

- if `session=False`, the paused response exposes `process_id` instead of `session_id`
- resume by sending `client_service_results` with the same client and same session/vcache scope

### Stream `accepted` event

```json
{
    "type": "accepted",
    "data": {"request_id": "summary-2026-10-06-01"}
}
```

### Stream `result` event

```json
{
    "type": "result",
    "process_id": "proc_ops_desk_alice_01",
    "session_id": "proc_ops_desk_alice_01",
    "data": {
        "status": "completed",
        "code": null,
        "retryable": false,
        "request_id": "summary-2026-10-06-01",
        "final_response": "...",
        "iterations": 3,
        "usage": {"prompt_tokens": 123, "completion_tokens": 45, "total_tokens": 168},
        "vcache": {"name": "ops-desk", "cache_id": "alice"}
    }
}
```

### Stream service events

Service execution surfaces in stream mode as readable progress plus structured events:

```json
{
    "type": "model_delta",
    "data": {
        "text": "\n> Browsing the web\n",
        "progress": true,
        "synthetic": true
    }
}
```

```json
{
    "type": "service_call",
    "session_id": "proc_ops_desk_alice_01",
    "data": {
        "service_name": "search_web",
        "client_managed": false,
        "call_id": "call_12"
    }
}
```

```json
{
    "type": "service_response",
    "session_id": "proc_ops_desk_alice_01",
    "data": {
        "service_name": "search_web",
        "success": true
    }
}
```

### Stream `awaiting_client_services` event

Emitted in stream mode when the model has paused to wait for caller-managed service results (manual `include_service` mode):

```json
{
    "type": "awaiting_client_services",
    "session_id": "proc_abc123",
    "data": {
        "status": "awaiting_client_services",
        "pending_client_calls": [
            {
                "call_id": "call_1",
                "service_name": "get_weather",
                "params": {"location": "Lagos"}
            }
        ]
    }
}
```

After receiving this event, collect results and resume with `client_service_results` using the same client and session scope (see Recipe 9).

### Validated structured output

When `response_schema` is supplied, completed output is withheld until validation.
Consume `validated_output` or the final `result`, rather than treating a model
delta as a valid application object. For example:

```json
{
    "type": "validated_output",
    "request_id": "summary-2026-10-06-01",
    "data": {"text": "{\"summary\":\"The release is Friday.\",\"next_step\":null}"}
}
```

This validates the final response against your schema. It does not authorize or
execute an application action. Service callbacks have their own schemas and
authorization checks.

A valid structure does not prove that facts are correct or that a requested
operation was proposed. Validate application rules before execution, and use
actual callback receipts to confirm success. An empty action list or a prose
promise is not evidence that an operation ran.

### Dependency error envelope

An operational failure is a control outcome, not application JSON:

```json
{
    "status": "guard_unavailable",
    "code": "guard_unavailable",
    "retryable": true,
    "retry_after": 60,
    "request_id": "summary-2026-10-06-01",
    "message": "The required security review is temporarily unavailable."
}
```

The delay is illustrative; use the value returned by the server. The SDK raises
`NinethAPIError` for this outcome. If SSE has already begun, the transport can
remain HTTP 200 while an `error` event terminates the request. Catch errors during
stream iteration as well as when creating the stream.

### VCache lifecycle responses

`client.vcache.delete(...)` returns:

```json
{
    "success": true,
    "found": true,
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "context_id": "sdk/ops-desk/alice",
    "message": "vcache deleted."
}
```

`client.vcache.rename(...)` returns:

```json
{
    "success": true,
    "previous_vcache": {"name": "ops-desk", "cache_id": "alice"},
    "vcache": {"name": "ops-archive", "cache_id": "alice"},
    "previous_context_id": "sdk/ops-desk/alice",
    "context_id": "sdk/ops-archive/alice",
    "message": "vcache renamed."
}
```

`client.vcache.upsert(...)` returns:

```json
{
    "success": true,
    "vcache": {"name": "ops-desk", "cache_id": "alice"},
    "context_id": "sdk/ops-desk/alice",
    "written": 2,
    "message": "wrote 2 state entries to vcache buffer."
}
```

## Outcome contract

SDK 0.15.0 preserves `status`, `code`, `retryable`, `request_id` and applicable
`retry_after` fields. The API's status describes the model outcome; HTTP status
describes the transport. They are distinct.

| Outcome / code | Buffered HTTP status | Consumer handling |
| --- | --- | --- |
| `completed` | 200 | Read the completed answer; validate application actions separately. |
| `blocked` / `guard_rejected` | 403 | Stop. Do not retry as a JSON-formatting problem or rotate visitor identities. |
| `guard_unavailable` | 503 | Required review could not complete; no unsafe action runs. Retry only according to returned retryability and delay. Provider outages do not add abuse strikes. |
| `error` / `quota_exhausted` | 503 | Honor returned retryability and delay. A monthly AIR allocation cannot recover through immediate retries. |
| `error` / `model_unavailable` | 503 | Report capacity failure and use a bounded retry only when explicitly retryable. |
| `error` / `input_capacity_exceeded` | 422 | Reduce protected input or service scope. Repeating the same oversized request cannot repair it. |
| `error` / `structured_output_invalid` | 422 | No validated application result is available. Do not execute proposed actions from rejected output. |
| `error` / `validation_failed` | 422 | Fix invalid request or callback input before retrying. |
| `error` / `access_denied` | 403 | Check caller permissions; do not retry unchanged. |
| `awaiting_client_services` | 200 | Authorize and execute pending calls in your backend, then resume with actual results. This is a paused workflow. |
| `service_schema_validation_failed` / `callback_validation_failed` | 200 | Inspect `violations`, repair the callback contract, and do not interpret this as a completed model answer. |

HTTP statuses above apply to typed model outcomes before streaming starts. An
error after SSE acceptance arrives in an event; the SDK still raises a typed
exception. Authentication and framework request-validation errors can use their
own HTTP envelope, so fields may be absent. Missing retryability does not mean
permission to retry.

Security screening remains active for direct API and SDK requests, including requests with
`base_system=False` or disabled caller-facing services. A backend-supplied
`user_id` isolates visitor guard state within the authenticated caller; API-key
abuse, access and funding controls remain active. Changing IDs is not an approved
way to evade a rejection.

## Error Handling

`structured_output_invalid` remains nonretryable. An optional `diagnostics`
object identifies the validation phase, `validator`, `instance_path`,
`schema_path`, `actual_type`, generation `finish_reason` and available
`token_usage`. Paths use JSON Pointer notation; undeclared dynamic keys are
redacted. Union schemas may include a bounded `branches` list with the same
structural fields. Rejected values and expected constants are not returned.
Check a failing field against your schema. A `length` finish reason can indicate
truncation; review the completion cap and requested response size. A `stop`
finish reason does not prove schema validity. If the contract and budget are
appropriate, report the request ID and these diagnostics to support rather than
retrying automatically or executing actions from rejected output.

Python exposes the object as `NinethAPIError.diagnostics`; JavaScript exposes
`error.diagnostics`. Older SDK patches may omit it; the API error body and SSE
error event retain it. Absent diagnostics mean the boundary could not supply
those facts, not that validation passed.

Python exposes HTTP status as `status_code` and outcome status as `status`.

```python
from nineth import NinethClient, NinethAPIError

with NinethClient(default_model="1984-m3-0424") as client:
    try:
        result = client.model.request(
            "Explain photosynthesis in two sentences.",
            max_tokens=128,
            request_id="summary-2026-10-06-01",
        )
    except NinethAPIError as exc:
        print({
            "http_status": exc.status_code,
            "outcome": exc.status,
            "code": exc.code,
            "retryable": exc.retryable,
            "retry_after": exc.retry_after,
            "request_id": exc.request_id,
            "diagnostics": exc.diagnostics,
        })
        # Classify the failure here. Only schedule a bounded retry if allowed;
        # do not treat a guard/capacity outcome as malformed application JSON.
```

For streams, put the entire `for event in client.model.request(..., stream=True)`
loop inside the `try` block. The iterator may raise after earlier events were
received. Catch transport timeouts separately from typed API errors; a timeout
does not prove that an earlier callback or service action failed.

JavaScript exposes HTTP status as `status`, outcome status as `outcome`, and uses
camel case for `retryAfter` and `requestId`:

```javascript
try {
  const result = await client.model.request("Explain photosynthesis briefly.", {
    maxTokens: 128,
    requestId: "summary-2026-10-06-01",
  });
} catch (error) {
  if (!(error instanceof NinethAPIError)) throw error;
  console.error({
    httpStatus: error.status,
    outcome: error.outcome,
    code: error.code,
    retryable: error.retryable,
    retryAfter: error.retryAfter,
    requestId: error.requestId,
  });
}
```

Import `NinethAPIError` from your installed package alongside `NinethClient`.
Response objects still use wire keys such as `request_id`.

Retryability describes dependency recovery, not permission to repeat a write.
Deduplicate callback executions using durable application records and `call_id`,
and resume with their recorded results. A caller `request_id` supplies tracing;
it does not itself make an action idempotent. Avoid browser/JSON-correction retry
loops for deterministic guard rejections and capacity failures.

Authentication missing raises `ValueError` before Python request dispatch.

## Practical Patterns

- Create one long-lived client per worker process to maximize HTTP connection reuse.
- Use `stream_timeout` with `read=None` for long-running SSE sessions.
- Use `response_format="json"` only when your prompt explicitly asks for strict JSON.
- Use `response_schema` for a validated contract, and check outcome status before parsing application data.
- Set a completion cap appropriate to the task; allow longer client deadlines for AIR cold starts.
- Derive `user_id` on the authenticated backend and carry one `request_id` through your application logs and callbacks.
- Prefer `default_service=[...]` over broad `True` in production to keep service scope tight.
- For include-service workflows, choose one mode per integration:
  - SDK-managed callback URL mode for autonomous orchestration.
  - caller-managed mode when you need full deterministic control.

## Troubleshooting

- `ValueError: Authentication required`: set `NINETH_API_KEY` or pass `api_key=`.
- `401/403 on health endpoint`: `/health` is protected; verify `NINETH_API_KEY` (or explicit `api_key=`) matches server-side key registry.
- `ValueError: A model is required`: set client `default_model` or pass `model=` per request.
- `client_service_results requires session=True`: set `session=True` and reuse the same client (and vcache scope, if provided) before sending callback results.
- `cache_id is required unless this client has already remembered one for that vcache name.`: pass an explicit `cache_id` to `client.vcache.delete()` / `client.vcache.rename()`, or make sure you have already made a successful `model.request()` call with that `vcache.name` on the same client instance (which causes the SDK to remember the resolved id).
- callback responses not progressing: verify callback endpoint returns HTTP 200 JSON object.
- `guard_unavailable`: dependency outage, not evidence of malicious input. Honor the returned delay and contact the operator if it persists; do not bypass security checks.
- `guard_rejected`: stop deterministic retries and retain the safe classification for the caller.
- Automatic text aliases use shared provider readiness. Unknown/depleted paid routes select AIR immediately while bounded synthetic CPU probes check recovery in parallel; later turns can use a recovered route. Explicit model/manual and media requests retain their contract. This is server behavior and needs no new SDK flag. Cold starts and finite funded capacity still apply.
- `quota_exhausted`: do not repeatedly hit depleted routes. Follow `retry_after`; AIR shares finite funded production capacity across callers and required reviews.
- Buffered Python calls default to a 300-second HTTP timeout. Allow cold required review and generation in outer application deadlines too; a 75-second override can expire before the server finishes. Avoid retrying a timed-out request while its remote work may still be running.
- `input_capacity_exceeded`: AIR preserves current input and service contracts. Reduce those protected sections or use a capable healthy route; smaller output caps alone cannot fit an arbitrarily large input.
- `structured_output_invalid`: do not execute actions from unvalidated deltas or coerce an error envelope into your application schema.
- Legacy nested `final_response.status="blocked"`: upgrade to SDK 0.15.0, which recognizes this control error before application JSON handling.
- stalled stream with include services: confirm pending calls are resumed via `client_service_results` (manual mode) or callback endpoint handling (managed mode).
- `404 on /openapi.json or /docs`: expected in hardened deployments. Ask operators to enable `ROOSTER_EXPOSE_OPENAPI=true` only for controlled internal debugging.
- deputy call rejected with `"Deputy execution is disabled"`: the request was sent without `use_deputy=True`; add it when the task warrants parallel analysis.
- `"total_deputies must exactly match"` error in deputy observation: set `total_deputies` equal to the array length.
- high token usage on deputy tasks: tune `max_tokens` per deputy or reduce `total_deputies` to stay within the combined budget.
- Ridge appears stopped or stale: inspect `ridge_heartbeat`, `ridge_status`, and `ridge_telemetry`, then contact the operator with the request ID if the problem persists.

## Versioning and Compatibility

- Public SDK API is centered on `NinethClient`, `AsyncNinethClient`, `AVAILABLE_MODELS`, and `NinethAPIError`.
- Legacy aliases (`system_prompt`, `debug`, `services`, `service_names`) remain compatibility surfaces but should be considered migration paths, not preferred new usage.
- This guide describes the current SDK/API contract. Existing 0.15.x clients remain compatible; upgrade to the latest patch to receive exception diagnostics. Installing the package does not grant additional service capacity or administrative access.

## Maintainer Link

For server architecture and internal operations, see [README.md](../README.md).


## Scheduled task context and sleep

Telegram and Lati can call `set_alarm` with `seconds`, `reason`, optional
`task_context` (up to 8000 characters), and `notify` (defaults false).
`task_context` should state the objective, relevant facts, constraints, next
action, and references to durable journal/scratch entries. The server saves it
and the current plan snapshot outside rolling conversation history.

```json
{"seconds": 1800, "reason": "Review the strategy comparison", "task_context": "Compare variants A and B from journal entry 7; explain uncertainty and report findings.", "notify": false}
```

Task deadlines resume the scheduled task; call `done` after scheduling.
Provide concrete instructions and durable source references. The model must
perform the saved task and send its result; a task label alone is not a reminder.
A timer does not watch feeds or collect evidence between checks. For sampled monitoring,
perform an initial check and save a concrete next check with its baseline.
For an explicitly requested human reminder, set `notify=true`; its notice is delivered before model
continuation. The resumed task receives its saved context and conversation
when the task is due. A wakeup cannot approve a payment. Check service success
before announcing an alarm. `get_alarm` reports pending, dispatching, completed,
or aborted state; completion is a task outcome, not proof of human receipt.

`schedule_at` accepts an entire ISO timestamp with an offset, or `HH:MM` in the
provided IANA `timezone` or the service's configured timezone (UTC by default). Past ISO
timestamps are rejected. There is one pending timer per context; setting another
replaces it. Persistent Telegram delays are limited to 23 hours. Missed or failed
work is aborted without an automatic retry; `cancel_alarm` cancels the pending task.
These are best-effort deadlines with a 60-second start grace, not a guaranteed
delivery SLA. SDK calls without a Telegram context do not promise durable
Telegram delivery after the active task exits.

Deputies are temporary workers, enabled by `use_deputy=True`. They cannot sleep,
schedule alarms, send channel messages, submit payouts, or delegate recursively.
`continuous=true` is rejected. Their completion budget includes every permitted
iteration (`sum(max_tokens * max_iterations)`), so reduce either limit if the
batch exceeds the server budget. Return scheduling needs to the parent task.

The existing SDK `computer` alias does not include the newly added `get_alarm`;
request it explicitly with `default_service=["get_alarm"]` when needed.

## Bank payout availability (October 4, 2026)

Bank payouts are temporarily disabled by the operator. Calls to
`payment_send_money`, including calls carrying a previous confirmation token,
return `mode="payouts_disabled"`, `retry_allowed=false`, and make no provider
request. Report that payouts are unavailable and stop; do not ask for another
confirmation or prepare another transfer. Existing submissions remain available
to the read-only `payment_reconcile_transfer` tool. A previous submission may
still have an unknown outcome even when a new send attempt is disabled.

The model cannot select a replacement payment route. This does not change SDK
methods or versions.


## Research, Ridge and Resend updates (October 4, 2026)

`read_many` reads up to 12 unique public HTTP(S) URLs with bounded concurrency.
`deepsearch` returns a source-labelled dossier with partial failures; the model
writes the final synthesis. Additional search categories are opt-in to conserve quota.

`ridge_control(operation, payload)` is available in the `ridge` and `fund`
service groups. Start with `operation="help"`; the returned guide covers safe
universe/data/contract creation, discovery, benchmarking and paper deployment.
A contract is feature/signal plus its risk model; the whole output goes through
validation. Read-only account/history/reconciliation and paper evaluation tools
are available. Real execution requires a connected owned account and operator authorization.

Email template shape placeholders accept both `{{KEY}}` and `{{{KEY}}}`. The
server normalizes plain names to Resend's triple-brace syntax and rejects
unsupported expressions before HTTP. A local template name is not implicitly a
provider alias. Check provisioning and `used_template`/`template_fallback_reason`
on sends.
