Provider-neutral ToolSearch: source-backed API seams for safe staged tool activation #524
Replies: 3 comments
|
I kept coming back to this audit while trying a smaller compatibility path on the current public surface. That work is now published as dsh-progressive-tools ( The plugin leaves ToolRuntime visibility and execution authority live, then owns only the model-facing projection. That narrower design avoids changing restrictions inside a search call and keeps a stable top-level tool prefix across ordinary catalog registration changes. It also reconstructs Native disclosures from durable There is still a sharp edge that lines up with the seams requested here: DSH exposes Install, for anyone who wants to compare the compatibility approach with the staged-restriction prototype: dsh plugin --profile web add dsh-progressive-toolsThe README documents the stable surfaces, cache behavior, and the pi-ai boundary in detail. I would be interested in comparing traces from the two approaches, especially around hot catalog changes and resumed sessions. |
|
One concrete data point from the MCP side: a useful user-facing reduction path already exists today without requiring core retrieval policy in Harness. I maintain MCP Lens, a community plugin now published as On the fixed synthetic 1,000-tool fixture we used for release verification, the model-facing schema JSON changed from Why this seems relevant to the seams in this thread: it suggests the problem can be split cleanly into two layers.
So from a maintainer perspective, the highest-leverage core API still looks like catalog/lifecycle consistency, not "which retriever should core bless". Reference: |
|
你这份审计做得很细,而且把问题问在了正确的位置——"能不能有一个事务性的时点,让任意 retriever 安全地改变下一次请求可见的工具集"。 我认为这个时点今天已经存在,只是它不叫 tools 相关的名字。 这条我们实测过,可能会直接关掉你六个缺口里的几个。
|
Uh oh!
There was an error while loading. Please reload this page.
Harness's scoped
ctx.tools.restrict()already gives progressive disclosurethe right visibility primitive: presentation, lookup, and execution all derive
from the same scoped view. Building a safe, out-of-tree ToolSearch plugin
against the current public API, however, runs into six concrete lifecycle and
observability gaps audited below. This is not a proposal to “add embeddings to
the core” — retrieval policy can remain completely out of tree. The narrower
question is whether Harness can expose a revisioned catalog and provide a
transactional point at which any retriever can safely change the next request's
visible tool set.
I audited the source at a fixed commit, then built and tested an independent
community compatibility adapter to check whether those six gaps are real and
whether four small seams would close them.
The practical motivation came from an external system already operating at
roughly 1,700 tools with both name lookup and purpose-oriented keyword/vector
lookup. That is context for this proposal, not a claim that Harness itself
already has 1,700 tools or a semantic tool retriever. The harder problem is how
discovery, reversible activation, model-visible schemas, execution lookup, hot
updates, and replay remain consistent as overlapping plugins accumulate.
This analysis is pinned to commit
47f943859bef60e4160492346772ded9b24f765a.The prototype, benchmark, and API sketch are available at
https://github.com/Ashley-QwQ/dsh-hybrid-toolsearch. It is an independent
project, not an official or endorsed DeepSeek implementation.
At this audited revision, the contribution guide says external pull requests
are not currently accepted; it points proposals to Discussions and encourages
independent plugin repositories with the
dsh-plugintopic:CONTRIBUTING.md#L9-L16.The observed problem
At the audited commit, MCP discovery drains
tools/listand registers eachadvertised definition in
ctx.tools; a latertools/list_changednotificationrepeats the synchronization. There is no query-relevance or top-k step in that
path:
packages/mcp/mcp-client/src/tools.ts#L128-L173packages/mcp/mcp-client/src/connection.ts#L254-L269The existing Skill mechanism is useful progressive disclosure for bodies, but
its model-facing catalog still contains every invocable name and description;
loading is by exact name. Provider
rankresolves duplicate providers ratherthan ranking a natural-language query:
packages/skill/tool-skill/src/index.ts#L50-L57packages/skill/tool-skill/src/index.ts#L127-L155packages/skill/tool-skill/src/index.ts#L220-L228Harness already has the right visibility primitive. Inherited definitions are
filtered by scoped restrictions, exact-agent tools are added after inherited
filtering, and the derived view supplies schemas, lookup, and execution. The
extension cookbook explicitly identifies ToolSearch/progressive disclosure as
a scoped
ctx.tools.restrict()replacement use case:packages/core/tools/src/index.ts#L1152-L1192packages/core/tools/src/index.ts#L1204-L1236docs/cookbook/extension-cookbook.md#L95-L113The missing pieces are lifecycle and observability seams, rather than a
particular retrieval algorithm.
Why an out-of-tree plugin cannot safely finish the job today
No unfiltered scoped catalog after activation. Public
schemas(agent)returns the post-restriction view, while the completeinherited name sets are private. An indexer cannot reliably discover a new
hidden tool or rebuild from authoritative unfiltered state.
Catalog and presentation changes are indistinguishable. Registration,
unregistration, and restriction changes emit the same synchronous,
payload-free
tools/change. A plugin cannot observe name, scope, action,revision, or origin, so its own restriction can feed back into reindexing:
index.ts#L199-L207,index.ts#L811-L814,index.ts#L1110-L1115.Restriction replacement is not atomic.
restrict()returns a disposer,and multiple restrictions intersect. Installing the new restriction before
disposing the old one is fail-closed but exposes a temporary intersection
and emits two events; disposing first creates an unacceptable temporarily
unrestricted interval:
index.ts#L713-L744,index.ts#L1064-L1097.There is no awaited safe commit boundary. Tool schemas are derived
during system-prompt assembly, before
agent/pre-step. A restriction changethere can make execution disagree with the already-rendered prompt.
step/endobservation is synchronous and is not an awaited transaction:dispatch.ts#L167-L175,agent.ts#L225-L242,system-prompt/index.ts#L467-L535,session/index.ts#L381-L398.Changing visibility inside the search call is unsafe. Calls from one
model response are planned before dispatch, but later siblings can re-read
live visibility. Code Mode captures generated bindings and also rechecks
live lookup on nested dispatch. A search body that narrows immediately can
invalidate a sibling call or a still-running program:
tool-calls.ts#L70-L92,tool-calls.ts#L198-L229,code-mode.ts#L594-L607.Request audit data does not restore a plugin restriction. Headers record
the old rendered view, but resume assembles a new current composition;
ephemeral restriction ownership is not replayed:
session/types.ts#L196-L209,agent.ts#L458-L470.Requested provider-neutral seams
Would the maintainers consider four small, retrieval-agnostic APIs?
Read-only unfiltered scoped catalog
The snapshot would contain stable runtime/provider identity, cloned schemas,
schema hashes, and a monotonic catalog revision. Reading it would not change
execution visibility.
Structured catalog changes, separate from presentation changes
Restriction changes would keep the backward-compatible
tools/changeeventbut would not masquerade as catalog mutations.
Atomic restriction replacement
Readers would observe either the old or new immutable view, never a temporary
intersection or open set. Validation/notification failure would retain the
old handle, and one structured visibility notification would be emitted.
Awaited before-assembly boundary
It must run after all sibling calls or the complete Code program settle and
before native schemas, SDK text, request headers, or provider requests are
derived. Rejection should preserve the old prompt/view pair and prevent
dispatch.
An optional explicit bootstrap/control-tool marker could replace reliance on
exact-scope registration while still applying normal guards and approval.
These seams do not put BM25, embeddings, a vector database, top-k policy, or a
vendor in Harness core. They only let an independently chosen retrieval policy
remain consistent with prompt assembly, lookup, execution, hot updates, and
session replay.
To check whether these seams are actually sufficient and correctly shaped, I
built and tested an independent compatibility adapter against the current
public API where that is possible, leaving the rest as explicit, disclosed
workarounds.
Prototype design
The prototype deliberately separates retrieval policy from Harness lifecycle:
Each Discovery Card has a stable ID, canonical name/aliases, provider,
capabilities, effects, input/output and negative constraints, risk/cost/latency
hints, version, schema hash, and catalog revision. Each selection event records
the query, candidates and all lane/constraint/fusion scores, selected names,
revision, and commit status.
The exact-scope search tool remains visible independently of the inherited
allow-list. Search only stages. At the next safe boundary, replacement is
install-new-before-dispose-old; failure retains the old restriction. Resume or
fork restores the last compatible committed event. A revision mismatch records
an explicit invalidation and remains control-only until re-search. Empty,
low-confidence, failed-dense, and no-result paths never fall back to the full
catalog.
The frozen reference adapter also installs the inherited empty/replayed
restriction before registering the session's control tool, avoiding a bootstrap
full-open interval. On hot update it durably appends invalidation plus a
control-only fallback before publishing the new in-memory revision; a durable
write failure leaves the old revision and restriction live and surfaces a
retryable failure. The event store is still non-transactional across those two
records, as disclosed below.
For
native, the next request receives selected function schemas. Forcode,the generated SDK and all nested dispatches retain one view for the entire
program. For
both, native and SDK presentations share one revision and theirduplicated presentation cost is measured separately. Code Mode is not assumed
to be inherently cheaper.
The compatibility adapter and reference runtime test those invariants, but the
audited public API cannot attach the required awaited boundary. This is a
reference host contract, not a claim of safe drop-in integration today. Full
design and tests:
Reference retrieval implementation — validation results
The seams above are retrieval-policy-agnostic; they do not require BM25,
embeddings, or any specific algorithm. To exercise them concretely, the same
adapter drives one reference retrieval implementation (exact/alias + BM25 + a
replaceable dense provider, fused with RRF). That implementation's own quality
claim is disclosed in full below because it is part of the same evidence trail
as the source audit — not because it is what I am asking maintainers to adopt.
Environment: Node v24.19.0, Windows x64, Intel Core i5-10600T (12 logical
cores), seed
1592598566. Both quality comparisons use the fixed synthetic1,000-tool catalog. The gate was unchanged: hybrid Recall@5 must be at least
90%, Recall@10 at least 97%, and Recall@5 must exceed the better keyword/dense
single lane by at least 5.00 percentage points.
Historical holdout-v1 — FAIL
The original 48-query sealed result is retained unchanged:
Hybrid gained two hits over the better single lane: 4.17 points, not the
required 5.00. The sole hybrid miss placed the gold tool at rank 6. This result
was not used for later tuning and was never rerun with the new ranking code.
Preregistered holdout-v2 one-shot — FAIL
Option A used 144 independently authored, provider-blind, text-only semantic
queries. They were sealed before development changes. Final implementation
commit
62d8f2b302fd27bea3517ebc1b46899dc35569f4was bound by manifest SHA-2563da516b09e55be0aec8e68e1ef971689f8c5427dda64419e670df6e6f5243fd4.Run
78276923-eed1-4471-b8a8-6936765e82c7durably wrote its started ledgerbefore loading sealed text and completed exactly once:
Hybrid recovered only one additional top-5 query, an improvement of
0.69 points. It therefore failed the 5-point relative gate. Its 92.36%
Recall@10 also failed the 97% absolute gate; the 92.36% absolute Recall@5 gate
passed. Separate exact suites achieved 24/24 canonical names and 24/24 stable
IDs at Rank@1.
The like-for-like development raw-text result had shown a 5.36-point gain, so
the v2 result records a material development/holdout transfer failure. I did
not change weights, redefine a baseline, lower a threshold, or rerun the
one-shot after disclosure. Both failed validations remain first-class evidence.
The corpus is synthetic, v2 is process-isolated model-authored rather than
production-log or external-human data, and the local embedding is a
deterministic test provider. The roughly six-second 10k cold build/query,
MRR@10 truncation, presentation-character proxy, and frozen implementation
edge cases are disclosed in the linked limitations rather than hidden behind
the passing latency/schema gates.
Evidence:
Reproduction from a checkout uses Node
^22.19.0 || >=24.0.0and pnpm11.19.0. It verifies the completed ledger/artifact hashes and recomputes thedisclosed deterministic projection; it is not a second independent holdout:
Security and limitations
restrict()is a scoped composition/visibility mechanism, not an authorizationboundary. ToolSearch can reduce what the model sees and what ordinary scoped
lookup resolves, but it must not replace Harness guards, approvals, credentials,
or sandboxing. Risk metadata in the prototype affects ranking only.
The prototype is synthetic, single-process, and uses a deterministic local
embedding provider. It failed the relative quality gate twice and has not
demonstrated production vector-store behavior or a safe dynamic attachment to
the audited public API. The tested adapter is a reference host contract, not a
drop-in Harness plugin.
Known frozen edge cases were deliberately documented rather than repaired
after the one-shot result:
differently scaled coarse scores outside that window are mixed back into the
final ordering; the top-100 window is therefore not a hard candidate bound;
exactalso contains contained-name/token-overlap heuristics,although the separate strict identity suites passed 48/48;
final manifest, not that fingerprint alone, binds every implementation byte;
engine ranking fingerprint, and staging does not prove every selected item
came from the supplied candidate list;
run_codecollisions and every disposer-failure retry path arenot fully preflighted;
partial failure can make cold replay more restrictive than the live process;
failure are deterministic fail-closed paths but do not all return a normal
search fallback with equally detailed diagnostics.
The complete list, including benchmark measurement limits, is in
known-limitations.md.
These points are not excuses to reinterpret the failed result. They are the
frozen boundary for deciding whether another iteration is worthwhile.
Those limitations are why I am asking primarily about the four
lifecycle/catalog seams and their invariants, not proposing the ranking
implementation for core adoption.
I would especially appreciate feedback on whether the proposed boundary should
be
agent/before-assemble, a transactional replacement around system-promptassembly, or another lifecycle point that also covers the initial request,
zero-tool turns, retry, resume, fork, and complete Code programs.
All reactions