Skip to content

feat(discover): pluggable ToolRanker with BM25 fallback and compare mode - #185

Merged
senamakel merged 10 commits into
mainfrom
jev-tool-search
Sep 22, 2026
Merged

senamakel merged 10 commits into
mainfrom
jev-tool-search

Conversation

@senamakel

@senamakel senamakel commented Sep 22, 2026

Copy link
Copy Markdown
Member

Summary

Makes tool::discover — the intrinsic tool_search / tool_call bridge over ToolExposure::Deferred registrations — rank with a pluggable tinytools::ToolRanker, so a host can plug in a decision model (Jev via tinytools-jev) or an embedding index without touching the loop.

  • ToolDiscoveryPolicy gains ranker: Option<Arc<dyn ToolRanker>> and rank_mode: DiscoveryRankMode { Ranker, Bm25, Compare }. Compare serves the host ranker and carries the BM25 ranking alongside so the two can be judged on live traffic without changing what the model sees.
  • DeferredCatalog::rank (async) serves the host ranker and falls back to BM25 on error or an empty answer — a search never fails, since a failed search would leave every deferred tool unreachable for the turn. The reason lands in RankedSearch::fallback. Every hit is resolved through catalog.get, so a key a ranker invented never reaches the model.
  • answer_tool_search is async and returns a SearchAnswer; AgentEvent::ToolSearched gains ranker, top_confidence, fallback, shadow_matched, latency_ms (all serde(default), so persisted journals still decode).
  • The catalogue carries Tool::family (ToolRegistry::deferred_schemas_with_families) as ranking text.
  • Bm25Index / tokenize moved to tinytools::rank and are re-exported from their old path.

Depends on tinyhumansai/tinytools#17 (the vendor/tinytools gitlink here points at that branch). Consumed by the openhuman PR that opts the orchestrator into discovery.

Test plan

  • cargo test --workspace — 2,655 passed (new: scripted-ranker served with confidence, failure → BM25 with reason, empty answer → BM25, compare mode shadow, Bm25 mode ignores the ranker, policy PartialEq/Debug by ranker kind)
  • tests/tool_deferral.rs still asserts the tools array is byte-identical across a run
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • docs/modules/harness/tool-discovery.md + module README updated

Co-authored-by: Medulla medulla@tinyhumans.ai

Summary by CodeRabbit

  • New Features

    • Tool discovery now supports configurable ranking modes: host ranker, BM25, or comparison.
    • Search results automatically fall back to BM25 when the primary ranker is unavailable, fails, or returns no matches.
    • Tool search events now include ranker details, confidence, fallback reasons, comparison results, and latency.
    • Deferred tools retain family information, improving visibility into each discovered tool’s origin.
  • Documentation

    • Updated tool-discovery documentation to explain ranking behavior, searchable fields, fallback handling, and event details.

senamakel and others added 6 commits September 22, 2026 05:20
The tinytools submodule is updated to a newer commit, incorporating upstream fixes and improvements.

Auto-committed-on: macbook
The discovery module previously relied on a default feature flag that was not enabled in all build configurations, causing tools to be missing from the registry. This change makes the module unconditionally compiled, ensuring discovery works regardless of feature selection.

Auto-committed-on: macbook
The file previously lacked a trailing newline, which could cause issues with some tooling and version control systems. This change adds the newline to ensure the file ends properly.

Auto-committed-on: macbook
The discovery bridge now returns ranking metadata alongside search results, including which ranker was used, confidence scores, fallback reasons, shadow rankings, and latency. This information is surfaced in the `ToolSearched` event so callers can observe and audit how deferred tool discovery ranked results. The schema lookup also now pairs each deferred tool with its family name, and the BM25 index types are re-exported from the `rank` module to keep the public API aligned with the underlying crate.

Auto-committed-on: macbook
The change makes `answer_tool_search` async and updates its return type to include ranking information, allowing tests to verify ranker behavior and fallback logic. It also adds tests for host ranker integration, including failure fallback to BM25 and compare mode behavior.

Auto-committed-on: macbook
ToolDiscoveryPolicy gains a host ranker (any tinytools::ToolRanker) and a
DiscoveryRankMode; DeferredCatalog::rank serves it, falls back to BM25 on
failure or an empty answer, and in Compare mode reports the BM25 ranking
alongside. answer_tool_search is async and returns a SearchAnswer;
ToolSearched carries the ranker, top confidence, fallback reason, shadow
ranking and latency. The BM25 index moved to tinytools::rank and is
re-exported from its old path; the catalogue carries Tool::family.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 0566db81-2d12-4990-b78c-fc5b9075355c

📥 Commits

Reviewing files that changed from the base of the PR and between 492bac9 and e5c2411.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • crates/tinyagents-graph/Cargo.toml
  • crates/tinyagents-harness/Cargo.toml
  • crates/tinyagents-harness/src/agent_loop/tools.rs
  • crates/tinyagents-harness/src/events/types.rs
  • crates/tinyagents-harness/src/tool/discover/README.md
  • crates/tinyagents-harness/src/tool/discover/bridge.rs
  • crates/tinyagents-harness/src/tool/discover/index.rs
  • crates/tinyagents-harness/src/tool/discover/mod.rs
  • crates/tinyagents-harness/src/tool/discover/test.rs
  • crates/tinyagents-harness/src/tool/discover/types.rs
  • crates/tinyagents-harness/src/tool/mod.rs
  • crates/tinyagents-integration-tests/Cargo.toml
  • crates/tinyagents-orchestration/Cargo.toml
  • crates/tinyagents-registry/Cargo.toml
  • crates/tinyagents-runtime/Cargo.toml
  • docs/modules/harness/tool-discovery.md
  • vendor/tinytools
 ___________________________________
< Colonel Panic is my arch-nemesis. >
 -----------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 22, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-22T02:44:29.429511Z e5c2411 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

senamakel and others added 2 commits September 22, 2026 05:08
The tinytools subproject reference has been updated to a new commit, which includes a dirty state indicating local modifications. This change aligns the vendor dependency with the latest upstream changes.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Updated the tinytools subproject to the latest commit, incorporating upstream fixes and improvements.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@tinysweeper

tinysweeper Bot commented Sep 22, 2026

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 6 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Changes requested
Priority: critical
Reviewed head: e5c2411b78c5
Updated: 1790045521 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 7 Active findings 9
Tests 2 Noted findings 0
Documentation 2 Resolved findings 76
Configuration 5 Pending checks/questions 0

Completeness: Complete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

  • critical · critique · Update every ToolSearch event constructor — Adding fields to a struct-like enum variant changes the required fields for every `AgentEvent::ToolSearch { ... }` literal. Existing constructors that only provide `query` and `mat (crates/tinyagents\-harness/src/events/types\.rs:145)
  • medium · critique · Exercise answer_tool_search with an unknown ranker hit — This only proves that the fixture catalog lacks `invented`; it never installs a ranker or calls `answer_tool_search`. An implementation that emits unknown ranker keys in the model- (crates/tinyagents\-harness/src/tool/discover/test\.rs:396)
  • medium · critique · Exercise rankers that exceed the requested limit — The scripted ranker truncates its own response with `.take(limit)`, so the tests cannot detect whether `answer_tool_search` caps a host ranker's overlong result. A ranker is allowe (crates/tinyagents\-harness/src/tool/discover/test\.rs:271)
  • medium · critique · Document only fields that BM25 actually indexes — The built-in BM25 path does not index `Tool::family`: `DeferredTool::from_schema` builds its searchable text from the name, split name, description, and property names, while `fami (docs/modules/harness/tool\-discovery\.md:38)
  • medium · critique · Enforce the requested limit on ranker results — The host-ranker path passes `limit` to the ranker but then converts every returned hit into `RankedSearch::names` without truncating the result. A ranker that returns more than the (docs/modules/harness/tool\-discovery\.md:40)
  • medium · security · Exercise answer_tool_search with unknown ranker hits — This test only proves that the fixture catalog does not contain `invented`; it never installs a ranker or invokes `answer_tool_search`. An implementation that accidentally includes (crates/tinyagents\-harness/src/tool/discover/test\.rs:396)
  • medium · description · Cap ranker results to the requested limit — The `rank` method passes the full vector of hits from the host ranker into `names` without truncating to the requested `limit`. If the ranker returns more results than `limit`, the (\(pull request description\))
  • medium · description · Exercise answer_tool_search when testing unknown ranker hits — This test only checks that the fixture catalog does not contain `invented`. It never installs a ranker or calls `answer_tool_search`, so an implementation that includes unknown ran (\(pull request description\))

Previously reported and still active

  • Exercise answer\_tool\_search when testing unknown ranker hits

Resolved this pass

  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Exercise answer_tool_search when testing unknown ranker hits
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update callers for the asynchronous search function
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Exercise answer_tool_search when testing unknown ranker hits
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Exercise answer_tool_search when testing unknown ranker hits
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Exercise answer_tool_search when testing unknown ranker hits
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Exercise answer_tool_search when testing unknown ranker hits
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Exercise answer_tool_search when testing unknown ranker hits
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Enforce the requested limit on ranker results
  • Cap ranker results to the requested limit
  • Update callers for the asynchronous search function
  • Update callers for the asynchronous SearchAnswer API
  • Update every ToolSearch constructor for the new fields
  • Update callers for the asynchronous search function

Before merge

  • Address carried finding Exercise answer\_tool\_search when testing unknown ranker hits.
  • Address Update every ToolSearch event constructor (crates/tinyagents\-harness/src/events/types\.rs).

How this fits together

flowchart LR
  n0["answer_tool_search<br/>changed"]:::changed
  n1["tool_call_schema<br/>changed"]:::changed
  n2["bridge_schemas_are_byte_stable_across_builds<br/>changed<br/>3 findings"]:::flagged
  n3["DeferredCatalog<br/>changed"]:::changed
  n4["catalog"]:::impacted
  n5["bridge_schemas"]:::impacted
  n6["tool_search_schema"]:::impacted
  n7["...ol_search_clamps_limit_and_handles_misses"]:::impacted
  n8["...search_does_not_panic_on_a_zero_max_limit"]:::impacted
  n9["...rch_schema_normalizes_inconsistent_limits"]:::impacted
  n0 -->|uses| n3
  n2 -->|calls| n4
  n2 -->|tests| n4
  n2 -->|calls| n5
  n2 -->|tests| n5
  n4 -->|uses| n3
  n5 -->|calls| n1
  n5 -->|uses| n3
  n5 -->|calls| n6
  n6 -->|uses| n3
  n7 -->|calls| n0
  n7 -->|tests| n0
  n7 -->|calls| n4
  n7 -->|tests| n4
  n8 -->|calls| n0
  n8 -->|tests| n0
  n8 -->|calls| n4
  n8 -->|tests| n4
  n9 -->|calls| n4
  n9 -->|tests| n4
  n9 -->|calls| n5
  n9 -->|tests| n5
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Failure
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 10 files; 5 findings. (1 earlier finding(s) still open) (1 observation(s) grouped into shared inline comments) _The code index is behind this pull request (indexed at `f4d1cf23b6c7`), so retrieved context may be out of date._ _3 memory call(s) failed (model: cortex: v1/answer: timed out after 20s), so this review saw part of what the engine holds._
  • Evidence: crates/tinyagents\-harness/src/events/types\.rs — Update every ToolSearch event constructor
  • Evidence: crates/tinyagents\-harness/src/tool/discover/test\.rs — Exercise answer_tool_search with an unknown ranker hit
  • Evidence: crates/tinyagents\-harness/src/tool/discover/test\.rs — Exercise rankers that exceed the requested limit
  • Evidence: docs/modules/harness/tool\-discovery\.md — Document only fields that BM25 actually indexes
  • Evidence: docs/modules/harness/tool\-discovery\.md — Enforce the requested limit on ranker results

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 9 files; 1 finding. 1 file was not security-reviewed: docs/modules/harness/tool-discovery.md (prose or tabular data). (1 earlier finding(s) still open) _The code index is behind this pull request (indexed at `f4d1cf23b6c7`), so retrieved context may be out of date._ _3 memory call(s) failed (model: cortex: v1/answer: timed out after 20s), so this review saw part of what the engine holds._
  • Evidence: crates/tinyagents\-harness/src/tool/discover/test\.rs — Exercise answer_tool_search with unknown ranker hits

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The diff I was handed is a fragment of the surrounding context — it shows a DeferredCatalog::rank method that shadows BM25 with a host ranker and falls back on error, a new tools() accessor, and a doc-comment change; it contains no tests at all, so there is nothing to evaluate under this task's rubric. Any code shown is part of the surrounding change, not a test under review, and the instruction to report the absence of a failing test would be speculation about a PR I cannot see in full. (7 earlier finding(s) still open) _The code index is behind this pull request (indexed at `f4d1cf23b6c7`), so retrieved context may be out of date._ _3 memory call(s) failed (model: cortex: v1/answer: timed out after 20s), so this review saw part of what the engine holds._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change adds pluggable ToolRanker support with BM25 fallback and compare mode, and updates callers and events accordingly. The critical compile-break findings from earlier cycles are fixed, but two medium-severity findings remain unresolved: the unknown-ranker-key filtering is not exercised through the full `answer_tool_search` path, and the catalog does not cap the host ranker's output to the requested limit. (2 earlier finding(s) still open) _The code index is behind this pull request (indexed at `f4d1cf23b6c7`), so retrieved context may be out of date._ _3 memory call(s) failed (model: cortex: v1/answer: timed out after 20s), so this review saw part of what the engine holds._
  • Evidence: \(pull request description\) — Cap ranker results to the requested limit
  • Evidence: \(pull request description\) — Exercise answer_tool_search when testing unknown ranker hits

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: No end-to-end harness in this repository: no e2e test files and no e2e workflow.
Evidence and run details
  • Models: ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash, deepseek-v4-flash
  • Spend: $0.041979
  • Tokens: 777890 input · 32825 output · 106974 cached · 1214 embedding
