Skip to main content

Build and deploy focused tooig's model harnesses on Nineth and Rooster

Project description

Bridge

bridge is the application framework for building focused model-powered software on top of Nineth and Rooster.

Choose a harness, write one handler, add the Python services your application needs, and let Bridge manage model selection, workers, sessions, durable memory, local service callbacks, terminal testing, scheduled jobs, and Modal deployment.

Bridge currently includes three harnesses:

Harness What it combines Good fits
messaging Telegram and email Support desks, operations assistants, notification workflows, communication drafting
finance Shop contracts plus Fund market and portfolio tools Strategy development, portfolio analysis, trading-environment operations, scheduled market reviews
research Search, page reading, and recursive deep research Due diligence, competitive intelligence, policy research, source-grounded reports

Bridge is intentionally higher-level than Nineth. Use Bridge when you want an application with an opinionated capability set and lifecycle. Use Nineth directly when you need low-level request control, raw callback handling, or streaming.

Table of Contents

Install

pip install --upgrade nineth-bridge

Bridge requires Python 3.10 or newer and installs Nineth, Textual, Modal, and HTTPX.

The three names used below refer to different surfaces of the same package:

Surface Name Example
PyPI distribution nineth-bridge pip install --upgrade nineth-bridge
Python package bridge from bridge import Bridge
Command-line program bridge bridge init research

This distinction matters when checking an installation: use pip show nineth-bridge for package metadata, but import bridge in Python and run bridge in the shell.

Set a Nineth API key before running a model turn:

# PowerShell
$env:NINETH_API_KEY = "..."

# POSIX shells
export NINETH_API_KEY="..."

Optional environment variables are listed in Configuration Reference.

What Bridge Is For

Bridge is useful when the application should expose a deliberate bundle of model capabilities instead of the entire Rooster service catalog.

Typical applications

Messaging

  • triage a support request, consult local account data, draft a response, and send after approval
  • turn operational events into concise Telegram updates
  • prepare or reply to templated email without exposing mail-provider details to application code
  • run scheduled communication summaries

Finance

  • inspect balances, prices, historical candles, live ticks, portfolios, and performance in one conversation
  • scaffold, edit, apply, and observe model-authored contracts (also called strategies)
  • monitor a contract and produce scheduled summaries
  • provide a read-only portfolio assistant by narrowing the service list

Research

  • produce a cited competitive landscape from search and primary pages
  • investigate a company, market, regulation, or technical topic across multiple sources
  • combine private application data from local services with public web evidence
  • schedule recurring intelligence reports

When to use Nineth directly instead

Use Nineth instead of Bridge when you need:

  • raw SSE streaming or token-by-token UI output
  • manual include_service pause/resume control
  • a request that does not fit one of the three harness capability sets
  • direct control over session, vcache, default_service, messaging, or callback payloads
  • an async-native client inside an existing event loop

Bridge uses Nineth internally; choosing Nineth directly does not remove access to Rooster.

Bridge, Nineth, and Rooster

The layers have separate responsibilities:

Layer Responsibility
Your project Business rules, handler branching, local services, job prompts
Bridge Harness presets, model routing, session identity, local service loop, CLI, deployment
Nineth HTTP transport, request shaping, process continuation, vcache lifecycle, callback protocol
Rooster Model workers, built-in services, memory internals, browser, messaging, Shop/Fund, telemetry

Bridge does not reimplement the worker or service engine. It supplies a high-level configuration to Nineth and handles the caller-managed parts Nineth deliberately leaves to applications.

Request Lifecycle

For a normal bridge.invoke(...) call:

  1. Bridge validates the message and resolves a session_id.
  2. Bridge reuses or creates one Nineth client for that session.
  3. Bridge loads decorated functions from the project's services/ directory.
  4. Your @bridge.handle function receives the message and a Context.
  5. context.respond(...) selects a model:
    • a manual model wins when configured
    • otherwise 1984-c1-mini chooses from the harness allowlist
    • invalid router output falls back to the harness default
  6. Bridge layers policy in this order: harness policy, application policy, turn policy.
  7. Bridge sends the task to Nineth with:
    • session=True
    • a caller-scoped vcache when memory is enabled
    • the harness's built-in Rooster services
    • schemas for discovered local services
  8. Rooster runs the worker and built-in services.
  9. If Rooster pauses for a local service, Bridge executes the Python function and resumes the same Nineth session.
  10. Bridge returns a normalized BridgeResponse.

The caller does not create worker IDs, process IDs, cache IDs, callback payloads, or service-result envelopes.

Choose a Harness

Choose based on the primary job of the application, not every possible tool it may need.

Question Choose
Is the output primarily an email, Telegram message, reply, or communication operation? messaging
Is the application inspecting markets, portfolios, or creating/running contracts? finance
Is the core task finding, reading, comparing, and citing web evidence? research

Local services can supply domain-specific capabilities to any harness. For example:

  • a messaging Bridge can add lookup_customer
  • a finance Bridge can add get_internal_risk_limit
  • a research Bridge can add query_private_documents

Do not choose a broad harness merely to gain one unrelated tool. A smaller service surface makes model behavior easier to understand and audit.

Quick Start

Create a research project:

bridge init research
cd bridge

The generated research.py is intentionally small:

from bridge import Bridge

bridge = Bridge("research")


@bridge.handle
def handle(message, context):
    return context.respond(message)

Run it in the terminal:

