Skip to content

Commit 8b6495e

Browse files
HumanBean17claude
andcommitted
add HINTS-STRUCTURED proposal for machine-parseable next-action objects
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent d775cea commit 8b6495e

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# HINTS-STRUCTURED — machine-parseable next-action objects alongside road-sign strings
2+
3+
## Status
4+
Proposal — not yet implemented.
5+
6+
**Tracks:** [#195](https://github.com/HumanBean17/java-codebase-rag/issues/195) (item 7).
7+
8+
**Depends on (landed):** v1–v4 hint catalogs (`propose/completed/HINTS-ROAD-SIGNS-PROPOSE.md`, `HINTS-V2`, `HINTS-V3`, `HINTS-V4-SUCCESS-PATH-PROPOSE.md`), `mcp_hints.py` `generate_hints`, response models in `mcp_v2.py`.
9+
10+
## Problem Statement
11+
12+
Battle testing (issue #195) shows agents copying `hints` strings literally into MCP calls. Templates like:
13+
14+
```
15+
clients via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_CLIENT'])
16+
```
17+
18+
use **Python-style list syntax** (`['…']`). When an agent copies that into a JSON `ids` parameter, it sends `"['<id>']"` — invalid JSON (single quotes), which FastMCP's `json.loads` rejects. The call fails with `Unknown id prefix for '['…]'`.
19+
20+
The root cause is that `hints: list[str]` embeds pseudo-call syntax that is:
21+
1. **Not machine-parseable** — agents must reverse-engineer tool name, positional args, and kwargs from freeform text.
22+
2. **Syntactically ambiguous** — Python lists vs JSON arrays, single vs double quotes, positional vs keyword args.
23+
3. **Unreliable to fix by coercion** — heuristic `_coerce_ids()` in `mcp_v2` handles one symptom but not the underlying gap.
24+
25+
String hints remain valuable as **human-readable** road signs (operator logs, debug traces). But LLM agents need a **structured** form they can use directly.
26+
27+
## Proposed Solution
28+
29+
Add an optional `hints_structured` field to all five MCP output models. Each element is a typed object `{tool, args}` that maps 1:1 to an MCP tool call. Generation reuses the same trigger logic as string hints; rendering switches from template strings to structured arg dicts.
30+
31+
### Shape
32+
33+
```python
34+
class StructuredHint(BaseModel):
35+
tool: Literal["search", "find", "describe", "neighbors", "resolve"]
36+
args: dict[str, Any]
37+
```
38+
39+
All five `*Output` models gain:
40+
41+
```python
42+
hints_structured: list[StructuredHint] = Field(default_factory=list)
43+
```
44+
45+
### Mapping from string templates
46+
47+
Every `TPL_*` constant that embeds a `neighbors(…)` / `search(…)` / `find(…)` / `resolve(…)` call pattern gains a **structured counterpart** — a function or constant that returns `StructuredHint` instead of a formatted string.
48+
49+
Example mapping (describe type rollup):
50+
51+
| String template | Structured equivalent |
52+
|---|---|
53+
| `clients via members: neighbors(['{id}'],'out',['DECLARES.DECLARES_CLIENT'])` | `StructuredHint(tool="neighbors", args={"ids": [id], "direction": "out", "edge_types": ["DECLARES.DECLARES_CLIENT"]})` |
54+
| `routes via members: neighbors(['{id}'],'out',['DECLARES.EXPOSES'])` | `StructuredHint(tool="neighbors", args={"ids": [id], "direction": "out", "edge_types": ["DECLARES.EXPOSES"]})` |
55+
| `handler: neighbors(['{id}'],'in',['EXPOSES'])` | `StructuredHint(tool="neighbors", args={"ids": [id], "direction": "in", "edge_types": ["EXPOSES"]})` |
56+
| `no match — try search(query='{identifier}') for ranked fuzzy lookup` | `StructuredHint(tool="search", args={"query": identifier})` |
57+
| `no matches — try resolve(identifier='{kind}', hint_kind='…')` | `StructuredHint(tool="resolve", args={"identifier": identifier, "hint_kind": kind})` |
58+
59+
**Prose-only hints** (e.g. `results look weak — narrow the query`, `many CALLS — consider filtering`) map to `StructuredHint` with only `tool` set and an `args` dict that encodes the advisory signal as a structured recommendation, **not** a direct tool call. Agents can inspect `tool` to decide if the hint is actionable.
60+
61+
| String | Structured |
62+
|---|---|
63+
| `results look weak — narrow the query or try find(role=…)` | `StructuredHint(tool="find", args={"filter": {"role": "SERVICE"}})` (suggests a concrete starting filter) |
64+
| `{n} CALLS — consider filtering by target microservice` | `StructuredHint(tool="neighbors", args={"ids": [id], "direction": "out", "edge_types": ["CALLS"], "edge_filter": {}})` (placeholder; agent fills in filter) |
65+
66+
**Open question:** whether prose-only hints should use a separate `action: "suggest"` field or embed a partial `args` — see Open Questions.
67+
68+
### Coexistence with `hints`
69+
70+
- `hints: list[str]`**unchanged**, backward compatible. Remains the human-readable field.
71+
- `hints_structured: list[StructuredHint]` — new field, machine-parseable.
72+
- Both fields are populated from the same trigger logic. Same priority, same cap (5), same dedup.
73+
- Clients that ignore `hints_structured` continue working identically.
74+
75+
### Generation refactor
76+
77+
`generate_hints` returns `(list[str], list[StructuredHint])` instead of `list[str]`. Call sites in `mcp_v2.py` destructure the tuple:
78+
79+
```python
80+
str_hints, struct_hints = generate_hints("describe", hint_payload)
81+
return DescribeOutput(success=True, record=record, hints=str_hints, hints_structured=struct_hints)
82+
```
83+
84+
Alternatively (less invasive): a parallel `generate_structured_hints` function that reads the same payload and returns `list[StructuredHint]`, called alongside `generate_hints` at each call site. This avoids changing the `generate_hints` return type but duplicates trigger logic.
85+
86+
**Recommended:** single `generate_hints` returning both. The trigger logic is already complex; maintaining two parallel generators is fragile. The return-type change is internal to `mcp_hints.py` + `mcp_v2.py`; no external API break.
87+
88+
### Prose-only hints: `actionable` flag
89+
90+
Add an optional field to `StructuredHint`:
91+
92+
```python
93+
class StructuredHint(BaseModel):
94+
tool: Literal["search", "find", "describe", "neighbors", "resolve"]
95+
args: dict[str, Any]
96+
actionable: bool = True # False = advisory, args may be partial/placeholder
97+
```
98+
99+
When `actionable=False`, the hint is a recommendation (e.g. "consider filtering by role"), not a direct call. Agents can skip or use as guidance. Direct-call hints (`neighbors(['id'],'out',['EXPOSES'])`) always set `actionable=True` with complete args.
100+
101+
This avoids over-engineering prose-only hints into fake tool calls while still giving agents structured data for every hint slot.
102+
103+
### Batch-placeholder hints (N2, N3, N4, N5, N6, N7 from v4)
104+
105+
v4 templates like `HTTP targets: neighbors(client_ids,'out',['HTTP_CALLS'])` use batch placeholders (`client_ids`, `route_ids`) that are never substituted — the agent is expected to pick ids from results. For structured hints, these map to:
106+
107+
```python
108+
StructuredHint(
109+
tool="neighbors",
110+
args={"ids": [], "direction": "out", "edge_types": ["HTTP_CALLS"]},
111+
actionable=False, # ids placeholder — agent must fill from results
112+
)
113+
```
114+
115+
Alternatively, the structured hint could carry `args.ids` populated from the payload's result ids when available. See Open Questions.
116+
117+
## Scope
118+
119+
- New `StructuredHint` model in `mcp_v2.py`
120+
- `hints_structured` field on `SearchOutput`, `FindOutput`, `DescribeOutput`, `NeighborsOutput`, `ResolveOutput`
121+
- Refactor `generate_hints` in `mcp_hints.py` to return `(list[str], list[StructuredHint])`
122+
- Update all 5 call sites in `mcp_v2.py`
123+
- Tests in `tests/test_mcp_hints.py`
124+
- `MCP_HINTS_FIELD_DESCRIPTION` update in `mcp_hints.py`
125+
- README mention under MCP tool reference `hints` paragraph
126+
127+
## Schema / Ontology / Re-index impact
128+
129+
- Ontology bump: **not required** (MCP response shape only, no graph/index changes)
130+
- Re-index required: **no**
131+
- Config/tool surface changes: new response field `hints_structured` on all five tools (additive; no removals)
132+
133+
## Tests / Validation
134+
135+
| Test name | Asserts |
136+
|---|---|
137+
| `test_structured_hint_describe_type_rollup_clients` | `hints_structured` contains `StructuredHint(tool="neighbors", args={"ids": [id], …})` matching string `TPL_DESCRIBE_TYPE_CLIENTS_VIA_MEMBERS` |
138+
| `test_structured_hint_describe_type_rollup_routes` | Same for `DECLARES.EXPOSES` |
139+
| `test_structured_hint_describe_method_overriders` | `OVERRIDDEN_BY` mapped correctly |
140+
| `test_structured_hint_find_route_handler` | F1 → `StructuredHint(tool="neighbors", args={"ids": [id], "direction": "in", "edge_types": ["EXPOSES"]})` |
141+
| `test_structured_hint_resolve_none_search` | `StructuredHint(tool="search", args={"query": identifier})` |
142+
| `test_structured_hint_resolve_none_find_route` | `StructuredHint(tool="find", args={"kind": "route", "filter": {"path_prefix": seed}})` |
143+
| `test_structured_hint_neighbors_empty_wrong_kind` | `actionable=False` on structural hint |
144+
| `test_structured_hint_prose_only_not_actionable` | weak-score / high-fanout hints have `actionable=False` |
145+
| `test_structured_hints_cap_5` | `len(hints_structured) <= 5` on payloads that generate many triggers |
146+
| `test_structured_hints_dedup` | Identical `(tool, args)` deduped like string hints |
147+
| `test_structured_hints_parity_with_string_hints` | For every output where `hints != []`, `len(hints_structured) == len(hints)` (1:1 mapping) |
148+
| `test_structured_hint_round_trip` | Building structured hint args into an actual MCP call succeeds (integration with `neighbors_v2`) |
149+
150+
Regression: all existing string-hint tests continue passing unchanged.
151+
152+
## Open Questions ([TBD])
153+
154+
1. **Should `generate_hints` return a tuple or should `generate_structured_hints` be a separate function?**
155+
— Recommended: single function returning `(list[str], list[StructuredHint])`. Keeps trigger logic unified.
156+
157+
2. **Should batch-placeholder structured hints (v4 N2–N7) carry concrete result ids in `args.ids`?**
158+
— Recommended: **yes** when available from the payload. If the payload contains `results[*].other.id`, populate `args.ids` with those ids and set `actionable=True`. If ids cannot be extracted (meta-hints), leave `args.ids` empty with `actionable=False`.
159+
160+
3. **Should prose-only hints (weak-score, high-fanout) have `actionable=False` with partial args, or should they be omitted from `hints_structured` entirely?**
161+
— Recommended: include with `actionable=False`. Agents that only process actionable hints can filter; agents that want all signals get structured data either way. Maintains 1:1 parity with string hints.
162+
163+
4. **Should `StructuredHint.args` be validated against MCP tool schemas?**
164+
— Recommended: **no** in v1. Args are advisory; strict validation couples hints to tool signature changes. Add later if traces show agents breaking on stale args.
165+
166+
5. **Should `hints_structured` be added to `MCP_HINTS_FIELD_DESCRIPTION` or get its own field description?**
167+
— Recommended: own field description on the new field, referencing `hints` for the human-readable form.
168+
169+
6. **String hint template refactor: should string templates be derived from structured hints (DRY), or kept as independent constants?**
170+
— Recommended: **keep independent** in v1. String templates have char caps and prose labels ("clients via members:") that don't map to `StructuredHint` fields. Deriving strings from structured hints risks losing the concise human-readable format. Revisit if drift becomes a problem.
171+
172+
## Out of scope
173+
174+
- Removing or deprecating `hints: list[str]` (kept indefinitely)
175+
- Changing `neighbors_v2` argument parsing or adding `_coerce_ids()` (separate fix from issue #195)
176+
- Reindexing or ontology changes
177+
- Per-row structured hints (output-level only, matching v1–v4 discipline)
178+
- `hints_version` field
179+
- Conditioning on `attrs.match` / confidence values
180+
- FastMCP `ast.literal_eval` fallback (upstream; out of scope)
181+
182+
## Sequencing / Follow-ups
183+
184+
**Single PR** recommended (response model + generation + tests):
185+
186+
1. Add `StructuredHint` model and `hints_structured` field to all outputs
187+
2. Refactor `generate_hints` return type
188+
3. Map all existing template triggers to structured equivalents
189+
4. Update call sites in `mcp_v2.py`
190+
5. Add tests
191+
6. README mention
192+
193+
After landing: close issue #195 item 7. Items 1–6 from the issue (string template fix, coercion, docs) remain independent and can land before or after this proposal.

0 commit comments

Comments
 (0)