Metadata-Version: 2.1
Name: nineth
Version: 0.9.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 SDK for the district's model API.

This guide is caller-facing and SDK-specific.

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

## Table of Contents

- [Install](#install)
- [Quick Start](#quick-start)
- [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 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 7: Built-in services with `default_service` (alias table)](#recipe-7-built-in-services-with-default_service)
  - [Recipe 7b: Shop strategy loop](#recipe-7b-shop-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)
- [Error Handling](#error-handling)
- [Practical Patterns](#practical-patterns)
- [Troubleshooting](#troubleshooting)
- [Versioning and Compatibility](#versioning-and-compatibility)

## Install

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

## Quick Start

### 1) Basic synchronous request

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0424") as client:
    response = client.model.request("Give me a concise BTC market brief.")
    print(response["final_response"])
```

Typical response shape:

```json
{
  "final_response": "BTC is range-bound with ...",
  "iterations": 2,
  "usage": {
    "prompt_tokens": 1200,
    "completion_tokens": 310,
    "total_tokens": 1510
  },
  "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`
- `service_call`
- `service_response`
- `awaiting_client_services` (manual callback mode)
- `result`
- `error`

## 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. All model names — including `1984-*`, `amari-*`, and `openrouter/...` slugs — are passed directly as the `model` argument. No special handling is required in the SDK; the server resolves the provider from the model string. See the server `README.md` for the full routing matrix and code examples.

## Model Catalog

Current SDK `AVAILABLE_MODELS`:

- `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.
- Server deployments can expose additional raw provider names or future aliases that remain valid when passed as `model=` or `default_model=`.
- Gemma 4 aliases use Google's [native function-call convention](https://ai.google.dev/gemma/docs/capabilities/text/function-calling-gemma4), including `<|tool_call>call:name{...}<tool_call|>`. Rooster normalizes this automatically; SDK callers do not parse these tokens.

## 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. |
| **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. |
| **Loop controls** | | |
| `max_iterations` | `int` | Cap on agentic loop iterations. |
| `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 runtime policy text appended to the model system prompt. |
| `guardrail` | `str` | ADAM extension text for the default SDK/API path. |
| `base_system` | `bool` | Legacy provider-path compatibility control. |
| **Memory continuity** | | |
| `session` | `bool` | Keep using the active hot conversational session for the same memory scope. |
| `vcache` | `dict` | Caller-owned persistent memory scope rooted at `/knowledge/sdk/{name}/{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 all services, enable all, or provide an explicit allowlist. |
| `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. |
| `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. |
| `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`
- `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`
- `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 `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 + vcache scope.

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 whole `/knowledge/sdk/{name}/{cache_id}/` tree
- `client.vcache.rename(...)` moves the same durable memory tree to `/knowledge/sdk/{new_name}/{cache_id}/`
- `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 outcomes are recorded in the server's `vcache_actions` audit table under that API key name
- 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
- `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

## 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,
                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=[
                        {
                                "call_id": "client_1_1",
                                "service_name": "get_weather",
                                "success": True,
                                "result": {"location": "Nairobi", "forecast": "Partly cloudy"},
                        }
                ],
                callback_url="https://app.example.com/unified-callback",
                stream=False,
                response_format="json",
                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

```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,
    "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_web"],
    "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"]
                }
            }
        ]
    },
    "client_service_results": [
        {
            "call_id": "client_1_1",
            "service_name": "get_weather",
            "success": true,
            "result": {"location": "Nairobi", "forecast": "Partly cloudy"}
        }
    ],
    "callback_url": "https://app.example.com/unified-callback",
    "response_format": "json",
    "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

### Exhaustive buffered response shape

```json
{
    "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:

- `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 traces of executed tools
- `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 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, namespaced under a caller-controlled scope name. The server creates a persistent directory tree at `/knowledge/sdk/{name}/{cache_id}/` that accumulates across sessions._

```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 `/knowledge/sdk/{name}/{cache_id}/...` on the server
- 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 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`, `shop`.
- `"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`, `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` |
| `shop` | `shop_apply`, `shop_read`, `shop_patch`, `shop_delete`, `shop_list`, `shop_status`, `shop_observe`, `shop_watch`, `shop_stop`, `shop_start`, `shop_restart`, `shop_scaffold`, `shop_glossary` |

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: Shop strategy loop

`shop` lets the model author and iterate Python or Rust trading strategies against the server-side nursery wells. Python `.py` strategies hot-load under `/shop/strategies/python`; Rust `.rs` strategies compile under `/shop/strategies/rust`. SDK callers enable the service surface; the model performs the plan -> write/patch -> observe -> watch loop through built-in service calls.

```python
from nineth import NinethClient

with NinethClient(default_model="1984-m3-0614") as client:
    response = client.model.request(
        """
        Build a conservative paper-trading momentum strategy for EURUSD.
        Plan first, inspect the shop glossary, write a Python on_tick strategy
        or scaffold Rust if needed, run a sandbox check if useful, call shop_observe, and attach
        shop_watch if the strategy is ready for long-running monitoring.
        Optimize for risk-adjusted return, not raw PnL.
        """,
        session=True,
        continuous=True,
        vcache={"name": "trading", "cache_id": "eurusd-momentum"},
        default_service=["shop", "computer", "knowledge"],
        max_iterations=20,
    )
    print(response["final_response"])
```

Operational notes:

- The API deployment owns Shop execution. SDK callers do not deploy Modal functions directly.
- Server operators deploy `entry-stub.py`; it contains both the API and the state-aware `shop(mode="continuous" | "once" | "compile")` Modal function. Continuous mode hot-discovers Python strategies and compiles/runs Rust strategies when present.
- `shop_observe` is the compact feedback surface. Healthy state is `mode="ack"`; failures are `mode="alert"` with issue details.
- `shop_watch` binds the current continuous worker to runner alerts so the model wakes only when the runner has a real issue.
- Use `stream=True` for long-running SDK calls when you want to display service progress events while the model plans and patches strategy code.

### 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.",
    "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,
        )
```

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

_Use when you want the server to forward tool-call requests to your HTTP callback endpoint. The model executes tool schemas, and results are returned asynchronously to your callback URL rather than inline._

```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",
            },
        },
    )