bridge run research

Try a concrete prompt:

Compare the official pricing and data-retention policies of three hosted vector
databases. Use primary sources, include URLs, and call out missing information.

Inspect routing and raw model/service data while developing:

bridge run research --debug

Deploy after the behavior is correct locally:

bridge deploy research

Generated Project Layout

bridge init {harness} creates ./bridge by default:

bridge/
  cookbook.md
  research.py
  services/
    run_sql.py
    post_chart.py
  jobs/
    monday-summary.py
Path Purpose
cookbook.md Guide tailored to the selected harness, including prompts and extension examples
{harness}.py One Bridge instance and its business-logic handler
services/*.py Local model-callable Python functions decorated with @service
jobs/*.py Scheduled functions decorated with @job
artifacts/ Created on demand by the starter post_chart service

Use a different destination when needed:

bridge init messaging --path apps/support-bridge

Bridge refuses to overwrite a non-empty directory unless --force is passed. --force replaces known starter files but does not delete unrelated files.

The run and deploy commands search the directory given by --project, then the current directory, then ./bridge:

bridge run research --project apps/research-bridge
bridge deploy research --project apps/research-bridge

Public Surface

The package exports:

from bridge import (
    Bridge,
    BridgeConfigurationError,
    BridgeDeploymentError,
    BridgeError,
    BridgeResponse,
    Context,
    DebugEvent,
    JobDefinition,
    Memory,
    ServiceDefinition,
    job,
    service,
)

Most projects only need Bridge, service, and job.

Bridge Construction

bridge = Bridge(
    "research",
    model=None,
    router_model=None,
    models=None,
    policy=None,
    messaging=None,
    memory=True,
    project_dir=None,
    max_service_rounds=10,
)

Constructor arguments

Argument Type Default Meaning
harness str required messaging, finance, or research
model `str None` None
router_model `str None` 1984-c1-mini
models iterable harness candidates Models the smart router may choose
policy `str None` None
messaging mapping None Email/Telegram defaults sent on messaging requests
memory bool True Provision a durable caller-scoped vcache
project_dir path None Directory whose services/ files are discovered
client Nineth-compatible client None Inject one client; mainly useful for tests and controlled embedding
client_factory callable None Advanced factory for creating one client per Bridge session
max_service_rounds int 10 Maximum local-service pause/resume rounds per turn

The CLI binds project_dir automatically after loading {harness}.py.

Lifecycle methods

Method Purpose
bridge.handle(function) Register the project's single handler; normally used as a decorator
bridge.invoke(message, session_id=None, debug=False, debug_sink=None) Execute one application turn
bridge.bind_project(path) Set the project directory used for service discovery
bridge.close() Close all unique Nineth clients created by this Bridge instance

Handler Contract

The recommended handler receives both the normalized message and the per-turn context:

@bridge.handle
def handle(message: str, context: Context):
    return context.respond(message)

Exactly one handler may be registered. Bridge also accepts:

@bridge.handle
def handle(message):
    return "A deterministic response that does not call a model."
@bridge.handle
def handle(context):
    return context.respond(context.message)
@bridge.handle
async def handle(message, context):
    metadata = await load_metadata(message)
    return context.respond(f"Metadata: {metadata}\n\nRequest: {message}")

The one-argument form receives Context only when the parameter is named context or ctx; otherwise it receives the message.

A handler may return:

  • context.respond(...) or another BridgeResponse
  • a dict with final_response
  • a string
  • another JSON-serializable value

Non-string values are serialized into the normalized response.

Business-logic branching

The handler is the right place for deterministic application behavior:

@bridge.handle
def handle(message, context):
    if message == "/health":
        return "The support Bridge is ready."
    if message.startswith("/draft "):
        return context.respond(
            message.removeprefix("/draft "),
            policy="Draft only. Do not send email or Telegram messages.",
            services=False,
        )
    return context.respond(message)

Context and respond

Each invocation creates a Context with:

Attribute Meaning
context.message Current normalized caller message
context.session_id Stable Bridge caller/session identity
context.model Selected downstream model after respond begins
context.memory Memory wrapper for the current vcache, or None
context.services Discovered local ServiceRegistry
context.client Underlying Nineth client for advanced use
context.debug Whether verbose/debug behavior is enabled

context.respond

response = context.respond(
    prompt=None,
    model=None,
    policy=None,
    services=True,
    reasoning="medium",
    max_iterations=12,
    response_format="text",
)
Argument Meaning
prompt Task sent to the model; defaults to context.message
model Per-turn manual model override
policy Per-turn policy appended after harness and application policy
services=True Enable all harness services and all discovered local services
services=False Disable harness and local services for this turn
services=[...] Replace the built-in harness allowlist with these names; local services remain available
other keyword arguments Forwarded to NinethClient.model.request

Useful forwarded options include:

  • reasoning
  • temperature, top_p, min_p, top_k
  • max_iterations, continuous
  • response_format
  • compute
  • use_deputy
  • images, audio

Bridge reserves and rejects direct values for model, session, vcache, default_service, include_service, client_service_results, messaging, verbose, and policy inside forwarded options. Use Bridge's explicit arguments and constructor settings instead.

Bridge expects buffered model responses. For SSE streaming, use Nineth directly.

Harness Reference

Each harness is a service allowlist plus a base policy and model-routing defaults.

Messaging Harness

Use messaging when communication is the main action.

bridge init messaging
bridge run messaging

Built-in capabilities:

Area Services
Telegram text send_message, send_rich_message, edit_message, edit_rich_message, edit_message_caption
Telegram media send_photo, preview_photo, send_voice
Telegram state get_messaging_status
Email delivery send_email, send_reply
Email templates list_email_templates, get_email_template, request_template_interlude
Email state get_email_status

Generated configuration:

import os
from bridge import Bridge

messaging = {
    "telegram": {
        "botId": os.getenv("TELEGRAM_BOT_ID"),
        "chatId": os.getenv("TELEGRAM_CHAT_ID"),
    },
    "email": {
        "email": os.getenv("BRIDGE_EMAIL_ADDRESS"),
        "name": os.getenv("BRIDGE_EMAIL_NAME", "Bridge"),
    },
}

bridge = Bridge("messaging", messaging=messaging)

Empty environment values are removed before the Nineth request is sent.

The base policy tells the model to confirm the recipient and final content before consequential outbound sends unless the caller already supplied an explicit approved message. That policy is behavior guidance, not authorization. Enforce user permissions outside the model.

Good prompts:

Draft a calm reply to this customer. Do not send it yet: ...
The following message is approved. Send it to the configured Telegram chat exactly
as written: Maintenance begins at 22:00 WAT and should finish within 20 minutes.

Finance Harness

Use finance when the application works with market data, portfolios, or executable contracts.

bridge init finance
bridge run finance

Contract is Bridge's canonical term for model-authored executable trading logic. "Strategy" is accepted as informal language.

Area Services
Contract source shop_scaffold, shop_read, shop_patch, shop_apply, shop_delete, shop_list, shop_glossary
Contract runtime shop_status, shop_observe, shop_watch, shop_start, shop_stop, shop_restart
Market data data_get_current_price, data_get_historical_ohlc, data_get_market_buffer, data_get_live_ticks, data_get_available_symbols
Fund state fund_balances
Portfolio portfolio_list, portfolio_add, portfolio_update, portfolio_remove
Performance performance

The base policy requires explicit caller intent before placing or activating live trades and forbids invented balances, fills, prices, or performance. This policy is not a brokerage risk control. Keep broker permissions, demo/live separation, position limits, and human approval in the execution environment.

Read-only example:

READ_ONLY_FINANCE = [
    "shop_list",
    "shop_status",
    "shop_observe",
    "shop_read",
    "shop_glossary",
    "fund_balances",
    "data_get_current_price",
    "data_get_historical_ohlc",
    "data_get_market_buffer",
    "data_get_live_ticks",
    "data_get_available_symbols",
    "portfolio_list",
    "performance",
]


@bridge.handle
def handle(message, context):
    return context.respond(message, services=READ_ONLY_FINANCE)

Research Harness

Use research when the primary job is discovery, reading, comparison, and source-grounded synthesis.

bridge init research
bridge run research
Area Services
General discovery search_web, search_news, search_discussions, search_unified, search_context
Rich verticals search_rich, search_videos, search_images, search_answers
Local/places search_places, search_local_pois, search_poi_descriptions
Reading read
Recursive investigation deepsearch

The base policy asks the model to prefer primary sources, distinguish source claims from inference, preserve direct URLs, and state uncertainty.

Good prompt structure:

Question: What changed in the vendor's enterprise data policy during the last year?
Scope: Official policy pages and release notes only.
Output: Executive summary, dated change table, implications, and source URLs.
Uncertainty: State what could not be verified.

Model Routing

Bridge supports manual and smart selection.

Selection precedence

  1. context.respond(..., model="...")
  2. Bridge(..., model="...")
  3. BRIDGE_MODEL
  4. Smart router
  5. Harness fallback when smart routing fails

Manual models are passed through to Nineth, including provider-qualified names.

Smart router

With no manual model, Bridge sends a separate stateless request to 1984-c1-mini. The routing turn:

  • has session=False
  • has no services
  • receives the harness name, candidate list, and task
  • must return one candidate as JSON

It does not inspect memory and does not act as a security layer.

Defaults:

Harness Candidates Fallback
messaging 1984-m2-light, 1984-m3-0614, 1984-c1-0614, amari-0524 1984-m2-light
finance 1984-m3-0614, 1984-c1-0614, amari-0524 1984-m3-0614
research 1984-m3-0614, 1984-c1-0614, amari-0524 1984-m3-0614

Constrain or replace the candidate list:

bridge = Bridge(
    "research",
    router_model="1984-c1-mini",
    models=["1984-m2-light", "1984-m3-0614"],
)

When the router errors, returns malformed output, or selects a non-candidate, Bridge uses the fallback and emits the error in debug mode.

Sessions and Memory

Session identity

bridge.invoke accepts an application-level session_id:

response = bridge.invoke("Continue the report", session_id="account-42")

Bridge maintains one Nineth client and one re-entrant lock per session ID. Calls in the same session reuse Nineth's remembered process and vcache IDs and execute serially. Different session IDs use different client state.

If session_id is omitted, Bridge generates a random ID and returns it. Save it if the caller should continue later.

Surface conventions:

Surface Session behavior
Textual runner Always uses terminal
HTTP API Uses request session_id, otherwise generates and returns one
Starter Monday job Uses job-monday-summary
Direct Python Uses the value passed to bridge.invoke

Do not use a display name alone as a session ID in multi-tenant software. Use a stable, collision-resistant application identifier.

Durable memory

With memory=True, Bridge sends this vcache name:

bridge-{harness}-{session_id}

Names are sanitized and long values are truncated with a hash suffix. Nineth remembers the server-generated cache_id; Rooster owns the hot buffer, structured state, journal, VHEC, scratch, and durable knowledge files.

Bridge keeps the resolved cache/process identifiers in the in-memory Nineth client. A process or Modal container restart does not automatically reconnect a session_id to the previous generated cache ID. The durable files may still exist server-side, but applications that require restart-safe identity should persist explicit memory identifiers through Nineth directly until Bridge exposes a persistent session registry.

Disable vcache memory while retaining isolated hot sessions:

bridge = Bridge("messaging", memory=False)

Direct memory operations

context.memory exposes:

context.memory.upsert(data=[...])
context.memory.rename("new-name")
context.memory.delete()

The vcache must first be established by a successful context.respond on the same session, unless the Nineth client already knows its cache ID. A safe pattern is:

@bridge.handle
def handle(message, context):
    response = context.respond(message)
    if message.startswith("Remember preference:"):
        context.memory.upsert([{"preference": message.split(":", 1)[1].strip()}])
    return response

For normal conversational memory, do not call these methods. context.respond already provisions and reuses memory automatically.

rename changes the active Nineth vcache name, while Bridge derives a fresh default name from the session on each new Context. Do not rename an automatically managed scope when later Bridge turns must continue using it; use Nineth directly for an explicitly named, persisted vcache lifecycle.

Local Services

Local services let the model use your application's APIs, database queries, calculations, or internal actions without adding them to Rooster.

Declare a service

Create a Python file in services/:

# services/lookup_customer.py
from bridge import service


@service(description="Look up a customer record by account ID.")
def lookup_customer(account_id: str, include_orders: bool = False) -> dict:
    customer = load_customer(account_id)
    return {
        "account_id": customer.id,
        "plan": customer.plan,
        "orders": customer.orders if include_orders else [],
    }

Bridge discovers every *.py file except names beginning with _.

Schema generation

The function name becomes the service name unless name= is supplied. The decorator description, docstring, or generated fallback becomes the service description.

Supported annotations are converted to JSON Schema:

Python JSON Schema
str string
int integer
float number
bool boolean
list[T] array with item schema
dict object with additional properties
Optional[T] schema for T, optional when a default exists
unions anyOf

Parameters without defaults are required. JSON-serializable defaults are included.

Override the public service name:

@service(name="get_order", description="Fetch an order visible to the current caller.")
def fetch_order(order_id: str) -> dict:
    ...

Injected parameters

These parameters are never exposed to the model and are injected by Bridge when declared:

Parameter Injected value
context Current Bridge Context
session_id Current Bridge session ID
context_id Vcache name when memory is enabled, otherwise session ID
@service(description="Return caller-specific feature flags.")
def get_feature_flags(session_id: str) -> dict:
    return flags_for_session(session_id)

Execution lifecycle

  1. Bridge sends local schemas to Nineth as caller-managed services.
  2. Rooster may return status="awaiting_client_services" with pending calls.
  3. Bridge finds each handler, injects context values, and executes it.
  4. Sync and async handlers are supported.
  5. Bridge returns a result envelope for every call, including errors.
  6. Bridge resumes the same model session.
  7. The loop repeats up to max_service_rounds.

Service exceptions become failed tool results so the model can explain or recover. Unknown and duplicate service names raise configuration errors.

Local services run with the permissions of the local process or Modal container. Validate authorization and inputs inside the service. JSON Schema is not an authorization boundary.

Jobs

Jobs are autonomous functions in jobs/*.py:

from bridge import job


@job("0 9 * * 1", name="monday-summary")
def monday_summary(bridge):
    return bridge.invoke(
        "Prepare the Monday briefing from current memory.",
        session_id="job-monday-summary",
    )

The schedule must be a five-field cron expression:

minute hour day-of-month month day-of-week

Examples:

Schedule Meaning
0 9 * * 1 Monday at 09:00
0 8 * * * Every day at 08:00
*/30 * * * * Every 30 minutes

