feat(trace): stamp the effective profile digest on every span - #219
Merged
Conversation
A trace with several agents in it could not be read per agent. Every span carried the run id, both session ids, the model and the token usage, and no agent identity — so with several agents sharing one bridge the only discriminator left was `gen_ai.request.model`, which is not an identity: two agents on one model are indistinguishable, and one agent whose model changed reads as two. Measured 2026-09-10 on a live multi-agent trace file: 94 spans, zero carrying any agent identity. The join key existed on both sides the whole time. agent-runtime records `profileDigest` / `authoredProfileDigest` per node; the bridge computes an `effectiveProfileDigest` for every materialization receipt and every session profile binding. It was missing only from the one artifact a trace tool reads. `cli_bridge.profile.effective_digest` closes that. The value is not a second computation that could disagree with the receipt: `resolveAgentProfile` memoizes per request, so the span, the receipt and the binding read one profile object and hash it once. `cli_bridge.profile.name` carries the caller's own `agent_profile.name` beside it as a readable label, never as an identity. Recorded from the route the moment the effective profile is settled, which is after the span opens — the run's creator opens the span before the profile is resolved, and the profile may arrive on the request OR be carried by the session being resumed. That second case is the multi-agent one: an agent's turns after the first carry no profile on the wire. Both attributes land on the tool spans as well as the request span, so filtering the file to one agent's work needs no tree walk first. Both are absent — never a placeholder — on a request that carried no `agent_profile`. A digest, not the profile: a profile carries prompts and instructions, and the existing rule that a trace file must not become a second copy of what the agent read applies to it too. The attributes are namespaced under `cli_bridge.` per the rule already stated on BRIDGE_ATTR: no semantic convention names agent identity, and the contract package's own `agent.*` vocabulary covers loops, branches and outcomes only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014G45Uj6RV5945HXvEkKnfh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap, measured
A trace with several agents in it could not be read per agent.
Measured 2026-09-10 against a live bridge's own export
(
~/.local/state/cli-bridge-8913/data/traces/spans.jsonl, 94 spans at the timeof the check, names
chat opencodeandbash). Every span carried:cli_bridge.run.id,cli_bridge.session.id,cli_bridge.backend.session_id,cli_bridge.trace_contract.build,gen_ai.request.model,gen_ai.system,token usage,
gen_ai.tool.call.id,gen_ai.tool.name.Zero carried any agent identity. With several agents sharing one bridge the
only discriminator left is
gen_ai.request.model— and a model id is not anidentity. Two agents on one model are indistinguishable, and one agent whose
model changed reads as two. Asking "how are these agents collaborating?" of that
file has no grounded answer.
The join key existed on both sides the whole time:
agent-runtimerecordsprofileDigestandauthoredProfileDigestper node onits spawn journal.
effectiveProfileDigestfor every profile-materializationreceipt (
GET /v1/runs/:id) and every session profile binding.It was missing from the one artifact a trace tool reads.
What this stamps
cli_bridge.profile.effective_digestcanonicalAgentProfileDigestof the effective profile — the join keycli_bridge.profile.nameagent_profile.name, verbatim — a readable label, not an identityThe digest is not a second computation that could disagree with the receipt.
resolveAgentProfilememoizes per request (WeakMap<ChatRequest, AgentProfile>),so the span, the materialization receipt and the session binding all read one
profile object; this call site reuses the digest the binding just produced, so it
adds no hashing at all.
Placed at the point where the effective profile is settled for both sources
that can supply it — the request body and the session being resumed. The span
opens before that (its creator opens it before admission), so the identity is
recorded the way
cli_bridge.backend.session_idalready is: folded in once therequest establishes it. The resume case is the multi-agent one — an agent's turns
after the first carry no
agent_profileon the wire.Both attributes land on the tool spans as well as the request span, so
filtering the file to one agent's work does not require rebuilding the tree first.
Both are absent, never a placeholder, on a request that carried no
agent_profile: a synthesized label would be indistinguishable from one thecaller chose.
Why
cli_bridge.*and not a new contract keyBRIDGE_ATTRalready states the rule: anything a standard names uses thestandard key; anything describing THIS producer's execution context is namespaced
under
cli_bridge.. Checked the installed contract(
@tangle-network/agent-trace-contract@1.0.2, the buildcli_bridge.trace_contract.buildpins): itsATTRvocabulary covers span kind,model, system, tokens, cost, tool name, and
agent.loop.*/agent.branch.*/agent.outcome. It names no agent identity, so there is no standard key touse and no contract change to make here. The addition goes through
BRIDGE_ATTR,the emitter's documented vocabulary, and the README's bridge-attribute table —
not as a key bolted onto the span builder.
Safety
paths; the existing rule that a trace file must not become a second copy of what
the agent read applies to it too. Same reason tool arguments are dropped.
MAX_ATTR_CHARSas every other attribute, so a pathologicalcaller-supplied name cannot blow up a span.
recordNodecannot throw into a request, like every other entry point on therecorder. The failure mode stays "a lost stamp", never "a lost completion".
computed unconditionally one line earlier by
exactSessionProfileBinding.Tests
tests/trace-node-identity.test.ts, 12 cases, red before the change and greenafter (verified by reverting the route call site alone: 7 route-level cases fail
with
expected undefined to be 'sha256:…').Route-level, through
mountChatCompletionswith a capturing sink:agent_profilestamps the digest and the name;effectiveProfileDigestthe run stored — thejoin key cannot drift from the receipt;
stamped;
gen_ai.request.modelagrees across both — the gap itself;
Recorder-level: last-write-wins, empty digest ignored, empty name omitted,
pathological name bounded to 256 chars.
tsc --noEmitclean. Full suite: 1062 passed / 12 failed. 11 of those 12 failidentically on unmodified
origin/mainin this same environment (Dockerexecutor, macOS seatbelt jail, Pi session-dir realpath — all environment-bound,
none touching the trace path). The 12th,
retained-sessions > prunes acknowledged interaction records, is a 5s-timeoutflake under full-suite load: it passes in isolation and in a five-file run with
this change applied, and this change adds no work to that path.
Deployment
Deployment waits for a run boundary. Bridge 8913 is serving a live
multi-agent workload; restarting it kills in-flight turns. Do not restart or
redeploy 8913 or 8912 to pick this up — land it, then deploy at the next run
boundary.
Spans already emitted are not retrofitted by this change. They can still be
attributed after the fact by joining the run id or session id on the span to the
bridge's own
retained_run_admissions.snapshot_json.profileMaterializationandsessions.metadata_json.agent_profile_binding, which is what an out-of-treeretrofit script does for the current run. Measured on the same file: 45 of 92
turns attributable that way, the other 47 being requests that carried no
agent_profileat all and so have no identity to recover. After this lands, noneof that joining is needed.
🤖 Generated with Claude Code