message rendering polishing

frdel committed Feb 4, 2026 at 08:47 UTC fe46f045fef64ac83d9f0edc2b79115f34eb25d7
9 files changed +454 -393
agent.py
+1 -3
@@ -590,7 +590,7 @@ class Agent:
590 error_message = errors.format_error(e)
591
592 self.context.log.log(
593 - type="warning", content="Critical error occurred, retrying..."
593 + type="warning", heading="Critical error occurred, retrying...", content=error_message
594 )
595 PrintStyle(font_color="orange", padding=True).print(
596 "Critical error occurred, retrying..."
@@ -626,9 +626,7 @@ class Agent:
626 PrintStyle(font_color="red", padding=True).print(error_message)
627 self.context.log.log(
628 type="error",
629 - heading="Error",
629 content=error_message,
631 - kvps={"text": error_text},
630 )
631 PrintStyle(font_color="red", padding=True).print(
632 f"{self.agent_name}: {error_text}"
python/extensions/monologue_end/_50_memorize_fragments.py
+144 -146
@@ -1,5 +1,5 @@
1 import asyncio
2 -from python.helpers import settings
2 +from python.helpers import settings, errors
3 from python.helpers.extension import Extension
4 from python.helpers.memory import Memory
5 from python.helpers.dirty_json import DirtyJson
@@ -33,168 +33,166 @@ class MemorizeMemories(Extension):
33
34 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
35
36 + try:
37 + set = settings.get_settings()
38
37 - await asyncio.sleep(15)
38 -
39 - set = settings.get_settings()
40 -
41 - db = await Memory.get(self.agent)
42 -
43 - # get system message and chat history for util llm
44 - system = self.agent.read_prompt("memory.memories_sum.sys.md")
45 - msgs_text = self.agent.concat_messages(self.agent.history)
46 -
47 - # # log query streamed by LLM
48 - # async def log_callback(content):
49 - # log_item.stream(content=content)
50 -
51 - # call util llm to find info in history
52 - memories_json = await self.agent.call_utility_model(
53 - system=system,
54 - message=msgs_text,
55 - # callback=log_callback,
56 - background=True,
57 - )
58 -
59 - # log data < no need for streaming utility messages
60 - log_item.update(content=memories_json)
61 -
62 - # Add validation and error handling for memories_json
63 - if not memories_json or not isinstance(memories_json, str):
64 - log_item.update(heading="No response from utility model.")
65 - return
39 + db = await Memory.get(self.agent)
40
67 - # Strip any whitespace that might cause issues
68 - memories_json = memories_json.strip()
41 + # get system message and chat history for util llm
42 + system = self.agent.read_prompt("memory.memories_sum.sys.md")
43 + msgs_text = self.agent.concat_messages(self.agent.history)
44
70 - if not memories_json:
71 - log_item.update(heading="Empty response from utility model.")
72 - return
45 + # # log query streamed by LLM
46 + # async def log_callback(content):
47 + # log_item.stream(content=content)
48
74 - try:
75 - memories = DirtyJson.parse_string(memories_json)
76 - except Exception as e:
77 - log_item.update(heading=f"Failed to parse memories response: {str(e)}")
78 - return
49 + # call util llm to find info in history
50 + memories_json = await self.agent.call_utility_model(
51 + system=system,
52 + message=msgs_text,
53 + # callback=log_callback,
54 + background=True,
55 + )
56
80 - # Validate that memories is a list or convertible to one
81 - if memories is None:
82 - log_item.update(heading="No valid memories found in response.")
83 - return
57 + # log data < no need for streaming utility messages
58 + log_item.update(content=memories_json)
59
85 - # If memories is not a list, try to make it one
86 - if not isinstance(memories, list):
87 - if isinstance(memories, (str, dict)):
88 - memories = [memories]
89 - else:
90 - log_item.update(heading="Invalid memories format received.")
60 + # Add validation and error handling for memories_json
61 + if not memories_json or not isinstance(memories_json, str):
62 + log_item.update(heading="No response from utility model.")
63 return
64
93 - if not isinstance(memories, list) or len(memories) == 0:
94 - log_item.update(heading="No useful information to memorize.")
95 - return
96 - else:
97 - memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
98 - log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
99 -
100 - # Process memories with intelligent consolidation
101 - total_processed = 0
102 - total_consolidated = 0
103 - rem = []
65 + # Strip any whitespace that might cause issues
66 + memories_json = memories_json.strip()
67
105 - for memory in memories:
106 - # Convert memory to plain text
107 - txt = f"{memory}"
68 + if not memories_json:
69 + log_item.update(heading="Empty response from utility model.")
70 + return
71
109 - if set["memory_memorize_consolidation"]:
110 -
111 - try:
112 - # Use intelligent consolidation system
113 - from python.helpers.memory_consolidation import create_memory_consolidator
114 - consolidator = create_memory_consolidator(
115 - self.agent,
116 - similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
117 - max_similar_memories=8,
118 - max_llm_context_memories=4
119 - )
72 + try:
73 + memories = DirtyJson.parse_string(memories_json)
74 + except Exception as e:
75 + log_item.update(heading=f"Failed to parse memories response: {str(e)}")
76 + return
77
121 - # Create memory item-specific log for detailed tracking
122 - memory_log = None # too many utility messages, skip log for now
123 - # memory_log = self.agent.context.log.log(
124 - # type="util",
125 - # heading=f"Processing memory fragment: {txt[:50]}...",
126 - # update_progress="none" # Don't affect status bar
127 - # )
128 -
129 - # Process with intelligent consolidation
130 - result_obj = await consolidator.process_new_memory(
131 - new_memory=txt,
132 - area=Memory.Area.FRAGMENTS.value,
133 - metadata={"area": Memory.Area.FRAGMENTS.value},
134 - log_item=memory_log
135 - )
78 + # Validate that memories is a list or convertible to one
79 + if memories is None:
80 + log_item.update(heading="No valid memories found in response.")
81 + return
82
137 - # Update the individual log item with completion status but keep it temporary
138 - if result_obj.get("success"):
139 - total_consolidated += 1
140 - if memory_log:
141 - memory_log.update(
142 - result="Fragment processed successfully",
143 - heading=f"Memory fragment completed: {txt[:50]}...",
144 - update_progress="none" # Show briefly then disappear
145 - )
146 - else:
147 - if memory_log:
148 - memory_log.update(
149 - result="Fragment processing failed",
150 - heading=f"Memory fragment failed: {txt[:50]}...",
151 - update_progress="none" # Show briefly then disappear
152 - )
153 - total_processed += 1
154 -
155 - except Exception as e:
156 - # Log error but continue processing
157 - log_item.update(consolidation_error=str(e))
158 - total_processed += 1
159 -
160 - # Update final results with structured logging
161 - log_item.update(
162 - heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
163 - memories=memories_txt,
164 - result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
165 - memories_processed=total_processed,
166 - memories_consolidated=total_consolidated,
167 - update_progress="none"
168 - )
83 + # If memories is not a list, try to make it one
84 + if not isinstance(memories, list):
85 + if isinstance(memories, (str, dict)):
86 + memories = [memories]
87 + else:
88 + log_item.update(heading="Invalid memories format received.")
89 + return
90
91 + if not isinstance(memories, list) or len(memories) == 0:
92 + log_item.update(heading="No useful information to memorize.")
93 + return
94 else:
95 + memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
96 + log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
97 +
98 + # Process memories with intelligent consolidation
99 + total_processed = 0
100 + total_consolidated = 0
101 + rem = []
102 +
103 + for memory in memories:
104 + # Convert memory to plain text
105 + txt = f"{memory}"
106 +
107 + if set["memory_memorize_consolidation"]:
108 +
109 + try:
110 + # Use intelligent consolidation system
111 + from python.helpers.memory_consolidation import create_memory_consolidator
112 + consolidator = create_memory_consolidator(
113 + self.agent,
114 + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
115 + max_similar_memories=8,
116 + max_llm_context_memories=4
117 + )
118 +
119 + # Create memory item-specific log for detailed tracking
120 + memory_log = None # too many utility messages, skip log for now
121 + # memory_log = self.agent.context.log.log(
122 + # type="util",
123 + # heading=f"Processing memory fragment: {txt[:50]}...",
124 + # update_progress="none" # Don't affect status bar
125 + # )
126 +
127 + # Process with intelligent consolidation
128 + result_obj = await consolidator.process_new_memory(
129 + new_memory=txt,
130 + area=Memory.Area.FRAGMENTS.value,
131 + metadata={"area": Memory.Area.FRAGMENTS.value},
132 + log_item=memory_log
133 + )
134 +
135 + # Update the individual log item with completion status but keep it temporary
136 + if result_obj.get("success"):
137 + total_consolidated += 1
138 + if memory_log:
139 + memory_log.update(
140 + result="Fragment processed successfully",
141 + heading=f"Memory fragment completed: {txt[:50]}...",
142 + update_progress="none" # Show briefly then disappear
143 + )
144 + else:
145 + if memory_log:
146 + memory_log.update(
147 + result="Fragment processing failed",
148 + heading=f"Memory fragment failed: {txt[:50]}...",
149 + update_progress="none" # Show briefly then disappear
150 + )
151 + total_processed += 1
152 +
153 + except Exception as e:
154 + # Log error but continue processing
155 + log_item.update(consolidation_error=str(e))
156 + total_processed += 1
157 +
158 + # Update final results with structured logging
159 + log_item.update(
160 + heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
161 + memories=memories_txt,
162 + result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
163 + memories_processed=total_processed,
164 + memories_consolidated=total_consolidated,
165 + update_progress="none"
166 + )
167
172 - # remove previous fragments too similiar to this one
173 - if set["memory_memorize_replace_threshold"] > 0:
174 - rem += await db.delete_documents_by_query(
175 - query=txt,
176 - threshold=set["memory_memorize_replace_threshold"],
177 - filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
168 + else:
169 +
170 + # remove previous fragments too similiar to this one
171 + if set["memory_memorize_replace_threshold"] > 0:
172 + rem += await db.delete_documents_by_query(
173 + query=txt,
174 + threshold=set["memory_memorize_replace_threshold"],
175 + filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
176 + )
177 + if rem:
178 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
179 + log_item.update(replaced=rem_txt)
180 +
181 + # insert new memory
182 + await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
183 +
184 + log_item.update(
185 + result=f"{len(memories)} entries memorized.",
186 + heading=f"{len(memories)} entries memorized.",
187 )
188 if rem:
180 - rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
181 - log_item.update(replaced=rem_txt)
182 -
183 - # insert new memory
184 - await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
185 -
186 - log_item.update(
187 - result=f"{len(memories)} entries memorized.",
188 - heading=f"{len(memories)} entries memorized.",
189 - )
190 - if rem:
191 - log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
192 -
189 + log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
190 +
191
192
193
196 - # except Exception as e:
197 - # err = errors.format_error(e)
198 - # self.agent.context.log.log(
199 - # type="error", heading="Memorize memories extension error:", content=err
200 - # )
194 + except Exception as e:
195 + err = errors.format_error(e)
196 + self.agent.context.log.log(
197 + type="warning", heading="Memorize memories extension error", content=err
198 + )
python/extensions/monologue_end/_51_memorize_solutions.py
+147 -147
@@ -1,5 +1,5 @@
1 import asyncio
2 -from python.helpers import settings
2 +from python.helpers import settings, errors
3 from python.helpers.extension import Extension
4 from python.helpers.memory import Memory
5 from python.helpers.dirty_json import DirtyJson
@@ -31,171 +31,171 @@ class MemorizeSolutions(Extension):
31 return task
32
33 async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
34 + try:
35 + set = settings.get_settings()
36
35 - set = settings.get_settings()
36 -
37 - db = await Memory.get(self.agent)
38 -
39 - # get system message and chat history for util llm
40 - system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41 - msgs_text = self.agent.concat_messages(self.agent.history)
37 + db = await Memory.get(self.agent)
38
43 - # log query streamed by LLM
44 - # async def log_callback(content):
45 - # log_item.stream(content=content)
39 + # get system message and chat history for util llm
40 + system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41 + msgs_text = self.agent.concat_messages(self.agent.history)
42
47 - # call util llm to find solutions in history
48 - solutions_json = await self.agent.call_utility_model(
49 - system=system,
50 - message=msgs_text,
51 - # callback=log_callback,
52 - background=True,
53 - )
43 + # log query streamed by LLM
44 + # async def log_callback(content):
45 + # log_item.stream(content=content)
46
55 - # log query < no need for streaming utility messages
56 - log_item.update(content=solutions_json)
47 + # call util llm to find solutions in history
48 + solutions_json = await self.agent.call_utility_model(
49 + system=system,
50 + message=msgs_text,
51 + # callback=log_callback,
52 + background=True,
53 + )
54
55 + # log query < no need for streaming utility messages
56 + log_item.update(content=solutions_json)
57
58
60 - # Add validation and error handling for solutions_json
61 - if not solutions_json or not isinstance(solutions_json, str):
62 - log_item.update(heading="No response from utility model.")
63 - return
59
65 - # Strip any whitespace that might cause issues
66 - solutions_json = solutions_json.strip()
60 + # Add validation and error handling for solutions_json
61 + if not solutions_json or not isinstance(solutions_json, str):
62 + log_item.update(heading="No response from utility model.")
63 + return
64
68 - if not solutions_json:
69 - log_item.update(heading="Empty response from utility model.")
70 - return
65 + # Strip any whitespace that might cause issues
66 + solutions_json = solutions_json.strip()
67
72 - try:
73 - solutions = DirtyJson.parse_string(solutions_json)
74 - except Exception as e:
75 - log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76 - return
68 + if not solutions_json:
69 + log_item.update(heading="Empty response from utility model.")
70 + return
71
78 - # Validate that solutions is a list or convertible to one
79 - if solutions is None:
80 - log_item.update(heading="No valid solutions found in response.")
81 - return
72 + try:
73 + solutions = DirtyJson.parse_string(solutions_json)
74 + except Exception as e:
75 + log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76 + return
77
83 - # If solutions is not a list, try to make it one
84 - if not isinstance(solutions, list):
85 - if isinstance(solutions, (str, dict)):
86 - solutions = [solutions]
87 - else:
88 - log_item.update(heading="Invalid solutions format received.")
78 + # Validate that solutions is a list or convertible to one
79 + if solutions is None:
80 + log_item.update(heading="No valid solutions found in response.")
81 return
82
91 - if not isinstance(solutions, list) or len(solutions) == 0:
92 - log_item.update(heading="No successful solutions to memorize.")
93 - return
94 - else:
95 - solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96 - log_item.update(
97 - heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98 - )
83 + # If solutions is not a list, try to make it one
84 + if not isinstance(solutions, list):
85 + if isinstance(solutions, (str, dict)):
86 + solutions = [solutions]
87 + else:
88 + log_item.update(heading="Invalid solutions format received.")
89 + return
90
100 - # Process solutions with intelligent consolidation
101 - total_processed = 0
102 - total_consolidated = 0
103 - rem = []
104 -
105 - for solution in solutions:
106 - # Convert solution to structured text
107 - if isinstance(solution, dict):
108 - problem = solution.get('problem', 'Unknown problem')
109 - solution_text = solution.get('solution', 'Unknown solution')
110 - txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
91 + if not isinstance(solutions, list) or len(solutions) == 0:
92 + log_item.update(heading="No successful solutions to memorize.")
93 + return
94 else:
112 - # If solution is not a dict, convert it to string
113 - txt = f"# Solution\n {str(solution)}"
114 -
115 - if set["memory_memorize_consolidation"]:
116 - try:
117 - # Use intelligent consolidation system
118 - from python.helpers.memory_consolidation import create_memory_consolidator
119 - consolidator = create_memory_consolidator(
120 - self.agent,
121 - similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122 - max_similar_memories=6, # Fewer for solutions (more complex)
123 - max_llm_context_memories=3
124 - )
125 -
126 - # Create solution-specific log for detailed tracking
127 - solution_log = None # too many utility messages, skip log for now
128 - # solution_log = self.agent.context.log.log(
129 - # type="util",
130 - # heading=f"Processing solution: {txt[:50]}...",
131 - # update_progress="none" # Don't affect status bar
132 - # )
133 -
134 - # Process with intelligent consolidation
135 - result_obj = await consolidator.process_new_memory(
136 - new_memory=txt,
137 - area=Memory.Area.SOLUTIONS.value,
138 - metadata={"area": Memory.Area.SOLUTIONS.value},
139 - log_item=solution_log
140 - )
141 -
142 - # Update the individual log item with completion status but keep it temporary
143 - if result_obj.get("success"):
144 - total_consolidated += 1
145 - if solution_log:
146 - solution_log.update(
147 - result="Solution processed successfully",
148 - heading=f"Solution completed: {txt[:50]}...",
149 - update_progress="none" # Show briefly then disappear
150 - )
151 - else:
152 - if solution_log:
153 - solution_log.update(
154 - result="Solution processing failed",
155 - heading=f"Solution failed: {txt[:50]}...",
156 - update_progress="none" # Show briefly then disappear
157 - )
158 - total_processed += 1
159 -
160 - except Exception as e:
161 - # Log error but continue processing
162 - log_item.update(consolidation_error=str(e))
163 - total_processed += 1
164 -
165 - # Update final results with structured logging
95 + solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96 log_item.update(
167 - heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168 - solutions=solutions_txt,
169 - result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170 - solutions_processed=total_processed,
171 - solutions_consolidated=total_consolidated,
172 - update_progress="none"
97 + heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98 )
174 - else:
175 - # remove previous solutions too similiar to this one
176 - if set["memory_memorize_replace_threshold"] > 0:
177 - rem += await db.delete_documents_by_query(
178 - query=txt,
179 - threshold=set["memory_memorize_replace_threshold"],
180 - filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
99 +
100 + # Process solutions with intelligent consolidation
101 + total_processed = 0
102 + total_consolidated = 0
103 + rem = []
104 +
105 + for solution in solutions:
106 + # Convert solution to structured text
107 + if isinstance(solution, dict):
108 + problem = solution.get('problem', 'Unknown problem')
109 + solution_text = solution.get('solution', 'Unknown solution')
110 + txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
111 + else:
112 + # If solution is not a dict, convert it to string
113 + txt = f"# Solution\n {str(solution)}"
114 +
115 + if set["memory_memorize_consolidation"]:
116 + try:
117 + # Use intelligent consolidation system
118 + from python.helpers.memory_consolidation import create_memory_consolidator
119 + consolidator = create_memory_consolidator(
120 + self.agent,
121 + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122 + max_similar_memories=6, # Fewer for solutions (more complex)
123 + max_llm_context_memories=3
124 + )
125 +
126 + # Create solution-specific log for detailed tracking
127 + solution_log = None # too many utility messages, skip log for now
128 + # solution_log = self.agent.context.log.log(
129 + # type="util",
130 + # heading=f"Processing solution: {txt[:50]}...",
131 + # update_progress="none" # Don't affect status bar
132 + # )
133 +
134 + # Process with intelligent consolidation
135 + result_obj = await consolidator.process_new_memory(
136 + new_memory=txt,
137 + area=Memory.Area.SOLUTIONS.value,
138 + metadata={"area": Memory.Area.SOLUTIONS.value},
139 + log_item=solution_log
140 + )
141 +
142 + # Update the individual log item with completion status but keep it temporary
143 + if result_obj.get("success"):
144 + total_consolidated += 1
145 + if solution_log:
146 + solution_log.update(
147 + result="Solution processed successfully",
148 + heading=f"Solution completed: {txt[:50]}...",
149 + update_progress="none" # Show briefly then disappear
150 + )
151 + else:
152 + if solution_log:
153 + solution_log.update(
154 + result="Solution processing failed",
155 + heading=f"Solution failed: {txt[:50]}...",
156 + update_progress="none" # Show briefly then disappear
157 + )
158 + total_processed += 1
159 +
160 + except Exception as e:
161 + # Log error but continue processing
162 + log_item.update(consolidation_error=str(e))
163 + total_processed += 1
164 +
165 + # Update final results with structured logging
166 + log_item.update(
167 + heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168 + solutions=solutions_txt,
169 + result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170 + solutions_processed=total_processed,
171 + solutions_consolidated=total_consolidated,
172 + update_progress="none"
173 + )
174 + else:
175 + # remove previous solutions too similiar to this one
176 + if set["memory_memorize_replace_threshold"] > 0:
177 + rem += await db.delete_documents_by_query(
178 + query=txt,
179 + threshold=set["memory_memorize_replace_threshold"],
180 + filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
181 + )
182 + if rem:
183 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184 + log_item.update(replaced=rem_txt)
185 +
186 + # insert new solution
187 + await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188 +
189 + log_item.update(
190 + result=f"{len(solutions)} solutions memorized.",
191 + heading=f"{len(solutions)} solutions memorized.",
192 )
193 if rem:
183 - rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184 - log_item.update(replaced=rem_txt)
185 -
186 - # insert new solution
187 - await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188 -
189 - log_item.update(
190 - result=f"{len(solutions)} solutions memorized.",
191 - heading=f"{len(solutions)} solutions memorized.",
192 - )
193 - if rem:
194 - log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
194 + log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
195
196
197 - # except Exception as e:
198 - # err = errors.format_error(e)
199 - # self.agent.context.log.log(
200 - # type="error", heading="Memorize solutions extension error:", content=err
201 - # )
197 + except Exception as e:
198 + err = errors.format_error(e)
199 + self.agent.context.log.log(
200 + type="warning", heading="Memorize solutions extension error", content=err
201 + )
python/extensions/monologue_end/_90_waiting_for_input_msg.py
+3
@@ -8,3 +8,6 @@ class WaitingForInputMsg(Extension):
8 if self.agent.number == 0:
9 self.agent.context.log.set_initial_progress()
10
11 + self.agent.context.log.log(
12 + type="hint", heading="Waiting for input...", content="test content", dumy_kvp1=3, dumy_kvp2="test test"
13 + )
python/helpers/errors.py
+14 -4
@@ -1,6 +1,7 @@
1 import re
2 import traceback
3 import asyncio
4 +from typing import Literal
5
6
7 def handle_error(e: Exception):
@@ -13,7 +14,7 @@ def error_text(e: Exception):
14 return str(e)
15
16
16 -def format_error(e: Exception, start_entries=20, end_entries=15):
17 +def format_error(e: Exception, start_entries=20, end_entries=15, error_message_position:Literal["top", "bottom", "none"] = "top"):
18 # format traceback from the provided exception instead of the most recent one
19 traceback_text = ''.join(traceback.format_exception(type(e), e, e.__traceback__))
20 # Split the traceback into lines
@@ -50,13 +51,22 @@ def format_error(e: Exception, start_entries=20, end_entries=15):
51 error_message = line
52 break
53
54 + if error_message and error_message_position in ("top", "bottom", "none"):
55 + for i in range(len(trimmed_lines) - 1, -1, -1):
56 + if trimmed_lines[i].strip() == error_message.strip():
57 + trimmed_lines = trimmed_lines[:i] + trimmed_lines[i + 1 :]
58 + break
59 +
60 # Combine the trimmed traceback with the error message
61 if not trimmed_lines:
55 - result = error_message
62 + result = "" if error_message_position == "none" else error_message
63 else:
64 result = "Traceback (most recent call last):\n" + "\n".join(trimmed_lines)
58 - if error_message:
59 - result += f"\n\n{error_message}"
65 +
66 + if error_message and error_message_position == "top":
67 + result = f"{error_message}\n\n{result}" if result else error_message
68 + elif error_message and error_message_position == "bottom":
69 + result = f"{result}\n\n{error_message}" if result else error_message
70
71 # at least something
72 if not result:
python/helpers/settings.py
+2 -2
@@ -482,7 +482,7 @@ def get_default_settings() -> Settings:
482 return Settings(
483 version=_get_version(),
484 chat_model_provider=get_default_value("chat_model_provider", "openrouter"),
485 - chat_model_name=get_default_value("chat_model_name", "openai/gpt-4.1"),
485 + chat_model_name=get_default_value("chat_model_name", "openai/gpt-5.2-chat"),
486 chat_model_api_base=get_default_value("chat_model_api_base", ""),
487 chat_model_kwargs=get_default_value("chat_model_kwargs", {"temperature": "0"}),
488 chat_model_ctx_length=get_default_value("chat_model_ctx_length", 100000),
@@ -492,7 +492,7 @@ def get_default_settings() -> Settings:
492 chat_model_rl_input=get_default_value("chat_model_rl_input", 0),
493 chat_model_rl_output=get_default_value("chat_model_rl_output", 0),
494 util_model_provider=get_default_value("util_model_provider", "openrouter"),
495 - util_model_name=get_default_value("util_model_name", "openai/gpt-4.1-mini"),
495 + util_model_name=get_default_value("util_model_name", "google/gemini-3-flash-preview"),
496 util_model_api_base=get_default_value("util_model_api_base", ""),
497 util_model_ctx_length=get_default_value("util_model_ctx_length", 100000),
498 util_model_ctx_input=get_default_value("util_model_ctx_input", 0.7),
webui/css/messages.css
+18
@@ -286,6 +286,24 @@
286 margin-bottom: var(--spacing-sm);
287 }
288
289 +/* Warning messages styled like process groups */
290 +.message-warning .msg-heading {
291 + display: flex;
292 + align-items: center;
293 + gap: 6px;
294 + margin-bottom: var(--spacing-xs);
295 +}
296 +
297 +.message-warning .msg-heading h4 {
298 + display: flex;
299 + align-items: center;
300 + font-size: var(--font-size-medium);
301 + font-weight: 500;
302 + color: var(--color-warning-text);
303 + opacity: 0.9;
304 + margin-bottom: var(--spacing-sm);
305 +}
306 +
307 /* Terminal styling moved to new terminal block above */
308
309 /* Agent and AI Info */
webui/index.css
+4
@@ -24,6 +24,7 @@
24 --color-input-focus-dark: #101010;
25 --color-chat-background-dark: #212121;
26 --color-error-text-dark:#e72323;
27 + --color-warning-text-dark:#e79c23;
28 --color-table-row-dark: #272727;
29
30 /* Light mode */
@@ -42,6 +43,7 @@
43 --color-input-focus-light: #dadada;
44 --color-chat-background-light: #fafafa;
45 --color-error-text-light:#920000;
46 + --color-warning-text-light:#936214;
47 --color-table-row-light: #edededf3;
48
49
@@ -62,6 +64,7 @@
64 --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
65 --color-chat-background: var(--color-chat-background-dark);
66 --color-error-text:var(--color-error-text-dark);
67 + --color-warning-text:var(--color-warning-text-dark);
68 --color-table-row: var(--color-table-row-dark);
69
70 /* Spacing variables */
@@ -108,6 +111,7 @@
111 --color-background-hover: color-mix(in srgb, var(--color-border) 50%, transparent);
112 --color-chat-background: var(--color-chat-background-light);
113 --color-error-text:var(--color-error-text-light);
114 + --color-warning-text:var(--color-warning-text-light);
115 --color-table-row: var(--color-table-row-light);
116 }
117
webui/js/messages.js
+121 -91
@@ -15,9 +15,9 @@ import { Scroller } from "./scroller.js";
15
16 // Delay before collapsing previous steps when a new step is added
17 const STEP_COLLAPSE_DELAY = {
18 - "agent": 2000,
19 - "other": 4000, // tools should stay longer as next gen step is placed quickly
20 -}
18 + agent: 2000,
19 + other: 4000, // tools should stay longer as next gen step is placed quickly
20 +};
21 // delay collapse when hovering
22 const STEP_COLLAPSE_HOVER_DELAY_MS = 5000;
23
@@ -75,10 +75,14 @@ export function setMessages(messages) {
75 const cutoff = isLargeAppend ? Math.max(0, messages.length - 2) : 0;
76 const massRender = historyEmpty || isLargeAppend;
77
78 - const mainScroller = new Scroller(history, { smooth: !massRender, toleranceRem: 4, reapplyDelayMs: 1000 });
78 + const mainScroller = new Scroller(history, {
79 + smooth: !massRender,
80 + toleranceRem: 4,
81 + reapplyDelayMs: 1000,
82 + });
83 +
84 + const results = [];
85
80 - const results = []
81 -
86 // process messages
87 for (let i = 0; i < messages.length; i++) {
88 _massRender = historyEmpty || (isLargeAppend && i < cutoff);
@@ -90,7 +94,7 @@ export function setMessages(messages) {
94
95 const shouldScroll = historyEmpty || !results[results.length - 1]?.dontScroll;
96
93 - if(shouldScroll) mainScroller.reApplyScroll();
97 + if (shouldScroll) mainScroller.reApplyScroll();
98 }
99
100 // entrypoint called from poll/WS communication, this is how all messages are rendered and updated
@@ -246,7 +250,11 @@ function drawProcessStep({
250 const isGroupComplete = isProcessGroupComplete(group);
251
252 // Set start timestamp on group when first step is created
249 - if (isNewStep && !group.hasAttribute("data-start-timestamp") && log.timestamp) {
253 + if (
254 + isNewStep &&
255 + !group.hasAttribute("data-start-timestamp") &&
256 + log.timestamp
257 + ) {
258 group.setAttribute("data-start-timestamp", String(log.timestamp));
259 }
260
@@ -260,10 +268,13 @@ function drawProcessStep({
268 step.setAttribute("data-log-type", log.type);
269 step.setAttribute("data-step-id", id);
270 step.setAttribute("data-agent-number", log.agentno);
263 -
271 +
272 // set timestamp attribute (convert to milliseconds for duration calculation)
273 if (log.timestamp) {
266 - step.setAttribute("data-timestamp", String(Math.round(log.timestamp * 1000)));
274 + step.setAttribute(
275 + "data-timestamp",
276 + String(Math.round(log.timestamp * 1000)),
277 + );
278 }
279
280 // apply step classes
@@ -302,8 +313,14 @@ function drawProcessStep({
313 stepsContainer
314 .querySelectorAll(".process-step.expanded")
315 .forEach((expandedStep) => {
305 - const delay = STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] || STEP_COLLAPSE_DELAY.other;
306 - console.log("collapsing", expandedStep.getAttribute("data-log-type"), delay);
316 + const delay =
317 + STEP_COLLAPSE_DELAY[expandedStep.getAttribute("data-log-type")] ||
318 + STEP_COLLAPSE_DELAY.other;
319 + console.log(
320 + "collapsing",
321 + expandedStep.getAttribute("data-log-type"),
322 + delay,
323 + );
324 scheduleStepCollapse(expandedStep, delay);
325 });
326 step.classList.add("expanded");
@@ -340,7 +357,8 @@ function drawProcessStep({
357 stepDetail,
358 ".process-step-detail-scroll",
359 "div",
343 - "process-step-detail-scroll" );
360 + "process-step-detail-scroll",
361 + );
362
363 // set click handlers
364 setupProcessStepHandlers(step, stepHeader);
@@ -367,7 +385,8 @@ function drawProcessStep({
385
386 // auto-scroller of the step detail
387 const detailScroller = new Scroller(stepDetailScroll, {
370 - smooth: !isMassRender(), toleranceRem: 4
388 + smooth: !isMassRender(),
389 + toleranceRem: 4,
390 }); // scroller for step detail content
391
392 // update KVPs of the step detail
@@ -489,7 +508,8 @@ export function _drawMessage({
508
509 // Update message classes (preserve collapsible state)
510 const preserve = ["message-collapsible", "expanded", "has-overflow"]
492 - .filter((c) => messageDiv.classList.contains(c)).join(" ");
511 + .filter((c) => messageDiv.classList.contains(c))
512 + .join(" ");
513 messageDiv.className = `message ${mainClass} ${messageClasses.join(" ")} ${preserve}`;
514
515 // Handle heading (important for error/rate_limit messages that show context)
@@ -718,7 +738,7 @@ export function drawMessageResponse({
738 }) {
739 // response of subordinate agent - render as process step
740 if (agentno && agentno > 0) {
721 - const title = getStepTitle(heading, kvps, type);
741 + const title = getStepTitle(heading, content, type);
742 const contentText = String(content ?? "");
743 const actionButtons = contentText.trim()
744 ? [
@@ -773,7 +793,7 @@ export function drawMessageResponse({
793 markdown: true,
794 latex: true,
795 mainClass: "message-agent-response",
776 - smoothStream: false ,// smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode
796 + smoothStream: false, // smooth render disabled, not reliable yet !isMassRender(), // stream smoothly if not in mass render mode
797 });
798
799 // Collapsible with action buttons
@@ -784,7 +804,12 @@ export function drawMessageResponse({
804 createActionButton("copy", "", () => copyToClipboard(responseText)),
805 ].filter(Boolean)
806 : [];
787 - setupCollapsible(messageDiv, ":scope > .step-action-buttons", !isMassRender(), responseActionButtons);
807 + setupCollapsible(
808 + messageDiv,
809 + ":scope > .step-action-buttons",
810 + !isMassRender(),
811 + responseActionButtons,
812 + );
813
814 if (group) updateProcessGroupHeader(group);
815
@@ -798,7 +823,6 @@ export function drawMessageUser({
823 kvps = null,
824 ...additional
825 }) {
801 -
826 // end last process group on any user message
827 completeLastProcessGroup();
828
@@ -940,11 +964,10 @@ export function drawMessageTool({
964 agentno = 0,
965 ...additional
966 }) {
943 -
967 const tool_name = kvps?._tool_name || "";
968
946 - if(!tool_name){
947 - return drawMessageToolSimple({ ...arguments[0] });
969 + if (!tool_name) {
970 + return drawMessageToolSimple({ ...arguments[0] });
971 } else if (kvps._tool_name === "skills_tool") {
972 return drawMessageToolSimple({ ...arguments[0], code: "SKL" });
973 } else if (kvps._tool_name === "vision_load") {
@@ -958,7 +981,6 @@ export function drawMessageTool({
981 } else {
982 return drawMessageToolSimple({ ...arguments[0] });
983 }
961 -
984 }
985
986 export function drawMessageToolSimple({
@@ -1249,7 +1271,6 @@ export function drawMessageUtil({
1271 allowCompletedGroup: true,
1272 });
1273
1252 -
1274 result.dontScroll = !preferencesStore.showUtils;
1275 return result;
1276 }
@@ -1264,7 +1285,7 @@ export function drawMessageHint({
1285 agentno = 0,
1286 ...additional
1287 }) {
1267 - const title = getStepTitle(heading, kvps, type);
1288 + const title = getStepTitle(heading, content, type);
1289 const contentText = String(content ?? "");
1290 const actionButtons = contentText.trim()
1291 ? [
@@ -1316,12 +1337,13 @@ export function drawMessageProgress({
1337
1338 export function drawMessageWarning({
1339 id,
1340 + type,
1341 heading,
1342 content,
1343 kvps = null,
1344 ...additional
1345 }) {
1324 - const title = cleanStepTitle(heading || content);
1346 + const title = getStepTitle(heading, content, type);
1347 let displayKvps = { ...kvps };
1348 const contentText = String(content ?? "");
1349 const actionButtons = contentText.trim()
@@ -1331,39 +1353,46 @@ export function drawMessageWarning({
1353 ].filter(Boolean)
1354 : [];
1355
1334 - //TODO: if process group is running, append there instead
1335 - // return drawProcessStep({
1336 - // id,
1337 - // title,
1338 - // code: "WRN",
1339 - // classes: null,
1340 - // kvps: displayKvps,
1341 - // content,
1342 - // // contentClasses: [],
1343 - // log: arguments[0],
1344 - // });
1356 + //if process group is running, append there
1357 + const group = getLastProcessGroup(false);
1358 + if (group) {
1359 + return drawProcessStep({
1360 + id,
1361 + title,
1362 + code: "WRN",
1363 + // classes: null,
1364 + kvps: displayKvps,
1365 + content,
1366 + // contentClasses: [],
1367 + actionButtons,
1368 + log: arguments[0],
1369 + });
1370 + }
1371 +
1372 + // if no process group is running, draw as standalone
1373 return drawStandaloneMessage({
1374 id,
1347 - heading,
1375 + title,
1376 content,
1377 position: "mid",
1378 containerClasses: ["ai-container", "center-container"],
1379 mainClass: "message-warning",
1352 - kvps,
1380 + kvps: displayKvps,
1381 actionButtons,
1382 });
1383 }
1384
1385 export function drawMessageError({
1386 id,
1387 + type,
1388 heading,
1389 content,
1390 kvps = null,
1391 ...additional
1392 }) {
1393 const contentText = String(content ?? "");
1365 - const errorText = kvps?.text || "Error";
1366 - const errorHeading = errorText ? `Error - ${errorText}` : "Error";
1394 + let title = getStepTitle(heading, content, type);
1395 + let displayKvps = { ...kvps };
1396 const actionButtons = [
1397 createActionButton("detail", "", () =>
1398 stepDetailStore.showStepDetail(
@@ -1377,11 +1406,12 @@ export function drawMessageError({
1406
1407 return drawStandaloneMessage({
1408 id,
1380 - heading: errorHeading,
1381 - content,
1409 + heading: title,
1410 + content: contentText,
1411 position: "mid",
1412 containerClasses: ["ai-container", "center-container"],
1413 mainClass: "message-error",
1414 + kvps: displayKvps,
1415 actionButtons,
1416 });
1417 }
@@ -1542,7 +1572,6 @@ function convertFilePaths(str) {
1572 return str.replace(/file:\/\//g, "/download_work_dir_file?path=");
1573 }
1574
1545 -
1575 function escapeHTML(str) {
1576 const escapeChars = {
1577 "&": "&amp;",
@@ -1743,7 +1772,10 @@ function getNestedContainer(parentStep) {
1772 * Schedule a step to collapse after a delay
1773 * Automatically handles cancellation on click and reset on hover
1774 */
1746 -function scheduleStepCollapse(stepElement, delayMs=STEP_COLLAPSE_DELAY.other) {
1775 +function scheduleStepCollapse(
1776 + stepElement,
1777 + delayMs = STEP_COLLAPSE_DELAY.other,
1778 +) {
1779 // skip if any existing timeout for this step
1780 if (stepElement.hasAttribute("data-collapse-timeout-id")) return;
1781 // skip already collapsed steps
@@ -1830,33 +1862,14 @@ function findParentDelegationStep(group, agentno) {
1862 /**
1863 * Get a concise title for a process step
1864 */
1833 -function getStepTitle(heading, kvps, type) {
1865 +function getStepTitle(heading, content, type) {
1866 // Try to get a meaningful title from heading or kvps
1867 if (heading && heading.trim()) {
1836 - return cleanStepTitle(heading, 100);
1837 - }
1838 -
1839 - // For warnings/errors without heading, use content preview as title
1840 - if (type === "warning" || type === "error") {
1841 - // We'll show full content in detail, so just use type as title
1842 - return type === "warning" ? "Warning" : "Error";
1868 + return cleanStepTitle(heading, 60);
1869 }
1870
1845 - if (kvps) {
1846 - // Try common fields for title
1847 - if (kvps.tool_name) {
1848 - const headline = kvps.headline ? cleanStepTitle(kvps.headline, 60) : "";
1849 - return `${kvps.tool_name}${headline ? ": " + headline : ""}`;
1850 - }
1851 - if (kvps.headline) {
1852 - return cleanStepTitle(kvps.headline, 100);
1853 - }
1854 - if (kvps.query) {
1855 - return truncateText(kvps.query, 100);
1856 - }
1857 - if (kvps.thoughts) {
1858 - return truncateText(String(kvps.thoughts), 100);
1859 - }
1871 + if (content && content.trim()) {
1872 + return cleanStepTitle(content, 60);
1873 }
1874
1875 // Fallback: capitalize type (backend is source of truth)
@@ -1898,14 +1911,10 @@ export function convertIcons(html, classes = "") {
1911 */
1912 function cleanStepTitle(text, maxLength = 100) {
1913 if (!text) return "";
1901 - let cleaned = String(text);
1902 -
1903 - // Remove icon:// patterns (e.g., "icon://network_intelligence" or "icon://network_intelligence[Tooltip]")
1904 - cleaned = cleaned.replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, "");
1905 -
1906 - // Trim whitespace
1907 - cleaned = cleaned.trim();
1908 -
1914 + let cleaned = String(text)
1915 + .replace(/icon:\/\/[a-zA-Z0-9_]+(\[(?:\\.|[^\]])*\])?\s*/g, "")
1916 + .replace(/\s+/g, " ")
1917 + .trim();
1918 return truncateText(cleaned, maxLength);
1919 }
1920
@@ -1959,12 +1968,16 @@ function updateProcessGroupHeader(group) {
1968
1969 // Update step count in metrics - All GEN steps from all agents per process group
1970 const stepMetricContainerEl = metricsEl?.querySelector(".metric-steps");
1962 - const stepsMetricValEl = stepMetricContainerEl?.querySelector(".metric-value");
1971 + const stepsMetricValEl =
1972 + stepMetricContainerEl?.querySelector(".metric-value");
1973 if (stepsMetricValEl) {
1964 - let genSteps = group.querySelectorAll('.process-step[data-log-type="agent"]').length;
1974 + let genSteps = group.querySelectorAll(
1975 + '.process-step[data-log-type="agent"]',
1976 + ).length;
1977 genSteps -= 1; // don't count response as step
1978 stepsMetricValEl.textContent = genSteps.toString();
1967 - if (genSteps <= 0) stepMetricContainerEl.classList.add("display-none"); // hide when no steps
1979 + if (genSteps <= 0)
1980 + stepMetricContainerEl.classList.add("display-none"); // hide when no steps
1981 else stepMetricContainerEl.classList.remove("display-none");
1982 }
1983
@@ -1982,8 +1995,8 @@ function updateProcessGroupHeader(group) {
1995 dateStyle: "medium",
1996 timeStyle: "short",
1997 });
1985 - timeMetricContainerEl.title = timeMetricContainerEl.dataset.bsOriginalTitle =
1986 - fullDateTime;
1998 + timeMetricContainerEl.title =
1999 + timeMetricContainerEl.dataset.bsOriginalTitle = fullDateTime;
2000 }
2001 }
2002
@@ -2003,8 +2016,10 @@ function updateProcessGroupHeader(group) {
2016 lastTimestampMs > 0 &&
2017 formatDuration(Math.max(0, lastTimestampMs - firstTimestampMs));
2018
2006 - const durationMetricContainerEl = metricsEl?.querySelector(".metric-duration");
2007 - const durationMetricValEl = durationMetricContainerEl?.querySelector(".metric-value");
2019 + const durationMetricContainerEl =
2020 + metricsEl?.querySelector(".metric-duration");
2021 + const durationMetricValEl =
2022 + durationMetricContainerEl?.querySelector(".metric-value");
2023 if (durationMetricContainerEl && durationMetricValEl && durationText) {
2024 durationMetricValEl.textContent = durationText;
2025 durationMetricContainerEl.classList.remove("display-none");
@@ -2084,11 +2099,21 @@ function ensureChild(parent, selector, tagName, ...classNames) {
2099 }
2100
2101 // Setup collapsible message with expand button and action buttons
2087 -function setupCollapsible(messageDiv, containerSelector, initialExpanded, actionButtons = []) {
2102 +function setupCollapsible(
2103 + messageDiv,
2104 + containerSelector,
2105 + initialExpanded,
2106 + actionButtons = [],
2107 +) {
2108 messageDiv.classList.add("message-collapsible");
2109 messageDiv.classList.toggle("expanded", initialExpanded);
2110
2091 - const container = ensureChild(messageDiv, containerSelector, "div", "step-action-buttons");
2111 + const container = ensureChild(
2112 + messageDiv,
2113 + containerSelector,
2114 + "div",
2115 + "step-action-buttons",
2116 + );
2117 container.textContent = "";
2118
2119 const btn = ensureChild(container, ".expand-btn", "button", "expand-btn");
@@ -2102,8 +2127,8 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action
2127 btn.onclick = () => {
2128 messageDiv.classList.toggle("expanded");
2129 syncBtn();
2105 - messageDiv.classList.contains("expanded")
2106 - || (messageDiv.querySelector(".message-body").scrollTop = 0);
2130 + messageDiv.classList.contains("expanded") ||
2131 + (messageDiv.querySelector(".message-body").scrollTop = 0);
2132 };
2133
2134 actionButtons.filter(Boolean).forEach((b) => container.appendChild(b));
@@ -2111,11 +2136,16 @@ function setupCollapsible(messageDiv, containerSelector, initialExpanded, action
2136 // Detect overflow after render
2137 requestAnimationFrame(() => {
2138 const body = messageDiv.querySelector(".message-body");
2114 - const fontSize = parseFloat(getComputedStyle(body || document.documentElement).fontSize || "16");
2139 + const fontSize = parseFloat(
2140 + getComputedStyle(body || document.documentElement).fontSize || "16",
2141 + );
2142 const maxHeight = messageDiv.classList.contains("expanded")
2143 ? fontSize * 15
2117 - : (body?.clientHeight || 0);
2118 - messageDiv.classList.toggle("has-overflow", (body?.scrollHeight || 0) > maxHeight);
2144 + : body?.clientHeight || 0;
2145 + messageDiv.classList.toggle(
2146 + "has-overflow",
2147 + (body?.scrollHeight || 0) > maxHeight,
2148 + );
2149 });
2150 }
2151
@@ -2129,7 +2159,7 @@ function isMassRender() {
2159 function smoothRender(element, newContent, delay = 350) {
2160 // skip on mass render
2161 if (isMassRender()) {
2132 - element.innerHTML = newContent;
2162 + element.innerHTML = newContent;
2163 return;
2164 }
2165