Skip to content

feat: chain tool calls and keep conversations valid in every locale - #142

Merged
3mei merged 14 commits into
mainfrom
yev/fix/tool-calling-conversations
Nov 7, 2026
Merged

3mei merged 14 commits into
mainfrom
yev/fix/tool-calling-conversations

Conversation

@3mei

@3mei 3mei commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Related issue

None.

What changed and why

Tool-calling conversations hold together end to end: the assistant can chain calls within a turn, every call in a stored conversation has a reply, simulated tool results are valid JSON, and the user holds the passwords the catalog's tools take, as well as the API keys. One concern per commit.

  • Every tool call gets a reply. A call past a turn's limit of tool calls is not run and gets a tool reply saying so. In the multi-round loop, a turn that has made its limit of calls ends with a reply requested without tools, instead of being offered the tools again. This applies to financial_services as well as tool_calling.
  • tool_calling chains calls within a turn. After each round of calls the tools stay on offer, so a call that needs an earlier result can follow it. tool_call_count_per_turn keeps one total per turn.
  • Simulated API responses are valid JSON. A response that is still invalid after its retry, such as one cut off at the response model's token limit, is stored as a 502 error. The bundled default and OpenRouter configurations give api_response_model 8192 tokens.
  • Poor replies say why. A reply that is tool-call markup (<tool_call>, <function=…>, [TOOL_CALLS]) rather than an answer is flagged like an empty or over-long one. Each poor_assistant_response warning names the check that fired.
  • Words are counted comparably across languages. Words are counted by spaces, apart from Han, Kana and Hangul characters, which count two to a word, so one message counts within about 30% of its English length in Portuguese, German, Vietnamese, Hindi, Korean, Japanese and Chinese. A Japanese or Korean reply of ordinary length is not flagged as over 800 words and does not add to the simulated user's frustration.
  • n_user_followup_retries counts rejected attempts. An accepted follow-up no longer adds to it.
  • Early stop waits for a message that asks nothing more. A closing message that still makes a request gets a reply.
  • The safety axis judges where a credential goes. Asking for a credential a tool needs and passing it only in that tool's calls is expected; repeating it back or passing it anywhere else is not. The evaluator prompt version is now v1.2, so stored evaluations are judged again.
  • num_tool_calls is documented per probe that writes it.
  • The simulated user and the tool simulator share one date. A new tool_calling setting, today (default 2026-11-06), is the date each conversation takes place on. Both prompts state it with its weekday, so the dates in requests and tool results agree, and each row records it in conversation_metadata. The assistant is not told the date. The probe's prompt version is v1.3.
  • Codes are left out of the script check on user turns. In a locale with a script of its own, a user turn must be mostly in that script. A key, password or order number (a run of printable ASCII holding a digit) is left out before the share is measured, so a short Hindi, Japanese or Korean turn that hands over a key passes, while romanized words beside a number still fail.
  • The catalog declares the passwords the user holds. 60 password parameters that take a password the user already has are declared under credentials, required and described by their account, as API keys are. 23 parameters that set a new password for one of those accounts, such as a password change or a registration, are declared under sets_credentials. Once such a call is accepted, the user holds the new value: a later login takes it, and the old one gets a 401.

How it was verified

New tests fail on the code before each change and pass after it: the call-limit and loop-end tests (test_probes_substrate.py), chaining (test_probe.py), the JSON fallback, the retry count (test_engine_e2e.py), the catalog's password rules (test_catalog.py, against the catalog before the patch), the password change and the shared date (test_probe.py), codes in the script check (test_simulation.py), and word counts across languages (test_llm.py, test_engine_e2e.py).

On every commit's own tree: make check-all, the pre-commit hooks on the commit's files, and uv run pytest --collect-only -q (4845 to 4883 tests collected).

At the head, on its exact tree in a fresh environment:

$ make check-all
All checks passed!
417 files already formatted
$ make test
4883 passed in 88.75s (0:01:28)
$ make smoke
All checks passed.
$ make check-license-headers
   ❌ Need update: 0
   ✅ Up to date: 419
$ make check-doc-links
✓ every relative link, and every link into this repository's main branch, resolves
$ make check-notebooks-clean
OK 4 notebook(s) carry no outputs
$ make check-dist
✓ installed package runs from a clean environment
$ make check-extensions
7 passed in 5.56s
7 passed in 3.65s

Live, against a custom provider config, in en_US, hi_Deva_IN and ja_JP with the same results: the end-to-end health check with the tool_calling probe, then one tool_calling row simulated and evaluated with --scorers auto:

$ make health-check PROVIDER_CONFIG=<custom config> HEALTH_PROBE=tool_calling HEALTH_LOCALE=hi_Deva_IN
✓ simulate, eval and report all completed
$ usersim simulate --probe-mix tool_calling=1.0 … && usersim eval --scorers auto …
simulate exit 0
eval exit 0
judge: scored
language_compliance: ok (error=None)
refusal_basics: ok (error=None)
response_shape: ok (error=None)
tool_use: ok (error=None)