```

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.

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 sends this through Telegram's `sendRichMessage` method. Rich Markdown supports headings, lists, tables, quotations, details blocks, footnotes, formulas, and HTTP(S) media blocks. Private inbound Telegram sessions also stream temporary rich drafts while the model works; the final response is still sent as a persistent message.

Inbound Telegram work is durably handed from the webhook to a separate Modal worker invocation after the update is written under `/knowledge`. `set_alarm` and `schedule_at` persist deadlines in the same context; a deployment-time wallclock tick claims due alarms and resumes the worker. Callers should treat alarms as durable, non-blocking checkpoints, not as dynamically created cron jobs or guarantees that one HTTP request/container remains alive for the delay.

### 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
// ./templates/order_fallbacks.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` is caller runtime instruction overlay.
- `guardrail` augments ADAM in the default SDK/API ingress path.

### 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. When enabled, each deputy is an ephemeral nested worker with its own nonpersistent in-memory context across iterations and only services granted from the governor's request-scoped 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 governor 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. The governor receives a single observation with per-deputy outputs, success flags, merged token usage, and any service call/response traces, then synthesises the findings into 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 the governor's allowlist are rejected, and outbound messaging services are always removed.
- `total_deputies` must exactly match the number of entries in `deputies`.
- Total `max_tokens` across all deputies is capped by the server's `M_SERIES_DEPUTY_TOKEN_BUDGET` (default 20 000).
- 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
{
    "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:

- `final_response` is always present
- `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",
    "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": {}
}
```

### Stream `result` event

```json
{
    "type": "result",
    "process_id": "proc_ops_desk_alice_01",
    "session_id": "proc_ops_desk_alice_01",
    "data": {
        "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).

### 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",
    "deleted_path": "/knowledge/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",
    "moved_from": "/knowledge/sdk/ops-desk/alice",
    "moved_to": "/knowledge/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."
}
```

## Error Handling

SDK raises `NinethAPIError` for API/server failures.

```python
from nineth import NinethClient, NinethAPIError

with NinethClient(default_model="1984-m3-0424") as client:
    try:
        client.model.request("test")
    except NinethAPIError as exc:
        print("request failed:", exc)
```

Authentication missing raises `ValueError` before 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.
- 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.
- 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: the governor emitted a mismatched count; hint the model to 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 `M_SERIES_DEPUTY_TOKEN_BUDGET`.
- shop appears stopped or stale: ask the model to call `shop_status` or `shop_observe`; server operators can trigger one cycle with `modal run entry-stub.py --mode once` or a build-only check with `modal run entry-stub.py --mode compile`.

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

## Maintainer Link

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