Render plain Responses completions

When Responses mode accepts a plain-text final answer, reuse the active generating log item as a finished response entry so the WebUI does not remain on the Calling LLM step. Skip normal tool JSON and existing live response-tool logs, and cover the fallback with focused regression tests.

Alessandro committed Jun 26, 2026 at 15:00 UTC 48087de008bbe6efab5656b204e297356c8e60e9
3 files changed +111
extensions/python/_functions/AGENTS.md
+1
@@ -15,6 +15,7 @@
15 - Do not flatten nested qualname paths into retired legacy folder names.
16 - Extension functions must match the implicit hook's supplied arguments.
17 - Preserve ordering prefixes where exception handling, watchdog registration, or cleanup depends on them.
18 +- Hooks that mirror persisted AI responses into UI logs must reuse existing stream log items and avoid duplicating live response-tool logs.
19
20 ## Work Guidance
21
extensions/python/_functions/agent/Agent/hist_add_ai_response/end/_10_log_plain_responses.py new
+44
@@ -0,0 +1,44 @@
1 +from typing import Any
2 +
3 +from helpers import extract_tools
4 +from helpers.extension import Extension
5 +
6 +
7 +class LogPlainResponses(Extension):
8 + def execute(self, data: dict[str, Any] | None = None, **kwargs):
9 + if not self.agent or not isinstance(data, dict):
10 + return
11 +
12 + call_kwargs = data.get("kwargs")
13 + if not isinstance(call_kwargs, dict):
14 + call_kwargs = {}
15 +
16 + llm_result = call_kwargs.get("llm_result")
17 + if getattr(llm_result, "mode", "") != "responses":
18 + return
19 +
20 + message = call_kwargs.get("message")
21 + call_args = data.get("args")
22 + if message is None and isinstance(call_args, tuple) and len(call_args) > 1:
23 + message = call_args[1]
24 + if not isinstance(message, str) or not message:
25 + return
26 + if extract_tools.json_parse_dirty(message) is not None:
27 + return
28 +
29 + params = getattr(getattr(self.agent, "loop_data", None), "params_temporary", None)
30 + if not isinstance(params, dict) or "log_item_response" in params:
31 + return
32 +
33 + log_item = params.get("log_item_generating")
34 + if log_item is None:
35 + return
36 +
37 + params["log_item_response"] = log_item
38 + log_item.update(
39 + type="response",
40 + heading="",
41 + content=message,
42 + finished=True,
43 + update_progress="none",
44 + )
tests/test_plain_response_logging.py new
+66
@@ -0,0 +1,66 @@
1 +from types import SimpleNamespace
2 +
3 +from helpers.log import Log
4 +from extensions.python._functions.agent.Agent.hist_add_ai_response.end._10_log_plain_responses import (
5 + LogPlainResponses,
6 +)
7 +
8 +
9 +def _agent_with_generating_log():
10 + log = Log()
11 + item = log.log(type="agent", heading="A0: Calling LLM...", id="msg-1")
12 + agent = SimpleNamespace(
13 + loop_data=SimpleNamespace(params_temporary={"log_item_generating": item})
14 + )
15 + return agent, item
16 +
17 +
18 +def test_responses_plain_text_completion_finishes_generating_log_as_response():
19 + agent, item = _agent_with_generating_log()
20 + data = {
21 + "args": (agent, "Plain final answer."),
22 + "kwargs": {"id": "msg-1", "llm_result": SimpleNamespace(mode="responses")},
23 + }
24 +
25 + LogPlainResponses(agent=agent).execute(data=data)
26 +
27 + assert item.type == "response"
28 + assert item.heading == ""
29 + assert item.content == "Plain final answer."
30 + assert item.update_progress == "none"
31 + assert item.kvps["finished"] is True
32 + assert agent.loop_data.params_temporary["log_item_response"] is item
33 +
34 +
35 +def test_responses_tool_json_keeps_generating_log_as_agent_step():
36 + agent, item = _agent_with_generating_log()
37 + data = {
38 + "args": (
39 + agent,
40 + '{"tool_name":"search_engine","tool_args":{"query":"today news"}}',
41 + ),
42 + "kwargs": {"id": "msg-1", "llm_result": SimpleNamespace(mode="responses")},
43 + }
44 +
45 + LogPlainResponses(agent=agent).execute(data=data)
46 +
47 + assert item.type == "agent"
48 + assert item.heading == "A0: Calling LLM..."
49 + assert item.content == ""
50 + assert "log_item_response" not in agent.loop_data.params_temporary
51 +
52 +
53 +def test_responses_plain_text_completion_does_not_replace_live_response_log():
54 + agent, item = _agent_with_generating_log()
55 + live_response = Log().log(type="response", content="Already live")
56 + agent.loop_data.params_temporary["log_item_response"] = live_response
57 + data = {
58 + "args": (agent, "Plain final answer."),
59 + "kwargs": {"id": "msg-1", "llm_result": SimpleNamespace(mode="responses")},
60 + }
61 +
62 + LogPlainResponses(agent=agent).execute(data=data)
63 +
64 + assert item.type == "agent"
65 + assert item.content == ""
66 + assert agent.loop_data.params_temporary["log_item_response"] is live_response