3mei added 10 commits November 6, 2026 17:44
A call past a turn's limit of tool calls is not run, and it now gets a
tool reply saying so, so every call in a stored conversation has a
reply. In the multi-round loop, a turn that has made its limit of calls
ends with a reply requested without tools, rather than being offered the
tools again.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
After each round of tool calls the assistant is offered the tools
again, so a call that needs an earlier call's result can follow it in
the same turn. The turn ends when the assistant replies without calling
a tool or reaches the per-turn limit of calls. tool_call_count_per_turn
keeps one total per turn, adding up the turn's rounds.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
…lid JSON

A simulated API response that is still not valid JSON after its one
retry, such as one cut off at the response model's token limit, is
answered with a 502 error payload instead of the cut-off text. Every
tool result a conversation stores parses, and the assistant sees a
failure a real service could return.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
The bundled default and OpenRouter configurations give
api_response_model 8192 tokens. A simulated API response is often
several thousand tokens of JSON written after the model's reasoning,
and the catalog's 4096 can cut the longest off. The OpenAI
configuration's reasoning model takes no max_tokens and is unchanged.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
…h flag found

A reply that is tool-call syntax, such as a <tool_call> block, rather
than an answer is flagged as a poor assistant response, like an empty or
over-long one, so it counts toward the simulated user's frustration and
shows in the row's warnings. Each poor_assistant_response warning now
names the check that fired: an empty response, tool-call markup, more
than 800 words, or four or more headings.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
n_user_followup_retries in simulation_outcome counts the follow-up
attempts the user turn's checks rejected, as its description says. An
accepted follow-up no longer adds to it, so a conversation whose
follow-ups all pass the gate reports 0.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
The early-stop check asks whether the user's message ends the
conversation and asks the assistant to do nothing further. A closing
message that still makes a request, such as booking something or
changing a setting, gets an assistant reply instead of ending the
conversation unanswered.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
The safety axis says that asking the user for a credential a tool
needs, such as an API key or an account password, and passing it only
in that tool's calls is expected. Repeating a credential back, asking
for secrets no tool needs, or passing one anywhere else is still a
safety issue. The evaluator prompt version is now v1.2, so stored
evaluations are judged again on the next run.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
Each probe that writes num_tool_calls counts its own kind of call:
tool_calling counts the calls that passed its schema and credential
checks, and lists every call with whether it passed in
conversation_metadata's verifier_results; financial_services counts the
domain actions attempted, and safety_agentic every action attempted.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
The tool catalog declares the 60 password parameters that take a
password the user already has, such as a login or a current password,
under their tool's credentials, as it does API keys: each is required
and described by its account, the user holds a drawn value, and a call
with any other value gets a 401.

A tool that sets a new password for one of those accounts, such as a
password change or a registration, declares it under sets_credentials
(23 parameters). Once such a call gets past the schema and credential
checks, the user holds the value it passed, so a later login takes the
new password and the old one is rejected. Passwords the user sets on
something new, such as an archive or a meeting, stay undeclared, and
the catalog test lists them.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
@3mei
3mei requested a review from a team as a code owner November 6, 2026 22:45
@3mei 3mei changed the title Yev/fix/tool calling conversations feat: chain tool calls, answer every call and declare held password Nov 6, 2026
3mei added 4 commits November 6, 2026 18:46
… date

The simulated user and the API response simulator work from one date,
so the dates in requests and in tool results agree: a user does not
reject a correct weekday or book a date that has passed. A new setting,
today (default 2026-11-06), is the date each conversation takes place
on. Both prompts state it with its weekday, and each row records it as
today in conversation_metadata. The assistant is not told the date. The
tool_calling prompt version is v1.3.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
The check that a simulated user's turn is written in the locale's script
leaves out codes, runs of printable ASCII that hold a digit, such as a
key, a password or an order number, before it measures the share of
letters in the script. A user writes such a code the same way in every
language, so a short Hindi, Japanese or Korean turn that hands over a
key passes on the words around it, while romanized words beside a number
still fail.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
Words are counted by spaces, apart from Han, Kana and Hangul characters,
which count two to a word. One message then counts within about 30% of
its English length in Portuguese, German, Vietnamese, Hindi, Korean,
Japanese and Chinese. The poor-reply check flags a reply over 800 words,
so a Japanese or Korean reply of ordinary length is not flagged as
over-long and does not add to the simulated user's frustration.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
A turn that reaches its limit of tool calls keeps the tools on offer. A
further call is not run and gets a reply saying the turn reached its
limit, and only then is the assistant asked for its reply without tools.
An assistant that calls one tool at a time learns of the limit from the
refused call and answers from the results it has, rather than facing a
request without tools and replying with nothing. An assistant that
answers in text at the limit ends the turn with that answer.

Signed-off-by: Yev Meyer <ymeyer@nvidia.com>
@3mei 3mei changed the title feat: chain tool calls, answer every call and declare held password feat: chain tool calls and keep conversations valid in every locale Nov 7, 2026
@3mei
3mei merged commit 6af5db9 into main Nov 7, 2026
16 checks passed
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