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
redesign cross-service: boundary-stop instead of seamless traversal
Cross-service traversal now stops at service boundaries instead of
automatically following into downstream services. Rationale:
- Prevents context explosion (multi-service results defeat pruning)
- Preserves agent autonomy (agent decides whether to continue)
- Avoids hop budget waste on scaffolding edges
Changes:
- BFS records cross-service edges with cross_service_boundary=True
and stops (does not add downstream Route to frontier)
- Downstream Route node included in nodes dict for agent visibility
- Scaffolding edges (DECLARES_CLIENT/PRODUCER) followed only to
reach cross-service edge, not into downstream service
- Removed: scaffolding edge hop-counting caveats, mid-traversal
advisories, EXPOSES-following into downstream handler
- Added: cross_service_boundary field on TraceEdge
- Added: "Why not seamless traversal" section explaining rationale
- Updated: algorithm pseudocode, tests, hints, decision tree,
Appendix B comparison table
- New test: test_trace_cross_service_boundary_stops
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@@ -160,7 +168,7 @@ The `trace` tool's value is not "do what the agent could do but faster" -- it is
160
168
161
169
**Trivial chain collapsing (`collapse_trivial`)**: Wrapper/delegate patterns are common in Spring microservices. `OrderServiceImpl#createOrder` calls `orderValidator#validate` calls `ValidationHelper#doValidate` calls `RulesEngine#check`. The intermediate wrapper and helper add no semantic value. Collapsing these into `OrderServiceImpl -> RulesEngine` shortens paths and keeps the agent focused on the real flow.
162
170
163
-
**Cross-service seamless traversal**: When BFS encounters a `Symbol` with outgoing `DECLARES_CLIENT` or `DECLARES_PRODUCER`, and `HTTP_CALLS`/`ASYNC_CALLS` is in the requested `edge_types`, the engine follows through: `method -> Client -> HTTP_CALLS -> Route -> EXPOSES <- handler method`. This is a 4-hop traversal across service boundaries that would require 4 separate `neighbors` calls today. The engine follows it in one step because it has the full graph in Kuzu. Cross-service edges carry `confidence`, `strategy`, and `match` attributes so the agent can assess reliability.
171
+
**Cross-service boundary detection**: When BFS encounters a `Symbol` with outgoing `DECLARES_CLIENT` or `DECLARES_PRODUCER`, and `HTTP_CALLS`/`ASYNC_CALLS` is in the requested `edge_types`, the engine follows to the Client/Producer and then to the downstream Route — but **stops at the boundary**. The cross-service edge is recorded with `cross_service_boundary: True` and full attributes (`confidence`, `strategy`, `match`). The downstream Route is included in the `nodes` dict so the agent can see what service and endpoint is being called. The agent decides whether to continue tracing in the downstream service via a separate `trace` call.
164
172
165
173
### Edge type handling
166
174
@@ -174,7 +182,7 @@ label_params = [f"l{i}" for i in range(len(flat_labels))]
174
182
label_predicate = "(" + " OR ".join(f"label(e) = ${name}" for name in label_params) + ")"
175
183
```
176
184
177
-
Cross-service traversal is implicit: when `HTTP_CALLS` or `ASYNC_CALLS` is in `edge_types`, the engine follows the full chain through Client/Producer nodes and Route nodes, collecting the intermediate edges as part of the same BFS.
185
+
Cross-service traversal is a boundary signal: when `HTTP_CALLS` or `ASYNC_CALLS` is in `edge_types`, the engine follows scaffolding edges to reach the cross-service edge, records it with `cross_service_boundary: True`, and stops. The downstream service is not traversed — the agent issues a separate `trace` call if needed.
178
186
179
187
### Composability
180
188
@@ -236,7 +244,7 @@ PR-TRACE-3 must add the following `trace`-aware hints:
236
244
237
245
4.**Trace budget hit hint**: When `stats.budget_hit=True`, emit: `"trace hit the node discovery budget (N nodes). Results are partial. Increase max_depth or add prune_roles and re-run."`
238
246
239
-
5.**Cross-service boundary hint**: When `trace` discovers cross-service edges, emit: `"Cross-service boundary detected. Use describe(route_id) on downstream routes for handler details."`
247
+
5.**Cross-service boundary hint**: When `trace` discovers edges with `cross_service_boundary=True`, emit: `"Cross-service boundary: Client X calls Route Y (confidence=N). Use trace(route_id, 'out', ['EXPOSES','CALLS'], max_depth=4) to continue in the downstream service, or describe(route_id) for route details."`
240
248
241
249
#### Skill decision tree update
242
250
@@ -247,7 +255,7 @@ The decision tree in `skills/explore-codebase/SKILL.md` must be updated to inclu
247
255
| "What happens when route R is called?" |`find(kind="route")` then `trace(route_id, "out", ["EXPOSES","CALLS"], max_depth=4)`|`describe` on key nodes |
248
256
| "Impact of changing method M" |`resolve` / `find` then `trace(id, "in", ["CALLS","OVERRIDES"], max_depth=3)`|`describe` on callers |
249
257
| "Trace from X to database" |`trace(id, "out", ["CALLS"], max_depth=4, prune_roles=["DTO","EXCEPTION"])`|`neighbors` for pruned detail |
250
-
| "What calls this across services?" |`trace(id, "out", ["CALLS","HTTP_CALLS","ASYNC_CALLS"], max_depth=5)`|`describe` on downstream routes|
258
+
| "What calls this across services?" |`trace(id, "out", ["CALLS","HTTP_CALLS","ASYNC_CALLS"], max_depth=5)`|`trace` on downstream route_id if needed|
251
259
252
260
The existing `neighbors` rows remain unchanged. `trace` rows are additive — they cover the cases where the current table says "loop neighbors" or "no magic tool."
253
261
@@ -260,40 +268,78 @@ The existing `neighbors` rows remain unchanged. `trace` rows are additive — th
260
268
- The engine stops early if the frontier is exhausted before `max_depth`.
261
269
-`max_nodes_discovered` defaults to **500** and is clamped to 100..2000. This is a **compute guardrail**, not an output guarantee. It counts nodes discovered *before* pruning — this is intentional because the cost is in the Cypher queries and BFS traversal, not in the output serialization. Aggressive `prune_roles` may result in fewer output nodes for the same budget. When the budget is hit, BFS stops mid-traversal and reports `stats.budget_hit = True` plus an advisory: `"trace stopped early: discovered {N} of ~{M} nodes before budget. Reduce max_depth or add prune_roles to focus."`
262
270
263
-
### Cross-service traversal
271
+
### Cross-service traversal: boundary signals, not seamless traversal
264
272
265
-
When `HTTP_CALLS` or `ASYNC_CALLS` is in `edge_types`, the BFS engine follows cross-service edges seamlessly:
273
+
When BFS encounters a cross-service edge (`HTTP_CALLS` or `ASYNC_CALLS`), the engine **stops traversal at the boundary**. It does not follow into the downstream service. Instead, it records the cross-service edge as a **boundary signal** and includes enough data for the agent to decide whether to continue tracing in the downstream service.
266
274
267
-
1. At a `Symbol` node with `DECLARES_CLIENT` -> `Client` -> `HTTP_CALLS` -> `Route`, the engine records:
268
-
-`Symbol --DECLARES_CLIENT--> Client` (hop N)
269
-
-`Client --HTTP_CALLS--> Route` (hop N+1)
270
-
- The Route's `EXPOSES` handler is discovered at hop N+2 (if `EXPOSES` is in `edge_types`)
271
-
2. Cross-service edges carry their full attribute set: `confidence`, `strategy`, `match`, `raw_uri`/`raw_topic`.
272
-
3. The `stats` object reports cross-service hops separately so the agent knows when it crossed a service boundary.
275
+
#### Why not seamless traversal
273
276
274
-
This is the highest-value feature of `trace`: a 4-hop cross-service trace that would require 8-12 `neighbors` calls today is a single `trace`call.
277
+
Automatic cross-service traversal was initially proposed as "seamless" — the engine would follow `Symbol -> Client -> HTTP_CALLS -> Route -> EXPOSES handler` in a single call. This was rejected for three reasons:
275
278
276
-
#### Cross-service edge-following rules
279
+
1.**Context explosion**. A trace starting in `order-service` that follows into `payment-service` now returns two services' worth of paths. A depth-5 trace across 3 services could return hundreds of edges, defeating the pruning that `trace` exists to provide.
277
280
278
-
The engine follows cross-service edges when `HTTP_CALLS` or `ASYNC_CALLS` is in the user's `edge_types`. Internally, it also follows **scaffolding edges** that connect the user's edge types across node kinds. These scaffolding edges are *not* required to be in `edge_types` — the engine follows them automatically:
281
+
2.**Agent autonomy**. The GPS metaphor says the tool returns structure, the agent decides. Automatically crossing service boundaries is the GPS deciding the agent also needs to see what happens in the next city. The agent asked about one service — it should choose whether to follow into another.
|`HTTP_CALLS`|`DECLARES_CLIENT` (outbound from Symbol to Client) |`client`|
283
-
|`HTTP_CALLS`|`EXPOSES` (inbound from Route to handler Symbol) |`symbol`|
284
-
|`ASYNC_CALLS`|`DECLARES_PRODUCER` (outbound from Symbol to Producer) |`producer`|
285
-
|`ASYNC_CALLS`|`EXPOSES` (inbound from Route to handler Symbol) |`symbol`|
283
+
3.**Hop budget waste**. Scaffolding edges (DECLARES_CLIENT, EXPOSES) would consume 2 of the 5 available hops just crossing the boundary. The agent gets fewer useful hops in both services.
284
+
285
+
#### Boundary signal behavior
286
+
287
+
When BFS discovers a cross-service edge during traversal:
288
+
289
+
1.**Record the edge** with `cross_service_boundary: True` and full attributes (`confidence`, `strategy`, `match`, `raw_uri`/`raw_topic`).
290
+
2.**Include the downstream node** (Route or Producer endpoint) in the `nodes` dict with its full `NodeRef` data (id, kind, fqn, microservice, etc.).
291
+
3.**Include the Client/Producer node** that owns the cross-service edge — this is the node the BFS was traversing from, so it's already in the result.
292
+
4.**Stop** — do not add the downstream Route to the frontier. Do not follow EXPOSES into the downstream handler. This branch ends at the boundary.
293
+
5.**Emit hint**: `"Cross-service boundary: Client X calls Route Y (confidence=0.85, strategy=URI_PATH_MATCH). Use trace(route_id, 'out', ['EXPOSES','CALLS'], max_depth=4) to continue tracing in the downstream service, or describe(route_id) for route details."`
294
+
295
+
The downstream Route's `NodeRef` includes its `fqn` (e.g., `payment-service:/api/payments:POST`), so the agent can see at a glance what service and endpoint is being called without issuing another tool call.
296
+
297
+
#### What the agent sees
298
+
299
+
Example: Agent calls `trace(id, "out", ["CALLS", "HTTP_CALLS"], max_depth=4)` from an `order-service` method:
300
+
301
+
```yaml
302
+
edges:
303
+
- from_id: "sym:OrderServiceImpl#createOrder"
304
+
to_id: "sym:OrderRepository#save"
305
+
edge_type: "CALLS"
306
+
hop: 1
307
+
cross_service_boundary: false
308
+
attrs: {confidence: 1.0}
309
+
310
+
- from_id: "sym:OrderServiceImpl#createOrder"
311
+
to_id: "client:PaymentClient"
312
+
edge_type: "DECLARES_CLIENT"# auto-followed to reach HTTP_CALLS
- text: "Cross-service boundary: PaymentClient calls payment-service:/api/payments:POST (confidence=0.85, strategy=URI_PATH_MATCH). Use trace(route_id, 'out', ['EXPOSES','CALLS'], max_depth=4) to continue tracing in the downstream service."
334
+
```
335
+
336
+
The agent now has a clear picture: `OrderServiceImpl` calls the repository (internal) and also calls `payment-service` via HTTP (cross-service boundary). It can choose to trace into `payment-service` or stop here.
286
337
287
-
**All scaffolding edges appear in the result's `edges` list** with their actual edge type (e.g., `DECLARES_CLIENT`, `EXPOSES`). They are not hidden from the agent. They count toward `max_nodes_discovered` and `stats.total_edges_discovered` like any other edge.
338
+
#### DECLARES_CLIENT / DECLARES_PRODUCER handling
288
339
289
-
**`hop` numbering**: Scaffolding edges consume hop slots. A `Symbol --DECLARES_CLIENT(hop 2)--> Client --HTTP_CALLS(hop 3)--> Route` sequence uses two hops, not one. This means cross-service traces reach `max_depth` faster — the agent should account for this when choosing depth. The advisory system warns when a cross-service boundary was detected but `max_depth` was exhausted before the downstream handler was reached.
340
+
To reach the cross-service edge, BFS must first traverse from a Symbol to its Client/Producer via `DECLARES_CLIENT`/`DECLARES_PRODUCER`. These are **scaffolding edges** — they're followed only when `HTTP_CALLS` or `ASYNC_CALLS` is in the user's `edge_types`. They appear in the result with their actual edge type and consume a hop, but are not required to be in the user's `edge_types`.
- Hop 1: callee Symbols —CALLS--> deeper Symbols (some have DECLARES_CLIENT)
294
-
- Hop 2: Symbol —DECLARES_CLIENT--> Client (scaffolding, auto-followed)
295
-
- Hop 3: Client —HTTP_CALLS--> Route (user-requested edge type)
296
-
- Hop 4 would be needed for Route —EXPOSES--> handler, but `max_depth=4` is exhausted. Advisory: `"cross-service boundary detected at hop 3 but max_depth=4 exhausted before downstream handler. Increase max_depth to 5 or use neighbors on the discovered Route."`
342
+
This is the only case where the engine follows edge types not in `edge_types`: the scaffolding hop from Symbol to Client/Producer is necessary to reach the cross-service edge the agent asked for. The engine never follows edges into the downstream service.
297
343
298
344
## Scope
299
345
@@ -331,7 +377,7 @@ The engine follows cross-service edges when `HTTP_CALLS` or `ASYNC_CALLS` is in
331
377
| `test_trace_prune_roles` | With `prune_roles=["DTO"]`, DTO nodes are excluded from traversal |
332
378
| `test_trace_fan_out_cap` | With `fan_out_cap=2`, a node with 8 outbound CALLS returns at most 2 edges |
333
379
| `test_trace_collapse_trivial` | Wrapper chain A->B->C where B has degree 2 is collapsed to A->C |
334
-
|`test_trace_cross_service_http`| Traces from a method through DECLARES_CLIENT -> HTTP_CALLS -> Route -> EXPOSES handler across services|
380
+
| `test_trace_cross_service_http` | Traces from a method through DECLARES_CLIENT -> HTTP_CALLS; stops at Route boundary with `cross_service_boundary=True`; Route in nodes dict but not in frontier |
335
381
| `test_trace_cross_service_async` | Same for ASYNC_CALLS through Producer |
336
382
| `test_trace_max_paths_cap` | Result paths list does not exceed `max_paths` |
337
383
| `test_trace_budget_stops_early` | BFS stops when `max_nodes_discovered` is hit; `stats.budget_hit=True`; advisory message present |
@@ -346,7 +392,8 @@ The engine follows cross-service edges when `HTTP_CALLS` or `ASYNC_CALLS` is in
346
392
| `test_trace_edge_filter_calls` | EdgeFilter with `min_confidence` filters CALLS edges during traversal |
347
393
| `test_trace_include_unresolved` | UnresolvedCallSite edges are interleaved when `include_unresolved=True, edge_types=["CALLS"], direction="out"` |
348
394
| `test_trace_paths_root_to_leaf` | Each path starts at a seed and ends at a leaf with no further outbound edges in the result |
349
-
|`test_trace_cross_service_edge_attrs`| Cross-service edges include `confidence`, `strategy`, `match` attributes |
395
+
| `test_trace_cross_service_edge_attrs` | Cross-service boundary edges include `confidence`, `strategy`, `match` attributes and `cross_service_boundary=True` |
396
+
| `test_trace_cross_service_boundary_stops` | BFS does not follow past cross-service boundary; downstream Route appears in nodes but no EXPOSES/CALLS edges from it |
350
397
351
398
### Integration validation
352
399
@@ -375,7 +422,7 @@ These questions were resolved during review (PR #234). Deferred items are tracke
375
422
376
423
6. **Legacy `find_callers`/`find_callees`** — **Coexist for v1.** These methods serve the CLI, not the MCP surface — different consumers. Once the trace engine is proven on MCP, add a `java-codebase-rag trace` CLI command reusing the engine, then deprecate legacy methods — tracked in #241.
377
424
378
-
7.**Cross-service scaffolding edges** — **Always followed when `HTTP_CALLS`/`ASYNC_CALLS`is in `edge_types`, always visible in output.** Scaffolding edges (`DECLARES_CLIENT`, `DECLARES_PRODUCER`, `EXPOSES`) do not need to be in the user's `edge_types`. They appear in the `edges` list with their actual edge type. They consume `hop` slots. Advisories fire when `max_depth` is exhausted mid-cross-service traversal.
425
+
7. **Cross-service traversal: boundary-stop, not seamless** — **BFS stops at service boundaries.** When the engine encounters `HTTP_CALLS` or `ASYNC_CALLS` edges, it records them with `cross_service_boundary: True`, includes the downstream Route/Producer node in the result, but does not add it to the frontier. The agent decides whether to trace into the downstream service via a separate `trace` call. Scaffolding edges (`DECLARES_CLIENT`, `DECLARES_PRODUCER`) are followed only to reach the cross-service edge and appear in the result. See "Cross-service traversal: boundary signals, not seamless traversal" section for rationale.
379
426
380
427
8. **`collapsed` marker on TraceEdge** — **Yes.** Collapsed edges carry `collapsed: True` and `collapsed_intermediates: [node_ids]` so agents can detect shortcuts and drill down via `neighbors`.
381
428
@@ -449,7 +496,7 @@ All PR-TRACE PRs target the `experimental` branch. They do not merge to `master`
449
496
- Implement role-based pruning (`prune_roles`).
450
497
- Implement fan-out throttling (`fan_out_cap`) with confidence-based ranking + role tiebreaker.
451
498
- Implement trivial chain collapsing (`collapse_trivial`) with `collapsed`/`collapsed_intermediates` markers on TraceEdge.
452
-
- Implement cross-service traversal (HTTP_CALLS, ASYNC_CALLS seamless hop-through) per the scaffolding edge rules.
499
+
- Implement cross-service boundary detection (HTTP_CALLS, ASYNC_CALLS boundary-stop with `cross_service_boundary` marker) per the scaffolding edge rules.
0 commit comments