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