You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: propose/active/TRACE-TOOL-V2-PROPOSE.md
+72-16Lines changed: 72 additions & 16 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,9 +74,10 @@ TreeNode:
74
74
edge_from_parent: EdgeFromParent | None # null for seed nodes
75
75
children: list[TreeNode] # nested children (empty list for leaves)
76
76
collapsed: bool = False # true if this node was produced by collapsing
77
-
collapsed_intermediates: list[str] # node IDs of collapsed intermediates
77
+
collapsed_intermediates: list[str] # node IDs of collapsed intermediates (retained in nodes dict)
78
78
79
79
EdgeFromParent:
80
+
direction: Literal["in", "out"] # traversal direction that produced this edge
80
81
edge_type: str
81
82
hop: int
82
83
cross_service_boundary: bool = False
@@ -95,6 +96,8 @@ RankedLeaf:
95
96
- The `tree` format is what agents actually consume. The flat format was a v1 compromise to avoid token-cost concerns on large traces. Re-scoping as a replacement eliminates the redundancy.
96
97
- `ranked_leaves`is a lightweight summary (node_id + score) that preserves the "which paths matter most" signal without duplicating the full edge lists.
97
98
99
+
**Collapsed intermediates accessibility.** Unlike v1 (which removed collapsed nodes from the `nodes` dict), v2 **retains** collapsed intermediate nodes in `nodes` so agents can inspect them via `describe` or `neighbors`. The `collapsed_intermediates` list on the `TreeNode` references valid keys in `nodes`. The intermediate is simply removed from the tree structure (its children are not nested under it) but remains accessible as a standalone entry.
100
+
98
101
**Token budget.** A nested tree with `TreeNode` objects reuses node IDs from the shared `nodes` dict -- node payloads are not duplicated at each nesting level. Empirical: a depth-3 trace on the bank-chat fixture produces ~30 TreeNodes vs ~24 TraceEdges (the tree format is slightly larger due to nesting structure but eliminates the `paths` list).
Replace the static `_ROLE_PRIORITY` dict with a **source-relative** priority that shifts based on the source node's role:
124
+
Replace the static `_ROLE_PRIORITY` dict with a **source-relative** priority that shifts based on the source node's role. The priority table is built from `VALID_ROLES` (via `java_ontology.py`) to avoid duplicating role string literals across modules:
122
125
123
126
```python
124
-
# From SERVICE: REPOSITORY is most relevant downstream
125
-
# From CONTROLLER: SERVICE is most relevant downstream
126
-
# From REPOSITORY: no strong signal, fall back to static order
127
-
127
+
# Coupled to VALID_ROLES in java_ontology.py — when roles are added/removed there,
128
+
# this table must be updated. The keys are source-node roles; the inner dicts map
129
+
# target-node roles to priority values (higher = more relevant from that source).
All role strings in this table must be members of `VALID_ROLES`. A startup assertion validates this so the table cannot silently drift from the ontology.
139
+
136
140
When the source node's role is not in `_SOURCE_RELATIVE_PRIORITY`, the existing static `_ROLE_PRIORITY` is used as fallback. This is a zero-config change -- the behavior improves automatically without new parameters.
137
141
138
142
**Post-pruning budget** (`min_result_nodes`): When aggressive pruning (`prune_roles`, `fan_out_cap`) would reduce the result below `min_result_nodes`, the engine relaxes the fan-out cap incrementally until the target is met. This prevents the common failure mode where a trace returns 2 nodes because 48 were pruned.
@@ -166,7 +170,7 @@ if direction == "both":
166
170
167
171
**Merge semantics:**
168
172
- The `nodes` dict is a union of both traversals (deduplicated by node ID).
169
-
- The `tree` has the seed at the root with two subtrees: `in_children`and `out_children`. In practice, the tree format already supports this -- the seed node has children from both directions.
173
+
- The `tree` has the seed at the root with a single `children` list containing nodes from both directions. Directionality is preserved via `edge_from_parent.direction` on each child -- agents can distinguish "who calls me" (`direction="in"`) from "what do I call" (`direction="out"`).
170
174
- `stats`aggregates both traversals (nodes discovered = in + out, etc.).
171
175
- `ranked_leaves`merges and re-ranks leaves from both directions.
172
176
- If the same node is discovered in both directions, it appears once in `nodes` and once in the tree (in whichever direction it was first discovered; the other direction records the edge but does not duplicate the node).
@@ -181,7 +185,8 @@ if direction == "both":
181
185
182
186
1. **`mcp_trace.py`**: Replace `TraceEdge`, `TracePath` with `TreeNode`, `EdgeFromParent`, `RankedLeaf`. Refactor `_collapse_trivial_chains` to use configurable heuristic. Refactor `_fan_out_sort_key` to use source-relative priority. Add bidirectional BFS merge logic. Add `min_result_nodes` retry logic.
183
187
2. **`server.py`**: Update `trace` tool registration to reflect new parameters (`collapse_roles`, `collapse_min_chain_length`, `min_result_nodes`, `direction="both"`).
184
-
3. **Breaking API change**: `TraceOutput.edges`and `TraceOutput.paths` are removed. `TraceOutput.tree` and `TraceOutput.ranked_leaves` are added.
188
+
3. **`mcp_hints.py`**: Update `_trace_structured_hints` to consume `tree` and `ranked_leaves` instead of `edges` and `paths`. The hint at line 526 reads `payload.get("edges")` -- this must be rewritten to walk the tree structure. Cross-service boundary detection (line 571) must traverse tree nodes checking `edge_from_parent.cross_service_boundary` instead of iterating a flat `edges` list. Pruned/collapsed drill-down hint (line 557) must check `TreeNode.collapsed` on tree nodes instead of `e.get("collapsed")` on edges. The `_high_fanout_trace_hint` function (used by `neighbors` and `describe` paths) is unaffected -- it does not read trace output fields.
189
+
4. **Breaking API change**: `TraceOutput.edges`and `TraceOutput.paths` are removed. `TraceOutput.tree` and `TraceOutput.ranked_leaves` are added.
185
190
4. **No graph schema changes**: No new node kinds, edge types, or edge attributes.
186
191
5. **No re-index required**: The tool reads the existing graph.
187
192
6. **No ontology bump**: No changes to `java_ontology.py`.
@@ -192,6 +197,7 @@ if direction == "both":
192
197
- `search`, `find`, `describe`, `resolve` are unchanged.
193
198
- `kuzu_queries.py`is not modified.
194
199
- `mcp_v2.py`types (`NodeFilter`, `EdgeFilter`, `NodeRef`) are unchanged.
200
+
- `mcp_hints.py`hint functions for non-trace tools (`_high_fanout_trace_hint` used by `neighbors`/`describe`) are unchanged -- only `_trace_structured_hints` is updated.
195
201
- Indexer, graph builder, CLI are unchanged.
196
202
197
203
## Schema / Ontology / Re-index impact
@@ -204,14 +210,63 @@ if direction == "both":
204
210
205
211
### Updated unit tests
206
212
207
-
Tests from v1 that reference `edges` and `paths` fields are updated to reference `tree` and `ranked_leaves`. New tests:
213
+
All 40 existing v1 tests in `tests/test_mcp_trace.py` must be updated. Every test that asserts on `result.edges` or `result.paths` changes to assert on `result.tree` and `result.ranked_leaves`. The table below lists each existing test and the required change:
| `test_trace_bank_chat_cross_service_http_flow` | `result.edges` → tree walk; full flow assertion on nested structure |
257
+
258
+
New tests (v2 features):
208
259
209
260
| Test name | Asserts |
210
261
|-----------|---------|
211
262
| `test_trace_tree_root_is_seed` | Tree root node matches seed ID |
263
+
| `test_trace_tree_seed_no_edge_from_parent` | Seed nodes have `edge_from_parent=None` (replaces `test_trace_parent_edge_id_seed_null`) |
264
+
| `test_trace_tree_edge_from_parent_chain` | Non-root nodes have `edge_from_parent` with valid edge_type, hop, and direction (replaces `test_trace_parent_edge_id_chain`) |
265
+
| `test_trace_tree_edge_from_parent_direction` | `edge_from_parent.direction` is set ("in" or "out") for all non-root nodes |
212
266
| `test_trace_tree_children_nested` | Children are nested TreeNodes, not flat |
213
-
| `test_trace_tree_edge_from_parent` | Non-root nodes have `edge_from_parent` with correct edge_type and hop |
214
267
| `test_trace_tree_collapsed_node` | Collapsed intermediates carry `collapsed=True` and `collapsed_intermediates` |
268
+
| `test_trace_tree_collapse_intermediates_in_nodes` | Collapsed intermediate node IDs exist in `nodes` dict (accessible for describe/neighbors) |
269
+
| `test_trace_tree_collapse_children_reparented` | After collapsing A→B→C, C appears as child of A in tree (replaces `test_trace_collapse_parent_edge_id_consistency`) |
215
270
| `test_trace_ranked_leaves_capped` | `ranked_leaves` does not exceed `max_paths` |
216
271
| `test_trace_ranked_leaves_scores` | Leaves are sorted by descending score |
217
272
| `test_trace_collapse_roles_custom` | `collapse_roles=["OTHER","SERVICE"]` collapses SERVICE intermediates |
@@ -233,15 +288,15 @@ Tests from v1 that reference `edges` and `paths` fields are updated to reference
233
288
- `ruff check`clean on all changed files.
234
289
- Existing tool tests (`test_mcp_v2.py`, `test_server.py`) must pass unchanged.
235
290
236
-
## Open Questions ([TBD])
291
+
## Resolved Decisions
237
292
238
-
1. **`tree` token cost on deep traces** -- The nested format adds structural overhead compared to the flat edge list. On a depth-5 trace with 50+ nodes, the tree JSON may be 15-20% larger. Should we add an optional `flat: bool = False` parameter that falls back to the old `edges` format for agents that prefer it? -- Recommended: No. The tree format is strictly better for LLM consumption. If token cost is a concern, reduce `max_depth` or `max_paths`.
293
+
1. **`tree` token cost on deep traces** -- No fallback to flat format. The tree format is strictly better for LLM consumption. If token cost is a concern, reduce `max_depth` or `max_paths`.
239
294
240
-
2. **`min_result_nodes` retry budget** -- Currently one retry with doubled `fan_out_cap`. Should this be a configurable number of retries? -- Recommended: No. One retry is sufficient. If the doubled cap still produces too few nodes, the graph genuinely has few reachable nodes and more retries won't help.
295
+
2. **`min_result_nodes` retry budget** -- One retry with doubled `fan_out_cap`. No configurable retry count. If the doubled cap still produces too few nodes, the graph genuinely has few reachable nodes and more retries won't help.
241
296
242
-
3. **Bidirectional `edge_types` per direction** -- Should the `direction="both"` mode allow different `edge_types` for in vs out? (e.g., `edge_types_in=["CALLS", "OVERRIDES"], edge_types_out=["CALLS", "HTTP_CALLS"]`). -- Recommended: No for v2. Use the same `edge_types` for both directions. If agents need different edge types per direction, they issue two unidirectional calls (same as today).
297
+
3. **Bidirectional `edge_types` per direction** -- Same `edge_types` for both directions. If agents need different edge types per direction, they issue two unidirectional calls (same as today).
243
298
244
-
4. **`collapse_roles` interaction with `prune_roles`** -- `collapse_roles` determines which nodes are trivial (collapsible). `prune_roles` determines which nodes stop the frontier. Should a role in `collapse_roles` also be eligible for `prune_roles`? -- Recommended: Yes, but independently configured. A role can be in one, both, or neither list. Default: `collapse_roles=["OTHER"]`, `prune_roles` unset.
299
+
4. **`collapse_roles` interaction with `prune_roles`** -- Independently configured. A role can be in one, both, or neither list. Default: `collapse_roles=["OTHER"]`, `prune_roles` unset.
0 commit comments