At deployment Bridge discovers jobs and creates one modal.Cron function per job. A job receives the loaded Bridge instance when its function accepts one argument. Sync and async jobs are supported.

Use a stable, job-specific session ID when recurring runs should share memory. Use separate IDs when runs must be isolated.

Cron expressions are interpreted by Modal. Verify the deployment's effective timezone rather than assuming the developer machine's local timezone.

Bridge schedules jobs through Modal deployment; it does not currently provide a local bridge job command.

Local Terminal Runner

bridge run research

The Textual UI has a transcript and one input. It uses session ID terminal, so turns in one process share state.

Controls:

  • /exit
  • /quit
  • Ctrl+C

Debug mode:

bridge run research --debug

Debug output includes:

  • manual, smart, or fallback routing decision
  • router model and raw routing response
  • local service calls and results
  • verbose raw Nineth response

Debug data can contain prompts, local service outputs, and server telemetry. Do not treat debug transcripts as secret-safe logs.

Modal Deployment

Prerequisites

  1. Authenticate the Modal CLI.
  2. Create a Modal secret containing NINETH_API_KEY and all provider/application secrets.
  3. Use the default secret name bridge or pass --modal-secret.
  4. Configure the alias control plane, or use --no-alias.
bridge deploy research
bridge deploy research --modal-secret bridge-production
bridge deploy research --no-alias

