Skip to content

rollup decomposition affordance: describe surfaces composed edges that neighbors can't directly traverse #118

@HumanBean17

Description

@HumanBean17

What happened

After PR #106 (rollup keys for type Symbols) and PR #110 (override-axis rollup) landed, describe on a class returns composed dot-keys in edge_summary:

"edge_summary": {
  "DECLARES": {"in": 0, "out": 4},
  "DECLARES.DECLARES_CLIENT": {"in": 0, "out": 3}
}

The schema description on NodeRecord.edge_summary (mcp_v2.py:127–138) correctly warns: "do not pass them to neighbors(edge_types=…)". Decision #11 in PR #89's propose locked dot-keys as read-only signals, enforced by Pydantic on the neighbors input (_NEIGHBOR_EDGE_TYPES_ADAPTER).

But a real agent run produced this exchange:

  1. Agent calls describe(class_id) → sees DECLARES.DECLARES_CLIENT: 3.
  2. Agent calls neighbors(class_id, direction="out", edge_types=["DECLARES", "DECLARES_CLIENT"]) — note: not the dot-key, just the legitimate atomic edge types.
  3. neighbors returns 4 DECLARES edges (the methods) and 0 DECLARES_CLIENT edges, because DECLARES_CLIENT originates from method Symbols, not from the class.
  4. Agent is misled: rollup promised 3 Clients reachable, single-hop traversal returned 0, no error.

This is working as designed. neighbors runs one hop; the rollup is a 2-hop summary projected onto the class for class-grain convenience. The correct walk requires two calls:

describe(class_id)                                              # sees rollup
neighbors(class_id, "out", ["DECLARES"])                        # → 4 method ids
neighbors([method_ids], "out", ["DECLARES_CLIENT"])             # → 3 client ids

The agent has to infer this decomposition from the dot syntax and the schema description prose. Nothing in the tool output mechanically teaches it.

Why this matters

Sibling failure mode to issue #117 (silent-drop filter fields). Different surface, same principle:

A tool surfaces a signal the agent cannot directly consume, with no machine-readable affordance for translating signal → action.

In #117 the agent submits a filter that's silently ignored. Here the agent reads a rollup count, asks the obvious follow-up traversal, gets silent-zero. Both train the agent to distrust the surface.

The cost is one round of agent confusion per workflow that uses rollups — bounded, but consistent, and impossible to debug from the tool surface alone without reading the propose doc.

Three frame options

(A) Stand on the current decision

Rollup is read-only signal. Agent learns the 2-hop decomposition from the dot syntax and schema description.

Pros

Cons

  • Failure mode persists. Every agent's first rollup workflow hits the silent-zero, learns from it, and self-corrects. The N+1th agent pays the same tax.
  • The schema description prose is doing all the teaching work. LLMs often skip prose under context pressure. Inference-required > mechanically-derivable.
  • Agent's only mechanical signal that the dot-key is composed is the literal dot character — which is fragile pattern-matching.

(B) Surface decomposition in describe

Add a rollup_paths (or similar) field next to edge_summary that machine-readably tells the agent how to traverse each composed key. Example:

"edge_summary": {
  "DECLARES.DECLARES_CLIENT": {"in": 0, "out": 3}
},
"rollup_paths": {
  "DECLARES.DECLARES_CLIENT": {
    "hops": [
      {"edge": "DECLARES",       "direction": "out", "target_kind": "symbol"},
      {"edge": "DECLARES_CLIENT", "direction": "out", "target_kind": "client"}
    ]
  }
}

Pros

Cons

  • New field on DescribeOutput. Backwards-compatible (additive), but expands the contract.
  • Slight redundancy — the dot-key syntax already encodes the path; rollup_paths makes it explicit. Worth the redundancy if it prevents the silent-zero, debatable otherwise.
  • Need to decide whether the agent is expected to consume rollup_paths programmatically or just read it as documentation. Affects schema design.

(C) Make neighbors accept dot-notation as traversal shortcut

Flip decision #11 in PR #89: neighbors(edge_types=["DECLARES.DECLARES_CLIENT"]) runs the 2-hop internally and returns the final-hop targets (or both-hop targets, design choice).

Pros

  • Strongest agent convenience. Symmetric: same key used in describe is consumable by neighbors. No translation step.
  • Eliminates the silent-zero entirely — the dot-key becomes a first-class traversal primitive.
  • One call instead of two for the common case.

Cons

Where (Computer) lands

(B) is the natural pairing with issue #117's principle: make the tool's output self-describing instead of relying on prose-side documentation. (A) is cheap but the failure mode is durable. (C) is powerful but reopens a frame question PR #89 just locked.

But this is a real design choice and worth the same thinking-time as #117. Bundling temptation: both #117 and this one are about "the tool surface promises something the agent can't directly act on." Resist the bundle for now — different surfaces, different decisions, easier to reason about separately. Cross-link only.

Open sub-questions if (B)

  1. Field name: rollup_paths, composed_edges, traversal_hints? Naming carries the intent.
  2. Schema: structured per-key path metadata as above, or simpler {key: human_readable_walk_string}?
  3. Coverage: include rollup_paths for all composed keys, or only the ones with non-zero counts? Latter is symmetric with edge_summary's zero-omission convention.
  4. Consumer model: is the agent expected to mechanically derive neighbors calls from rollup_paths, or is it documentation-grade? Affects whether the schema is fully typed or string-tolerant.

Open sub-questions if (C)

  1. Edge attributes on composed results: which hop's metadata wins?
  2. Cardinality: one Edge per path, or one Edge per distinct target?
  3. Filter applicability: do filters apply to intermediate hops or only the final target?
  4. Mixing: can edge_types contain both atomic and composed keys in the same call?
  5. Depth-2 only, or do we open the door to depth-N composed keys later?
  6. PR propose: synthetic (via members) rollup keys in describe.edge_summary (clients + routes) #89 decisions Implement PR-A2: SpEL/constant-ref routes and route MCP tools #7 and plan: Tier 1B (B2b + B6) plan + per-PR Cursor prompts #11 — explicitly relitigated, or quietly reframed?

Related

Next step

Wait for thinking-time on issue #117 frame question first — the answer there constrains the answer here. If #117 lands strict-frame (or hybrid), (B) is the natural pairing. If #117 lands permissive, (C) becomes more defensible.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions