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]

Reply via email to