What deployment generates

Bridge creates a temporary Modal entrypoint that:

  • starts from a Python 3.11 Debian image
  • installs compatible Nineth and HTTPX versions
  • includes the locally installed Bridge package source
  • includes the project at /root/bridge_project
  • exposes one ASGI endpoint
  • creates one scheduled function per job
  • attaches the selected Modal secret

The temporary local entrypoint is deleted after modal deploy returns.

Alias registration

By default Bridge requests:

https://{deployment_id}.bridge.tooig.com

It sends this request to BRIDGE_ALIAS_API_URL, defaulting to https://bridge.tooig.com/api/aliases:

{
  "deployment_id": "32-character UUID hex",
  "harness": "research",
  "target_url": "https://...modal.run",
  "requested_url": "https://{deployment_id}.bridge.tooig.com"
}

When BRIDGE_API_KEY exists, Bridge uses it as a Bearer token. The control plane must return url, alias, or a successful empty object after binding the requested URL.

If alias registration fails after Modal succeeds, BridgeDeploymentError includes the working Modal URL. --no-alias skips registration and reports the Modal URL directly.

DNS, wildcard TLS, and the alias reverse proxy are external infrastructure, not implemented by the Python package.

Deployed HTTP API

Health

GET /health
{
  "status": "ok",
  "harness": "research"
}

