feat: assistant IA — approbation groupee des actions, bloc d'etapes, refresh UI et bouton Stop (BUG-074, BUG-075, BUG-076, BUG-077)
This commit is contained in:
+34
-13
@@ -281,12 +281,13 @@ class TestConfirmationResume:
|
||||
assert assistant_tool_msgs[0]["tool_calls"][0]["id"] == "call_9"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_with_parallel_calls_keeps_conversation_valid(self, monkeypatch):
|
||||
"""BUG-050: pausing on one tool call of a batch must answer the others.
|
||||
async def test_confirmation_batches_all_writes_and_keeps_conversation_valid(self, monkeypatch):
|
||||
"""BUG-075 (was BUG-050): one pause batches every mutating call.
|
||||
|
||||
The assistant message lists every tool call of the response, so the
|
||||
provider rejects the resumed turn when a ``tool_call_id`` has no tool
|
||||
result (the "create a folder and a file inside" scenario).
|
||||
result. Mutating calls of the batch are now all pending (one approval
|
||||
applies them together); no dangling id remains.
|
||||
"""
|
||||
_register(monkeypatch, "_write", lambda ctx, params: {"done": params}, risk=ToolRisk.WRITE)
|
||||
|
||||
@@ -297,14 +298,15 @@ class TestConfirmationResume:
|
||||
paused = await run_agent([{"role": "user", "content": "write both"}], ctx=_ctx(), llm=llm1)
|
||||
assert paused.stopped == STOP_CONFIRMATION_REQUIRED
|
||||
assert paused.pending["error"]["id"] == "1"
|
||||
|
||||
# The call that was not reached is answered right away; the pending one
|
||||
# gets its result on resume, when the user applies it.
|
||||
# Both mutating calls are batched into the single confirmation.
|
||||
actions = paused.pending["actions"]
|
||||
assert [a["id"] for a in actions] == ["1", "2"]
|
||||
assert actions[0]["step"]["key"] == "generic"
|
||||
# Nothing is executed nor deferred while waiting for the approval.
|
||||
answered = {m["tool_call_id"] for m in paused.messages if m.get("role") == "tool"}
|
||||
assert "2" in answered
|
||||
assert "1" not in answered
|
||||
assert answered == set()
|
||||
|
||||
# Resume: the pending call is applied, the next turn stays valid.
|
||||
# Resume: both pending calls are applied, the next turn stays valid.
|
||||
llm2 = ScriptedLLM([LLMResponse(content="ok")])
|
||||
resumed = await run_agent(
|
||||
[{"role": "user", "content": "write both"}],
|
||||
@@ -315,8 +317,8 @@ class TestConfirmationResume:
|
||||
)
|
||||
assert resumed.stopped == STOP_DONE
|
||||
assert resumed.content == "ok"
|
||||
assert len(resumed.tool_calls) == 1
|
||||
assert resumed.tool_calls[0].ok is True
|
||||
assert [r.name for r in resumed.tool_calls] == ["_write", "_write"]
|
||||
assert all(r.ok for r in resumed.tool_calls)
|
||||
# Before the resumed LLM call, every announced tool_call_id is answered.
|
||||
resumed_messages = llm2.calls[0]["messages"]
|
||||
assistant = next(
|
||||
@@ -326,12 +328,31 @@ class TestConfirmationResume:
|
||||
announced = {tc["id"] for tc in assistant["tool_calls"]}
|
||||
answered = {m["tool_call_id"] for m in resumed_messages if m.get("role") == "tool"}
|
||||
assert announced <= answered
|
||||
# The skipped call is flagged "deferred" so the model can re-issue it.
|
||||
# No deferred result: the batched calls were approved, not skipped.
|
||||
deferred = [
|
||||
m for m in resumed_messages
|
||||
if m.get("role") == "tool" and json.loads(m["content"]).get("status") == "deferred"
|
||||
]
|
||||
assert [m["tool_call_id"] for m in deferred] == ["2"]
|
||||
assert deferred == []
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_confirmation_runs_read_calls_of_the_batch_immediately(self, monkeypatch):
|
||||
"""Only mutating calls are batched; read-only calls of the turn run now."""
|
||||
_register(monkeypatch, "_write", lambda ctx, params: {"done": params}, risk=ToolRisk.WRITE)
|
||||
_register(monkeypatch, "_read", lambda ctx, params: {"value": 1})
|
||||
|
||||
llm1 = ScriptedLLM([LLMResponse(tool_calls=[
|
||||
ToolCall(id="1", name="_write", arguments={"x": 1}),
|
||||
ToolCall(id="2", name="_read", arguments={}),
|
||||
ToolCall(id="3", name="_write", arguments={"x": 3}),
|
||||
])])
|
||||
paused = await run_agent([{"role": "user", "content": "write and read"}], ctx=_ctx(), llm=llm1)
|
||||
assert paused.stopped == STOP_CONFIRMATION_REQUIRED
|
||||
# The read ran while the two writes are pending.
|
||||
assert [r.name for r in paused.tool_calls] == ["_read"]
|
||||
assert [a["id"] for a in paused.pending["actions"]] == ["1", "3"]
|
||||
answered = {m["tool_call_id"] for m in paused.messages if m.get("role") == "tool"}
|
||||
assert answered == {"2"}
|
||||
|
||||
|
||||
class TestAgentPermissions:
|
||||
|
||||
Reference in New Issue
Block a user