Co-authored-by: Alexis Tacnet <alexis@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com> Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com> Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com> Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
192 lines
7.1 KiB
Python
192 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
from acp.schema import TextContentBlock
|
|
import pytest
|
|
|
|
from vibe.acp.acp_agent_loop import VibeAcpAgentLoop
|
|
from vibe.acp.exceptions import InvalidRequestError
|
|
from vibe.core.agents.models import BuiltinAgentName
|
|
from vibe.core.session.session_id import extract_suffix
|
|
from vibe.core.types import FunctionCall, LLMMessage, Role, ToolCall
|
|
|
|
|
|
class TestACPForkSession:
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_clones_history_and_mode(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
|
|
await acp_agent_loop.set_session_mode(
|
|
session_id=source_session.id, mode_id=BuiltinAgentName.PLAN
|
|
)
|
|
await acp_agent_loop.prompt(
|
|
prompt=[TextContentBlock(type="text", text="Say hi")],
|
|
session_id=source_session.id,
|
|
)
|
|
|
|
response = await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
|
|
)
|
|
|
|
assert response.session_id != source_session.id
|
|
assert response.modes is not None
|
|
assert response.modes.current_mode_id == BuiltinAgentName.PLAN
|
|
assert response.models is not None
|
|
assert (
|
|
response.models.current_model_id
|
|
== source_session.agent_loop.config.active_model
|
|
)
|
|
assert response.config_options is not None
|
|
assert len(response.config_options) == 3
|
|
|
|
forked_session = acp_agent_loop.sessions[response.session_id]
|
|
assert forked_session.agent_loop.agent_profile.name == BuiltinAgentName.PLAN
|
|
|
|
source_messages = [
|
|
message.model_dump(mode="json", exclude_none=True)
|
|
for message in source_session.agent_loop.messages
|
|
if message.role != Role.system
|
|
]
|
|
forked_messages = [
|
|
message.model_dump(mode="json", exclude_none=True)
|
|
for message in forked_session.agent_loop.messages
|
|
if message.role != Role.system
|
|
]
|
|
assert forked_messages == source_messages
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_from_user_message_keeps_full_turn(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
source_session.agent_loop.messages.extend([
|
|
LLMMessage(role=Role.user, content="First", message_id="user-1"),
|
|
LLMMessage(
|
|
role=Role.assistant, content="First answer", message_id="assistant-1"
|
|
),
|
|
LLMMessage(
|
|
role=Role.assistant,
|
|
content="",
|
|
message_id="assistant-2",
|
|
tool_calls=[
|
|
ToolCall(
|
|
id="call-1",
|
|
index=0,
|
|
function=FunctionCall(
|
|
name="read", arguments='{"file_path":"a.txt"}'
|
|
),
|
|
)
|
|
],
|
|
),
|
|
LLMMessage(role=Role.tool, content="contents", tool_call_id="call-1"),
|
|
LLMMessage(role=Role.user, content="Second", message_id="user-2"),
|
|
LLMMessage(
|
|
role=Role.assistant, content="Second answer", message_id="assistant-3"
|
|
),
|
|
])
|
|
|
|
response = await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()),
|
|
session_id=source_session.id,
|
|
mcp_servers=[],
|
|
messageId="user-1",
|
|
)
|
|
|
|
forked_session = acp_agent_loop.sessions[response.session_id]
|
|
assert [
|
|
(message.role, message.content, message.message_id, message.tool_call_id)
|
|
for message in forked_session.agent_loop.messages
|
|
if message.role != Role.system
|
|
] == [
|
|
(Role.user, "First", "user-1", None),
|
|
(Role.assistant, "First answer", "assistant-1", None),
|
|
(Role.assistant, "", "assistant-2", None),
|
|
(Role.tool, "contents", None, "call-1"),
|
|
]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_rejects_non_user_message_id(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
source_session.agent_loop.messages.extend([
|
|
LLMMessage(role=Role.user, content="First", message_id="user-1"),
|
|
LLMMessage(
|
|
role=Role.assistant, content="First answer", message_id="assistant-1"
|
|
),
|
|
])
|
|
|
|
with pytest.raises(
|
|
InvalidRequestError,
|
|
match="Fork from message_id is only supported for user messages",
|
|
):
|
|
await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()),
|
|
session_id=source_session.id,
|
|
mcp_servers=[],
|
|
messageId="assistant-1",
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_sets_parent_session_id(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
|
|
response = await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
|
|
)
|
|
|
|
forked_session = acp_agent_loop.sessions[response.session_id]
|
|
assert forked_session.agent_loop.parent_session_id == source_session.id
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_rejects_running_session(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
source_session.set_prompt_task(asyncio.sleep(10))
|
|
|
|
with pytest.raises(
|
|
InvalidRequestError,
|
|
match="Cannot fork a session while the agent loop is running",
|
|
):
|
|
await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
|
|
)
|
|
|
|
await source_session.cancel_prompt()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fork_session_preserves_session_id_suffix(
|
|
self, acp_agent_loop: VibeAcpAgentLoop
|
|
) -> None:
|
|
session_response = await acp_agent_loop.new_session(
|
|
cwd=str(Path.cwd()), mcp_servers=[]
|
|
)
|
|
source_session = acp_agent_loop.sessions[session_response.session_id]
|
|
|
|
response = await acp_agent_loop.fork_session(
|
|
cwd=str(Path.cwd()), session_id=source_session.id, mcp_servers=[]
|
|
)
|
|
|
|
assert extract_suffix(response.session_id) == extract_suffix(source_session.id)
|