Invoke

POST /
Content-Type: application/json

{
  "message": "Compare the vendors using primary sources.",
  "session_id": "account-42",
  "debug": false
}

input is accepted as an alias for message.

{
  "final_response": "...",
  "model": "1984-m3-0614",
  "session_id": "account-42"
}

When session_id is omitted, save the returned generated value and send it on the next request.

debug=true adds raw to the response.

Status Meaning
200 Successful health or model response
400 Invalid JSON, message, or session input
404 Unknown path or method
422 Bridge configuration failure
500 Unexpected execution failure; details are not exposed

Request bodies are limited to 1 MiB.

The endpoint does not add application authentication. Put it behind an authenticated gateway or alias control plane before exposing private data, messaging, or financial actions.

JavaScript and TypeScript Client

Install the npm client when a Node.js service or frontend needs to call a deployed Bridge. It does not replace the Python framework or provide bridge init, run, or deploy; those commands remain in the Python distribution.

npm install nineth-bridge
import { BridgeClient } from "nineth-bridge";

const client = new BridgeClient({
  endpoint: "https://your-alias.bridge.tooig.com",
});

const result = await client.invoke(
  "Compare the vendors using primary sources and direct URLs.",
);
console.log(result.final_response);

BridgeClient remembers the returned session_id and sends it on the next call. Use one instance per user, pass an explicit sessionId, or call resetSession() when a new conversation should begin. The npm-specific README is in ../npm/nineth-bridge/README.md.

Configuration Reference

Variable Required Purpose
NINETH_API_KEY Yes for model turns Nineth authentication
NINETH_BASE_URL No Override the Nineth/Rooster API URL
BRIDGE_MODEL No Environment-level manual model override
BRIDGE_ROUTER_MODEL No Small model used for smart routing
BRIDGE_API_KEY For protected alias APIs Bearer credential for alias registration
BRIDGE_ALIAS_API_URL No Alias registration endpoint
TELEGRAM_BOT_ID Messaging-dependent Generated messaging project's bot identity
TELEGRAM_CHAT_ID Messaging-dependent Generated messaging project's default chat
BRIDGE_EMAIL_ADDRESS Messaging-dependent Generated messaging project's mailbox identity
BRIDGE_EMAIL_NAME No Email sender display name; defaults to Bridge
BRIDGE_SQLITE_PATH Starter service only SQLite path used by generated run_sql

Provider-specific Rooster services may require additional server-side or Modal secrets.

Cookbook

These recipes build on the technical contract above. Each starts from a generated project unless noted.

Recipe 1: Build a source-grounded research brief

bridge init research --path vendor-research
bridge run research --project vendor-research

Update vendor-research/research.py:

from bridge import Bridge

bridge = Bridge(
    "research",
    policy=(
        "Prefer official documentation, regulatory filings, and first-party release notes. "
        "End every report with a source table containing title, URL, date, and relevance."
    ),
)


@bridge.handle
def handle(message, context):
    prompt = f"""
Research question:
{message}

Required output:
1. Executive summary
2. Evidence grouped by claim
3. Conflicting evidence
4. Unknowns and limitations
5. Source table with direct URLs
"""
    return context.respond(prompt, reasoning="medium", max_iterations=20)

Example prompt:

How have the three largest European cloud providers changed their sovereign-cloud
offerings since January 2025, and what remains unavailable?

Use a stable terminal or API session for follow-ups such as "verify the second claim using only primary sources."

Recipe 2: Build a customer-support messaging assistant

bridge init messaging --path support-bridge

Add a customer lookup service:

# support-bridge/services/lookup_customer.py
from bridge import service


@service(description="Get the support-safe customer summary for an account.")
def lookup_customer(account_id: str) -> dict:
    return {
        "account_id": account_id,
        "plan": "business",
        "open_ticket": "T-1042",
        "status": "awaiting replacement",
    }

Customize messaging.py:

@bridge.handle
def handle(message, context):
    return context.respond(
        message,
        policy=(
            "Use lookup_customer before making account-specific claims. "
            "Draft first. Send only when the caller explicitly says approved and supplies "
            "the final recipient and content."
        ),
        max_iterations=12,
    )

