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