Observability modules¶
This page maps orchestrator/observability/ — the four surfaces that watch a run without steering it: the analytics
sink and everything downstream of it, the parser that meters one finished agent run, the Streamlit page over the
operator's Postgres target, and the file-backed trajectory viewer beside it — together with the two streamlit run
targets under orchestrator/apps/ that compose the pages. It is split out of
../architecture.md#top-level-layout, which keeps the top-level map and the
naming rules that hold for the tree as a whole. The packages these owners observe are in
platform-modules.md and workflow-modules.md.
Each entry below is the responsibility its package holds, and it answers there and on no second site. What each sink
writes, what the database a replay fills holds, and what the two pages report over them are in
../observability.md and the focused pages under it; the knobs are in
../configuration/observability.md.
Enforced boundaries¶
The first four rules below are held by checks that walk the tree off disk, so an owner added anywhere under it is covered the day it lands. The last two are held against a declared list instead — the analytics owners' own import guard for the first, and the owner's own tests for each channel of the second, save the sink's, which is convention.
- Observation-only. Nothing here sits on the workflow's decision path, so no module may import the workflow
engine, a stage, or an application entry point — the CLI and the runtime loop on one side, the two
streamlit runtargets underapps/on the other. The dependency runs one way and an entry point composes these owners rather than the reverse. That is also what makes every surface safe to truncate, rotate, or delete. - An initializer binds nothing, with two declared exceptions. Importing one owner must not charge the importer
for its siblings: the recording path runs inside every tracked agent run, and a binding would put the query owners
and the database driver behind that import.
usage/andanalytics/recording/pay that cost deliberately, since each is reached through its package rather than through an owner, and the check that excuses them is keyed on their__all__, so a third publisher is a deliberate edit rather than a silent one. What a publisher may charge for beyond its own owners is declared per package: recording buys the analytics configuration, the shared sink, the usage parsers, and the trajectory writers, and nothing else. - No second site. Nothing under the tree carries an export manifest, a resolver hook, or a
.pyistub — a re-export is the owner's own object, bound once at import, so a lookup lands on the module that defines the name rather than on something answering for it. The same check holds the declared package inventory against what is on disk and requires a mirrored tests package for each. - Streamlit and Plotly stay function-local. Both live in the optional
dashboarddependency group, so every module has to import cleanly with the two blocked outright and with no attempt on either recorded — a module-scope import that swallows its ownImportErroris still a load in the install that has the package. Pandas joins them one level up: no launch path underapps/may cost one of the three at import. - One knob binder, and one lazy reach past it.
analytics/environment.pyparses the six sink and database knobs — the disable vocabulary with them, so what "off" spells is settled once — andanalytics/settings.pyis the sole module that calls those parses and the sole module that binds them, so a knob answers the same way wherever it is read. Reading one back is the configuration owner beside them: every adapter resolves a knob off a holder inside the call rather than at import, which is what lets a caller settle which holder answers — the trajectory app hands its page one explicitly, while the analytics page resolves the live one through the configuration owner as it reads. That holder is also the only place a knob is read out oforchestrator.config, for theLOG_DIRthe default sink lives under, and the parse defers that import to a call so nothing on the append path pays for it until a record is written. The one other reach into the configuration layer is not a knob at all: the trajectory writer imports the secret redactor inside the write that uses it, so a producer on the recording path pays for the credential owner only when the opt-in sink is actually on. - Operator log channels are spelled literally rather than derived from
__name__, because an operator's level and handler selection is keyed on them:orchestrator.analyticsfor a refused sink write,orchestrator.analytics.syncfor a replay,orchestrator.analytics.connectionfor a read-path driver failure,orchestrator._dashboard_read_dispatchfor the onedashboard.load:line an operator A/Bs the fan-out with, andorchestrator.trajectory_readerfor a trajectory file the viewer could not read. A module moved between packages does not take its channel with it.
The map¶
Each line is the responsibility its package holds; which owner inside it decides what is in that module's own docstring. A package publishes a surface only where the entry says so — everywhere else the initializer is a marker and callers import an owner directly.
orchestrator/
observability/ the four surfaces that watch a run without steering it, one package each; nothing sits flat
beside them
analytics/ the JSONL sink and everything downstream of it: the parse of the six sink and database
knobs, the process-wide holder bound over it and the view an adapter reads one back
through, the record envelope and locked line both sinks reach disk through, and the by-age
prune that bounds each of them
recording/ the append side, publishing the seven recorders a producer appends through (`__all__`): the
envelope and the append beneath them, and the token, cost, skill, and catalog steps a
finished agent run is summarized by before one of them writes
query/ the read side of the Postgres target: the keyword vocabulary a read is called by, the
selection it narrows to, what it dials with, and the four families it is answered by — the
events table, the day-bucketed rollup above it, the per-run breakdowns whose grouping key
that rollup threw away, and the skill facts recorded in an `agent_exit` row's `extras` —
each over frozen result models a caller can consume with no driver installed
sync/ the JSONL → Postgres replay and the `-m` command an operator schedules: what a record must
carry and hash to, the dedup and batching one pass is made of, the driver boundary beneath
it, and the transaction shape a run guarantees
trajectories/ the opt-in per-run reasoning sink: the caps a record is measured against, the redaction and
head/tail truncation it passes through, and the fail-open write the whole of it rides
usage/ the provider payload parsers, publishing the parser surface and the result types it returns
(`__all__`): token and cost metering, skill evidence, and per-run trajectories, one family
per backend over a shared JSONL vocabulary and the first-party rate tables
dashboard/ the Streamlit analytics page: the visual theme both pages are drawn in, the window and
filters one run of it carries, the two waves a load is staged into with the read adapters
they are made of, the banners and headline numbers drawn between them, and the panels,
tables, and inline markup the rest of the page is
charts/ the Plotly figures those reads are drawn as: what every family is built out of, the frame
the horizontal cost families share, the weekday-by-hour grid and the per-day throughput
strip, and the usage hero figure
trajectory_viewer/ the file-backed trajectory page: the read model over the sink's JSONL, the filters and
totals a page narrows it to, the inline HTML it is drawn as, and the controls, picker, and
rendering one run of the page is driven by
apps/ the two `streamlit run` targets, one per page — the analytics dashboard and the trajectory
viewer — over the standard-library `sys.path` shim a script launch needs; the polling loop
is launched at `cli.py` instead
How these packages depend on each other¶
- Inside
analytics/, the environment parse and the configuration view over the holder it fills are the bottom, and the shared sink sits above both write packages: it imports neitherrecording/nortrajectories/, so anagent_exitcomposing a trajectory write reaches the envelope and the line above both rather than back through the recorders that called it, and the direction runs one way — recording names the trajectory writers, never the reverse. Each sink's lock is minted on that shared owner once per process, which is what makes an append and the prune that rewrites the file under it serialize against each other without either sink blocking on the other's file. query/andsync/both keep psycopg behind a call — the read path defers the driver import to its connect factory, the replay keeps it inside the two adapters a caller may replace — so the result models, the row layout, and the content hash a replay deduplicates on are all usable on a machine with no driver installed.dashboard/composes downward and nothing reads back up. The controls normalize a run's selections into the filters and the pair of cache keys both waves are bound to, and a panel is the card and the figure together: a section names its own builder undercharts/rather than being handed a chart handle. The style owners are read back under one name, and a page passes that object down to the sections whose own arithmetic needs a hue or a formatter; the sections drawn from a figure or a hand-rolled table take none, since the chart family reads its hues off the palette owner directly and the table markup is painted by the stylesheet the page injects. Undercharts/, a family names only the style owners, its own siblings, and thequery/result model its rows arrive as.trajectory_viewer/runs the same way: the parse sits above the record and the views bound onto it, the file read above the parse, and the filter, summary, and markup owners open nothing. Which file a read opens comes off the settings holder the page hands down rather than one an owner captured, and nothing here imports Streamlit — the owners that draw take it in as an argument.- The two
streamlit runtargets are the one launch path each page has and own nothing the page decides: everything they compose is imported inside the pass that reaches it, Streamlit and pandas included, so importing an app costs thesys.pathshim and nothing else.