Development flow:

Draft a reply for account AC-42 explaining the replacement status. Do not send.

Then:

Approved. Send that final reply to customer@example.com.

Application authorization must confirm that the caller may access AC-42 and send from the configured mailbox.

Recipe 3: Send an approved Telegram update

Set TELEGRAM_BOT_ID and TELEGRAM_CHAT_ID, then use an explicit command convention:

@bridge.handle
def handle(message, context):
    if not message.startswith("APPROVED: "):
        return context.respond(
            message,
            policy="Draft a Telegram update only. Do not send it.",
            services=False,
        )
    approved = message.removeprefix("APPROVED: ")
    return context.respond(
        f"Send this text exactly to the configured Telegram chat: {approved}",
        services=["send_message", "get_messaging_status"],
    )

This narrows the action turn to plain text delivery and status inspection. The APPROVED: prefix is only a local convention; authenticate the caller separately.

Recipe 4: Inspect a finance environment without trading

from bridge import Bridge

bridge = Bridge(
    "finance",
    policy="This application is read-only. Never create, patch, apply, start, or restart contracts.",
)

READ_ONLY = [
    "shop_list",
    "shop_read",
    "shop_status",
    "shop_observe",
    "shop_glossary",
    "fund_balances",
    "data_get_current_price",
    "data_get_historical_ohlc",
    "data_get_market_buffer",
    "data_get_live_ticks",
    "data_get_available_symbols",
    "portfolio_list",
    "performance",
]


@bridge.handle
def handle(message, context):
    return context.respond(message, services=READ_ONLY, max_iterations=12)

Example prompts:

Summarize balances, open portfolio exposures, recent performance, and stale contract
runtime state. Do not modify anything.
Compare BTC and ETH daily volatility over the available 90-day history and explain
what that means for the current portfolio weights.

Recipe 5: Develop and observe a trading contract

Use a demo environment and make the lifecycle explicit:

bridge = Bridge(
    "finance",
    policy=(
        "Operate only on the demo well. Begin with shop_glossary and current state. "
        "Show the proposed contract source and risk assumptions before applying it. "
        "Do not start a contract until the caller separately approves activation."
    ),
)


@bridge.handle
def handle(message, context):
    return context.respond(message, reasoning="medium", max_iterations=25)

Use separate turns:

Design a Python mean-reversion contract for the demo well. Inspect the glossary and
available symbols, scaffold the source, and show it to me. Do not apply or start it.
Patch the contract to cap position size at 1% of demo equity. Apply it, but do not start it.
I approve activation in the demo environment. Start the contract, bind a watch, and
report the first observation.

Model policy is not sufficient for production trading approval. Enforce environment and account restrictions in broker credentials and services.

Recipe 6: Add an application-specific local service

This pattern works with every harness:

# services/get_internal_policy.py
from bridge import service


@service(description="Read an approved internal policy by exact policy ID.")
def get_internal_policy(policy_id: str, context_id: str) -> dict:
    policy = policy_store.read(policy_id, caller_context=context_id)
    return {
        "policy_id": policy.id,
        "title": policy.title,
        "body": policy.body,
        "updated_at": policy.updated_at.isoformat(),
    }

Then ask the model to combine it with harness capabilities:

Read policy RET-12 and compare it with the vendor's current public retention terms.
Cite the public sources and list every conflict.

The research harness can browse; the local service supplies private policy data.

Recipe 7: Use the generated SQL and chart services

The starter run_sql accepts read-only SQLite SELECT, WITH, and PRAGMA statements and returns at most 200 rows.

$env:BRIDGE_SQLITE_PATH = "C:\data\analytics.db"
bridge run research

Example prompt:

Use run_sql to summarize monthly active accounts from the events table. Then use
post_chart to save a bar-chart specification as monthly-active.json. Explain any
assumptions you made about the schema.

post_chart writes JSON under artifacts/. It does not publish a hosted chart. Replace the starter with your charting or storage integration when sharing is required.

Recipe 8: Keep user sessions isolated

from bridge.project import load_project

app = load_project("research", "./bridge")

alice = app.invoke("Research vendor A", session_id="tenant-7:user-alice")
bob = app.invoke("Research vendor B", session_id="tenant-7:user-bob")

alice_follow_up = app.invoke(
    "Now verify the pricing claim.",
    session_id=alice.session_id,
)

Use the same ID only when callers should share conversational and durable context.

Recipe 9: Seed and manage durable memory

Establish the vcache with a model response before direct mutation:

@bridge.handle
def handle(message, context):
    response = context.respond(message)
    if message.startswith("Remember preference:"):
        preference = message.split(":", 1)[1].strip()
        context.memory.upsert([{"type": "preference", "value": preference}])
    return response

For destructive lifecycle operations, use explicit confirmation commands and authorization:

if message == "/confirm-forget-session":
    context.memory.delete()
    return "Session memory deleted."

After deletion, the next model turn provisions a new vcache scope for that session name.

Recipe 10: Pin or constrain model selection

Pin every turn:

bridge = Bridge("research", model="1984-m3-0614")

Allow smart routing across a smaller set:

bridge = Bridge(
    "messaging",
    models=["1984-m2-light", "1984-m3-0614"],
)

Override one turn:

return context.respond(message, model="1984-c1-0614")

