Agentic Workflow Testing¶
Agent frameworks wrap an LLM in a tool-calling loop:
The loop terminates when the model stops requesting tools and produces a plain-text answer. Naïve mocks can't replicate this. They either always return a tool call (the agent loops forever) or never return one (tool tests can't fire). VidaiMock does both correctly — it behaves like a real model that knows when it's done.
This is the single biggest reason to use VidaiMock for agent development: run Google ADK, LangGraph, or LangChain Runner loops end-to-end in CI with zero live-provider spend — the loop terminates naturally, just like it does against the real API.
The two branches¶
The bundled chat templates decide based on the request history:
- Tools defined, no tool result yet → emit a
tool_call/tool_use/functionCall. - Tools defined, tool result already in history → emit a plain-text
answer with
finish_reason: "stop"/stop_reason: "end_turn".
How it detects a tool result¶
A built-in Tera helper, has_tool_result(messages, provider), inspects the
request's conversation history. It's implemented in Rust (not Tera) because
deep JSON-array inspection is unreliable in templates.
provider |
Detection signal |
|---|---|
openai |
any message with role: "tool" |
anthropic |
a user message whose content[] contains a block with type: "tool_result" |
gemini |
user content whose parts[] contains a functionResponse |
Default is openai when provider is omitted. Malformed or missing input
returns false rather than erroring — safe to use unconditionally inside
{% if %} guards in custom templates.
The full round trip (OpenAI, no API key, no cost)¶
Turn 1 — the agent asks; the mock returns a tool call because tools
are declared and there's no tool result yet:
curl -s http://localhost:8100/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"gpt-4o",
"tools":[{"type":"function","function":{"name":"get_weather","parameters":{}}}],
"messages":[{"role":"user","content":"Weather in London?"}]}'
# -> finish_reason: "tool_calls", message.tool_calls: [...]
Your agent framework executes get_weather, appends the result, and calls
again.
Turn 2 — same tools, now with a role:tool result in history. The mock
detects it and synthesises a plain-text answer instead of looping:
curl -s http://localhost:8100/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"gpt-4o",
"tools":[{"type":"function","function":{"name":"get_weather","parameters":{}}}],
"messages":[
{"role":"user","content":"Weather in London?"},
{"role":"assistant","tool_calls":[{"id":"c1","type":"function",
"function":{"name":"get_weather","arguments":"{}"}}]},
{"role":"tool","tool_call_id":"c1","content":"15°C cloudy"}
]}'
# -> finish_reason: "stop", message.content: "Based on the tool results..."
The agent loop terminates. No infinite recursion, no real tokens spent.
Works in streaming too¶
The same heuristic applies to streaming requests. An Anthropic stream with a
tool_result already in history emits proper text deltas with
stop_reason: end_turn — agentic loops over streaming also terminate
cleanly in mock mode.
Use in custom templates¶
If you write your own provider template, call the helper directly:
{% if json.tools and has_tool_result(messages=json.messages, provider="openai") %}
{# loop-terminating: emit plain text #}
{% elif json.tools %}
{# emit a tool call #}
{% else %}
{# default text #}
{% endif %}
See Writing templates for the full helper catalogue.