Workbench — the Orqest agent-runtime container¶
Every production Orqest agent eventually needs the same four pieces of
infrastructure: a MemoryStore for durable facts, a Tracer for span
capture, an EventBus for observability fan-out, and a ring-buffer of
recent events so reconnecting clients can catch up on what they
missed. Wiring these four together was repeated across the Orqest demo
and every downstream consumer.
Workbench packages the quartet in one container. Construct it once
per session (or once per process for single-tenant apps), reach through
it to the underlying primitives, and pass it around instead of plumbing
four objects through every function signature.
Minimal example¶
import asyncio
from orqest import Workbench
from orqest.memory import LocalMemoryStore, MemoryEntry
from orqest.observability import AgentEvent
async def main() -> None:
wb = Workbench(memory=LocalMemoryStore(db_path="/tmp/memory.db"))
# Memory — persisted facts
await wb.memory.store(MemoryEntry(content="User prefers tetrahedral elements"))
# Tracing — span tree
span = wb.tracer.start_span("generate_mesh")
# ... do work ...
wb.tracer.end_span(span, status="ok")
# Events — fan-out
await wb.event_bus.emit(AgentEvent(event_type="plan.init", agent_name="demo"))
# One snapshot for the UI sidecar
snap = wb.snapshot()
assert "trace" in snap and "events" in snap
asyncio.run(main())
Lifecycle patterns¶
- Process-level (demos, single-tenant CLIs) — build one workbench at startup, share it across requests.
- Per-session (chat apps) — build a workbench per session key; memory is a shared singleton injected in, tracer + bus are fresh.
- Per-request (multi-tenant backends) — fresh tracer + bus per turn, shared memory singleton.
Workbench doesn't force a pattern. Construct it however the consumer's
lifecycle demands; pass memory= always, optionally share or
instantiate the other three.
Reset semantics¶
wb.reset() clears the tracer and the recent-events buffer. Memory
is not cleared — the point of memory is to outlive resets. If a
test needs a clean slate, reset memory explicitly through whatever
API the store exposes.
Snapshot shape¶
wb.snapshot() returns:
{
"trace": [<span_dict>, ...], # JSONTracer.export_json()
"events": [<event_dict>, ...], # bounded ring buffer
}
Memory is deliberately excluded — MemoryStore reads are async and
each backend decides its own query semantics. Consumers composing a
full sidecar response add memory alongside:
async def sidecar_state(wb: Workbench) -> dict:
snap = wb.snapshot()
snap["memories"] = await wb.memory.recall("", k=30)
return snap
Runnable demo¶
notebooks/04_orchestrated_workflow.ipynb — Workbench bundling tracer + bus across a full Router → Parallel → Pipeline → RefinementLoop run.
Reference¶
Workbench ¶
Workbench(
*,
memory: Any,
tracer: Tracer | None = None,
event_bus: EventBus | None = None,
buffer_size: int = 200,
ui_registry: Any = None,
auto_register_first_party_ui: bool = True,
)
Runtime container for memory, tracing, events, and replay.
Instance attributes are public — reach through the workbench to talk to the underlying primitives directly when the canonical helpers aren't enough.
Attributes:
| Name | Type | Description |
|---|---|---|
memory |
A :class: |
|
tracer |
Tracer
|
A :class: |
event_bus |
EventBus
|
A :class: |
recent_events |
deque[AgentEvent]
|
Ring buffer of the last |
Wire the infrastructure pieces together.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
memory
|
Any
|
A :class: |
required |
tracer
|
Tracer | None
|
Optional tracer; a fresh |
None
|
event_bus
|
EventBus | None
|
Optional event bus; a fresh one is created when not supplied. |
None
|
buffer_size
|
int
|
Max events retained in the |
200
|
ui_registry
|
Any
|
Optional :class: |
None
|
auto_register_first_party_ui
|
bool
|
When True (default) and
|
True
|
Source code in orqest/workbench/workbench.py
reset ¶
Clear tracer state and the recent-events buffer.
Memory is NOT cleared — the whole point of memory is to
outlive resets. Callers that want a full wipe (e.g. integration
tests) should call memory.reset() or equivalent themselves.
Source code in orqest/workbench/workbench.py
snapshot ¶
Return a JSON-safe snapshot of trace + events.
Memory is excluded because :class:MemoryStore access is
async and consumer-specific. Callers compose the memory view
themselves when serving a sidecar endpoint.
Source code in orqest/workbench/workbench.py
with_healing ¶
Construct a :class:HealingRunner wired to this workbench's bus.
Convenience factory. Lazy-imports :mod:orqest.healing so this
module stays import-light for consumers that don't use healing.
Construction alone does not start healing. The runner only
subscribes its watchdogs and starts the poll loop when entered
as an async context manager. Forgetting the async with
leaves you with a silent no-op — no detections, no recoveries,
no error. Always use the form below:
.. code-block:: python
async with workbench.with_healing(config) as runner:
... # agent work happens inside this block
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
Any
|
A :class: |
required |
api_key
|
Any | None
|
Single key or per-provider map for the fallback
model chain. Required only if |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
An unstarted :class: |
Any
|
with`` to actually wire the watchdogs. |
Source code in orqest/workbench/workbench.py
with_docker_sandbox ¶
with_docker_sandbox(
*,
user_id: str,
session_id: str,
image: str = "orqest/agent-runtime:latest",
allowed_packages: set[str] | None = None,
memory_mb: int = 2048,
cpus: float = 2.0,
pids_limit: int = 512,
promotion_policy: str = "threshold",
promotion_threshold: int = 3,
host_port: int | None = None,
hmac_secret: bytes | str | None = None,
**kwargs: Any,
) -> Any
Construct a :class:DockerSandbox wired to this workbench's bus.
user_id and session_id are required positional-keyword args
— strict by design. They are framework-controlled (not LLM-emitted)
and never appear in any LLM-visible context window: they're carried
in the MCP transport's HMAC-signed JWT bearer token, validated by
middleware inside the container.
user_id is the persistence boundary — the per-user SQLite tool
library mounted at /data inside the container is keyed by
orqest-user-<user_id>. Tools persisted by user A's sessions
survive into user A's future sessions but never cross to user B.
session_id is the per-container lifecycle key — mint a fresh
UUID per session. Reusing one across containers is undefined
behavior (the JWT will fail validation against the new container's
env-injected ORQEST_SESSION_ID).
Construction alone does not start the container. The sandbox
is an async context manager; docker run happens at
__aenter__, docker rm -f at __aexit__. Always use:
.. code-block:: python
async with workbench.with_docker_sandbox(
user_id="alice",
session_id=str(uuid4()),
) as sandbox:
...
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
user_id
|
str
|
Strict per-user identifier. The persistence boundary. |
required |
session_id
|
str
|
Strict per-session identifier. Typically |
required |
image
|
str
|
Docker image to run; defaults to |
'orqest/agent-runtime:latest'
|
allowed_packages
|
set[str] | None
|
Allowlist of pip packages the LLM can declare in
|
None
|
memory_mb
|
int
|
Container |
2048
|
cpus
|
float
|
Container |
2.0
|
pids_limit
|
int
|
Container |
512
|
promotion_policy
|
str
|
|
'threshold'
|
promotion_threshold
|
int
|
For |
3
|
host_port
|
int | None
|
Host port to publish; |
None
|
hmac_secret
|
bytes | str | None
|
Secret for signing JWTs. |
None
|
Returns:
| Type | Description |
|---|---|
Any
|
An unstarted :class: |
Source code in orqest/workbench/workbench.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |