Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai>
Co-Authored-By: Michel Thomazo <michel.thomazo@mistral.ai>
This commit is contained in:
Mathias Gesbert 2025-12-22 13:33:20 +01:00 committed by Mathias Gesbert
parent 402e898f39
commit 2e1e15120d
32 changed files with 391 additions and 549 deletions

View file

@ -44,10 +44,12 @@ def make_config(todo_permission: ToolPermission = ToolPermission.ALWAYS) -> Vibe
)
def make_todo_tool_call(call_id: str, action: str = "read") -> ToolCall:
def make_todo_tool_call(
call_id: str, index: int = 0, arguments: str | None = None
) -> ToolCall:
args = arguments if arguments is not None else '{"action": "read"}'
return ToolCall(
id=call_id,
function=FunctionCall(name="todo", arguments=f'{{"action": "{action}"}}'),
id=call_id, index=index, function=FunctionCall(name="todo", arguments=args)
)
@ -72,8 +74,8 @@ async def test_single_tool_call_executes_under_auto_approve() -> None:
mocked_tool_call_id = "call_1"
tool_call = make_todo_tool_call(mocked_tool_call_id)
backend = FakeBackend([
mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call]),
mock_llm_chunk(content="I retrieved 0 todos.", finish_reason="stop"),
[mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call])],
[mock_llm_chunk(content="I retrieved 0 todos.")],
])
agent = make_agent(auto_approve=True, backend=backend)
@ -108,14 +110,13 @@ async def test_tool_call_requires_approval_if_not_auto_approved() -> None:
auto_approve=False,
todo_permission=ToolPermission.ASK,
backend=FakeBackend([
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_2")],
),
mock_llm_chunk(
content="I cannot execute the tool without approval.",
finish_reason="stop",
),
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_2")],
)
],
[mock_llm_chunk(content="I cannot execute the tool without approval.")],
]),
)
@ -148,11 +149,13 @@ async def test_tool_call_approved_by_callback() -> None:
todo_permission=ToolPermission.ASK,
approval_callback=approval_callback,
backend=FakeBackend([
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_3")],
),
mock_llm_chunk(content="I retrieved 0 todos.", finish_reason="stop"),
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_3")],
)
],
[mock_llm_chunk(content="I retrieved 0 todos.")],
]),
)
@ -183,13 +186,13 @@ async def test_tool_call_rejected_when_auto_approve_disabled_and_rejected_by_cal
todo_permission=ToolPermission.ASK,
approval_callback=approval_callback,
backend=FakeBackend([
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_4")],
),
mock_llm_chunk(
content="Understood, I won't check the todos.", finish_reason="stop"
),
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_4")],
)
],
[mock_llm_chunk(content="Understood, I won't check the todos.")],
]),
)
@ -211,11 +214,13 @@ async def test_tool_call_skipped_when_permission_is_never() -> None:
auto_approve=False,
todo_permission=ToolPermission.NEVER,
backend=FakeBackend([
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_never")],
),
mock_llm_chunk(content="Tool is disabled.", finish_reason="stop"),
[
mock_llm_chunk(
content="Let me check your todos.",
tool_calls=[make_todo_tool_call("call_never")],
)
],
[mock_llm_chunk(content="Tool is disabled.")],
]),
)
@ -257,14 +262,20 @@ async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> No
todo_permission=ToolPermission.ASK,
approval_callback=approval_callback,
backend=FakeBackend([
mock_llm_chunk(
content="First check.", tool_calls=[make_todo_tool_call("call_first")]
),
mock_llm_chunk(content="First done.", finish_reason="stop"),
mock_llm_chunk(
content="Second check.", tool_calls=[make_todo_tool_call("call_second")]
),
mock_llm_chunk(content="Second done.", finish_reason="stop"),
[
mock_llm_chunk(
content="First check.",
tool_calls=[make_todo_tool_call("call_first")],
)
],
[mock_llm_chunk(content="First done.")],
[
mock_llm_chunk(
content="Second check.",
tool_calls=[make_todo_tool_call("call_second")],
)
],
[mock_llm_chunk(content="Second done.")],
]),
)
agent_ref = agent
@ -291,17 +302,16 @@ async def test_approval_always_sets_tool_permission_for_subsequent_calls() -> No
@pytest.mark.asyncio
async def test_tool_call_with_invalid_action() -> None:
tool_call = ToolCall(
id="call_5",
function=FunctionCall(name="todo", arguments='{"action": "invalid_action"}'),
)
tool_call = make_todo_tool_call("call_5", arguments='{"action": "invalid_action"}')
agent = make_agent(
auto_approve=True,
backend=FakeBackend([
mock_llm_chunk(content="Let me check your todos.", tool_calls=[tool_call]),
mock_llm_chunk(
content="I encountered an error with the action.", finish_reason="stop"
),
[
mock_llm_chunk(
content="Let me check your todos.", tool_calls=[tool_call]
)
],
[mock_llm_chunk(content="I encountered an error with the action.")],
]),
)
@ -320,24 +330,18 @@ async def test_tool_call_with_duplicate_todo_ids() -> None:
TodoItem(id="duplicate", content="Task 1"),
TodoItem(id="duplicate", content="Task 2"),
]
tool_call = ToolCall(
id="call_6",
function=FunctionCall(
name="todo",
arguments=json.dumps({
"action": "write",
"todos": [t.model_dump() for t in duplicate_todos],
}),
),
tool_call = make_todo_tool_call(
"call_6",
arguments=json.dumps({
"action": "write",
"todos": [t.model_dump() for t in duplicate_todos],
}),
)
agent = make_agent(
auto_approve=True,
backend=FakeBackend([
mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call]),
mock_llm_chunk(
content="I couldn't write todos with duplicate IDs.",
finish_reason="stop",
),
[mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call])],
[mock_llm_chunk(content="I couldn't write todos with duplicate IDs.")],
]),
)
@ -353,23 +357,18 @@ async def test_tool_call_with_duplicate_todo_ids() -> None:
@pytest.mark.asyncio
async def test_tool_call_with_exceeding_max_todos() -> None:
many_todos = [TodoItem(id=f"todo_{i}", content=f"Task {i}") for i in range(150)]
tool_call = ToolCall(
id="call_7",
function=FunctionCall(
name="todo",
arguments=json.dumps({
"action": "write",
"todos": [t.model_dump() for t in many_todos],
}),
),
tool_call = make_todo_tool_call(
"call_7",
arguments=json.dumps({
"action": "write",
"todos": [t.model_dump() for t in many_todos],
}),
)
agent = make_agent(
auto_approve=True,
backend=FakeBackend([
mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call]),
mock_llm_chunk(
content="I couldn't write that many todos.", finish_reason="stop"
),
[mock_llm_chunk(content="Let me write todos.", tool_calls=[tool_call])],
[mock_llm_chunk(content="I couldn't write that many todos.")],
]),
)
@ -394,7 +393,7 @@ async def test_tool_call_can_be_interrupted(
exception_class: type[BaseException],
) -> None:
tool_call = ToolCall(
id="call_8", function=FunctionCall(name="stub_tool", arguments="{}")
id="call_8", index=0, function=FunctionCall(name="stub_tool", arguments="{}")
)
config = VibeConfig(
session_logging=SessionLoggingConfig(enabled=False),
@ -405,8 +404,8 @@ async def test_tool_call_can_be_interrupted(
config,
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([
mock_llm_chunk(content="Let me use the tool.", tool_calls=[tool_call]),
mock_llm_chunk(content="Tool execution completed.", finish_reason="stop"),
[mock_llm_chunk(content="Let me use the tool.", tool_calls=[tool_call])],
[mock_llm_chunk(content="Tool execution completed.")],
]),
)
# no dependency injection available => monkey patch
@ -433,15 +432,11 @@ async def test_fill_missing_tool_responses_inserts_placeholders() -> None:
agent = Agent(
make_config(),
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
backend=FakeBackend(mock_llm_chunk(content="ok")),
)
tool_calls_messages = [
ToolCall(
id="tc1", function=FunctionCall(name="todo", arguments='{"action": "read"}')
),
ToolCall(
id="tc2", function=FunctionCall(name="todo", arguments='{"action": "read"}')
),
make_todo_tool_call("tc1", index=0),
make_todo_tool_call("tc2", index=1),
]
assistant_msg = LLMMessage(
role=Role.assistant, content="Calling tools...", tool_calls=tool_calls_messages
@ -473,7 +468,7 @@ async def test_ensure_assistant_after_tool_appends_understood() -> None:
agent = Agent(
make_config(),
mode=AgentMode.AUTO_APPROVE,
backend=FakeBackend([mock_llm_chunk(content="ok", finish_reason="stop")]),
backend=FakeBackend(mock_llm_chunk(content="ok")),
)
tool_msg = LLMMessage(
role=Role.tool, tool_call_id="tc_z", name="todo", content="Done"