Use --debug to see whether selection was manual, smart, or fallback.

Recipe 11: Request structured JSON

@bridge.handle
def handle(message, context):
    return context.respond(
        f"""
{message}

Return JSON with keys: summary, findings, risks, sources.
Each source must have title and url.
""",
        response_format="json",
        max_iterations=15,
    )

Bridge normalizes the final value into BridgeResponse.final_response, which is a string. Use json.loads(response.final_response) in application code when JSON was requested.

Recipe 12: Schedule autonomous work

Messaging

@job("0 9 * * 1", name="support-weekly-summary")
def support_summary(bridge):
    return bridge.invoke(
        "Summarize unresolved communication work. Draft the report; do not send.",
        session_id="job-support-weekly-summary",
    )

Finance

@job("0 8 * * *", name="daily-risk-review")
def daily_risk_review(bridge):
    return bridge.invoke(
        "Review balances, exposure, performance, and contract alerts. Do not trade or modify contracts.",
        session_id="job-daily-risk-review",
    )

Research

@job("0 7 * * 1", name="weekly-competitor-watch")
def competitor_watch(bridge):
    return bridge.invoke(
        "Find material competitor announcements from the last seven days. Use primary sources and URLs.",
        session_id="job-weekly-competitor-watch",
    )

Deploy to activate Modal schedules.

Recipe 13: Call Bridge from Python

from bridge.project import load_project

app = load_project("research", "./bridge")
try:
    response = app.invoke(
        "Prepare a cited market map for managed vector databases.",
        session_id="market-map-2026",
    )
    print(response.final_response)
    print("model:", response.model)
    print("session:", response.session_id)
finally:
    app.close()

Bridge.invoke is synchronous. In an async application, run it in a worker thread:

import asyncio

response = await asyncio.to_thread(
    app.invoke,
    message,
    session_id=session_id,
)

Recipe 14: Call a deployed Bridge from a frontend

Call Bridge through an authenticated same-origin backend route. That backend should map the authenticated principal to session_id and forward the request to the deployed Bridge:

import { BridgeClient } from "nineth-bridge";

const bridge = new BridgeClient({
  endpoint: "/api/bridge",
});

const result = await bridge.invoke("Continue the vendor comparison.");
document.querySelector("#answer").textContent = result.final_response;

Bridge does not add CORS or application authentication. Do not call a sensitive deployment directly from an untrusted browser. Add authentication, authorization, rate limiting, CORS when needed, and tenant-to-session mapping at the gateway or application backend.

Recipe 15: Add application policy and service limits

Policies layer rather than replace one another:

bridge = Bridge(
    "research",
    policy="Never include personal data in reports.",
)


@bridge.handle
def handle(message, context):
    return context.respond(
        message,
        policy="For this route, use official sources only.",
        services=["search_web", "search_context", "read"],
        max_iterations=10,
    )

Effective policy order:

  1. fixed harness policy
  2. Bridge(policy=...)
  3. context.respond(policy=...)

Service limits are enforcement at the request schema level. Policy is model instruction. Use both when narrowing behavior matters.

Response Shapes

Python response

BridgeResponse(
    final_response="...",
    model="1984-m3-0614",
    session_id="account-42",
    raw={...},
)

Fields:

Field Meaning
final_response Normalized text returned by the handler/model
model Selected model, or None when the handler did not call respond
session_id Bridge-level session identity
raw Raw model response or normalized handler value

Serialize for an API:

response.to_dict()
{
  "final_response": "...",
  "model": "1984-m3-0614",
  "session_id": "account-42"
}

Include raw data explicitly:

response.to_dict(debug=True)

Debug events

A programmatic caller can receive structured debug events:

events = []
response = bridge.invoke(
    "Investigate this claim",
    session_id="audit-1",
    debug=True,
    debug_sink=events.append,
)

for event in events:
    print(event.kind, event.data)

Current event kinds include model_routing, local_services, and model_response.

Error Handling

from bridge import BridgeConfigurationError, BridgeDeploymentError, BridgeError

try:
    response = bridge.invoke(message, session_id=session_id)
except BridgeConfigurationError as exc:
    print("Project or request configuration is invalid:", exc)
except BridgeDeploymentError as exc:
    print("Deployment failed:", exc)
except BridgeError as exc:
    print("Bridge failed:", exc)

Nineth transport and API failures may surface as NinethAPIError.

Common configuration errors include:

  • unknown harness name
  • missing @bridge.handle
  • more than one handler
  • empty message
  • duplicate local service name
  • invalid job cron shape
  • direct use of a Bridge-managed request option
  • local service callback rounds exceeding the configured limit

Security and Operational Boundaries

Bridge simplifies architecture; it does not remove the need for application controls.

  • Authentication: the deployed ASGI endpoint does not authenticate callers.
  • Authorization: harness policy is not permission checking. Validate users in your gateway and local services.
  • Finance: model confirmation language is not a brokerage control. Use demo/live separation, scoped credentials, limits, and human approval.
  • Messaging: verify sender ownership and recipient authorization outside model text.
  • Local services: functions execute with process/container permissions. Apply least privilege and validate all parameters.
  • Sessions: map authenticated principals to session IDs server-side. Do not trust arbitrary browser-provided tenant IDs.
  • Debug: raw responses and service results may contain sensitive data.
  • Secrets: provide credentials through environment variables or Modal secrets, never generated source files.
  • Alias: wildcard DNS/TLS and reverse-proxy behavior belong to the external control plane.

