nineth-bridge 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,1661 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nineth-bridge
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Build and deploy focused tooig's model harnesses on Nineth and Rooster
|
|
5
|
+
Project-URL: Homepage, https://github.com/districtt/rooster
|
|
6
|
+
Project-URL: Issues, https://github.com/districtt/rooster/issues
|
|
7
|
+
Author-email: "Tooig, Inc" <tooighq@gmail.com>, Oyebamijo <boy@oyebamijo.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: httpx<1.0,>=0.27.2
|
|
14
|
+
Requires-Dist: modal<2.0,>=0.64
|
|
15
|
+
Requires-Dist: nineth<1.0,>=0.8.9
|
|
16
|
+
Requires-Dist: textual<2.0,>=0.80
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# Bridge
|
|
20
|
+
|
|
21
|
+
`bridge` is the application framework for building focused model-powered software on top of [Nineth](../nineth.md) and Rooster.
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Bridge currently includes three harnesses:
|
|
26
|
+
|
|
27
|
+
| Harness | What it combines | Good fits |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `messaging` | Telegram and email | Support desks, operations assistants, notification workflows, communication drafting |
|
|
30
|
+
| `finance` | Shop contracts plus Fund market and portfolio tools | Strategy development, portfolio analysis, trading-environment operations, scheduled market reviews |
|
|
31
|
+
| `research` | Search, page reading, and recursive deep research | Due diligence, competitive intelligence, policy research, source-grounded reports |
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
## Table of Contents
|
|
36
|
+
|
|
37
|
+
- [Install](#install)
|
|
38
|
+
- [What Bridge Is For](#what-bridge-is-for)
|
|
39
|
+
- [Bridge, Nineth, and Rooster](#bridge-nineth-and-rooster)
|
|
40
|
+
- [Request Lifecycle](#request-lifecycle)
|
|
41
|
+
- [Choose a Harness](#choose-a-harness)
|
|
42
|
+
- [Quick Start](#quick-start)
|
|
43
|
+
- [Generated Project Layout](#generated-project-layout)
|
|
44
|
+
- [Public Surface](#public-surface)
|
|
45
|
+
- [Bridge Construction](#bridge-construction)
|
|
46
|
+
- [Handler Contract](#handler-contract)
|
|
47
|
+
- [Context and `respond`](#context-and-respond)
|
|
48
|
+
- [Harness Reference](#harness-reference)
|
|
49
|
+
- [Messaging Harness](#messaging-harness)
|
|
50
|
+
- [Finance Harness](#finance-harness)
|
|
51
|
+
- [Research Harness](#research-harness)
|
|
52
|
+
- [Model Routing](#model-routing)
|
|
53
|
+
- [Sessions and Memory](#sessions-and-memory)
|
|
54
|
+
- [Local Services](#local-services)
|
|
55
|
+
- [Jobs](#jobs)
|
|
56
|
+
- [Local Terminal Runner](#local-terminal-runner)
|
|
57
|
+
- [Modal Deployment](#modal-deployment)
|
|
58
|
+
- [Deployed HTTP API](#deployed-http-api)
|
|
59
|
+
- [Configuration Reference](#configuration-reference)
|
|
60
|
+
- [Cookbook](#cookbook)
|
|
61
|
+
- [Recipe 1: Build a source-grounded research brief](#recipe-1-build-a-source-grounded-research-brief)
|
|
62
|
+
- [Recipe 2: Build a customer-support messaging assistant](#recipe-2-build-a-customer-support-messaging-assistant)
|
|
63
|
+
- [Recipe 3: Send an approved Telegram update](#recipe-3-send-an-approved-telegram-update)
|
|
64
|
+
- [Recipe 4: Inspect a finance environment without trading](#recipe-4-inspect-a-finance-environment-without-trading)
|
|
65
|
+
- [Recipe 5: Develop and observe a trading contract](#recipe-5-develop-and-observe-a-trading-contract)
|
|
66
|
+
- [Recipe 6: Add an application-specific local service](#recipe-6-add-an-application-specific-local-service)
|
|
67
|
+
- [Recipe 7: Use the generated SQL and chart services](#recipe-7-use-the-generated-sql-and-chart-services)
|
|
68
|
+
- [Recipe 8: Keep user sessions isolated](#recipe-8-keep-user-sessions-isolated)
|
|
69
|
+
- [Recipe 9: Seed and manage durable memory](#recipe-9-seed-and-manage-durable-memory)
|
|
70
|
+
- [Recipe 10: Pin or constrain model selection](#recipe-10-pin-or-constrain-model-selection)
|
|
71
|
+
- [Recipe 11: Request structured JSON](#recipe-11-request-structured-json)
|
|
72
|
+
- [Recipe 12: Schedule autonomous work](#recipe-12-schedule-autonomous-work)
|
|
73
|
+
- [Recipe 13: Call Bridge from Python](#recipe-13-call-bridge-from-python)
|
|
74
|
+
- [Recipe 14: Call a deployed Bridge from a frontend](#recipe-14-call-a-deployed-bridge-from-a-frontend)
|
|
75
|
+
- [Recipe 15: Add application policy and service limits](#recipe-15-add-application-policy-and-service-limits)
|
|
76
|
+
- [Response Shapes](#response-shapes)
|
|
77
|
+
- [Error Handling](#error-handling)
|
|
78
|
+
- [Security and Operational Boundaries](#security-and-operational-boundaries)
|
|
79
|
+
- [Practical Patterns](#practical-patterns)
|
|
80
|
+
- [Troubleshooting](#troubleshooting)
|
|
81
|
+
- [Maintainer Guide](#maintainer-guide)
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
```console
|
|
86
|
+
pip install --upgrade bridge
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Bridge requires Python 3.10 or newer and installs Nineth, Textual, Modal, and HTTPX.
|
|
90
|
+
|
|
91
|
+
Set a Nineth API key before running a model turn:
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
# PowerShell
|
|
95
|
+
$env:NINETH_API_KEY = "..."
|
|
96
|
+
|
|
97
|
+
# POSIX shells
|
|
98
|
+
export NINETH_API_KEY="..."
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Optional environment variables are listed in [Configuration Reference](#configuration-reference).
|
|
102
|
+
|
|
103
|
+
## What Bridge Is For
|
|
104
|
+
|
|
105
|
+
Bridge is useful when the application should expose a deliberate bundle of model capabilities instead of the entire Rooster service catalog.
|
|
106
|
+
|
|
107
|
+
### Typical applications
|
|
108
|
+
|
|
109
|
+
**Messaging**
|
|
110
|
+
|
|
111
|
+
- triage a support request, consult local account data, draft a response, and send after approval
|
|
112
|
+
- turn operational events into concise Telegram updates
|
|
113
|
+
- prepare or reply to templated email without exposing mail-provider details to application code
|
|
114
|
+
- run scheduled communication summaries
|
|
115
|
+
|
|
116
|
+
**Finance**
|
|
117
|
+
|
|
118
|
+
- inspect balances, prices, historical candles, live ticks, portfolios, and performance in one conversation
|
|
119
|
+
- scaffold, edit, apply, and observe model-authored contracts (also called strategies)
|
|
120
|
+
- monitor a contract and produce scheduled summaries
|
|
121
|
+
- provide a read-only portfolio assistant by narrowing the service list
|
|
122
|
+
|
|
123
|
+
**Research**
|
|
124
|
+
|
|
125
|
+
- produce a cited competitive landscape from search and primary pages
|
|
126
|
+
- investigate a company, market, regulation, or technical topic across multiple sources
|
|
127
|
+
- combine private application data from local services with public web evidence
|
|
128
|
+
- schedule recurring intelligence reports
|
|
129
|
+
|
|
130
|
+
### When to use Nineth directly instead
|
|
131
|
+
|
|
132
|
+
Use [Nineth](../nineth.md) instead of Bridge when you need:
|
|
133
|
+
|
|
134
|
+
- raw SSE streaming or token-by-token UI output
|
|
135
|
+
- manual `include_service` pause/resume control
|
|
136
|
+
- a request that does not fit one of the three harness capability sets
|
|
137
|
+
- direct control over `session`, `vcache`, `default_service`, `messaging`, or callback payloads
|
|
138
|
+
- an async-native client inside an existing event loop
|
|
139
|
+
|
|
140
|
+
Bridge uses Nineth internally; choosing Nineth directly does not remove access to Rooster.
|
|
141
|
+
|
|
142
|
+
## Bridge, Nineth, and Rooster
|
|
143
|
+
|
|
144
|
+
The layers have separate responsibilities:
|
|
145
|
+
|
|
146
|
+
| Layer | Responsibility |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| Your project | Business rules, handler branching, local services, job prompts |
|
|
149
|
+
| Bridge | Harness presets, model routing, session identity, local service loop, CLI, deployment |
|
|
150
|
+
| Nineth | HTTP transport, request shaping, process continuation, vcache lifecycle, callback protocol |
|
|
151
|
+
| Rooster | Model workers, built-in services, memory internals, browser, messaging, Shop/Fund, telemetry |
|
|
152
|
+
|
|
153
|
+
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.
|
|
154
|
+
|
|
155
|
+
## Request Lifecycle
|
|
156
|
+
|
|
157
|
+
For a normal `bridge.invoke(...)` call:
|
|
158
|
+
|
|
159
|
+
1. Bridge validates the message and resolves a `session_id`.
|
|
160
|
+
2. Bridge reuses or creates one Nineth client for that session.
|
|
161
|
+
3. Bridge loads decorated functions from the project's `services/` directory.
|
|
162
|
+
4. Your `@bridge.handle` function receives the message and a `Context`.
|
|
163
|
+
5. `context.respond(...)` selects a model:
|
|
164
|
+
- a manual model wins when configured
|
|
165
|
+
- otherwise `1984-c1-mini` chooses from the harness allowlist
|
|
166
|
+
- invalid router output falls back to the harness default
|
|
167
|
+
6. Bridge layers policy in this order: harness policy, application policy, turn policy.
|
|
168
|
+
7. Bridge sends the task to Nineth with:
|
|
169
|
+
- `session=True`
|
|
170
|
+
- a caller-scoped vcache when memory is enabled
|
|
171
|
+
- the harness's built-in Rooster services
|
|
172
|
+
- schemas for discovered local services
|
|
173
|
+
8. Rooster runs the worker and built-in services.
|
|
174
|
+
9. If Rooster pauses for a local service, Bridge executes the Python function and resumes the same Nineth session.
|
|
175
|
+
10. Bridge returns a normalized `BridgeResponse`.
|
|
176
|
+
|
|
177
|
+
The caller does not create worker IDs, process IDs, cache IDs, callback payloads, or service-result envelopes.
|
|
178
|
+
|
|
179
|
+
## Choose a Harness
|
|
180
|
+
|
|
181
|
+
Choose based on the primary job of the application, not every possible tool it may need.
|
|
182
|
+
|
|
183
|
+
| Question | Choose |
|
|
184
|
+
| --- | --- |
|
|
185
|
+
| Is the output primarily an email, Telegram message, reply, or communication operation? | `messaging` |
|
|
186
|
+
| Is the application inspecting markets, portfolios, or creating/running contracts? | `finance` |
|
|
187
|
+
| Is the core task finding, reading, comparing, and citing web evidence? | `research` |
|
|
188
|
+
|
|
189
|
+
Local services can supply domain-specific capabilities to any harness. For example:
|
|
190
|
+
|
|
191
|
+
- a messaging Bridge can add `lookup_customer`
|
|
192
|
+
- a finance Bridge can add `get_internal_risk_limit`
|
|
193
|
+
- a research Bridge can add `query_private_documents`
|
|
194
|
+
|
|
195
|
+
Do not choose a broad harness merely to gain one unrelated tool. A smaller service surface makes model behavior easier to understand and audit.
|
|
196
|
+
|
|
197
|
+
## Quick Start
|
|
198
|
+
|
|
199
|
+
Create a research project:
|
|
200
|
+
|
|
201
|
+
```console
|
|
202
|
+
bridge init research
|
|
203
|
+
cd bridge
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The generated `research.py` is intentionally small:
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
from bridge import Bridge
|
|
210
|
+
|
|
211
|
+
bridge = Bridge("research")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@bridge.handle
|
|
215
|
+
def handle(message, context):
|
|
216
|
+
return context.respond(message)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Run it in the terminal:
|
|
220
|
+
|
|
221
|
+
```console
|
|
222
|
+
bridge run research
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Try a concrete prompt:
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
Compare the official pricing and data-retention policies of three hosted vector
|
|
229
|
+
databases. Use primary sources, include URLs, and call out missing information.
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Inspect routing and raw model/service data while developing:
|
|
233
|
+
|
|
234
|
+
```console
|
|
235
|
+
bridge run research --debug
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Deploy after the behavior is correct locally:
|
|
239
|
+
|
|
240
|
+
```console
|
|
241
|
+
bridge deploy research
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Generated Project Layout
|
|
245
|
+
|
|
246
|
+
`bridge init {harness}` creates `./bridge` by default:
|
|
247
|
+
|
|
248
|
+
```text
|
|
249
|
+
bridge/
|
|
250
|
+
cookbook.md
|
|
251
|
+
research.py
|
|
252
|
+
services/
|
|
253
|
+
run_sql.py
|
|
254
|
+
post_chart.py
|
|
255
|
+
jobs/
|
|
256
|
+
monday-summary.py
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
| Path | Purpose |
|
|
260
|
+
| --- | --- |
|
|
261
|
+
| `cookbook.md` | Guide tailored to the selected harness, including prompts and extension examples |
|
|
262
|
+
| `{harness}.py` | One Bridge instance and its business-logic handler |
|
|
263
|
+
| `services/*.py` | Local model-callable Python functions decorated with `@service` |
|
|
264
|
+
| `jobs/*.py` | Scheduled functions decorated with `@job` |
|
|
265
|
+
| `artifacts/` | Created on demand by the starter `post_chart` service |
|
|
266
|
+
|
|
267
|
+
Use a different destination when needed:
|
|
268
|
+
|
|
269
|
+
```console
|
|
270
|
+
bridge init messaging --path apps/support-bridge
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Bridge refuses to overwrite a non-empty directory unless `--force` is passed. `--force` replaces known starter files but does not delete unrelated files.
|
|
274
|
+
|
|
275
|
+
The `run` and `deploy` commands search the directory given by `--project`, then the current directory, then `./bridge`:
|
|
276
|
+
|
|
277
|
+
```console
|
|
278
|
+
bridge run research --project apps/research-bridge
|
|
279
|
+
bridge deploy research --project apps/research-bridge
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Public Surface
|
|
283
|
+
|
|
284
|
+
The package exports:
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
from bridge import (
|
|
288
|
+
Bridge,
|
|
289
|
+
BridgeConfigurationError,
|
|
290
|
+
BridgeDeploymentError,
|
|
291
|
+
BridgeError,
|
|
292
|
+
BridgeResponse,
|
|
293
|
+
Context,
|
|
294
|
+
DebugEvent,
|
|
295
|
+
JobDefinition,
|
|
296
|
+
Memory,
|
|
297
|
+
ServiceDefinition,
|
|
298
|
+
job,
|
|
299
|
+
service,
|
|
300
|
+
)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Most projects only need `Bridge`, `service`, and `job`.
|
|
304
|
+
|
|
305
|
+
## Bridge Construction
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
bridge = Bridge(
|
|
309
|
+
"research",
|
|
310
|
+
model=None,
|
|
311
|
+
router_model=None,
|
|
312
|
+
models=None,
|
|
313
|
+
policy=None,
|
|
314
|
+
messaging=None,
|
|
315
|
+
memory=True,
|
|
316
|
+
project_dir=None,
|
|
317
|
+
max_service_rounds=10,
|
|
318
|
+
)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Constructor arguments
|
|
322
|
+
|
|
323
|
+
| Argument | Type | Default | Meaning |
|
|
324
|
+
| --- | --- | --- | --- |
|
|
325
|
+
| `harness` | `str` | required | `messaging`, `finance`, or `research` |
|
|
326
|
+
| `model` | `str | None` | `None` | Pin every turn to one model and skip smart routing |
|
|
327
|
+
| `router_model` | `str | None` | `1984-c1-mini` | Small model used only for model selection |
|
|
328
|
+
| `models` | iterable | harness candidates | Models the smart router may choose |
|
|
329
|
+
| `policy` | `str | None` | `None` | Application-wide policy layered after the harness policy |
|
|
330
|
+
| `messaging` | mapping | `None` | Email/Telegram defaults sent on messaging requests |
|
|
331
|
+
| `memory` | `bool` | `True` | Provision a durable caller-scoped vcache |
|
|
332
|
+
| `project_dir` | path | `None` | Directory whose `services/` files are discovered |
|
|
333
|
+
| `client` | Nineth-compatible client | `None` | Inject one client; mainly useful for tests and controlled embedding |
|
|
334
|
+
| `client_factory` | callable | `None` | Advanced factory for creating one client per Bridge session |
|
|
335
|
+
| `max_service_rounds` | `int` | `10` | Maximum local-service pause/resume rounds per turn |
|
|
336
|
+
|
|
337
|
+
The CLI binds `project_dir` automatically after loading `{harness}.py`.
|
|
338
|
+
|
|
339
|
+
### Lifecycle methods
|
|
340
|
+
|
|
341
|
+
| Method | Purpose |
|
|
342
|
+
| --- | --- |
|
|
343
|
+
| `bridge.handle(function)` | Register the project's single handler; normally used as a decorator |
|
|
344
|
+
| `bridge.invoke(message, session_id=None, debug=False, debug_sink=None)` | Execute one application turn |
|
|
345
|
+
| `bridge.bind_project(path)` | Set the project directory used for service discovery |
|
|
346
|
+
| `bridge.close()` | Close all unique Nineth clients created by this Bridge instance |
|
|
347
|
+
|
|
348
|
+
## Handler Contract
|
|
349
|
+
|
|
350
|
+
The recommended handler receives both the normalized message and the per-turn context:
|
|
351
|
+
|
|
352
|
+
```python
|
|
353
|
+
@bridge.handle
|
|
354
|
+
def handle(message: str, context: Context):
|
|
355
|
+
return context.respond(message)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
Exactly one handler may be registered. Bridge also accepts:
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
@bridge.handle
|
|
362
|
+
def handle(message):
|
|
363
|
+
return "A deterministic response that does not call a model."
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
@bridge.handle
|
|
368
|
+
def handle(context):
|
|
369
|
+
return context.respond(context.message)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
@bridge.handle
|
|
374
|
+
async def handle(message, context):
|
|
375
|
+
metadata = await load_metadata(message)
|
|
376
|
+
return context.respond(f"Metadata: {metadata}\n\nRequest: {message}")
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
The one-argument form receives `Context` only when the parameter is named `context` or `ctx`; otherwise it receives the message.
|
|
380
|
+
|
|
381
|
+
A handler may return:
|
|
382
|
+
|
|
383
|
+
- `context.respond(...)` or another `BridgeResponse`
|
|
384
|
+
- a dict with `final_response`
|
|
385
|
+
- a string
|
|
386
|
+
- another JSON-serializable value
|
|
387
|
+
|
|
388
|
+
Non-string values are serialized into the normalized response.
|
|
389
|
+
|
|
390
|
+
### Business-logic branching
|
|
391
|
+
|
|
392
|
+
The handler is the right place for deterministic application behavior:
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
@bridge.handle
|
|
396
|
+
def handle(message, context):
|
|
397
|
+
if message == "/health":
|
|
398
|
+
return "The support Bridge is ready."
|
|
399
|
+
if message.startswith("/draft "):
|
|
400
|
+
return context.respond(
|
|
401
|
+
message.removeprefix("/draft "),
|
|
402
|
+
policy="Draft only. Do not send email or Telegram messages.",
|
|
403
|
+
services=False,
|
|
404
|
+
)
|
|
405
|
+
return context.respond(message)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Context and `respond`
|
|
409
|
+
|
|
410
|
+
Each invocation creates a `Context` with:
|
|
411
|
+
|
|
412
|
+
| Attribute | Meaning |
|
|
413
|
+
| --- | --- |
|
|
414
|
+
| `context.message` | Current normalized caller message |
|
|
415
|
+
| `context.session_id` | Stable Bridge caller/session identity |
|
|
416
|
+
| `context.model` | Selected downstream model after `respond` begins |
|
|
417
|
+
| `context.memory` | `Memory` wrapper for the current vcache, or `None` |
|
|
418
|
+
| `context.services` | Discovered local `ServiceRegistry` |
|
|
419
|
+
| `context.client` | Underlying Nineth client for advanced use |
|
|
420
|
+
| `context.debug` | Whether verbose/debug behavior is enabled |
|
|
421
|
+
|
|
422
|
+
### `context.respond`
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
response = context.respond(
|
|
426
|
+
prompt=None,
|
|
427
|
+
model=None,
|
|
428
|
+
policy=None,
|
|
429
|
+
services=True,
|
|
430
|
+
reasoning="medium",
|
|
431
|
+
max_iterations=12,
|
|
432
|
+
response_format="text",
|
|
433
|
+
)
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| Argument | Meaning |
|
|
437
|
+
| --- | --- |
|
|
438
|
+
| `prompt` | Task sent to the model; defaults to `context.message` |
|
|
439
|
+
| `model` | Per-turn manual model override |
|
|
440
|
+
| `policy` | Per-turn policy appended after harness and application policy |
|
|
441
|
+
| `services=True` | Enable all harness services and all discovered local services |
|
|
442
|
+
| `services=False` | Disable harness and local services for this turn |
|
|
443
|
+
| `services=[...]` | Replace the built-in harness allowlist with these names; local services remain available |
|
|
444
|
+
| other keyword arguments | Forwarded to `NinethClient.model.request` |
|
|
445
|
+
|
|
446
|
+
Useful forwarded options include:
|
|
447
|
+
|
|
448
|
+
- `reasoning`
|
|
449
|
+
- `temperature`, `top_p`, `min_p`, `top_k`
|
|
450
|
+
- `max_iterations`, `continuous`
|
|
451
|
+
- `response_format`
|
|
452
|
+
- `compute`
|
|
453
|
+
- `use_deputy`
|
|
454
|
+
- `images`, `audio`
|
|
455
|
+
|
|
456
|
+
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.
|
|
457
|
+
|
|
458
|
+
Bridge expects buffered model responses. For SSE streaming, use Nineth directly.
|
|
459
|
+
|
|
460
|
+
## Harness Reference
|
|
461
|
+
|
|
462
|
+
Each harness is a service allowlist plus a base policy and model-routing defaults.
|
|
463
|
+
|
|
464
|
+
### Messaging Harness
|
|
465
|
+
|
|
466
|
+
Use `messaging` when communication is the main action.
|
|
467
|
+
|
|
468
|
+
```console
|
|
469
|
+
bridge init messaging
|
|
470
|
+
bridge run messaging
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Built-in capabilities:
|
|
474
|
+
|
|
475
|
+
| Area | Services |
|
|
476
|
+
| --- | --- |
|
|
477
|
+
| Telegram text | `send_message`, `send_rich_message`, `edit_message`, `edit_rich_message`, `edit_message_caption` |
|
|
478
|
+
| Telegram media | `send_photo`, `preview_photo`, `send_voice` |
|
|
479
|
+
| Telegram state | `get_messaging_status` |
|
|
480
|
+
| Email delivery | `send_email`, `send_reply` |
|
|
481
|
+
| Email templates | `list_email_templates`, `get_email_template`, `request_template_interlude` |
|
|
482
|
+
| Email state | `get_email_status` |
|
|
483
|
+
|
|
484
|
+
Generated configuration:
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
import os
|
|
488
|
+
from bridge import Bridge
|
|
489
|
+
|
|
490
|
+
messaging = {
|
|
491
|
+
"telegram": {
|
|
492
|
+
"botId": os.getenv("TELEGRAM_BOT_ID"),
|
|
493
|
+
"chatId": os.getenv("TELEGRAM_CHAT_ID"),
|
|
494
|
+
},
|
|
495
|
+
"email": {
|
|
496
|
+
"email": os.getenv("BRIDGE_EMAIL_ADDRESS"),
|
|
497
|
+
"name": os.getenv("BRIDGE_EMAIL_NAME", "Bridge"),
|
|
498
|
+
},
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
bridge = Bridge("messaging", messaging=messaging)
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Empty environment values are removed before the Nineth request is sent.
|
|
505
|
+
|
|
506
|
+
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.
|
|
507
|
+
|
|
508
|
+
Good prompts:
|
|
509
|
+
|
|
510
|
+
```text
|
|
511
|
+
Draft a calm reply to this customer. Do not send it yet: ...
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
```text
|
|
515
|
+
The following message is approved. Send it to the configured Telegram chat exactly
|
|
516
|
+
as written: Maintenance begins at 22:00 WAT and should finish within 20 minutes.
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Finance Harness
|
|
520
|
+
|
|
521
|
+
Use `finance` when the application works with market data, portfolios, or executable contracts.
|
|
522
|
+
|
|
523
|
+
```console
|
|
524
|
+
bridge init finance
|
|
525
|
+
bridge run finance
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Contract** is Bridge's canonical term for model-authored executable trading logic. "Strategy" is accepted as informal language.
|
|
529
|
+
|
|
530
|
+
| Area | Services |
|
|
531
|
+
| --- | --- |
|
|
532
|
+
| Contract source | `shop_scaffold`, `shop_read`, `shop_patch`, `shop_apply`, `shop_delete`, `shop_list`, `shop_glossary` |
|
|
533
|
+
| Contract runtime | `shop_status`, `shop_observe`, `shop_watch`, `shop_start`, `shop_stop`, `shop_restart` |
|
|
534
|
+
| Market data | `data_get_current_price`, `data_get_historical_ohlc`, `data_get_market_buffer`, `data_get_live_ticks`, `data_get_available_symbols` |
|
|
535
|
+
| Fund state | `fund_balances` |
|
|
536
|
+
| Portfolio | `portfolio_list`, `portfolio_add`, `portfolio_update`, `portfolio_remove` |
|
|
537
|
+
| Performance | `performance` |
|
|
538
|
+
|
|
539
|
+
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.
|
|
540
|
+
|
|
541
|
+
Read-only example:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
READ_ONLY_FINANCE = [
|
|
545
|
+
"shop_list",
|
|
546
|
+
"shop_status",
|
|
547
|
+
"shop_observe",
|
|
548
|
+
"shop_read",
|
|
549
|
+
"shop_glossary",
|
|
550
|
+
"fund_balances",
|
|
551
|
+
"data_get_current_price",
|
|
552
|
+
"data_get_historical_ohlc",
|
|
553
|
+
"data_get_market_buffer",
|
|
554
|
+
"data_get_live_ticks",
|
|
555
|
+
"data_get_available_symbols",
|
|
556
|
+
"portfolio_list",
|
|
557
|
+
"performance",
|
|
558
|
+
]
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
@bridge.handle
|
|
562
|
+
def handle(message, context):
|
|
563
|
+
return context.respond(message, services=READ_ONLY_FINANCE)
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### Research Harness
|
|
567
|
+
|
|
568
|
+
Use `research` when the primary job is discovery, reading, comparison, and source-grounded synthesis.
|
|
569
|
+
|
|
570
|
+
```console
|
|
571
|
+
bridge init research
|
|
572
|
+
bridge run research
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
| Area | Services |
|
|
576
|
+
| --- | --- |
|
|
577
|
+
| General discovery | `search_web`, `search_news`, `search_discussions`, `search_unified`, `search_context` |
|
|
578
|
+
| Rich verticals | `search_rich`, `search_videos`, `search_images`, `search_answers` |
|
|
579
|
+
| Local/places | `search_places`, `search_local_pois`, `search_poi_descriptions` |
|
|
580
|
+
| Reading | `read` |
|
|
581
|
+
| Recursive investigation | `deepsearch` |
|
|
582
|
+
|
|
583
|
+
The base policy asks the model to prefer primary sources, distinguish source claims from inference, preserve direct URLs, and state uncertainty.
|
|
584
|
+
|
|
585
|
+
Good prompt structure:
|
|
586
|
+
|
|
587
|
+
```text
|
|
588
|
+
Question: What changed in the vendor's enterprise data policy during the last year?
|
|
589
|
+
Scope: Official policy pages and release notes only.
|
|
590
|
+
Output: Executive summary, dated change table, implications, and source URLs.
|
|
591
|
+
Uncertainty: State what could not be verified.
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## Model Routing
|
|
595
|
+
|
|
596
|
+
Bridge supports manual and smart selection.
|
|
597
|
+
|
|
598
|
+
### Selection precedence
|
|
599
|
+
|
|
600
|
+
1. `context.respond(..., model="...")`
|
|
601
|
+
2. `Bridge(..., model="...")`
|
|
602
|
+
3. `BRIDGE_MODEL`
|
|
603
|
+
4. Smart router
|
|
604
|
+
5. Harness fallback when smart routing fails
|
|
605
|
+
|
|
606
|
+
Manual models are passed through to Nineth, including provider-qualified names.
|
|
607
|
+
|
|
608
|
+
### Smart router
|
|
609
|
+
|
|
610
|
+
With no manual model, Bridge sends a separate stateless request to `1984-c1-mini`. The routing turn:
|
|
611
|
+
|
|
612
|
+
- has `session=False`
|
|
613
|
+
- has no services
|
|
614
|
+
- receives the harness name, candidate list, and task
|
|
615
|
+
- must return one candidate as JSON
|
|
616
|
+
|
|
617
|
+
It does not inspect memory and does not act as a security layer.
|
|
618
|
+
|
|
619
|
+
Defaults:
|
|
620
|
+
|
|
621
|
+
| Harness | Candidates | Fallback |
|
|
622
|
+
| --- | --- | --- |
|
|
623
|
+
| `messaging` | `1984-m2-light`, `1984-m3-0614`, `1984-c1-0614`, `amari-0524` | `1984-m2-light` |
|
|
624
|
+
| `finance` | `1984-m3-0614`, `1984-c1-0614`, `amari-0524` | `1984-m3-0614` |
|
|
625
|
+
| `research` | `1984-m3-0614`, `1984-c1-0614`, `amari-0524` | `1984-m3-0614` |
|
|
626
|
+
|
|
627
|
+
Constrain or replace the candidate list:
|
|
628
|
+
|
|
629
|
+
```python
|
|
630
|
+
bridge = Bridge(
|
|
631
|
+
"research",
|
|
632
|
+
router_model="1984-c1-mini",
|
|
633
|
+
models=["1984-m2-light", "1984-m3-0614"],
|
|
634
|
+
)
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
When the router errors, returns malformed output, or selects a non-candidate, Bridge uses the fallback and emits the error in debug mode.
|
|
638
|
+
|
|
639
|
+
## Sessions and Memory
|
|
640
|
+
|
|
641
|
+
### Session identity
|
|
642
|
+
|
|
643
|
+
`bridge.invoke` accepts an application-level `session_id`:
|
|
644
|
+
|
|
645
|
+
```python
|
|
646
|
+
response = bridge.invoke("Continue the report", session_id="account-42")
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
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.
|
|
650
|
+
|
|
651
|
+
If `session_id` is omitted, Bridge generates a random ID and returns it. Save it if the caller should continue later.
|
|
652
|
+
|
|
653
|
+
Surface conventions:
|
|
654
|
+
|
|
655
|
+
| Surface | Session behavior |
|
|
656
|
+
| --- | --- |
|
|
657
|
+
| Textual runner | Always uses `terminal` |
|
|
658
|
+
| HTTP API | Uses request `session_id`, otherwise generates and returns one |
|
|
659
|
+
| Starter Monday job | Uses `job-monday-summary` |
|
|
660
|
+
| Direct Python | Uses the value passed to `bridge.invoke` |
|
|
661
|
+
|
|
662
|
+
Do not use a display name alone as a session ID in multi-tenant software. Use a stable, collision-resistant application identifier.
|
|
663
|
+
|
|
664
|
+
### Durable memory
|
|
665
|
+
|
|
666
|
+
With `memory=True`, Bridge sends this vcache name:
|
|
667
|
+
|
|
668
|
+
```text
|
|
669
|
+
bridge-{harness}-{session_id}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
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.
|
|
673
|
+
|
|
674
|
+
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.
|
|
675
|
+
|
|
676
|
+
Disable vcache memory while retaining isolated hot sessions:
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
bridge = Bridge("messaging", memory=False)
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Direct memory operations
|
|
683
|
+
|
|
684
|
+
`context.memory` exposes:
|
|
685
|
+
|
|
686
|
+
```python
|
|
687
|
+
context.memory.upsert(data=[...])
|
|
688
|
+
context.memory.rename("new-name")
|
|
689
|
+
context.memory.delete()
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
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:
|
|
693
|
+
|
|
694
|
+
```python
|
|
695
|
+
@bridge.handle
|
|
696
|
+
def handle(message, context):
|
|
697
|
+
response = context.respond(message)
|
|
698
|
+
if message.startswith("Remember preference:"):
|
|
699
|
+
context.memory.upsert([{"preference": message.split(":", 1)[1].strip()}])
|
|
700
|
+
return response
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
For normal conversational memory, do not call these methods. `context.respond` already provisions and reuses memory automatically.
|
|
704
|
+
|
|
705
|
+
`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.
|
|
706
|
+
|
|
707
|
+
## Local Services
|
|
708
|
+
|
|
709
|
+
Local services let the model use your application's APIs, database queries, calculations, or internal actions without adding them to Rooster.
|
|
710
|
+
|
|
711
|
+
### Declare a service
|
|
712
|
+
|
|
713
|
+
Create a Python file in `services/`:
|
|
714
|
+
|
|
715
|
+
```python
|
|
716
|
+
# services/lookup_customer.py
|
|
717
|
+
from bridge import service
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@service(description="Look up a customer record by account ID.")
|
|
721
|
+
def lookup_customer(account_id: str, include_orders: bool = False) -> dict:
|
|
722
|
+
customer = load_customer(account_id)
|
|
723
|
+
return {
|
|
724
|
+
"account_id": customer.id,
|
|
725
|
+
"plan": customer.plan,
|
|
726
|
+
"orders": customer.orders if include_orders else [],
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Bridge discovers every `*.py` file except names beginning with `_`.
|
|
731
|
+
|
|
732
|
+
### Schema generation
|
|
733
|
+
|
|
734
|
+
The function name becomes the service name unless `name=` is supplied. The decorator description, docstring, or generated fallback becomes the service description.
|
|
735
|
+
|
|
736
|
+
Supported annotations are converted to JSON Schema:
|
|
737
|
+
|
|
738
|
+
| Python | JSON Schema |
|
|
739
|
+
| --- | --- |
|
|
740
|
+
| `str` | string |
|
|
741
|
+
| `int` | integer |
|
|
742
|
+
| `float` | number |
|
|
743
|
+
| `bool` | boolean |
|
|
744
|
+
| `list[T]` | array with item schema |
|
|
745
|
+
| `dict` | object with additional properties |
|
|
746
|
+
| `Optional[T]` | schema for `T`, optional when a default exists |
|
|
747
|
+
| unions | `anyOf` |
|
|
748
|
+
|
|
749
|
+
Parameters without defaults are required. JSON-serializable defaults are included.
|
|
750
|
+
|
|
751
|
+
Override the public service name:
|
|
752
|
+
|
|
753
|
+
```python
|
|
754
|
+
@service(name="get_order", description="Fetch an order visible to the current caller.")
|
|
755
|
+
def fetch_order(order_id: str) -> dict:
|
|
756
|
+
...
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Injected parameters
|
|
760
|
+
|
|
761
|
+
These parameters are never exposed to the model and are injected by Bridge when declared:
|
|
762
|
+
|
|
763
|
+
| Parameter | Injected value |
|
|
764
|
+
| --- | --- |
|
|
765
|
+
| `context` | Current Bridge `Context` |
|
|
766
|
+
| `session_id` | Current Bridge session ID |
|
|
767
|
+
| `context_id` | Vcache name when memory is enabled, otherwise session ID |
|
|
768
|
+
|
|
769
|
+
```python
|
|
770
|
+
@service(description="Return caller-specific feature flags.")
|
|
771
|
+
def get_feature_flags(session_id: str) -> dict:
|
|
772
|
+
return flags_for_session(session_id)
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
### Execution lifecycle
|
|
776
|
+
|
|
777
|
+
1. Bridge sends local schemas to Nineth as caller-managed services.
|
|
778
|
+
2. Rooster may return `status="awaiting_client_services"` with pending calls.
|
|
779
|
+
3. Bridge finds each handler, injects context values, and executes it.
|
|
780
|
+
4. Sync and async handlers are supported.
|
|
781
|
+
5. Bridge returns a result envelope for every call, including errors.
|
|
782
|
+
6. Bridge resumes the same model session.
|
|
783
|
+
7. The loop repeats up to `max_service_rounds`.
|
|
784
|
+
|
|
785
|
+
Service exceptions become failed tool results so the model can explain or recover. Unknown and duplicate service names raise configuration errors.
|
|
786
|
+
|
|
787
|
+
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.
|
|
788
|
+
|
|
789
|
+
## Jobs
|
|
790
|
+
|
|
791
|
+
Jobs are autonomous functions in `jobs/*.py`:
|
|
792
|
+
|
|
793
|
+
```python
|
|
794
|
+
from bridge import job
|
|
795
|
+
|
|
796
|
+
|
|
797
|
+
@job("0 9 * * 1", name="monday-summary")
|
|
798
|
+
def monday_summary(bridge):
|
|
799
|
+
return bridge.invoke(
|
|
800
|
+
"Prepare the Monday briefing from current memory.",
|
|
801
|
+
session_id="job-monday-summary",
|
|
802
|
+
)
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
The schedule must be a five-field cron expression:
|
|
806
|
+
|
|
807
|
+
```text
|
|
808
|
+
minute hour day-of-month month day-of-week
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Examples:
|
|
812
|
+
|
|
813
|
+
| Schedule | Meaning |
|
|
814
|
+
| --- | --- |
|
|
815
|
+
| `0 9 * * 1` | Monday at 09:00 |
|
|
816
|
+
| `0 8 * * *` | Every day at 08:00 |
|
|
817
|
+
| `*/30 * * * *` | Every 30 minutes |
|
|
818
|
+
|
|
819
|
+
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.
|
|
820
|
+
|
|
821
|
+
Use a stable, job-specific session ID when recurring runs should share memory. Use separate IDs when runs must be isolated.
|
|
822
|
+
|
|
823
|
+
Cron expressions are interpreted by Modal. Verify the deployment's effective timezone rather than assuming the developer machine's local timezone.
|
|
824
|
+
|
|
825
|
+
Bridge schedules jobs through Modal deployment; it does not currently provide a local `bridge job` command.
|
|
826
|
+
|
|
827
|
+
## Local Terminal Runner
|
|
828
|
+
|
|
829
|
+
```console
|
|
830
|
+
bridge run research
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
The Textual UI has a transcript and one input. It uses session ID `terminal`, so turns in one process share state.
|
|
834
|
+
|
|
835
|
+
Controls:
|
|
836
|
+
|
|
837
|
+
- `/exit`
|
|
838
|
+
- `/quit`
|
|
839
|
+
- `Ctrl+C`
|
|
840
|
+
|
|
841
|
+
Debug mode:
|
|
842
|
+
|
|
843
|
+
```console
|
|
844
|
+
bridge run research --debug
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Debug output includes:
|
|
848
|
+
|
|
849
|
+
- manual, smart, or fallback routing decision
|
|
850
|
+
- router model and raw routing response
|
|
851
|
+
- local service calls and results
|
|
852
|
+
- verbose raw Nineth response
|
|
853
|
+
|
|
854
|
+
Debug data can contain prompts, local service outputs, and server telemetry. Do not treat debug transcripts as secret-safe logs.
|
|
855
|
+
|
|
856
|
+
## Modal Deployment
|
|
857
|
+
|
|
858
|
+
### Prerequisites
|
|
859
|
+
|
|
860
|
+
1. Authenticate the Modal CLI.
|
|
861
|
+
2. Create a Modal secret containing `NINETH_API_KEY` and all provider/application secrets.
|
|
862
|
+
3. Use the default secret name `bridge` or pass `--modal-secret`.
|
|
863
|
+
4. Configure the alias control plane, or use `--no-alias`.
|
|
864
|
+
|
|
865
|
+
```console
|
|
866
|
+
bridge deploy research
|
|
867
|
+
bridge deploy research --modal-secret bridge-production
|
|
868
|
+
bridge deploy research --no-alias
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### What deployment generates
|
|
872
|
+
|
|
873
|
+
Bridge creates a temporary Modal entrypoint that:
|
|
874
|
+
|
|
875
|
+
- starts from a Python 3.11 Debian image
|
|
876
|
+
- installs compatible Nineth and HTTPX versions
|
|
877
|
+
- includes the locally installed Bridge package source
|
|
878
|
+
- includes the project at `/root/bridge_project`
|
|
879
|
+
- exposes one ASGI endpoint
|
|
880
|
+
- creates one scheduled function per job
|
|
881
|
+
- attaches the selected Modal secret
|
|
882
|
+
|
|
883
|
+
The temporary local entrypoint is deleted after `modal deploy` returns.
|
|
884
|
+
|
|
885
|
+
### Alias registration
|
|
886
|
+
|
|
887
|
+
By default Bridge requests:
|
|
888
|
+
|
|
889
|
+
```text
|
|
890
|
+
https://{deployment_id}.bridge.tooig.com
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
It sends this request to `BRIDGE_ALIAS_API_URL`, defaulting to `https://bridge.tooig.com/api/aliases`:
|
|
894
|
+
|
|
895
|
+
```json
|
|
896
|
+
{
|
|
897
|
+
"deployment_id": "32-character UUID hex",
|
|
898
|
+
"harness": "research",
|
|
899
|
+
"target_url": "https://...modal.run",
|
|
900
|
+
"requested_url": "https://{deployment_id}.bridge.tooig.com"
|
|
901
|
+
}
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
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.
|
|
905
|
+
|
|
906
|
+
If alias registration fails after Modal succeeds, `BridgeDeploymentError` includes the working Modal URL. `--no-alias` skips registration and reports the Modal URL directly.
|
|
907
|
+
|
|
908
|
+
DNS, wildcard TLS, and the alias reverse proxy are external infrastructure, not implemented by the Python package.
|
|
909
|
+
|
|
910
|
+
## Deployed HTTP API
|
|
911
|
+
|
|
912
|
+
### Health
|
|
913
|
+
|
|
914
|
+
```http
|
|
915
|
+
GET /health
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
```json
|
|
919
|
+
{
|
|
920
|
+
"status": "ok",
|
|
921
|
+
"harness": "research"
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Invoke
|
|
926
|
+
|
|
927
|
+
```http
|
|
928
|
+
POST /
|
|
929
|
+
Content-Type: application/json
|
|
930
|
+
|
|
931
|
+
{
|
|
932
|
+
"message": "Compare the vendors using primary sources.",
|
|
933
|
+
"session_id": "account-42",
|
|
934
|
+
"debug": false
|
|
935
|
+
}
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
`input` is accepted as an alias for `message`.
|
|
939
|
+
|
|
940
|
+
```json
|
|
941
|
+
{
|
|
942
|
+
"final_response": "...",
|
|
943
|
+
"model": "1984-m3-0614",
|
|
944
|
+
"session_id": "account-42"
|
|
945
|
+
}
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
When `session_id` is omitted, save the returned generated value and send it on the next request.
|
|
949
|
+
|
|
950
|
+
`debug=true` adds `raw` to the response.
|
|
951
|
+
|
|
952
|
+
| Status | Meaning |
|
|
953
|
+
| --- | --- |
|
|
954
|
+
| `200` | Successful health or model response |
|
|
955
|
+
| `400` | Invalid JSON, message, or session input |
|
|
956
|
+
| `404` | Unknown path or method |
|
|
957
|
+
| `422` | Bridge configuration failure |
|
|
958
|
+
| `500` | Unexpected execution failure; details are not exposed |
|
|
959
|
+
|
|
960
|
+
Request bodies are limited to 1 MiB.
|
|
961
|
+
|
|
962
|
+
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.
|
|
963
|
+
|
|
964
|
+
## Configuration Reference
|
|
965
|
+
|
|
966
|
+
| Variable | Required | Purpose |
|
|
967
|
+
| --- | --- | --- |
|
|
968
|
+
| `NINETH_API_KEY` | Yes for model turns | Nineth authentication |
|
|
969
|
+
| `NINETH_BASE_URL` | No | Override the Nineth/Rooster API URL |
|
|
970
|
+
| `BRIDGE_MODEL` | No | Environment-level manual model override |
|
|
971
|
+
| `BRIDGE_ROUTER_MODEL` | No | Small model used for smart routing |
|
|
972
|
+
| `BRIDGE_API_KEY` | For protected alias APIs | Bearer credential for alias registration |
|
|
973
|
+
| `BRIDGE_ALIAS_API_URL` | No | Alias registration endpoint |
|
|
974
|
+
| `TELEGRAM_BOT_ID` | Messaging-dependent | Generated messaging project's bot identity |
|
|
975
|
+
| `TELEGRAM_CHAT_ID` | Messaging-dependent | Generated messaging project's default chat |
|
|
976
|
+
| `BRIDGE_EMAIL_ADDRESS` | Messaging-dependent | Generated messaging project's mailbox identity |
|
|
977
|
+
| `BRIDGE_EMAIL_NAME` | No | Email sender display name; defaults to `Bridge` |
|
|
978
|
+
| `BRIDGE_SQLITE_PATH` | Starter service only | SQLite path used by generated `run_sql` |
|
|
979
|
+
|
|
980
|
+
Provider-specific Rooster services may require additional server-side or Modal secrets.
|
|
981
|
+
|
|
982
|
+
## Cookbook
|
|
983
|
+
|
|
984
|
+
These recipes build on the technical contract above. Each starts from a generated project unless noted.
|
|
985
|
+
|
|
986
|
+
### Recipe 1: Build a source-grounded research brief
|
|
987
|
+
|
|
988
|
+
```console
|
|
989
|
+
bridge init research --path vendor-research
|
|
990
|
+
bridge run research --project vendor-research
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
Update `vendor-research/research.py`:
|
|
994
|
+
|
|
995
|
+
```python
|
|
996
|
+
from bridge import Bridge
|
|
997
|
+
|
|
998
|
+
bridge = Bridge(
|
|
999
|
+
"research",
|
|
1000
|
+
policy=(
|
|
1001
|
+
"Prefer official documentation, regulatory filings, and first-party release notes. "
|
|
1002
|
+
"End every report with a source table containing title, URL, date, and relevance."
|
|
1003
|
+
),
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
@bridge.handle
|
|
1008
|
+
def handle(message, context):
|
|
1009
|
+
prompt = f"""
|
|
1010
|
+
Research question:
|
|
1011
|
+
{message}
|
|
1012
|
+
|
|
1013
|
+
Required output:
|
|
1014
|
+
1. Executive summary
|
|
1015
|
+
2. Evidence grouped by claim
|
|
1016
|
+
3. Conflicting evidence
|
|
1017
|
+
4. Unknowns and limitations
|
|
1018
|
+
5. Source table with direct URLs
|
|
1019
|
+
"""
|
|
1020
|
+
return context.respond(prompt, reasoning="medium", max_iterations=20)
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
Example prompt:
|
|
1024
|
+
|
|
1025
|
+
```text
|
|
1026
|
+
How have the three largest European cloud providers changed their sovereign-cloud
|
|
1027
|
+
offerings since January 2025, and what remains unavailable?
|
|
1028
|
+
```
|
|
1029
|
+
|
|
1030
|
+
Use a stable terminal or API session for follow-ups such as "verify the second claim using only primary sources."
|
|
1031
|
+
|
|
1032
|
+
### Recipe 2: Build a customer-support messaging assistant
|
|
1033
|
+
|
|
1034
|
+
```console
|
|
1035
|
+
bridge init messaging --path support-bridge
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
Add a customer lookup service:
|
|
1039
|
+
|
|
1040
|
+
```python
|
|
1041
|
+
# support-bridge/services/lookup_customer.py
|
|
1042
|
+
from bridge import service
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
@service(description="Get the support-safe customer summary for an account.")
|
|
1046
|
+
def lookup_customer(account_id: str) -> dict:
|
|
1047
|
+
return {
|
|
1048
|
+
"account_id": account_id,
|
|
1049
|
+
"plan": "business",
|
|
1050
|
+
"open_ticket": "T-1042",
|
|
1051
|
+
"status": "awaiting replacement",
|
|
1052
|
+
}
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
Customize `messaging.py`:
|
|
1056
|
+
|
|
1057
|
+
```python
|
|
1058
|
+
@bridge.handle
|
|
1059
|
+
def handle(message, context):
|
|
1060
|
+
return context.respond(
|
|
1061
|
+
message,
|
|
1062
|
+
policy=(
|
|
1063
|
+
"Use lookup_customer before making account-specific claims. "
|
|
1064
|
+
"Draft first. Send only when the caller explicitly says approved and supplies "
|
|
1065
|
+
"the final recipient and content."
|
|
1066
|
+
),
|
|
1067
|
+
max_iterations=12,
|
|
1068
|
+
)
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
Development flow:
|
|
1072
|
+
|
|
1073
|
+
```text
|
|
1074
|
+
Draft a reply for account AC-42 explaining the replacement status. Do not send.
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
Then:
|
|
1078
|
+
|
|
1079
|
+
```text
|
|
1080
|
+
Approved. Send that final reply to customer@example.com.
|
|
1081
|
+
```
|
|
1082
|
+
|
|
1083
|
+
Application authorization must confirm that the caller may access AC-42 and send from the configured mailbox.
|
|
1084
|
+
|
|
1085
|
+
### Recipe 3: Send an approved Telegram update
|
|
1086
|
+
|
|
1087
|
+
Set `TELEGRAM_BOT_ID` and `TELEGRAM_CHAT_ID`, then use an explicit command convention:
|
|
1088
|
+
|
|
1089
|
+
```python
|
|
1090
|
+
@bridge.handle
|
|
1091
|
+
def handle(message, context):
|
|
1092
|
+
if not message.startswith("APPROVED: "):
|
|
1093
|
+
return context.respond(
|
|
1094
|
+
message,
|
|
1095
|
+
policy="Draft a Telegram update only. Do not send it.",
|
|
1096
|
+
services=False,
|
|
1097
|
+
)
|
|
1098
|
+
approved = message.removeprefix("APPROVED: ")
|
|
1099
|
+
return context.respond(
|
|
1100
|
+
f"Send this text exactly to the configured Telegram chat: {approved}",
|
|
1101
|
+
services=["send_message", "get_messaging_status"],
|
|
1102
|
+
)
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
This narrows the action turn to plain text delivery and status inspection. The `APPROVED:` prefix is only a local convention; authenticate the caller separately.
|
|
1106
|
+
|
|
1107
|
+
### Recipe 4: Inspect a finance environment without trading
|
|
1108
|
+
|
|
1109
|
+
```python
|
|
1110
|
+
from bridge import Bridge
|
|
1111
|
+
|
|
1112
|
+
bridge = Bridge(
|
|
1113
|
+
"finance",
|
|
1114
|
+
policy="This application is read-only. Never create, patch, apply, start, or restart contracts.",
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
READ_ONLY = [
|
|
1118
|
+
"shop_list",
|
|
1119
|
+
"shop_read",
|
|
1120
|
+
"shop_status",
|
|
1121
|
+
"shop_observe",
|
|
1122
|
+
"shop_glossary",
|
|
1123
|
+
"fund_balances",
|
|
1124
|
+
"data_get_current_price",
|
|
1125
|
+
"data_get_historical_ohlc",
|
|
1126
|
+
"data_get_market_buffer",
|
|
1127
|
+
"data_get_live_ticks",
|
|
1128
|
+
"data_get_available_symbols",
|
|
1129
|
+
"portfolio_list",
|
|
1130
|
+
"performance",
|
|
1131
|
+
]
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
@bridge.handle
|
|
1135
|
+
def handle(message, context):
|
|
1136
|
+
return context.respond(message, services=READ_ONLY, max_iterations=12)
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
Example prompts:
|
|
1140
|
+
|
|
1141
|
+
```text
|
|
1142
|
+
Summarize balances, open portfolio exposures, recent performance, and stale contract
|
|
1143
|
+
runtime state. Do not modify anything.
|
|
1144
|
+
```
|
|
1145
|
+
|
|
1146
|
+
```text
|
|
1147
|
+
Compare BTC and ETH daily volatility over the available 90-day history and explain
|
|
1148
|
+
what that means for the current portfolio weights.
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
### Recipe 5: Develop and observe a trading contract
|
|
1152
|
+
|
|
1153
|
+
Use a demo environment and make the lifecycle explicit:
|
|
1154
|
+
|
|
1155
|
+
```python
|
|
1156
|
+
bridge = Bridge(
|
|
1157
|
+
"finance",
|
|
1158
|
+
policy=(
|
|
1159
|
+
"Operate only on the demo well. Begin with shop_glossary and current state. "
|
|
1160
|
+
"Show the proposed contract source and risk assumptions before applying it. "
|
|
1161
|
+
"Do not start a contract until the caller separately approves activation."
|
|
1162
|
+
),
|
|
1163
|
+
)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
@bridge.handle
|
|
1167
|
+
def handle(message, context):
|
|
1168
|
+
return context.respond(message, reasoning="medium", max_iterations=25)
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
Use separate turns:
|
|
1172
|
+
|
|
1173
|
+
```text
|
|
1174
|
+
Design a Python mean-reversion contract for the demo well. Inspect the glossary and
|
|
1175
|
+
available symbols, scaffold the source, and show it to me. Do not apply or start it.
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
```text
|
|
1179
|
+
Patch the contract to cap position size at 1% of demo equity. Apply it, but do not start it.
|
|
1180
|
+
```
|
|
1181
|
+
|
|
1182
|
+
```text
|
|
1183
|
+
I approve activation in the demo environment. Start the contract, bind a watch, and
|
|
1184
|
+
report the first observation.
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
Model policy is not sufficient for production trading approval. Enforce environment and account restrictions in broker credentials and services.
|
|
1188
|
+
|
|
1189
|
+
### Recipe 6: Add an application-specific local service
|
|
1190
|
+
|
|
1191
|
+
This pattern works with every harness:
|
|
1192
|
+
|
|
1193
|
+
```python
|
|
1194
|
+
# services/get_internal_policy.py
|
|
1195
|
+
from bridge import service
|
|
1196
|
+
|
|
1197
|
+
|
|
1198
|
+
@service(description="Read an approved internal policy by exact policy ID.")
|
|
1199
|
+
def get_internal_policy(policy_id: str, context_id: str) -> dict:
|
|
1200
|
+
policy = policy_store.read(policy_id, caller_context=context_id)
|
|
1201
|
+
return {
|
|
1202
|
+
"policy_id": policy.id,
|
|
1203
|
+
"title": policy.title,
|
|
1204
|
+
"body": policy.body,
|
|
1205
|
+
"updated_at": policy.updated_at.isoformat(),
|
|
1206
|
+
}
|
|
1207
|
+
```
|
|
1208
|
+
|
|
1209
|
+
Then ask the model to combine it with harness capabilities:
|
|
1210
|
+
|
|
1211
|
+
```text
|
|
1212
|
+
Read policy RET-12 and compare it with the vendor's current public retention terms.
|
|
1213
|
+
Cite the public sources and list every conflict.
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
The research harness can browse; the local service supplies private policy data.
|
|
1217
|
+
|
|
1218
|
+
### Recipe 7: Use the generated SQL and chart services
|
|
1219
|
+
|
|
1220
|
+
The starter `run_sql` accepts read-only SQLite `SELECT`, `WITH`, and `PRAGMA` statements and returns at most 200 rows.
|
|
1221
|
+
|
|
1222
|
+
```console
|
|
1223
|
+
$env:BRIDGE_SQLITE_PATH = "C:\data\analytics.db"
|
|
1224
|
+
bridge run research
|
|
1225
|
+
```
|
|
1226
|
+
|
|
1227
|
+
Example prompt:
|
|
1228
|
+
|
|
1229
|
+
```text
|
|
1230
|
+
Use run_sql to summarize monthly active accounts from the events table. Then use
|
|
1231
|
+
post_chart to save a bar-chart specification as monthly-active.json. Explain any
|
|
1232
|
+
assumptions you made about the schema.
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
`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.
|
|
1236
|
+
|
|
1237
|
+
### Recipe 8: Keep user sessions isolated
|
|
1238
|
+
|
|
1239
|
+
```python
|
|
1240
|
+
from bridge.project import load_project
|
|
1241
|
+
|
|
1242
|
+
app = load_project("research", "./bridge")
|
|
1243
|
+
|
|
1244
|
+
alice = app.invoke("Research vendor A", session_id="tenant-7:user-alice")
|
|
1245
|
+
bob = app.invoke("Research vendor B", session_id="tenant-7:user-bob")
|
|
1246
|
+
|
|
1247
|
+
alice_follow_up = app.invoke(
|
|
1248
|
+
"Now verify the pricing claim.",
|
|
1249
|
+
session_id=alice.session_id,
|
|
1250
|
+
)
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Use the same ID only when callers should share conversational and durable context.
|
|
1254
|
+
|
|
1255
|
+
### Recipe 9: Seed and manage durable memory
|
|
1256
|
+
|
|
1257
|
+
Establish the vcache with a model response before direct mutation:
|
|
1258
|
+
|
|
1259
|
+
```python
|
|
1260
|
+
@bridge.handle
|
|
1261
|
+
def handle(message, context):
|
|
1262
|
+
response = context.respond(message)
|
|
1263
|
+
if message.startswith("Remember preference:"):
|
|
1264
|
+
preference = message.split(":", 1)[1].strip()
|
|
1265
|
+
context.memory.upsert([{"type": "preference", "value": preference}])
|
|
1266
|
+
return response
|
|
1267
|
+
```
|
|
1268
|
+
|
|
1269
|
+
For destructive lifecycle operations, use explicit confirmation commands and authorization:
|
|
1270
|
+
|
|
1271
|
+
```python
|
|
1272
|
+
if message == "/confirm-forget-session":
|
|
1273
|
+
context.memory.delete()
|
|
1274
|
+
return "Session memory deleted."
|
|
1275
|
+
```
|
|
1276
|
+
|
|
1277
|
+
After deletion, the next model turn provisions a new vcache scope for that session name.
|
|
1278
|
+
|
|
1279
|
+
### Recipe 10: Pin or constrain model selection
|
|
1280
|
+
|
|
1281
|
+
Pin every turn:
|
|
1282
|
+
|
|
1283
|
+
```python
|
|
1284
|
+
bridge = Bridge("research", model="1984-m3-0614")
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
Allow smart routing across a smaller set:
|
|
1288
|
+
|
|
1289
|
+
```python
|
|
1290
|
+
bridge = Bridge(
|
|
1291
|
+
"messaging",
|
|
1292
|
+
models=["1984-m2-light", "1984-m3-0614"],
|
|
1293
|
+
)
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
Override one turn:
|
|
1297
|
+
|
|
1298
|
+
```python
|
|
1299
|
+
return context.respond(message, model="1984-c1-0614")
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
Use `--debug` to see whether selection was manual, smart, or fallback.
|
|
1303
|
+
|
|
1304
|
+
### Recipe 11: Request structured JSON
|
|
1305
|
+
|
|
1306
|
+
```python
|
|
1307
|
+
@bridge.handle
|
|
1308
|
+
def handle(message, context):
|
|
1309
|
+
return context.respond(
|
|
1310
|
+
f"""
|
|
1311
|
+
{message}
|
|
1312
|
+
|
|
1313
|
+
Return JSON with keys: summary, findings, risks, sources.
|
|
1314
|
+
Each source must have title and url.
|
|
1315
|
+
""",
|
|
1316
|
+
response_format="json",
|
|
1317
|
+
max_iterations=15,
|
|
1318
|
+
)
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
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.
|
|
1322
|
+
|
|
1323
|
+
### Recipe 12: Schedule autonomous work
|
|
1324
|
+
|
|
1325
|
+
**Messaging**
|
|
1326
|
+
|
|
1327
|
+
```python
|
|
1328
|
+
@job("0 9 * * 1", name="support-weekly-summary")
|
|
1329
|
+
def support_summary(bridge):
|
|
1330
|
+
return bridge.invoke(
|
|
1331
|
+
"Summarize unresolved communication work. Draft the report; do not send.",
|
|
1332
|
+
session_id="job-support-weekly-summary",
|
|
1333
|
+
)
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
**Finance**
|
|
1337
|
+
|
|
1338
|
+
```python
|
|
1339
|
+
@job("0 8 * * *", name="daily-risk-review")
|
|
1340
|
+
def daily_risk_review(bridge):
|
|
1341
|
+
return bridge.invoke(
|
|
1342
|
+
"Review balances, exposure, performance, and contract alerts. Do not trade or modify contracts.",
|
|
1343
|
+
session_id="job-daily-risk-review",
|
|
1344
|
+
)
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
**Research**
|
|
1348
|
+
|
|
1349
|
+
```python
|
|
1350
|
+
@job("0 7 * * 1", name="weekly-competitor-watch")
|
|
1351
|
+
def competitor_watch(bridge):
|
|
1352
|
+
return bridge.invoke(
|
|
1353
|
+
"Find material competitor announcements from the last seven days. Use primary sources and URLs.",
|
|
1354
|
+
session_id="job-weekly-competitor-watch",
|
|
1355
|
+
)
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
Deploy to activate Modal schedules.
|
|
1359
|
+
|
|
1360
|
+
### Recipe 13: Call Bridge from Python
|
|
1361
|
+
|
|
1362
|
+
```python
|
|
1363
|
+
from bridge.project import load_project
|
|
1364
|
+
|
|
1365
|
+
app = load_project("research", "./bridge")
|
|
1366
|
+
try:
|
|
1367
|
+
response = app.invoke(
|
|
1368
|
+
"Prepare a cited market map for managed vector databases.",
|
|
1369
|
+
session_id="market-map-2026",
|
|
1370
|
+
)
|
|
1371
|
+
print(response.final_response)
|
|
1372
|
+
print("model:", response.model)
|
|
1373
|
+
print("session:", response.session_id)
|
|
1374
|
+
finally:
|
|
1375
|
+
app.close()
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
`Bridge.invoke` is synchronous. In an async application, run it in a worker thread:
|
|
1379
|
+
|
|
1380
|
+
```python
|
|
1381
|
+
import asyncio
|
|
1382
|
+
|
|
1383
|
+
response = await asyncio.to_thread(
|
|
1384
|
+
app.invoke,
|
|
1385
|
+
message,
|
|
1386
|
+
session_id=session_id,
|
|
1387
|
+
)
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
### Recipe 14: Call a deployed Bridge from a frontend
|
|
1391
|
+
|
|
1392
|
+
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:
|
|
1393
|
+
|
|
1394
|
+
```javascript
|
|
1395
|
+
const response = await fetch("/api/bridge", {
|
|
1396
|
+
method: "POST",
|
|
1397
|
+
headers: { "Content-Type": "application/json" },
|
|
1398
|
+
body: JSON.stringify({
|
|
1399
|
+
message: "Continue the vendor comparison.",
|
|
1400
|
+
}),
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
if (!response.ok) {
|
|
1404
|
+
throw new Error(`Bridge request failed: ${response.status}`);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const result = await response.json();
|
|
1408
|
+
document.querySelector("#answer").textContent = result.final_response;
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
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.
|
|
1412
|
+
|
|
1413
|
+
### Recipe 15: Add application policy and service limits
|
|
1414
|
+
|
|
1415
|
+
Policies layer rather than replace one another:
|
|
1416
|
+
|
|
1417
|
+
```python
|
|
1418
|
+
bridge = Bridge(
|
|
1419
|
+
"research",
|
|
1420
|
+
policy="Never include personal data in reports.",
|
|
1421
|
+
)
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
@bridge.handle
|
|
1425
|
+
def handle(message, context):
|
|
1426
|
+
return context.respond(
|
|
1427
|
+
message,
|
|
1428
|
+
policy="For this route, use official sources only.",
|
|
1429
|
+
services=["search_web", "search_context", "read"],
|
|
1430
|
+
max_iterations=10,
|
|
1431
|
+
)
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
Effective policy order:
|
|
1435
|
+
|
|
1436
|
+
1. fixed harness policy
|
|
1437
|
+
2. `Bridge(policy=...)`
|
|
1438
|
+
3. `context.respond(policy=...)`
|
|
1439
|
+
|
|
1440
|
+
Service limits are enforcement at the request schema level. Policy is model instruction. Use both when narrowing behavior matters.
|
|
1441
|
+
|
|
1442
|
+
## Response Shapes
|
|
1443
|
+
|
|
1444
|
+
### Python response
|
|
1445
|
+
|
|
1446
|
+
```python
|
|
1447
|
+
BridgeResponse(
|
|
1448
|
+
final_response="...",
|
|
1449
|
+
model="1984-m3-0614",
|
|
1450
|
+
session_id="account-42",
|
|
1451
|
+
raw={...},
|
|
1452
|
+
)
|
|
1453
|
+
```
|
|
1454
|
+
|
|
1455
|
+
Fields:
|
|
1456
|
+
|
|
1457
|
+
| Field | Meaning |
|
|
1458
|
+
| --- | --- |
|
|
1459
|
+
| `final_response` | Normalized text returned by the handler/model |
|
|
1460
|
+
| `model` | Selected model, or `None` when the handler did not call `respond` |
|
|
1461
|
+
| `session_id` | Bridge-level session identity |
|
|
1462
|
+
| `raw` | Raw model response or normalized handler value |
|
|
1463
|
+
|
|
1464
|
+
Serialize for an API:
|
|
1465
|
+
|
|
1466
|
+
```python
|
|
1467
|
+
response.to_dict()
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
```json
|
|
1471
|
+
{
|
|
1472
|
+
"final_response": "...",
|
|
1473
|
+
"model": "1984-m3-0614",
|
|
1474
|
+
"session_id": "account-42"
|
|
1475
|
+
}
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
Include raw data explicitly:
|
|
1479
|
+
|
|
1480
|
+
```python
|
|
1481
|
+
response.to_dict(debug=True)
|
|
1482
|
+
```
|
|
1483
|
+
|
|
1484
|
+
### Debug events
|
|
1485
|
+
|
|
1486
|
+
A programmatic caller can receive structured debug events:
|
|
1487
|
+
|
|
1488
|
+
```python
|
|
1489
|
+
events = []
|
|
1490
|
+
response = bridge.invoke(
|
|
1491
|
+
"Investigate this claim",
|
|
1492
|
+
session_id="audit-1",
|
|
1493
|
+
debug=True,
|
|
1494
|
+
debug_sink=events.append,
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
for event in events:
|
|
1498
|
+
print(event.kind, event.data)
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
Current event kinds include `model_routing`, `local_services`, and `model_response`.
|
|
1502
|
+
|
|
1503
|
+
## Error Handling
|
|
1504
|
+
|
|
1505
|
+
```python
|
|
1506
|
+
from bridge import BridgeConfigurationError, BridgeDeploymentError, BridgeError
|
|
1507
|
+
|
|
1508
|
+
try:
|
|
1509
|
+
response = bridge.invoke(message, session_id=session_id)
|
|
1510
|
+
except BridgeConfigurationError as exc:
|
|
1511
|
+
print("Project or request configuration is invalid:", exc)
|
|
1512
|
+
except BridgeDeploymentError as exc:
|
|
1513
|
+
print("Deployment failed:", exc)
|
|
1514
|
+
except BridgeError as exc:
|
|
1515
|
+
print("Bridge failed:", exc)
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
Nineth transport and API failures may surface as `NinethAPIError`.
|
|
1519
|
+
|
|
1520
|
+
Common configuration errors include:
|
|
1521
|
+
|
|
1522
|
+
- unknown harness name
|
|
1523
|
+
- missing `@bridge.handle`
|
|
1524
|
+
- more than one handler
|
|
1525
|
+
- empty message
|
|
1526
|
+
- duplicate local service name
|
|
1527
|
+
- invalid job cron shape
|
|
1528
|
+
- direct use of a Bridge-managed request option
|
|
1529
|
+
- local service callback rounds exceeding the configured limit
|
|
1530
|
+
|
|
1531
|
+
## Security and Operational Boundaries
|
|
1532
|
+
|
|
1533
|
+
Bridge simplifies architecture; it does not remove the need for application controls.
|
|
1534
|
+
|
|
1535
|
+
- **Authentication:** the deployed ASGI endpoint does not authenticate callers.
|
|
1536
|
+
- **Authorization:** harness policy is not permission checking. Validate users in your gateway and local services.
|
|
1537
|
+
- **Finance:** model confirmation language is not a brokerage control. Use demo/live separation, scoped credentials, limits, and human approval.
|
|
1538
|
+
- **Messaging:** verify sender ownership and recipient authorization outside model text.
|
|
1539
|
+
- **Local services:** functions execute with process/container permissions. Apply least privilege and validate all parameters.
|
|
1540
|
+
- **Sessions:** map authenticated principals to session IDs server-side. Do not trust arbitrary browser-provided tenant IDs.
|
|
1541
|
+
- **Debug:** raw responses and service results may contain sensitive data.
|
|
1542
|
+
- **Secrets:** provide credentials through environment variables or Modal secrets, never generated source files.
|
|
1543
|
+
- **Alias:** wildcard DNS/TLS and reverse-proxy behavior belong to the external control plane.
|
|
1544
|
+
|
|
1545
|
+
## Practical Patterns
|
|
1546
|
+
|
|
1547
|
+
- Begin with the generated handler and one concrete prompt before adding services.
|
|
1548
|
+
- Keep one Bridge instance alive in a process to reuse HTTP connections and session state.
|
|
1549
|
+
- Use stable, namespaced session IDs such as `tenant:{tenant_id}:user:{user_id}`.
|
|
1550
|
+
- Use `services=[...]` for read-only or route-specific capability limits.
|
|
1551
|
+
- Put deterministic branching and authorization before `context.respond`.
|
|
1552
|
+
- Return compact, JSON-serializable local service results.
|
|
1553
|
+
- Give services action-oriented descriptions and precise parameter names.
|
|
1554
|
+
- Use separate job session IDs from interactive users.
|
|
1555
|
+
- Test with `--debug`, but disable debug in normal production responses.
|
|
1556
|
+
- Use Nineth directly for streaming rather than forcing a stream iterator through Bridge.
|
|
1557
|
+
|
|
1558
|
+
## Troubleshooting
|
|
1559
|
+
|
|
1560
|
+
### `Authentication required`
|
|
1561
|
+
|
|
1562
|
+
Set `NINETH_API_KEY` locally and in the Modal secret used by deployment.
|
|
1563
|
+
|
|
1564
|
+
### `Could not find research.py`
|
|
1565
|
+
|
|
1566
|
+
Run from the parent of `./bridge`, from the project itself, or pass `--project`:
|
|
1567
|
+
|
|
1568
|
+
```console
|
|
1569
|
+
bridge run research --project path/to/bridge
|
|
1570
|
+
```
|
|
1571
|
+
|
|
1572
|
+
### `must expose a Bridge instance named bridge or app`
|
|
1573
|
+
|
|
1574
|
+
The harness file must have:
|
|
1575
|
+
|
|
1576
|
+
```python
|
|
1577
|
+
bridge = Bridge("research")
|
|
1578
|
+
```
|
|
1579
|
+
|
|
1580
|
+
### `must register a handler with @bridge.handle`
|
|
1581
|
+
|
|
1582
|
+
Add exactly one decorated function.
|
|
1583
|
+
|
|
1584
|
+
### The wrong model was selected
|
|
1585
|
+
|
|
1586
|
+
Run with `--debug` to inspect routing. Pin `model=` or narrow `models=[...]` when deterministic selection matters.
|
|
1587
|
+
|
|
1588
|
+
### Memory is not continuing
|
|
1589
|
+
|
|
1590
|
+
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.
|
|
1591
|
+
|
|
1592
|
+
### Direct `memory.upsert` says `cache_id` is required
|
|
1593
|
+
|
|
1594
|
+
Run a successful `context.respond` first on that session so Nineth can remember the generated cache ID.
|
|
1595
|
+
|
|
1596
|
+
### A local service was not discovered
|
|
1597
|
+
|
|
1598
|
+
Check that:
|
|
1599
|
+
|
|
1600
|
+
- the file is directly under `services/`
|
|
1601
|
+
- the filename does not start with `_`
|
|
1602
|
+
- the function uses `@service`
|
|
1603
|
+
- importing the file does not raise
|
|
1604
|
+
- no other service uses the same public name
|
|
1605
|
+
|
|
1606
|
+
### Local service callbacks keep repeating
|
|
1607
|
+
|
|
1608
|
+
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.
|
|
1609
|
+
|
|
1610
|
+
### A job does not run locally
|
|
1611
|
+
|
|
1612
|
+
Jobs become `modal.Cron` functions during `bridge deploy`. There is currently no local scheduler command.
|
|
1613
|
+
|
|
1614
|
+
### Alias registration failed after deployment
|
|
1615
|
+
|
|
1616
|
+
Read the error for the working `modal.run` URL. Fix `BRIDGE_ALIAS_API_URL`/`BRIDGE_API_KEY` or deploy with `--no-alias`.
|
|
1617
|
+
|
|
1618
|
+
### The frontend loses conversation history
|
|
1619
|
+
|
|
1620
|
+
Persist `session_id` from the response and resend it. Do not generate a new ID for every request.
|
|
1621
|
+
|
|
1622
|
+
## Maintainer Guide
|
|
1623
|
+
|
|
1624
|
+
Bridge is a separate distribution under `rooster-sdk/bridge`.
|
|
1625
|
+
|
|
1626
|
+
| Module | Responsibility |
|
|
1627
|
+
| --- | --- |
|
|
1628
|
+
| `bridge/harnesses.py` | Harness services, policy, routing candidates, fallback |
|
|
1629
|
+
| `bridge/runtime.py` | Handler, session/client state, memory, request and local-service loops |
|
|
1630
|
+
| `bridge/routing.py` | Manual/smart selection and fallback |
|
|
1631
|
+
| `bridge/services.py` | Decorator schema generation, discovery, execution |
|
|
1632
|
+
| `bridge/jobs.py` | Job declaration, discovery, execution |
|
|
1633
|
+
| `bridge/project.py` | Generated implementation/cookbook/services/jobs and project loading |
|
|
1634
|
+
| `bridge/terminal.py` | Textual local runner |
|
|
1635
|
+
| `bridge/asgi.py` | Deployed HTTP contract |
|
|
1636
|
+
| `bridge/deployment.py` | Modal source, deploy command, endpoint parsing, alias registration |
|
|
1637
|
+
| `bridge/cli.py` | `init`, `run`, and `deploy` commands |
|
|
1638
|
+
|
|
1639
|
+
Run Bridge tests:
|
|
1640
|
+
|
|
1641
|
+
```console
|
|
1642
|
+
$env:PYTHONPATH = "rooster-sdk/bridge;rooster-sdk"
|
|
1643
|
+
python -m pytest tests/test_bridge_project.py tests/test_bridge_runtime.py tests/test_bridge_deployment.py tests/test_bridge_cli.py
|
|
1644
|
+
```
|
|
1645
|
+
|
|
1646
|
+
Run the delegated Nineth regressions:
|
|
1647
|
+
|
|
1648
|
+
```console
|
|
1649
|
+
python -m pytest tests/test_nineth_client.py tests/test_nineth_smoke.py
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
Build:
|
|
1653
|
+
|
|
1654
|
+
```console
|
|
1655
|
+
python -m build rooster-sdk/bridge
|
|
1656
|
+
python -m twine check rooster-sdk/bridge/dist/*
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
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.
|
|
1660
|
+
|
|
1661
|
+
For Rooster internals and release operations, see [README.md](../../README.md).
|