|
| 1 | +# Unify Trace `cubepi.run_id` with Business Run ID |
| 2 | + |
| 3 | +- Date: 2026-08-10 |
| 4 | +- Status: Implemented (pending release) |
| 5 | +- Repos: cubepi (primary), cubeplex (consumer; almost no code change) |
| 6 | +- Related: `dev/specs/2026-05-18-cubepi-tracing-design.md` §10.1, |
| 7 | + `dev/specs/2026-08-04-tracing-hitl-suspension.md` (HITL: one business run, |
| 8 | + multiple `invoke_agent` activations) |
| 9 | + |
| 10 | +## Goal |
| 11 | + |
| 12 | +One **run** concept end-to-end. |
| 13 | + |
| 14 | +The string that hosts put on `agent.prompt(run_id=...)`, that lands on |
| 15 | +`Message.run_id` / `cubepi_messages.run_id`, and that products use for SSE, |
| 16 | +cancel, steer, and billing **is the same string** stamped on every span as |
| 17 | +`cubepi.run_id`. |
| 18 | + |
| 19 | +There is no second, tracer-private "trace run id". |
| 20 | + |
| 21 | +## Context |
| 22 | + |
| 23 | +Today two UUIDs coexist for one agent activation: |
| 24 | + |
| 25 | +| Layer | Who mints | Example | |
| 26 | +|---|---|---| |
| 27 | +| Business / agent | host (`uuid7`) or `prompt` fallback (`uuid4().hex`) | cubeplex stream path, message ledger | |
| 28 | +| Tracing | recorder always `str(uuid.uuid4())` at `AgentStart` | span attr `cubepi.run_id`, stream log name | |
| 29 | + |
| 30 | +That split was never a deliberate product rule that "business run ≠ trace run". |
| 31 | +History: |
| 32 | + |
| 33 | +1. **2026-05-18** tracing Phase 1 defined `cubepi.run_id` as "uuid generated at |
| 34 | + root open" so JSONL could shard by run. |
| 35 | +2. **2026-05-19** PR #92 protected that attribute from being *clobbered by |
| 36 | + user metadata* (namespace → `cubepi.metadata.*`) — not a ban on adopting |
| 37 | + the agent run id. |
| 38 | +3. **2026-06-06** agent gained `prompt(run_id=...)` / `active_run_id` for the |
| 39 | + message ledger, fork, HITL. Tracing was never updated to read it. |
| 40 | +4. JSONL later **re-sharded by OTel `trace_id`**, so the original reason for a |
| 41 | + tracer-minted run id (file path) is already gone. Comments on the exporter |
| 42 | + still describe parent + subagent as same `trace_id`, different |
| 43 | + `cubepi.run_id`. |
| 44 | + |
| 45 | +Symptom hosts hit in the wild: UI/API `run_id` cannot find the matching |
| 46 | +`cubepi trace` file; investigators rediscover the run via `conversation_id`. |
| 47 | + |
| 48 | +## Concepts (normative) |
| 49 | + |
| 50 | +``` |
| 51 | +Business run_id ──► messages / SSE / billing / cubepi.run_id (span attr) |
| 52 | +OTel trace_id ──► span tree storage & CLI grouping (may hold multiple runs) |
| 53 | +OTel span_id ──► one span node |
| 54 | +``` |
| 55 | + |
| 56 | +- **Run** = one agent activation identity (`prompt` / `resume` / `respond` |
| 57 | + effective id, or oneshot session id). Product and tracing share it. |
| 58 | +- **Trace** = OTel distributed tree. Not a run. Nesting may put several runs |
| 59 | + under one `trace_id`. |
| 60 | +- **Span** = one node. Root `invoke_agent` starts a subtree; filtering "all |
| 61 | + spans of this run" uses the denormalized `cubepi.run_id` attribute (or a |
| 62 | + tree walk from the root `span_id`). The attribute must be the **business** |
| 63 | + id, not a second random UUID. |
| 64 | + |
| 65 | +## Non-goals |
| 66 | + |
| 67 | +- Continuing one OTel `trace_id` across HITL pause and resume (already out of |
| 68 | + scope per HITL suspension spec). Correlation across activations is the |
| 69 | + **shared business `run_id`** (and host metadata). |
| 70 | +- Changing JSONL sharding (stays `trace_id`). |
| 71 | +- Changing how hosts mint run ids (cubeplex keeps `uuid7()`). |
| 72 | +- Building a UI; only the id contract. |
| 73 | +- Removing the `cubepi.run_id` attribute name (keep it; change who fills it). |
| 74 | + |
| 75 | +## Design |
| 76 | + |
| 77 | +### 1. Recorder: prefer `agent.active_run_id` |
| 78 | + |
| 79 | +On `AgentStartEvent` → `_on_agent_start`: |
| 80 | + |
| 81 | +```text |
| 82 | +if attached agent has active_run_id set: |
| 83 | + run_id = that value |
| 84 | +else: |
| 85 | + run_id = str(uuid.uuid4()) # fallback only |
| 86 | +stamp CUBEPI_RUN_ID = run_id on root and all children (unchanged) |
| 87 | +``` |
| 88 | + |
| 89 | +Ordering already allows this: `Agent.prompt` / `resume` / `respond` set |
| 90 | +`active_run_id` **before** the loop emits `AgentStartEvent`. Recorder holds |
| 91 | +`self._agent` from `attach`. |
| 92 | + |
| 93 | +No change to the PR #92 rule: user `tracing_context(metadata=...)` still lands |
| 94 | +under `cubepi.metadata.*` and **must not** overwrite `cubepi.run_id`. Hosts |
| 95 | +pass the business id through `agent.prompt(run_id=...)`, not through metadata. |
| 96 | + |
| 97 | +### 2. Oneshot: keep minting, same semantic |
| 98 | + |
| 99 | +`Tracer.oneshot` has no agent. It continues to mint a run id for that session. |
| 100 | +That id **is** the oneshot's run id (only activation id that exists), not a |
| 101 | +parallel "trace run" concept. Optional later: accept `run_id=` on oneshot for |
| 102 | +host correlation. |
| 103 | + |
| 104 | +### 3. Nested subagents |
| 105 | + |
| 106 | +Unchanged structure: |
| 107 | + |
| 108 | +- Parent and child may share one OTel `trace_id` (existing nesting). |
| 109 | +- Each agent's activation has its own business `run_id` (child `prompt` with a |
| 110 | + fresh id, or host policy). |
| 111 | +- Spans of each activation carry **that** activation's id. |
| 112 | + |
| 113 | +Do not force child spans to inherit the parent's business run id. |
| 114 | + |
| 115 | +### 4. HITL pause / resume |
| 116 | + |
| 117 | +One business `run_id` across pause and resume (already true on the agent). |
| 118 | +Each activation still opens a new `invoke_agent` root / may be a new |
| 119 | +`trace_id`. After this change both roots' spans carry the **same** |
| 120 | +`cubepi.run_id`, so `cubepi trace` filter-by-run-id finds both halves. |
| 121 | + |
| 122 | +### 5. Stream recording path |
| 123 | + |
| 124 | +`record_stream` writes `<run_id>.stream.jsonl`. After the change the filename |
| 125 | +uses the business id. Sanitize as today (`_safe_filename`). Collision risk is |
| 126 | +the same as "two concurrent activations with the same run_id", which hosts |
| 127 | +already must not do for the ledger. |
| 128 | + |
| 129 | +### 6. Spec text update |
| 130 | + |
| 131 | +In the tracing design §10.1, change: |
| 132 | + |
| 133 | +| Attribute | Source (old) | Source (new) | |
| 134 | +|---|---|---| |
| 135 | +| `cubepi.run_id` | uuid generated at root open | agent `active_run_id` when set; else generated fallback | |
| 136 | + |
| 137 | +### 7. cubeplex |
| 138 | + |
| 139 | +Already passes `run_id` into `agent.prompt`. No required change for alignment. |
| 140 | + |
| 141 | +Optional follow-ups (separate PRs, not required for the contract): |
| 142 | + |
| 143 | +- Message action **Info** chip: document that the id is the cubepi/trace run id |
| 144 | + (already true once this lands). |
| 145 | +- Admin traces filter: accept the same id. |
| 146 | + |
| 147 | +## Change list (implementation) |
| 148 | + |
| 149 | +### cubepi |
| 150 | + |
| 151 | +| Area | Change | |
| 152 | +|---|---| |
| 153 | +| `cubepi/tracing/recorder.py` | `_on_agent_start`: resolve run id from `self._agent` active run; fallback uuid | |
| 154 | +| `cubepi/tracing/tracer.py` | oneshot: document that minted id *is* the run id; optional `run_id=` later | |
| 155 | +| `dev/specs/2026-05-18-cubepi-tracing-design.md` | §10.1 source line for `cubepi.run_id` | |
| 156 | +| Tests | See below | |
| 157 | +| Changelog | breaking-ish note for anyone who stored old tracer-minted ids | |
| 158 | + |
| 159 | +Public API surface: no new functions required. Behavior change only. |
| 160 | + |
| 161 | +### Tests (invariants) |
| 162 | + |
| 163 | +1. **`prompt(run_id="host-1")` → all spans `cubepi.run_id == "host-1"`** |
| 164 | + (root, turn, chat, tool if any). |
| 165 | +2. **`prompt()` without run_id → spans share one non-empty id; equals returned |
| 166 | + run id from `prompt`.** |
| 167 | + (Today `prompt` returns `effective_run_id`; tracing must match that, not a |
| 168 | + third uuid — so fallback should use the same source as the agent, not a |
| 169 | + second `uuid4()` in the recorder.) |
| 170 | + |
| 171 | + **Important refinement:** when the agent already chose `effective_run_id` |
| 172 | + (including self-minted `uuid.uuid4().hex`), the recorder must **read that**, |
| 173 | + not mint again. Fallback self-mint only if `active_run_id` is unexpectedly |
| 174 | + unset (defensive). |
| 175 | +3. **Metadata cannot clobber** (existing PR #92 test still passes). |
| 176 | +4. **Subagent:** parent and child different business ids → different |
| 177 | + `cubepi.run_id` on their respective span subtrees; same `trace_id` when |
| 178 | + nested. |
| 179 | +5. **Sequential runs** on one attached agent: run A then run B → no id leak. |
| 180 | +6. **Oneshot:** still produces a non-empty `cubepi.run_id` on root + chat. |
| 181 | + |
| 182 | +### Migration / compatibility |
| 183 | + |
| 184 | +- Old JSONL files keep historical tracer-minted ids; no migration. |
| 185 | +- Operators who bookmarked old `cubepi.run_id` values for in-flight runs will |
| 186 | + not match new spans; only new activations align. |
| 187 | +- CLI filters (`cubepi trace ls` by run id attribute) start matching host ids |
| 188 | + after upgrade — that is the intended win. |
| 189 | + |
| 190 | +## Success criteria |
| 191 | + |
| 192 | +1. Given cubeplex (or any host) `run_id=R` on `prompt`, every span of that |
| 193 | + activation has `cubepi.run_id=R`. |
| 194 | +2. `cubepi trace` lookup by the id shown in the product UI finds that run's |
| 195 | + spans (for agent activations; oneshot still uses its own id). |
| 196 | +3. No second uuid is minted by the recorder when `active_run_id` is set. |
| 197 | +4. JSONL layout, OTel parentage, and metadata namespacing remain unchanged. |
| 198 | +5. Existing "metadata cannot clobber reserved attrs" tests stay green. |
| 199 | + |
| 200 | +## Open points (non-blocking) |
| 201 | + |
| 202 | +- Expose `run_id=` on `Tracer.oneshot` for host-supplied correlation of |
| 203 | + memory/background jobs. |
| 204 | +- Whether `AgentStartEvent` should carry `run_id` explicitly (nice for pure |
| 205 | + observers; not required if recorder reads `active_run_id`). |
| 206 | +- Admin UI deep-link from message Info chip → traces filtered by run id |
| 207 | + (cubeplex product; after this lands). |
| 208 | + |
| 209 | +## Decision needed before implement |
| 210 | + |
| 211 | +None for the core path: **one run concept; recorder adopts `active_run_id`.** |
| 212 | + |
| 213 | +Approve this draft → implement in cubepi (tests first) → bump cubeplex's |
| 214 | +cubepi pin when released. |
0 commit comments