Head State Pass summary
48f9189979c8 changes requested 6 active finding(s), 0 resolved finding(s) (at 1790043360)
e5c2411b78c5 changes requested 8 active finding(s), 76 resolved finding(s) (at 1790045521)

tinysweeper 0.1.0

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae48b2c34f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +347 to +348
)
.await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound hosted ranking by the remaining run deadline

When an optional hosted ranker performs network I/O and stalls, this await is outside with_call_budget; the only deadline check happened before admission began. Consequently, a run with a configured wall-clock limit can remain stuck in tool_search indefinitely instead of returning TinyAgentsError::Timeout, so the ranking future should be raced against the run's remaining budget like model, tool, authorization, and screening calls.

Useful? React with 👍 / 👎.

Comment on lines +176 to +179
Self {
schema,
family,
searchable,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add family names to the BM25 search text

When a tool is discoverable primarily by its family/server name, the default and fallback BM25 paths cannot find it: family is stored separately here but never appended to searchable, and the catalogue's Bm25Index is built only from searchable. Host rankers receive the separate family field, but searches without a host ranker—or searches falling back after a ranker failure—silently lose the newly documented family matching.

Useful? React with 👍 / 👎.

Comment on lines +320 to +324
Ok(hits) if !hits.is_empty() => RankedSearch {
top_confidence: hits.first().and_then(|hit| hit.confidence),
names: hits.into_iter().map(|hit| hit.key).collect(),
ranker: ranker.kind(),
fallback: None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back after discarding unknown ranker hits

When a hosted ranker returns only stale or invented keys, this branch treats the response as successful because the raw hit list is nonempty; answer_tool_search subsequently drops every key through catalog.get and reports zero matches rather than using BM25. Since the code explicitly accepts that rankers may name unknown tools, validate hits against the catalogue before deciding that the hosted result is nonempty, otherwise deferred tools can become unreachable despite a valid BM25 match.

Useful? React with 👍 / 👎.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 2 lane(s) blocking, worst finding is critical.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0415 · 824,691 in / 28,848 out · 101,174 cached (12%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash · 1,156 embedded
critique:    $0.0205 · 382,749 in / 15,449 out · 23,403 cached (6%)   · gpt-5.6-luna, deepseek/deepseek-v4-flash
security:    $0.0188 · 371,504 in / 10,558 out · 19,232 cached (5%)   · gpt-5.6-luna
tests:       $0.0006 · 30,473 in  / 154 out    · 26,507 cached (87%)  · deepseek/deepseek-v4-flash
description: $0.0004 · 21,262 in  / 96 out     · 17,295 cached (81%)  · deepseek/deepseek-v4-flash

/// Ranks as `policy` says — the host ranker when one is active, BM25
/// otherwise or on failure — and returns the full schema of every hit so
/// the model can call it.
pub async fn answer_tool_search(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority critical critique confident

Update callers for the asynchronous SearchAnswer API

This changes answer_tool_search from a synchronous function returning (ToolResult, usize) to an async function returning SearchAnswer. The existing caller in agent_loop/tools.rs is not part of this diff, so it still calls and destructures the old API; it cannot compile because the result is now a future with different fields. Update that caller to await the function and use answer.result, answer.matched, and answer.ranking, or preserve the old API through a synchronous wrapper.


Additional security observation

priority critical confident

Update callers for the asynchronous search function

[RULE] build-break

answer_tool_search was previously synchronous, and this diff changes it to return a future. The complete diff contains no corresponding caller updates, so existing calls from the agent-loop tools path will fail to compile until they await this function and handle the new SearchAnswer return value.

[RULE] compile-break ·

query: String,
/// Number of deferred tools returned.
matched: usize,
/// Which ranker's answer was served: `"bm25"`, or the host ranker's

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority critical critique confident

Update every ToolSearch constructor for the new fields

These fields are required when constructing the AgentEvent::ToolSearch struct variant in Rust. #[serde(default)] only affects deserialization; it does not supply values to existing AgentEvent::ToolSearch { query, matched } expressions, so the event emission paths that construct this variant will fail to compile until they provide ranker, top_confidence, fallback, shadow_matched, and latency_ms (or the variant is given a compatible construction API).

[RULE] breaking-enum-constructor ·

}

#[test]
fn a_ranker_hit_naming_an_unknown_tool_is_dropped_from_the_answer() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Exercise answer_tool_search when testing unknown ranker hits

This test never installs a ranker or calls answer_tool_search; it only verifies that the fixture catalog does not contain invented. An implementation that includes unknown ranker keys in the returned answer would still pass, so the stated filtering guarantee is untested. Invoke answer_tool_search with a ranker returning invented and assert that the result contains no such match.

[RULE] insufficient-test-coverage ·

let shadow_names = (policy.rank_mode == DiscoveryRankMode::Compare)
.then(|| self.search_names(query, limit));
match hosted {
Ok(hits) if !hits.is_empty() => RankedSearch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Enforce the requested limit on ranker results

DeferredCatalog::rank promises to return at most limit hits, but this branch collects every item returned by the host ranker. A ranker that ignores or exceeds the requested limit can therefore cause tool_search to return more tools than ToolDiscoveryPolicy::max_limit allows, defeating the policy's prompt-size and exposure bound. Truncate the accepted hits to limit before building names (and use the same bounded set when deriving confidence).

[RULE] bounded-output ·

match hosted {
Ok(hits) if !hits.is_empty() => RankedSearch {
top_confidence: hits.first().and_then(|hit| hit.confidence),
names: hits.into_iter().map(|hit| hit.key).collect(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Cap ranker results to the requested limit

The policy's limit is intended to bound how many deferred tools a model can retrieve, but this trusts the host ToolRanker to honor it. A ranker can return more hits than requested, causing the bridge to emit an oversized or duplicated discovery response and bypassing the configured prompt/output bound. Truncate and, ideally, validate ranker results in this layer before constructing RankedSearch.

[RULE] bounded-output ·

@tinysweeper tinysweeper Bot added the priority: p0 Drop what you are doing. Data loss, a live break, or an exploitable hole. label Sep 22, 2026
senamakel and others added 2 commits September 22, 2026 08:08
…enDecide

Resolves the ToolSearched conflict by keeping both sides: the query is
recorded only under RunPolicy::capture.tool_io (main) and the event carries
the ranker, confidence, fallback, shadow ranking and latency (this branch).
Points vendor/tinytools at tinyhumansai/tinytools#19 (main + the family
strategy) and bumps every tinytools pin to 0.4.1.

Co-authored-by: Medulla <medulla@tinyhumans.ai>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e5c2411b78

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.map(|r| r.ranker.to_string())
.unwrap_or_default(),
top_confidence: ranking.as_ref().and_then(|r| r.top_confidence),
fallback: ranking.as_ref().and_then(|r| r.fallback.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact ranker errors when tool I/O capture is disabled

When capture.tool_io is false (the default), a hosted ranker can still leak the search query or upstream response data through its RankError reason: describe_failure copies that arbitrary string into ranking.fallback, and this line emits it without applying the capture gate used for query. Rankers that include request content or response bodies in backend errors therefore expose potentially sensitive tenant data to event sinks despite payload capture being disabled; redact the failure detail or gate it on capture.tool_io.

Useful? React with 👍 / 👎.

Comment thread Cargo.lock
"thiserror",
"tinyinference-core",
"tinytools-agent",
"tinytools-agent 0.3.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep tinyinference on the vendored tinytools version

After bumping the vendored dependency to 0.4.1, tinyinference-llm still requires 0.3.0, so the workspace patch no longer satisfies it: the updated lockfile now resolves tinytools-agent 0.3.0 and tinytools 0.3.0 from the Git URL in addition to the vendored 0.4.1 copies. Consequently a fresh offline build must fetch the old Git revision and the graph contains two protocol implementations, contradicting the one-copy assumption documented in the root Cargo.toml; update the tinyinference dependency/submodule in lockstep or otherwise make its requirement accept the vendored version.

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit 4aeae2b into main Sep 22, 2026
1 of 4 checks passed

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is critical.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0420 · 777,890 in / 32,825 out · 106,974 cached (14%) · ladder/vectors, gpt-5.6-luna, deepseek/deepseek-v4-flash, deepseek-v4-flash · 1,214 embedded
critique:    $0.0212 · 385,511 in / 14,612 out · 51,134 cached (13%)  · gpt-5.6-luna, deepseek/deepseek-v4-flash
security:    $0.0182 · 315,998 in / 10,573 out · 12,505 cached (4%)   · gpt-5.6-luna
tests:       $0.0007 · 32,469 in  / 206 out    · 26,523 cached (82%)  · deepseek/deepseek-v4-flash
description: $0.0006 · 23,132 in  / 4,578 out  · 0 cached (0%)        · deepseek-v4-flash

/// Which ranker's answer was served: `"bm25"`, or the host ranker's
/// [`tinytools::ToolRanker::kind`]. Empty when the query was rejected
/// before ranking.
#[serde(default)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority critical critique confident

Update every ToolSearch event constructor

Adding fields to a struct-like enum variant changes the required fields for every AgentEvent::ToolSearch { ... } literal. Existing constructors that only provide query and matched will fail to compile; #[serde(default)] only affects serialization and deserialization, not Rust construction. Update all constructors to populate these fields (or add a constructor that supplies defaults).

[RULE] compile-break ·

.all(|c| c.family.as_deref() == Some("fam"))
);
match &self.answer {
Ok(keys) => Ok(keys

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Exercise rankers that exceed the requested limit

The scripted ranker truncates its own response with .take(limit), so the tests cannot detect whether answer_tool_search caps a host ranker's overlong result. A ranker is allowed to return more hits than requested; add a fixture that ignores limit and assert that the served answer and matched count remain within the requested bound.

[RULE] insufficient-test-coverage ·

and top-level property names. Returns up to `limit` matches (default 5, max
20) as `{name, description, parameters}` with the **full** schema, or a
"no match" note. Its description embeds a manifest of every deferred tool:
- `tool_search { query, limit }` — ranks the catalogue's name, split

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Document only fields that BM25 actually indexes

The built-in BM25 path does not index Tool::family: DeferredTool::from_schema builds its searchable text from the name, split name, description, and property names, while family is stored separately on the rank candidate. As a result, a default BM25 search for a family-only term cannot match as this documentation promises. Either include the family in the indexed searchable text or remove it from the documented BM25 fields.

[RULE] documentation-contract ·

"no match" note. Its description embeds a manifest of every deferred tool:
- `tool_search { query, limit }` — ranks the catalogue's name, split
identifier, description, top-level property names and `Tool::family`.
Returns up to `limit` matches (default 5, max 20) as `{name, description,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Enforce the requested limit on ranker results

The host-ranker path passes limit to the ranker but then converts every returned hit into RankedSearch::names without truncating the result. A ranker that returns more than the requested limit therefore causes tool_search to expose more than limit matches, contradicting this documented contract. Enforce the bound after receiving ranker hits as well as when invoking the ranker.

[RULE] bounded-output ·

assert_eq!(ranker.calls(), 0);
}

#[test]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security confident

Exercise answer_tool_search with unknown ranker hits

This test only proves that the fixture catalog does not contain invented; it never installs a ranker or invokes answer_tool_search. An implementation that accidentally includes unknown ranker keys in the returned answer would still pass. Use a scripted ranker returning invented, call answer_tool_search, and assert that the result contains no unknown match.


Additional critique observation

priority medium confident

Exercise answer_tool_search with an unknown ranker hit

[RULE] insufficient-test-coverage

This only proves that the fixture catalog lacks invented; it never installs a ranker or calls answer_tool_search. An implementation that emits unknown ranker keys in the model-facing result would still pass. Make this an async test with a ranker returning invented, call answer_tool_search, and assert that the answer contains no unknown-tool match.

[RULE] insufficient-test-coverage ·

@senamakel
senamakel deleted the jev-tool-search branch September 23, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p0 Drop what you are doing. Data loss, a live break, or an exploitable hole.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant