refactor(chat_branching): ID-based log ↔ history linking for precise branch trimming
keyboardstaff committed
Mar 24, 2026 at 03:04 UTC
1bd5bc01d66cab860864769b830f3cac9f94ecfb
22 files changed
+165
-99
agent.py
+20
-15
@@ -320,6 +320,7 @@ class UserMessage:
320
message: str
321
attachments: list[str] = field(default_factory=list[str])
322
system_message: list[str] = field(default_factory=list[str])
323
+ id: str = ""
324
325
326
class LoopData:
@@ -466,18 +467,20 @@ class Agent:
467
self.loop_data.last_response == agent_response
468
): # if assistant_response is the same as last message in history, let him know
469
# Append the assistant's response to the history
469
- self.hist_add_ai_response(agent_response)
470
+ log_item = self.loop_data.params_temporary.get("log_item_generating")
471
+ self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
472
# Append warning message to the history
473
warning_msg = self.read_prompt("fw.msg_repeat.md")
472
- self.hist_add_warning(message=warning_msg)
474
+ wmsg = self.hist_add_warning(message=warning_msg)
475
PrintStyle(font_color="orange", padding=True).print(
476
warning_msg
477
)
476
- self.context.log.log(type="warning", content=warning_msg)
478
+ self.context.log.log(type="warning", content=warning_msg, id=wmsg.id)
479
480
else: # otherwise proceed with tool
481
# Append the assistant's response to the history
480
- self.hist_add_ai_response(agent_response)
482
+ log_item = self.loop_data.params_temporary.get("log_item_generating")
483
+ self.hist_add_ai_response(agent_response, id=log_item.id if log_item else "")
484
# process tools requested in agent message
485
tools_result = await self.process_tools(agent_response)
486
if tools_result: # final response of message loop available
@@ -637,7 +640,7 @@ class Agent:
640
641
@extension.extensible
642
def hist_add_message(
640
- self, ai: bool, content: history.MessageContent, tokens: int = 0
643
+ self, ai: bool, content: history.MessageContent, tokens: int = 0, id: str = ""
644
):
645
self.last_message = datetime.now(timezone.utc)
646
# Allow extensions to process content before adding to history
@@ -646,7 +649,7 @@ class Agent:
649
"hist_add_before", self, content_data=content_data, ai=ai
650
)
651
return self.history.add_message(
649
- ai=ai, content=content_data["content"], tokens=tokens
652
+ ai=ai, content=content_data["content"], tokens=tokens, id=id
653
)
654
655
@extension.extensible
@@ -674,30 +677,31 @@ class Agent:
677
content = {k: v for k, v in content.items() if v}
678
679
# add to history
677
- msg = self.hist_add_message(False, content=content) # type: ignore
680
+ msg = self.hist_add_message(False, content=content, id=message.id) # type: ignore
681
self.last_user_message = msg
682
return msg
683
684
@extension.extensible
682
- def hist_add_ai_response(self, message: str):
685
+ def hist_add_ai_response(self, message: str, id: str = ""):
686
self.loop_data.last_response = message
687
content = self.parse_prompt("fw.ai_response.md", message=message)
685
- return self.hist_add_message(True, content=content)
688
+ return self.hist_add_message(True, content=content, id=id)
689
690
@extension.extensible
688
- def hist_add_warning(self, message: history.MessageContent):
691
+ def hist_add_warning(self, message: history.MessageContent, id: str = ""):
692
content = self.parse_prompt("fw.warning.md", message=message)
690
- return self.hist_add_message(False, content=content)
693
+ return self.hist_add_message(False, content=content, id=id)
694
695
@extension.extensible
696
def hist_add_tool_result(self, tool_name: str, tool_result: str, **kwargs):
697
+ msg_id = kwargs.pop("id", "")
698
data = {
699
"tool_name": tool_name,
700
"tool_result": tool_result,
701
**kwargs,
702
}
703
extension.call_extensions_sync("hist_add_tool_result", self, data=data)
700
- return self.hist_add_message(False, content=data)
704
+ return self.hist_add_message(False, content=data, id=msg_id)
705
706
def concat_messages(
707
self, messages
@@ -927,18 +931,19 @@ class Agent:
931
error_detail = (
932
f"Tool '{raw_tool_name}' not found or could not be initialized."
933
)
930
- self.hist_add_warning(error_detail)
934
+ wmsg = self.hist_add_warning(error_detail)
935
PrintStyle(font_color="red", padding=True).print(error_detail)
936
self.context.log.log(
933
- type="warning", content=f"{self.agent_name}: {error_detail}"
937
+ type="warning", content=f"{self.agent_name}: {error_detail}", id=wmsg.id
938
)
939
else:
940
warning_msg_misformat = self.read_prompt("fw.msg_misformat.md")
937
- self.hist_add_warning(warning_msg_misformat)
941
+ wmsg = self.hist_add_warning(warning_msg_misformat)
942
PrintStyle(font_color="red", padding=True).print(warning_msg_misformat)
943
self.context.log.log(
944
type="warning",
945
content=f"{self.agent_name}: Message misformat, no valid tool request found.",
946
+ id=wmsg.id,
947
)
948
949
@extension.extensible
api/api_message.py
+4
-1
@@ -1,5 +1,6 @@
1
import base64
2
import os
3
+import uuid
4
from datetime import datetime, timedelta
5
from agent import AgentContext, UserMessage, AgentContextType
6
from helpers.api import ApiHandler, Request, Response
@@ -134,15 +135,17 @@ class ApiMessage(ApiHandler):
135
PrintStyle(font_color="white", padding=False).print(f"- {filename}")
136
137
# Add user message to chat history so it's visible in the UI
138
+ msg_id = str(uuid.uuid4())
139
context.log.log(
140
type="user",
141
heading="",
142
content=message,
143
kvps={"attachments": attachment_filenames},
144
+ id=msg_id,
145
)
146
147
# Send message to agent
145
- task = context.communicate(UserMessage(message=message, attachments=attachment_paths))
148
+ task = context.communicate(UserMessage(message=message, attachments=attachment_paths, id=msg_id))
149
result = await task.result()
150
151
# Clean up expired chats
api/message.py
+1
-1
@@ -68,4 +68,4 @@ class Message(ApiHandler):
68
# Log to console and UI using helper function
69
mq.log_user_message(context, message, attachment_paths, message_id)
70
71
- return context.communicate(UserMessage(message=message, attachments=attachment_paths)), context
71
+ return context.communicate(UserMessage(message=message, attachments=attachment_paths, id=message_id or "")), context
extensions/python/_functions/agent/Agent/handle_exception/end/_50_handle_repairable_exception.py
+2
-2
@@ -17,9 +17,9 @@ class HandleRepairableException(Extension):
17
if isinstance(data["exception"], RepairableException):
18
msg = {"message": errors.format_error(data["exception"])}
19
await extension.call_extensions_async("error_format", agent=self.agent, msg=msg)
20
- self.agent.hist_add_warning(msg["message"])
20
+ wmsg = self.agent.hist_add_warning(msg["message"])
21
PrintStyle(font_color="red", padding=True).print(msg["message"])
22
- self.agent.context.log.log(type="warning", content=msg["message"])
22
+ self.agent.context.log.log(type="warning", content=msg["message"], id=wmsg.id)
23
data["exception"] = None
24
25
extensions/python/agent_init/_10_initial_message.py
+2
-1
@@ -28,7 +28,7 @@ class InitialMessage(Extension):
28
self.agent.loop_data = LoopData(user_message=None)
29
30
# Add the message to history as an AI response
31
- self.agent.hist_add_ai_response(initial_message)
31
+ msg = self.agent.hist_add_ai_response(initial_message)
32
33
# json parse the message, get the tool_args text
34
initial_message_json = json.loads(initial_message)
@@ -40,4 +40,5 @@ class InitialMessage(Extension):
40
content=initial_message_text,
41
finished=True,
42
update_progress="none",
43
+ id=msg.id,
44
)
extensions/python/before_main_llm_call/_10_log_for_stream.py
+2
@@ -5,6 +5,7 @@ import asyncio
5
from helpers.log import LogItem
6
from helpers import log
7
import math
8
+import uuid
9
10
11
class LogForStream(Extension):
@@ -19,6 +20,7 @@ class LogForStream(Extension):
20
self.agent.context.log.log(
21
type="agent",
22
heading=build_default_heading(self.agent),
23
+ id=str(uuid.uuid4()),
24
)
25
)
26
extensions/python/response_stream/_20_live_response.py
+4
@@ -30,10 +30,14 @@ class LiveResponse(Extension):
30
31
# create log message and store it in loop data temporary params
32
if "log_item_response" not in loop_data.params_temporary:
33
+ # Share id with the agent log item so branching covers the response bubble
34
+ gen_item = loop_data.params_temporary.get("log_item_generating")
35
+ shared_id = gen_item.id if gen_item and gen_item.id else ""
36
loop_data.params_temporary["log_item_response"] = (
37
self.agent.context.log.log(
38
type="response",
39
heading=f"icon://chat {self.agent.agent_name}: Responding",
40
+ id=shared_id,
41
)
42
)
43
helpers/history.py
+9
-6
@@ -4,6 +4,7 @@ from collections import OrderedDict
4
from collections.abc import Mapping
5
import json
6
import math
7
+import uuid
8
from typing import Coroutine, Literal, TypedDict, cast, Union, Dict, List, Any
9
from helpers import messages, tokens, settings, call_llm
10
from enum import Enum
@@ -81,7 +82,8 @@ class Record:
82
83
84
class Message(Record):
84
- def __init__(self, ai: bool, content: MessageContent, tokens: int = 0):
85
+ def __init__(self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""):
86
+ self.id = id or str(uuid.uuid4())
87
self.ai = ai
88
self.content = content
89
self.summary: str = ""
@@ -115,6 +117,7 @@ class Message(Record):
117
def to_dict(self):
118
return {
119
"_cls": "Message",
120
+ "id": self.id,
121
"ai": self.ai,
122
"content": self.content,
123
"summary": self.summary,
@@ -124,7 +127,7 @@ class Message(Record):
127
@staticmethod
128
def from_dict(data: dict, history: "History"):
129
content = data.get("content", "Content lost")
127
- msg = Message(ai=data["ai"], content=content)
130
+ msg = Message(ai=data["ai"], content=content, id=data.get("id", ""))
131
msg.summary = data.get("summary", "")
132
msg.tokens = data.get("tokens", 0)
133
return msg
@@ -143,9 +146,9 @@ class Topic(Record):
146
return sum(msg.get_tokens() for msg in self.messages)
147
148
def add_message(
146
- self, ai: bool, content: MessageContent, tokens: int = 0
149
+ self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""
150
) -> Message:
148
- msg = Message(ai=ai, content=content, tokens=tokens)
151
+ msg = Message(ai=ai, content=content, tokens=tokens, id=id)
152
self.messages.append(msg)
153
return msg
154
@@ -332,10 +335,10 @@ class History(Record):
335
return self.current.get_tokens()
336
337
def add_message(
335
- self, ai: bool, content: MessageContent, tokens: int = 0
338
+ self, ai: bool, content: MessageContent, tokens: int = 0, id: str = ""
339
) -> Message:
340
self.counter += 1
338
- return self.current.add_message(ai, content=content, tokens=tokens)
341
+ return self.current.add_message(ai, content=content, tokens=tokens, id=id)
342
343
def new_topic(self):
344
if self.current.messages:
helpers/mcp_handler.py
+3
-1
@@ -102,11 +102,13 @@ class MCPTool(Tool):
102
"""MCP Tool wrapper"""
103
104
def get_log_object(self) -> LogItem:
105
+ import uuid
106
return self.agent.context.log.log(
107
type="mcp",
108
heading=f"icon://extension {self.agent.agent_name}: Using MCP tool '{self.name}'",
109
content="",
110
kvps={"tool_name": self.name, **self.args},
111
+ id=str(uuid.uuid4()),
112
)
113
114
async def execute(self, **kwargs: Any):
@@ -198,7 +200,7 @@ class MCPTool(Tool):
200
201
final_text_for_agent = raw_tool_response
202
201
- self.agent.hist_add_tool_result(self.name, final_text_for_agent)
203
+ self.agent.hist_add_tool_result(self.name, final_text_for_agent, id=self.log.id if self.log else "")
204
(
205
PrintStyle(
206
font_color="#1B4F72", background_color="white", padding=True, bold=True
helpers/message_queue.py
+6
-4
@@ -153,8 +153,9 @@ def send_message(context: "AgentContext", item: dict, source: str = " (from queu
153
154
message = item.get("text", "")
155
attachments = item.get("attachments", [])
156
- log_user_message(context, message, attachments, source=source)
157
- context.communicate(UserMessage(message, attachments))
156
+ msg_id = str(uuid.uuid4())
157
+ log_user_message(context, message, attachments, message_id=msg_id, source=source)
158
+ context.communicate(UserMessage(message, attachments, id=msg_id))
159
160
161
def send_next(context: "AgentContext") -> bool:
@@ -183,6 +184,7 @@ def send_all_aggregated(context: "AgentContext") -> int:
184
text = "\n\n---\n\n".join(i["text"] for i in items if i["text"])
185
attachments = [a for i in items for a in i.get("attachments", [])]
186
186
- log_user_message(context, text, attachments, source=" (queued batch)")
187
- context.communicate(UserMessage(text, attachments))
187
+ msg_id = str(uuid.uuid4())
188
+ log_user_message(context, text, attachments, message_id=msg_id, source=" (queued batch)")
189
+ context.communicate(UserMessage(text, attachments, id=msg_id))
190
return len(items)
helpers/task_scheduler.py
+4
-2
@@ -882,19 +882,21 @@ class TaskScheduler:
882
task_prompt = f"## Task:\n{current_task.prompt}"
883
884
# Log the message with message_id and attachments
885
+ msg_id = str(uuid.uuid4())
886
context.log.log(
887
type="user",
888
heading="",
889
content=task_prompt,
890
kvps={"attachments": attachment_filenames},
890
- id=str(uuid.uuid4()),
891
+ id=msg_id,
892
)
893
894
agent.hist_add_user_message(
895
UserMessage(
896
message=task_prompt,
897
system_message=[current_task.system_prompt],
897
- attachments=attachment_filenames))
898
+ attachments=attachment_filenames,
899
+ id=msg_id))
900
901
# Persist after setting up the context but before running the agent
902
# This ensures the task context is saved and can be found by polling
helpers/tool.py
+4
-2
@@ -53,17 +53,19 @@ class Tool:
53
54
async def after_execution(self, response: Response, **kwargs):
55
text = sanitize_string(response.message.strip())
56
- self.agent.hist_add_tool_result(self.name, text, **(response.additional or {}))
56
+ self.agent.hist_add_tool_result(self.name, text, id=self.log.id, **(response.additional or {}))
57
PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}'")
58
PrintStyle(font_color="#85C1E9").print(text)
59
self.log.update(content=text)
60
61
def get_log_object(self):
62
+ import uuid
63
+ pre_id = str(uuid.uuid4())
64
if self.method:
65
heading = f"icon://construction {self.agent.agent_name}: Using tool '{self.name}:{self.method}'"
66
else:
67
heading = f"icon://construction {self.agent.agent_name}: Using tool '{self.name}'"
66
- return self.agent.context.log.log(type="tool", heading=heading, content="", kvps=self.args, _tool_name=self.name)
68
+ return self.agent.context.log.log(type="tool", heading=heading, content="", kvps=self.args, _tool_name=self.name, id=pre_id)
69
70
def nice_key(self, key:str):
71
words = key.split('_')
plugins/_chat_branching/README.md
+17
-15
@@ -4,26 +4,28 @@ Create a new chat from any existing point in a conversation.
4
5
## What It Does
6
7
-This plugin adds an API endpoint that clones an existing chat context, trims its log history up to a selected message, gives the new chat a `(branch)` suffix, persists it immediately, and refreshes connected tabs so the new branch appears in the UI.
7
+Adds a **Branch** button to every chat message. Clicking it clones the current chat up to that message, creating a new conversation you can continue independently.
8
9
-## Main Behavior
9
+## How It Works
10
11
-- **Clone context**
12
- - Serializes the current chat context and deserializes it into a brand-new context with a new ID.
13
-- **Trim history**
14
- - Keeps log entries only up to and including the selected `log_no`.
15
- - Includes a fallback for cases where reloaded logs use sequential array indexes.
16
-- **Persist immediately**
17
- - Saves the newly created branched chat to temporary chat storage.
18
-- **Refresh UI state**
19
- - Marks state dirty for all tabs after the branch is created.
11
+1. **ID-based log ↔ history linking**
12
+ Every `LogItem` and `history.Message` share a UUID generated at creation time. The branch button is only shown on messages that carry this ID.
13
+
14
+2. **Clone & trim**
15
+ - Serializes the source context → deserializes into a new context with a fresh ID.
16
+ - Walks log entries: keeps everything up to the selected `log_no`, discards the rest.
17
+ - Collects the IDs of kept entries and uses them to trim `history.messages` so log and history stay consistent.
18
+
19
+3. **Persist & refresh**
20
+ - Saves the branched chat immediately.
21
+ - Marks UI state dirty so all connected tabs see the new branch.
22
23
## Entry Points
24
23
-- **API**
24
- - `api/branch_chat.py` implements the branching operation.
25
-- **Extensions**
26
- - `extensions/` contains integration glue for making the feature available in the app.
25
+| Path | Purpose |
26
+|---|---|
27
+| `api/branch_chat.py` | API endpoint — clone, trim, persist |
28
+| `extensions/webui/set_messages_after_loop/inject-branch-buttons.js` | Injects the Branch button into each message DOM element |
29
30
## Plugin Metadata
31
plugins/_chat_branching/api/branch_chat.py
+56
-36
@@ -9,55 +9,69 @@ from helpers.persist_chat import (
9
)
10
from agent import AgentContext
11
12
-# Log types that produce a history message (via hist_add_* in Agent)
13
-_HIST_LOG_TYPES = {"user", "agent", "tool", "warning"}
12
13
+def _trim_history_json(history_json: str, kept_ids: set[str], after_cut_ids: set[str]) -> str:
14
+ """Trim a serialized history JSON to keep only messages that appear
15
+ before the branch cut point.
16
16
-def _count_history_msgs(logs, agent_no):
17
- """Count log items that produced history messages for a specific agent."""
18
- return sum(
19
- 1 for item in logs
20
- if item["type"] in _HIST_LOG_TYPES and item.get("agentno", 0) == agent_no
21
- )
17
+ *kept_ids*: IDs of log items up to and including the cut point.
18
+ *after_cut_ids*: IDs of log items after the cut point.
19
23
-
24
-def _trim_history_json(history_json, keep_count):
25
- """Trim a serialized history JSON string to keep only the first
26
- *keep_count* un-summarized messages. Summarized bulks/topics are
27
- always preserved (they represent older history before any cut point)."""
20
+ Walk messages in order. A message is kept while the running state
21
+ is "keep". The state flips to "drop" the first time we encounter
22
+ a message whose id is in *after_cut_ids*. Messages whose id does
23
+ not appear in any log set (unpaired) inherit the current state.
24
+ Summarized topics/bulks are always preserved.
25
+ """
26
if not history_json:
27
return history_json
30
- hist = json.loads(history_json)
28
32
- # Flatten messages from topics + current, count and trim
33
- remaining = keep_count
29
+ hist = json.loads(history_json)
30
+ keep = True # running state
31
+
32
+ def filter_messages(messages: list[dict]) -> list[dict]:
33
+ nonlocal keep
34
+ result = []
35
+ for msg in messages:
36
+ mid = msg.get("id", "")
37
+ if mid and mid in after_cut_ids:
38
+ keep = False
39
+ elif mid and mid in kept_ids:
40
+ keep = True
41
+ # else: unpaired – inherit current state
42
+ if keep:
43
+ result.append(msg)
44
+ return result
45
+
46
+ # Bulks are already summarized old history – always keep
47
+ # Topics: keep summarized ones; filter unsummarized
48
trimmed_topics = []
35
-
49
for topic in hist.get("topics", []):
50
if topic.get("summary"):
38
- # Summarized topic — keep whole, don't count
51
trimmed_topics.append(topic)
52
continue
41
- msgs = topic.get("messages", [])
42
- if remaining >= len(msgs):
53
+ msgs = filter_messages(topic.get("messages", []))
54
+ if msgs:
55
+ topic["messages"] = msgs
56
trimmed_topics.append(topic)
44
- remaining -= len(msgs)
45
- elif remaining > 0:
46
- topic["messages"] = msgs[:remaining]
47
- trimmed_topics.append(topic)
48
- remaining = 0
49
- # else: drop entirely
50
-
57
+ if not keep:
58
+ break
59
hist["topics"] = trimmed_topics
60
53
- # Trim current topic
61
+ # Current topic
62
current = hist.get("current", {})
55
- if not current.get("summary"):
56
- msgs = current.get("messages", [])
57
- if remaining < len(msgs):
58
- current["messages"] = msgs[:remaining]
63
+ if not current.get("summary") and keep:
64
+ current["messages"] = filter_messages(current.get("messages", []))
65
+ elif not keep:
66
+ current["messages"] = []
67
+ hist["current"] = current
68
+
69
+ # Recount
70
+ total = sum(
71
+ len(t.get("messages", [])) for t in hist["topics"] if not t.get("summary")
72
+ ) + len(hist.get("current", {}).get("messages", []))
73
+ hist["counter"] = total
74
60
- hist["counter"] = keep_count
75
return json.dumps(hist, ensure_ascii=False)
76
77
@@ -98,12 +112,18 @@ class BranchChat(ApiHandler):
112
return Response("log_no not found in chat log", 400)
113
114
kept_logs = src_logs[: cut_idx + 1]
115
+ after_logs = src_logs[cut_idx + 1 :]
116
data["log"]["logs"] = kept_logs
117
103
- # Trim agent history to match the log cut point
118
+ # Build ID sets for history trimming
119
+ kept_ids = {item["id"] for item in kept_logs if item.get("id")}
120
+ after_cut_ids = {item["id"] for item in after_logs if item.get("id")}
121
+
122
+ # Trim each agent's history using ID matching
123
for ag in data.get("agents", []):
105
- msg_count = _count_history_msgs(kept_logs, ag["number"])
106
- ag["history"] = _trim_history_json(ag.get("history", ""), msg_count)
124
+ ag["history"] = _trim_history_json(
125
+ ag.get("history", ""), kept_ids, after_cut_ids
126
+ )
127
128
# Give the branch a distinguishable name
129
src_name = data.get("name") or "Chat"
plugins/_chat_branching/extensions/webui/set_messages_after_loop/inject-branch-buttons.js
+1
-1
@@ -9,7 +9,7 @@ export default async function injectBranchButtons(context) {
9
if (!context?.results?.length) return;
10
11
for (const { args, result } of context.results) {
12
- if (!result?.element || args.no == null) continue;
12
+ if (!result?.element || args.no == null || !args.id) continue;
13
14
const logNo = args.no;
15
for (const bar of result.element.querySelectorAll(".step-action-buttons")) {
plugins/_code_execution/tools/code_execution_tool.py
+3
-1
@@ -71,11 +71,13 @@ class CodeExecution(Tool):
71
return Response(message=response, break_loop=False)
72
73
def get_log_object(self):
74
+ import uuid
75
return self.agent.context.log.log(
76
type="code_exe",
77
heading=self.get_heading(),
78
content="",
79
kvps=self.args,
80
+ id=str(uuid.uuid4()),
81
)
82
83
def get_heading(self, text: str = ""):
@@ -86,7 +88,7 @@ class CodeExecution(Tool):
88
return f"icon://terminal {session_text}{truncate_text_string(text, 200)}"
89
90
async def after_execution(self, response, **kwargs):
89
- self.agent.hist_add_tool_result(self.name, response.message, **(response.additional or {}))
91
+ self.agent.hist_add_tool_result(self.name, response.message, id=self.log.id if self.log else "", **(response.additional or {}))
92
93
async def prepare_state(self, cfg: dict, reset=False, session: int | None = None):
94
self.state: State | None = self.agent.get_data("_cet_state")
plugins/_code_execution/tools/input.py
+3
-2
@@ -19,7 +19,8 @@ class Input(Tool):
19
return await cet.execute(**args)
20
21
def get_log_object(self):
22
- return self.agent.context.log.log(type="code_exe", heading=f"icon://keyboard {self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args)
22
+ import uuid
23
+ return self.agent.context.log.log(type="code_exe", heading=f"icon://keyboard {self.agent.agent_name}: Using tool '{self.name}'", content="", kvps=self.args, id=str(uuid.uuid4()))
24
25
async def after_execution(self, response, **kwargs):
25
- self.agent.hist_add_tool_result(self.name, response.message, **(response.additional or {}))
\ No newline at end of file
26
+ self.agent.hist_add_tool_result(self.name, response.message, id=self.log.id if self.log else "", **(response.additional or {}))
\ No newline at end of file
plugins/_email_integration/helpers/handler.py
+7
-2
@@ -8,6 +8,7 @@ import asyncio
8
import base64
9
import json
10
import os
11
+import uuid
12
13
from agent import Agent, AgentContext, AgentContextType, UserMessage
14
from helpers import guids, plugins, files, runtime
@@ -285,11 +286,13 @@ async def _start_new_chat(agent: Agent, handler_cfg: dict, msg: InboundMessage):
286
user_msg = _build_user_message(agent, msg, handler_cfg)
287
system_ctx = agent.read_prompt("fw.email.system_context.md")
288
288
- mq.log_user_message(context, user_msg, msg.attachments or [], source=" (email)")
289
+ msg_id = str(uuid.uuid4())
290
+ mq.log_user_message(context, user_msg, msg.attachments or [], message_id=msg_id, source=" (email)")
291
context.communicate(UserMessage(
292
message=user_msg,
293
system_message=[system_ctx],
294
attachments=msg.attachments,
295
+ id=msg_id,
296
))
297
298
PrintStyle.success(f"Email: new chat {context.id} for '{msg.subject}' from {msg.sender}")
@@ -322,10 +325,12 @@ async def _route_to_chat(
325
context.data[disp.CTX_EMAIL_REFERENCES] = " ".join(refs_list)
326
327
user_msg = _build_user_message(agent, msg, handler_cfg)
325
- mq.log_user_message(context, user_msg, msg.attachments or [], source=" (email)")
328
+ msg_id = str(uuid.uuid4())
329
+ mq.log_user_message(context, user_msg, msg.attachments or [], message_id=msg_id, source=" (email)")
330
context.communicate(UserMessage(
331
message=user_msg,
332
attachments=msg.attachments,
333
+ id=msg_id,
334
))
335
336
save_tmp_chat(context)
plugins/_error_retry/extensions/python/_functions/agent/Agent/handle_exception/end/_80_retry_critical_exception.py
+4
-1
@@ -35,10 +35,13 @@ class RetryCriticalException(Extension):
35
self.agent.set_data(DATA_NAME_COUNTER, counter + 1)
36
37
error_message = errors.format_error(exception)
38
+ import uuid as _uuid
39
+ msg_id = str(_uuid.uuid4())
40
self.agent.context.log.log(
41
type="warning",
42
heading="Critical error occurred, retrying...",
43
content=error_message,
44
+ id=msg_id,
45
)
46
PrintStyle(font_color="orange", padding=True).print(
47
"Critical error occurred, retrying..."
@@ -48,7 +51,7 @@ class RetryCriticalException(Extension):
51
agent_facing_error = self.agent.read_prompt(
52
"fw.msg_critical_error.md", error_message=error_message
53
)
51
- self.agent.hist_add_warning(message=agent_facing_error)
54
+ self.agent.hist_add_warning(message=agent_facing_error, id=msg_id)
55
PrintStyle(font_color="orange", padding=True).print(agent_facing_error)
56
57
data["exception"] = None
plugins/_infection_check/helpers/checker.py
+5
-1
@@ -328,11 +328,14 @@ class InfectionChecker:
328
return "terminate", "Max clarifications exceeded.", "\n\n".join(cot_parts)
329
330
def _do_terminate(self, agent: "Agent", detail: str, cot: str):
331
+ import uuid as _uuid
332
content = cot or detail or "Malicious behavior detected."
333
+ msg_id = str(_uuid.uuid4())
334
agent.context.log.log(
335
type="warning",
336
heading="Infection check: TERMINATED",
337
content=content,
338
+ id=msg_id,
339
)
340
341
# Replace last AI message with a blocked marker
@@ -341,7 +344,8 @@ class InfectionChecker:
344
if msgs and msgs[-1].ai:
345
msgs.pop()
346
agent.history.add_message(
344
- ai=True, content="[BLOCKED] Response terminated by security policy."
347
+ ai=True, content="[BLOCKED] Response terminated by security policy.",
348
+ id=msg_id,
349
)
350
except Exception:
351
pass
plugins/_telegram_integration/helpers/handler.py
+6
-3
@@ -246,10 +246,12 @@ async def handle_message(message: TgMessage, bot_name: str, bot_cfg: dict):
246
body=text,
247
)
248
249
- mq.log_user_message(context, user_msg, attachments, source=" (telegram)")
249
+ msg_id = str(uuid.uuid4())
250
+ mq.log_user_message(context, user_msg, attachments, message_id=msg_id, source=" (telegram)")
251
context.communicate(UserMessage(
252
message=user_msg,
253
attachments=attachments,
254
+ id=msg_id,
255
))
256
257
save_tmp_chat(context)
@@ -298,8 +300,9 @@ async def handle_callback_query(query: CallbackQuery, bot_name: str, bot_cfg: di
300
body=f"[Button pressed: {text}]",
301
)
302
301
- mq.log_user_message(context, user_msg, [], source=" (telegram)")
302
- context.communicate(UserMessage(message=user_msg))
303
+ msg_id = str(uuid.uuid4())
304
+ mq.log_user_message(context, user_msg, [], message_id=msg_id, source=" (telegram)")
305
+ context.communicate(UserMessage(message=user_msg, id=msg_id))
306
save_tmp_chat(context)
307
308
tools/vision_load.py
+2
-2
@@ -75,7 +75,7 @@ class VisionLoad(Tool):
75
f"Skipped images (max {self._get_max_embeds()} loaded at a time according to model configuration):\n{skipped_summary}"
76
)
77
if self.images_dict:
78
- self.agent.hist_add_tool_result(self.name, summary)
78
+ self.agent.hist_add_tool_result(self.name, summary, id=self.log.id if self.log else "")
79
for path, image in self.images_dict.items():
80
if image:
81
content.append(
@@ -97,7 +97,7 @@ class VisionLoad(Tool):
97
False, content=msg, tokens=TOKENS_ESTIMATE * len(content)
98
)
99
else:
100
- self.agent.hist_add_tool_result(self.name, summary if self.skipped_paths else "No images processed")
100
+ self.agent.hist_add_tool_result(self.name, summary if self.skipped_paths else "No images processed", id=self.log.id if self.log else "")
101
102
# print and log short version
103
message = (