Skip to content

One search command, three response shapes - #678

Merged
mkreyman merged 1 commit into
masterfrom
feat/one-search-command-format-param
Aug 12, 2026
Merged

One search command, three response shapes#678
mkreyman merged 1 commit into
masterfrom
feat/one-search-command-format-param

Conversation

@mkreyman

Copy link
Copy Markdown
Owner

Finishing what #670 started, and what I named in its own PR body as remaining and then
didn't come back to.

Not an ergonomics change

progressive_index/3 and get_context/3 are not different searches. The first calls
search_keyword/3 and then caps-and-stubs; the second is the combined search returning full
bodies. Exposed as separate tools, they asked an agent to decide, per query, which door to
knock on.

That decision is the problem. An agent's choice of entrypoint is unobservable, so it
confounds every measurement of the ranking behind it
— you cannot separate an algorithm's
effect from a choice you cannot see. A parameter is a variable we control; a tool choice is a
confounder we do not.

So the single command is the precondition for judging any ranking change from observed
behaviour, not tidying-up to do before it.

knowledge_search?format=

format shape
results (default) ranked results + snippets — unchanged. The only shape supporting keyset pagination, which is why the dispatch sits on the :none cursor branch.
stubs capped stubs with hub enrichment — survey a topic without pulling bodies into context
bodies full bodies + linked references — one deep read

Nothing is retired. The sibling tools and endpoints stay and now share this path.

Two deliberate refusals

  • An unknown format is a 400, not a silent fallback to ranked results — falling back
    answers a different question than the one asked.
  • A shaped format with no query is a 400. These are relevance shapes; there is no stub
    rendering of an enumeration page, and quietly returning ranked results would do the same
    thing.

Telemetry

Every shape is attributed to knowledge_search, with the shape in mode_used. Recording a
shaped call under the sibling tool's name would split one command's traffic across three
names and undo the comparability the parameter exists to create.

Verification

mix precommit green (7,514 tests). Mutation-verified in four directions — each turns tests
red:

mutation failures
unknown format falls through to ranked 1
shaped call recorded under the sibling tool name 2
shaped telemetry removed entirely 3
query-less shaped request allowed 1

format is documented in the endpoint's operation/2 spec per the doc-hygiene rule.

Next in the agreed order

MCP knowledge_search gains format (server-side is done and usable via REST today), then
growing the golden set from logged queries via cross-search consensus, then offline replay.
Variant assignment last, if ever — at ~12 agent searches/day an online experiment would
produce confident noise.

Reviewed inline (this session's system prompt forbids dispatching review agents; per CLAUDE.md
the gate is satisfied by the best available reviewer with that stated).

Finishing what #670 started and I then left named-but-undone in its own PR body.

progressive_index/3 and get_context/3 are not different searches. The first
calls search_keyword/3 and then caps-and-stubs; the second is the combined
search returning full bodies. Exposed as separate TOOLS they asked an agent to
decide, per query, which door to knock on.

That decision is the problem, and it is not an ergonomic one. An agent's choice
of entrypoint is unobservable, so it confounds every measurement of the ranking
behind it - you cannot separate an algorithm's effect from a choice you cannot
see. A parameter is a variable we control; a tool choice is a confounder we do
not. The single command is therefore the precondition for judging any ranking
change from observed behaviour, not a tidying-up before it.

knowledge_search now takes format:

  results (default) - ranked results plus snippets, unchanged. The only shape
                      that supports keyset pagination, which is why the dispatch
                      sits on the :none cursor branch.
  stubs             - capped stubs with hub enrichment, for surveying a broad
                      topic without pulling bodies into context.
  bodies            - full bodies plus linked references, for one deep read.

Nothing is retired. The sibling tools and endpoints stay and now share this
path.

Two deliberate refusals. An unknown format is a 400 rather than a silent
fallback to the ranked shape, because falling back answers a different question
than the one asked. A shaped format with no query is also a 400: these are
relevance shapes, there is no stub rendering of an enumeration page, and quietly
returning ranked results would do the same thing.

Telemetry attributes every shape to knowledge_search, with the shape in
mode_used. Recording a shaped call under the sibling tool's name would split one
command's traffic across three names and undo the comparability the parameter
exists to create.

Mutation-verified in four directions: an unknown format falling through, the
shaped call recorded under the sibling name, the shaped telemetry removed
entirely, and a query-less shaped request allowed - each turns tests red.
format is documented in the endpoint's operation/2 spec.
@mkreyman
mkreyman enabled auto-merge (squash) August 12, 2026 18:13
@mkreyman
mkreyman merged commit a5fa51f into master Aug 12, 2026
15 checks passed
@mkreyman
mkreyman deleted the feat/one-search-command-format-param branch August 12, 2026 18:17
mkreyman added a commit that referenced this pull request Aug 12, 2026
Server-side landed in #678; this is the half agents can actually reach.

knowledge_search takes format: results (default), stubs, or bodies. These are
response SHAPES of one search, not different searches - the server dispatches
stubs to the same progressive_index/3 and bodies to the same get_context/3 that
knowledge_progressive_index and knowledge_context call.

Nothing is retired. Both sibling tools stay registered and behave exactly as
before. The schema says so in words and a test asserts it, so a later reader
does not mistake this for a deprecation.

The description states both refusals rather than leaving an agent to discover
them: stubs and bodies REQUIRE a query, and an unknown value is a 400 rather
than a quiet downgrade to results. An agent expecting a silent downgrade would
read the error as an empty corpus.

ON THE TEST, which is deliberately not written like its neighbours. The suites
here re-implement handler bodies by design, since index.js has top-level await
and exports nothing. That strategy structurally cannot catch the one failure
that matters for a pass-through parameter: the schema advertising a field the
request builder drops. A re-implementation forwards it correctly and passes
while the real code does not. So this asserts against the index.js source, and
mutation-verified it catches exactly that - deleting the params.set line with
the schema left intact turns it red, as does removing a shape from the enum.

That is the third instance today of the same shape - a value computed or
declared in one layer and silently lost in the next, with a test green because
it checked a thing existed rather than what it contained (#669 read the wrong
keys off a result map, #672 never rendered keys onto one). Worth naming as a
pattern rather than three coincidences.

Version 2.74.0 with a CHANGELOG entry. Publishing to npm is a separate step and
is NOT done here: until it is published and sessions restart, agents keep the
2.73.0 schema - the same staleness that cost the first day of search telemetry.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant