12 KiB
C4 — Journal-backed progress projection & progress_tracing deletion
Status: execution plan (2026-07-04), written after a ground-truth code map of
both the OpenHuman progress surface and the vendored crate observability
primitives. This is the actionable plan for the C4 workstream's July 2026
continuation notes, and it is the gated prerequisite for doc 03 (V3) Step
5 — deleting the ProviderDelta bridge and progress_tracing.
1. Corrected architecture (what the map found)
- There is no crate
SpanCollector. The only span state machine is OpenHuman'sSpanCollectorinsrc/openhuman/agent/progress_tracing.rs(1272 lines). The crate does not build spans — it journals rawAgentObservations and lets exporters project. - The journal/status/persistence stack already exists and is attached to
every run.
run_turn_via_tinyagents_shared(src/openhuman/tinyagents/mod.rs:420) mints a run id, seeds theEventSinkwith it, andattach_turn_journal(src/openhuman/tinyagents/journal.rs:304) installsStoreEventJournal(overJsonlAppendStore) via aJournalSink → RedactingSink → FanOutSink, plus a durableFileStatusStore. Every run already durably records the crateAgentEventstream asAgentObservations. - Two producers of
AgentProgress:- Crate path:
OpenhumanEventBridge(src/openhuman/tinyagents/observability.rs:464) mapsAgentEvent→AgentProgresslive (stateful: iteration cursor, subagentscope,tool_namesrecovery, display labels, failure class). - Legacy path:
session/tool_progress.rsTurnProgress+spawn_delta_forwardermaps engine callbacks +ProviderDelta→AgentProgress(the Step-5 deletion target, 253 lines).
- Crate path:
SpanCollectorconsumesAgentProgress(the bridge output), while the journal captures theAgentEventinput. The web progress bridge (channels/providers/web/progress_bridge.rs:157) is a side-observer of theAgentProgresschannel:collector.record(&event, now)per event, thencollector.finish()+export_run_trace(config, spans)at loop exit.- Langfuse is exported twice, in two data models.
progress_tracing/langfuse.rs(825 lines) hand-rolls aTraceSpan→Langfuse ingestion batch; the crateobservability/langfuse/already builds the same batch from&[AgentObservation](LangfuseClient::send_observations,build_ingestion_batch). Same proxy path, same207handling.
2a. BLOCKER found during S1 (2026-07-04): the mapping is not journal-replayable
The S1 attempt to extract a pure event_to_progress revealed that the live
OpenhumanEventBridge mapping depends on live side-channels absent from the
journal, so "parity by construction via journal replay" (§2 below) does
not hold as written:
ToolCompleted(observability.rs:745): the crate eventToolCompleted { call_id, tool_name, started_at_ms, input, output }carries no outcome.success/elapsed_ms/output_chars/ failure class are read fromself.failure_map, acall_id → (ok, failure, elapsed, chars)map populated byToolOutcomeCaptureMiddleware— not journalled.record_usage(observability.rs:298): drainsusage_carry(the model adapter's providerUsageInfoFIFO) to restore charged USD, cache-creation and context-window tokens the crateUsagedrops. Not journalled.
A journal-only projection therefore yields structurally-correct spans with
degraded attributes (assumed-success tools, zero durations/sizes, $0
cost) — which cannot pass the S3/S5 parity gate.
Corrected prerequisite — S0: make the journal self-sufficient (crate work)
Before any journal projection can hit parity, the enriching data must live in
the journalled AgentEvents, not in OpenHuman side-channels:
- Enrich
AgentEvent::ToolCompletedin the crate with the outcome:success: bool,duration_ms,output_bytes(and an optional structured failure), populated intools.rs::finish_tool_callfrom theToolResultand thestarted_at_msit already tracks. OpenHuman's bridge then reads them from the event;failure_map/ToolOutcomeCaptureMiddlewareshrink to the product-specificToolFailureClassmapping (or that too moves onto the event). This is a V-series crate PR (additive event fields). - Usage accounting: decide whether charged-USD/provider-cost belongs on a
crate event (e.g. a
provider_cost/cache_creationextension onUsageRecorded/Usage) or stays a documented, accepted OpenHuman-only trace attribute filled at export time from the status/cost store rather than the live side-channel. (The cost roll-up is already persisted per-run; the trace can read it from there instead ofusage_carry.) - Only after S0 does §2's "reuse the mapping" become true by construction.
Sequencing impact: S0 (crate event enrichment) is now the first slice and gates S1→S2. It is separate crate work that should be PR'd upstream like V1/V2. The remaining S1–S6 below stand, rebased on S0.
2. The parity-preserving approach (valid only after S0)
Reproduce the span tree by construction, not by re-deriving it:
Replay journal
AgentObservations →AgentProgressusing the same mapping the live bridge uses → fold through the existingSpanCollector.
Concretely:
- Extract the bridge mapping into a pure, reusable function
event_to_progress(event: &AgentEvent, state: &mut BridgeState, scope) -> Vec<AgentProgress>(state = iteration cursor +tool_names+ scope). The liveOpenhumanEventBridge::on_eventbecomes a thin driver over it (refactor, no behavior change — guarded by the existing bridge tests). This makes the AgentEvent→AgentProgress mapping a single source of truth. - Journal projection
spans_from_observations(ctx: TraceContext, obs: &[AgentObservation]) -> Vec<TraceSpan>: fold each observation throughevent_to_progressintoAgentProgress, feed a freshSpanCollector::record(...)stamped withobs.ts_ms, thenfinish(). Because it reusesSpanCollector, span-shape parity is guaranteed for everyAgentProgressvariant the journal can produce. - Parity harness (
progress_tracing'stests.rsis the oracle): drive a representative synthetic run and assertspans_from_observations(journal)==SpanCollectorfed the liveAgentProgress. Cover: multi-iteration turn, tool calls (success + failed + unknown-tool recovery), model-call generation spans w/ usage & reasoning & cache-creation, nested sub-agents, cost roll-up.
Parity gaps to resolve (AgentProgress with no journal AgentEvent source)
SpanCollector ignores streaming/content deltas for spans
(progress_tracing.rs:1076), so TextDelta/ThinkingDelta/ToolCallArgsDelta
do not affect span parity — safe to drop on replay. The variants that do
carry span data but have no direct AgentEvent:
| AgentProgress | Span effect | Journal source | Resolution |
|---|---|---|---|
TurnContent{prompt, reply} |
root span input/output (gated on capture_content) |
ModelCompleted.input/output present in journal but shaped differently |
project root i/o from ModelStarted/ModelCompleted payloads, or emit a journalled content event |
TaskBoardUpdated |
none (no span) | n/a | ignore for spans |
SubagentAwaitingUser |
subagent span attr | partial (SubAgentStarted/Completed only) |
accept minor attr gap or add a crate event (V6) |
TurnCostUpdated |
root usage roll-up | UsageRecorded/CostRecorded in journal |
project from those (already in bridge) |
Document any accepted gap in the parity test as an explicit, reviewed exception.
3. Langfuse swap (separable, lower-risk slice)
Replace progress_tracing/langfuse.rs::push_spans with the crate
LangfuseClient::proxy(...).send_observations(trace_cfg, &obs) reading the run's
journal. Parity test: assert the crate batch matches the existing batch shape
for a fixture run (trace-create + per-observation generation/span/event,
usageDetails/costDetails, 207 handling, content gating from
agent_tracing.capture_content). This deletes ~825 lines independently of the
span-projection slice.
4. Sequenced slices (each: code + tests + green build; deletions gated)
- S0 — enrich crate
AgentEvent::ToolCompletedwithduration_ms/output_bytes/errorso the journal is self-sufficient for tool outcomes (§2a). DONE — tinyagents#18 (branchfeat/tool-completed-outcome, 933 tests green). Also enriches the crate Langfuse tool span. Merge + gitlink bump precede S1. (Usage/charged-USD accounting — §2a item 2 — is still open: fill it at export time from the persisted per-run cost store rather than theusage_carryside-channel.) - S1 — extract
event_to_progress(pure mapping) + make the live bridge a driver over it. No behavior change; existing bridge/observabilitytests are the gate. No deletion. - S2 —
spans_from_observationsjournal projection + parity harness vsSpanCollector. IN PROGRESS — additive projection module now covers the single-agent spine, S0 tool outcomes, rootTurnContentfrom captured model I/O, and sub-agent lifecycle/scoped child tool/model spans. Remaining known gap: per-call charged USD/cache-creation is still zero until export-time cost store reconciliation lands. - S3 — flip the web progress bridge to build spans from the journal at run
end (
spans_from_observations) instead of the liveAgentProgressside-observer; keep the old path behind a shadow-compare for one release (log divergences), matching the C1session_shadow_readspattern. STARTED — the web bridge now keeps live export as-is and logs a structural journal-projection shadow comparison keyed by the durable journal run id. - S4 — Langfuse swap to the crate exporter (§3). STARTED — when the
web bridge can read a durable run journal for S3 shadow comparison, the
remote
share_usage_datapush now sends thoseAgentObservations through the crateLangfuseClient; live spans remain the local tracing sink and fallback until S3 shadow parity is release-proven. The OpenHuman wrapper also injects aggregaterun.totalusage/cost from the durable run ledger so journal-backed Langfuse export preserves the top-level cost signal while per-call charged USD awaits a journal-native event. Later deleteprogress_tracing/langfuse.rs(~825 + tests). - S5 — delete
progress_tracing.rs+SpanCollectoronce S3 shadow shows no divergence for one release and V3 projection parity holds (the doc 07 gate). Tick the deletion ledger (~2k incl. tests). - S6 (= V3 Step 5) — with
AgentProgressnow a journal projection, delete the legacyProviderDeltabridge insession/tool_progress.rs(253) and thespawn_delta_forwarder, since the crate event path is the only producer.
5. Gates (doc 07)
progress_tracingdelete: C4 shadow parity (S3) for one release AND V3 projection parity. Langfuse (S4) may land earlier (independent shape parity).- Approval/security/redaction boundaries unchanged: the journal path already
runs through
RedactingSink(journal.rs:327); the projection must not re-introduce raw prompt/PII into spans beyond whatcapture_contentalready gates.
6. Key file references
Producer/bridge: tinyagents/observability.rs:464 (OpenhumanEventBridge),
tinyagents/journal.rs:304 (attach_turn_journal), tinyagents/mod.rs:420
(run_turn_via_tinyagents_shared). Span machine:
agent/progress_tracing.rs (SpanCollector:307, record:686, finish:1087,
export_run_trace:1254), driven at
channels/providers/web/progress_bridge.rs:157. Crate foundation:
harness/observability/{mod,types}.rs (HarnessEventJournal:156,
StoreEventJournal, JournalSink:581, AgentObservation:40),
harness/observability/langfuse/. Parity oracle:
agent/progress_tracing/tests.rs (1169 lines).