Practical Patterns

  • Begin with the generated handler and one concrete prompt before adding services.
  • Keep one Bridge instance alive in a process to reuse HTTP connections and session state.
  • Use stable, namespaced session IDs such as tenant:{tenant_id}:user:{user_id}.
  • Use services=[...] for read-only or route-specific capability limits.
  • Put deterministic branching and authorization before context.respond.
  • Return compact, JSON-serializable local service results.
  • Give services action-oriented descriptions and precise parameter names.
  • Use separate job session IDs from interactive users.
  • Test with --debug, but disable debug in normal production responses.
  • Use Nineth directly for streaming rather than forcing a stream iterator through Bridge.

Troubleshooting

Authentication required

Set NINETH_API_KEY locally and in the Modal secret used by deployment.

Could not find research.py

Run from the parent of ./bridge, from the project itself, or pass --project:

bridge run research --project path/to/bridge

must expose a Bridge instance named bridge or app

The harness file must have:

bridge = Bridge("research")

must register a handler with @bridge.handle

Add exactly one decorated function.

The wrong model was selected

Run with --debug to inspect routing. Pin model= or narrow models=[...] when deterministic selection matters.

Memory is not continuing

Reuse the exact session_id and the same long-lived Bridge process. In the HTTP API, store the returned ID. Bridge does not currently persist Nineth's resolved cache/process identifiers, so a process or Modal container restart does not automatically reconnect to the prior generated vcache.

Direct memory.upsert says cache_id is required

Run a successful context.respond first on that session so Nineth can remember the generated cache ID.

A local service was not discovered

Check that:

  • the file is directly under services/
  • the filename does not start with _
  • the function uses @service
  • importing the file does not raise
  • no other service uses the same public name

Local service callbacks keep repeating

Inspect debug events and service results. Make sure the service returns the fields the model needs. Increase max_service_rounds only when the repeated work is expected.

A job does not run locally

Jobs become modal.Cron functions during bridge deploy. There is currently no local scheduler command.

Alias registration failed after deployment

Read the error for the working modal.run URL. Fix BRIDGE_ALIAS_API_URL/BRIDGE_API_KEY or deploy with --no-alias.

The frontend loses conversation history

Persist session_id from the response and resend it. Do not generate a new ID for every request.

Maintainer Guide

Bridge is a separate distribution under rooster-sdk/bridge.

Module Responsibility
bridge/harnesses.py Harness services, policy, routing candidates, fallback
bridge/runtime.py Handler, session/client state, memory, request and local-service loops
bridge/routing.py Manual/smart selection and fallback
bridge/services.py Decorator schema generation, discovery, execution
bridge/jobs.py Job declaration, discovery, execution
bridge/project.py Generated implementation/cookbook/services/jobs and project loading
bridge/terminal.py Textual local runner
bridge/asgi.py Deployed HTTP contract
bridge/deployment.py Modal source, deploy command, endpoint parsing, alias registration
bridge/cli.py init, run, and deploy commands

Run Bridge tests:

$env:PYTHONPATH = "rooster-sdk/bridge;rooster-sdk"
python -m pytest tests/test_bridge_project.py tests/test_bridge_runtime.py tests/test_bridge_deployment.py tests/test_bridge_cli.py

Run the delegated Nineth regressions:

python -m pytest tests/test_nineth_client.py tests/test_nineth_smoke.py

Build:

python -m build rooster-sdk/bridge
python -m twine check rooster-sdk/bridge/dist/*

Release automation bumps and publishes Nineth and Bridge separately from .github/workflows/build.yml. Keep Bridge's Nineth lower bound aligned with the callback/session behavior it uses.

For Rooster internals and release operations, see README.md.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nineth_bridge-0.2.5.tar.gz (56.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nineth_bridge-0.2.5-py3-none-any.whl (45.4 kB view details)

Uploaded Python 3

File details

Details for the file nineth_bridge-0.2.5.tar.gz.

File metadata

  • Download URL: nineth_bridge-0.2.5.tar.gz
  • Upload date:
  • Size: 56.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nineth_bridge-0.2.5.tar.gz
Algorithm Hash digest
SHA256 5befadf3b356b49a8ab6d8167020134ee975e393a769c4420d156b087982efc3
MD5 bd39ab13479f8a17c6f7e3545d974564
BLAKE2b-256 c76f1d097b758fce6b450a351aeb2e0c05a6eaf86458f3053b14c57f23d6a49e

See more details on using hashes here.

Provenance

The following attestation bundles were made for nineth_bridge-0.2.5.tar.gz:

Publisher: build.yml on districtt/rooster

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nineth_bridge-0.2.5-py3-none-any.whl.

File metadata

  • Download URL: nineth_bridge-0.2.5-py3-none-any.whl
  • Upload date:
  • Size: 45.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for nineth_bridge-0.2.5-py3-none-any.whl
Algorithm Hash digest
SHA256 844e8653d2ba7b8b82c7830d59e3c4e8645d88ed37e2f60e5aa087b701ced817
MD5 43c8e66393e0339e41858b4522b32528
BLAKE2b-256 79bbe96afefe610b582c252c0fc8ec5548f890125bb29238af25ad233d8801be

See more details on using hashes here.

Provenance

The following attestation bundles were made for nineth_bridge-0.2.5-py3-none-any.whl:

Publisher: build.yml on districtt/rooster

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page