GitHub user rosemarYuan added a comment to the discussion: [Discussion] Agent observability: tracing & evaluation beyond metrics + event log
# Event Lineage: Reconstructing a Trace Tree Based on Event Causality > **TL;DR:** I suggest splitting Agent observability into two independent > phases. The first phase adds only two framework-managed causal fields to each > `Event` to build a minimal Event Trace Tree. A later phase can introduce a > complete record model that includes a run ID, execution levels, lifecycle information, and failure details. Based on [Discussion #710](https://github.com/apache/flink-agents/discussions/710), #841, and #886, this proposal focuses only on reconstructing the runtime causal path between business Events and Actions. This problem is independently useful and can be addressed through a smaller, lowerimpact change than the complete trace model proposed in Discussion #900. ### Goals The goal of this design is to rebuild a Trace Tree from the Event Log, with an `InputEvent` as the root, as shown below: ```text InputEvent E0 ├─ [Action: action_a] // branch a │ └─ ChatRequestEvent E1 │ └─ [Action: chat_model_action] │ └─ ChatResponseEvent E2 │ └─ [Action: output_a] │ └─ OutputEvent E3 └─ [Action: action_b] // branch b └─ ChatRequestEvent E4 └─ [Action: chat_model_action] └─ ChatResponseEvent E5 └─ [Action: output_b] └─ OutputEvent E6 ``` This makes it easy for users to see which Event triggered an Action and which later Events were output by that Action. ### EventLineage To rebuild the Trace Tree above, the Event Log must record each relationship in the path: ```test Event → triggered Action → new Event emitted by the Action ``` Consider the following causal chain:`E1 → [A1] → E2 → [A2] → E3` Using E2 as the reference point, the lineage can be recorded in either direction: * Forward: E2 records that it triggered A2 and that A2 emitted E3. * Backward: E2 records that it was emitted by A1, which was triggered by E1. The forward approach requires updating E2 after A2 completes, because E3 is not yet known when E2 is first recorded. The backward approach requires no later update. When A1 emits E2, the framework already knows E1, A1, and E2. Therefore, the first phase uses the backward approach. Therefore, the first phase uses the second approach. Each Event emitted by an Action records: * the ID of its direct causal parent Event; * the name of the Action that emitted it. * Following these references backward is sufficient to reconstruct the complete lineage tree for the available Event records. ## API `Event` already records an `id`. This design only adds two fields to `Event`: | **Field** | **Type** | **Meaning** | | --- | --- | --- | | `upstream_event_id` | UUID | ID of the Event consumed by the Action that emitted the current Event | | `upstream_action_name` | String | Name of the Action that emitted the current Event | These two fields are maintained automatically by the framework. Users do not need to set them when creating an Event, and they are not part of the business `attributes`. The Event Log will continue to serialize the complete Event, so the fields will appear directly in the `event` object without adding a new log record model. The Event Log continues to serialize the complete Event object. No new top-level Event Log record model is introduced in this phase. An Event returned by the Event Log could therefore look like: ```json { "timestamp": "2026-07-13T10:00:00.000Z", "eventType": "tool_request", "event": { "eventType": "tool_request", "id": "f9cbb921-664a-4acd-b1a0-050fa8857657", "upstreamEventId": "b438dd06-b619-4763-bfd7-8eb7388f01ac", "upstreamActionName": "chat_model_action", "attributes": {} } } ``` ## Design ### Lineage Design `ActionTask` already stores both the Event that triggered the current Action and the Action being run: ```text actionTask.event = the Event that triggered the current Action actionTask.action = the Action currently being run ``` Therefore, when processing an Event output by `ActionTask`, the framework can directly assign: ```text event.upstream_event_id = actionTask.event.id event.upstream_action_name = actionTask.action.name ``` Both values are available from the existing `ActionTask`, so no separate context object is needed. ### Why a Run ID Is Not Required in the First Phase Each non-root Event directly references its parent Event by ID. Each connected component in the resulting parent-child graph therefore forms one independently reconstructable lineage tree. For a complete log, following parent references is sufficient to associate every descendant Event with its root. This approach has limitations: * If the log only contains part of a run, the missing records cannot be recovered. * Without a run ID, locating every record for one run may require scanning or indexing Event IDs. * A run ID will still be valuable for the complete execution record model proposed in Discussion #900. These limitations are acceptable for the first-phase goal of reconstructing direct Event–Action causality. ### Trace Tree Reconstruction A local reader or script can rebuild and display the Event lineage tree from a saved JSONL Event Log. The reconstruction process is: 1. Read `EventLogRecord` entries from the JSONL file; 2. Build a node index by `event_id`; 3. Treat records with an empty `upstream_event_id` as root nodes; 4. Connect parent and child nodes by `upstream_event_id`; 5. Use `upstream_action_name` to create or reuse Action nodes, then attach the current Event under the matching Action node. The Trace Tree is derived on the reading side. It does not add any new fields to the Event Log. For example, the script renders an `InputEvent -> MiddleEvent -> OutputEvent` lineage as: ```text Trace Tree 1: _input_event (dad5ed00-80e2-4746-8bdb-f126bad504b5) [Action: action1] MiddleEvent (39361629-4f1d-4b62-b734-a48b181cb6e0) [Action: action2] _output_event (f452ce08-c672-4c9c-841f-d81d15a900c5) ``` The visualization provides two basic output formats: a text tree and Trace Tree JSON. The vertical order in the UI is only for easier reading. Action nodes are virtual nodes derived on the reading side; they do not represent separate Action log records. **Thanks again to** @addu390, @zxs1633079383, for the initial exploration of this topic and for giving me the opportunity to continue the design. With #900 proposing a more complete execution-based model for Event relationships, lifecycle information, and failure details, I believe the lineage layer here can remain intentionally minimal and focus only on the causal link needed to reconstruct the Event–Action–Event path.I would really appreciate your feedback on whether this simplified design and the proposed boundary between the two phases make sense. GitHub link: https://github.com/apache/flink-agents/discussions/710#discussioncomment-17671531 ---- This is an automatically sent email for [email protected]. To unsubscribe, please send an email to: [email protected]
