[nu-demo] hero_agent should support tool_choice="required" for grounded modes in llm_client.rs #150
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
Even with the right system prompt (see #149) and the right tool in the
always_includelist, well-behaved LLMs still don't always call the tool — "use this tool first" reads as a hint when it's phrased as a guideline, and models weigh it against their own confidence.For the Hero OS demo, when a user asks about Hero-stack topics, we want deterministic grounding: the model MUST call
search_hero_docs, period. The OpenAI tool-use API (and its OpenAI-compatible cousins) support this directly viatool_choice:"none""auto"(default)"required"{"type":"function","function":{"name":"search_hero_docs"}}Claude's tool-use extension (via Anthropic's OpenAI-compat endpoint — which is what aibroker is doing) honors all four.
Current state
hero_agent/crates/hero_agent/src/llm_client.rs::try_completion(line ~343) builds the request body as:There is no
tool_choiceever emitted — the field is entirely missing.Same gap in
try_streamaround line 505.Proposed change
tool_choice: Option<ToolChoice>field toLlmOptions:try_completion+try_stream, after settingtools, emit:In the agent's routing layer (
agent.rsorroutes.rs), heuristically settool_choice = Requiredon first turn of any conversation whose message contains hero-stack keywords (same list as prompt.rs MANDATORY block —hero_*,hero os, etc.).Expose a
force_tools: boolparam inagent.chatparams so callers (test harnesses, evaluation scripts) can override.Rollback / safety
tool_choice = "required"forces a tool call but the model still picks which tool — if the model picks the wrong one (e.g.list_servicesinstead ofsearch_hero_docs), we get junk grounding. UseSpecific("search_hero_docs")for the strongest guarantee.tool_choice = "auto"so the model can compose the final answer without being forced back into another tool call.Verification
Before (confirmed 2026-04-24): plain "What is Hero OS?" with
model=claude-haiku-4.5returns generic OS description.After: same query must return content citing
hero_os_guideand describing Hero OS as a "sovereign digital workspace" matching the docs_hero corpus.Related
Signed-off-by: mik-tf
Fixed in hero_agent commit
54ba5d5ondevelopment. Implements all 4 steps from the issue body.Step 1+2 —
LlmOptions.tool_choice+ body emission (llm_client.rs):In
build_request_body, after settingtools:The emit is gated inside the existing
if let Some(tools) = tools && !tools.is_empty()block —tool_choicewithouttoolsis meaningless and would 400 the provider, so the field is omitted.Step 3 — agent.rs heuristic for forced grounding:
New helper
message_contains_hero_keyword(messages)scans the LATEST user message only, case-insensitive, against the same hero_* trigger set used in prompt.rs (home#149). Earlier turns are explicitly ignored — otherwise we'd pin grounding on every iteration after the first match.In
agent_loop(), on iteration 0 only:Matches the issue body's "Rollback / safety": iterations >= 1 carry
tool_choice = Noneso the model can compose the final answer from tool_results without being forced into another tool call.Step 4 —
force_toolsagent.chat param: deferred. Thetool_choicefield onLlmOptionsis already public and can be threaded throughagent.chatif a test harness needs to force a specific tool — adding the explicit user-facing param can land in a follow-up if/when an integration test demands it.Tests added (9 new):
llm_client::tests::tool_choice(4):body_omits_tool_choice_when_none— preserves provider default ("auto")body_emits_required_when_requiredbody_emits_specific_when_specificbody_omits_tool_choice_without_tools_even_if_set— guard against 400agent::tests(5):Verification:
cargo fmt,cargo check -p hero_agentclean, all 98 hero_agent lib tests pass (89 pre-existing + 9 new).Pairing: This is the belt to home#149's prompt-directive suspenders. The prompt directive guides the model;
tool_choice = Specific(name)is a hard constraint at the API level. Together they ensure deterministic grounding for hero-stack questions without breaking the agent's freedom on non-hero queries.Meta-tracker: home#193.
Signed-off-by: mik-tf