The chat loop runs on rig - #548
Conversation
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: WaylandYang <wayland0916@gmail.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: WaylandYang <wayland0916@gmail.com>
|
Two more runs to separate the model from the prompt, both on this branch, same scratch base, twelve fresh data questions each (relation ×3, relation-again ×3, timeline ×3, board ×3). 1. Prompt redefined, same model (DeepSeek-V3). The reasons the model gave for
Halved, not gone. Two of the three remaining skips cite "general knowledge" while the prompt now says in those words that general knowledge is never a reason. The model reads the instruction and overrides it. 2. Same prompt, other model (Qwen2.5-72B-Instruct on the same endpoint; Qwen3-235B is disabled on this key). 12/12 called a real tool first; So: the skips are DeepSeek-V3's; the prompt's "user's data" framing made them more likely and is fixed on this branch. Every skip that remains is a stored |
Spike for #546. Draft: this is the branch the issue asked for, run against the harness, with the report below. Nothing here is meant to merge until the findings are read.
What changed
The hand-written loop in
chat.rsis gone.chat.rsnow assembles a rig agent, drives its stream, and translates rig's items into the SSE frames the frontend already consumes. Three new pieces:api/rig_model.rs:LlmClientbehind rig'sCompletionModel. rig's request (preamble, history, documents, tool definitions,tool_choice) is spelled out as the OpenAI-protocol JSONLlmClientalready speaks;LlmClient's turns and streams come back as rig's responses. The wire stays ours: read timeout, error bodies (LLM errors preserve their response bodies #538), out-of-credit versus rate-limit classification all survive, and the wholeanyhowchain travels through rig'sCompletionErrorso the loop can still classify a failure. Two decisions live here: rig's "documents" (the entities identified earlier in the conversation) become asystemmessage placed right before the current question, not a fakeusermessage (The chat loop is hand-written, and every policy lands as another branch #546's second finding: the model used to answer that block as if the user had written it); andToolChoice::Nonesends no tools field at all, which every endpoint accepts, instead of"tool_choice": "none", which not every endpoint does.api/agent.rs: the tools and the policy. The eight tools are rigDynamicTools built fromtools_schema(the list MCP also uses), still executed bytools::dispatch; a ninth,no_evidence_needed, is the terminal for questions that are not about the knowledge base. The policy is oneAgentHook:on_completion_callsetstool_choice: requireduntil some tool has run and, on the request after the budget, withdraws the tools and orders an answer;on_tool_callrunscheck_call(unchanged, its tests unchanged) and skips a call that fails it, feeding the model the same message as before;on_tool_resulthands the tool's UI step to the stream;on_model_turn_finishedcatches an endpoint that ignoresrequired(text-only first turn, no tool run) and sends it back once.utopia-llm: a typedRejectederror and atool_choicepass-through. Non-2xx responses that are neither out-of-credit nor rate-limit now carry their status code as a type (the text is unchanged). The loop degrades to one-shot RAG only when the first request with tools comes back 400/422, which is what "the endpoint rejects tool calling" looks like on the wire. Before, any first-round error did that (The chat loop is hand-written, and every policy lands as another branch #546's first finding).The #543 guard (
STALL_NUDGE,DONE, the no-evidence note) is deleted: termination is structural now. A turn cannot end before a tool has run, so "请稍等,我将调用工具" is no longer a possible final state.Unchanged: the SSE contract (
conversation,stepwith the UTF-16at,sources,delta,done,error, same order), reconnect throughlive, the four persisted fields (steps,sources,resolved,tool_exchange; the next turn replays them, now converted to rig messages), the record axis (tools untouched), the budget answer, the tool list MCP sees.The harness
The fifteen turns from #543's testing, DeepSeek-V3 through SiliconFlow, same scratch database. All fifteen finished with
done; noerror, no degradation, no stall.no_evidence_neededfind_entitiesno_evidence_neededno_evidence_neededfind_entities,entity_factsno_evidence_neededfind_entities,entity_factsno_evidence_neededno_evidence_neededno_evidence_neededsearch_chunksno_evidence_neededsearch_chunksno_evidence_neededfind_entitiesWhat fit
chat.rslost 630 lines.tool_choice: required: every first turn called a tool. The fallback for an endpoint that ignores it (the turn-finished hook) never fired; the fallback for an endpoint that rejects it (RigModelresends withouttool_choice) never fired either. Both are in place, neither has been exercised by a real endpoint yet.DONEevery time, including after promising a search. Here no turn ended on a promise, and no hidden second request was made.What did not
Structural termination guarantees a decision, not a correct one. Three of the six fresh data questions (turns 1, 6, 14) ended with
no_evidence_needed, and the stored reason is the same each time: the model assumed the knowledge base does not hold OpenAI. It hadfind_entitiesone call away and did not spend it. Turn 14 is the worst shape, a claim about the base's contents made without looking. The same question in turns 11 and 13 was searched and cited.What is different from before is that the miss is now an explicit, logged, stored decision: the call is in
tool_exchangewith the model's reason, andsourcesis empty. That makes #547's "no sources" mark exact, and it gives a number to watch per model. Under #543 the same miss was an absence.Not tried, on purpose: rewording
no_evidence_needed's description or result, or filtering the question's text. Both are the "more prompt wording" the issue ruled out. Two structural candidates, neither measured:no_evidence_neededfrom the first request of a conversation whose message names something (an entity lookup first, then the terminal is available) — this is text matching by another name and I do not recommend it;Dependencies
rig-coreandrig-agent0.42, both withdefault-features = false(no provider clients, no TLS stack of their own). Seventeen small crates,schemarsthe largest.Tests
rig_model: preamble first, documents as a system message before the question, tool exchange round-trips in protocol shape,tool_choicemapping,Nonedrops the tools field, cut-off arguments stay a string (socheck_callstill refuses them), a missing call id is minted.agent: the last exchange is replayed before its conclusion with tool names recovered; the entity block's shape and limit.check_call's six tests unchanged;utopia-llm's seventeen unchanged (error text identical).To decide
Whether the loop is replaced (this branch, plus the decision record) with the three misses accepted as the model's and #547 as the surface for them, or whether the terminal-result measurement above runs first.
🤖 Generated with Claude Code