litellm fixes - message hoarding and missing user message

frdel committed Jul 4, 2025 at 16:58 UTC 73778cdd34d1315838e2f2d78e7676f7aa069985
8 files changed +48 -28
models.py
+3 -1
@@ -278,12 +278,14 @@ class LiteLLMChatWrapper(SimpleChatModel):
278 self,
279 system_message="",
280 user_message="",
281 - messages: List[BaseMessage] = [],
281 + messages: List[BaseMessage]|None = None,
282 response_callback: Callable[[str, str], Awaitable[None]] | None = None,
283 reasoning_callback: Callable[[str, str], Awaitable[None]] | None = None,
284 tokens_callback: Callable[[str, int], Awaitable[None]] | None = None,
285 **kwargs: Any,
286 ) -> Tuple[str, str]:
287 + if not messages:
288 + messages = []
289 # construct messages
290 if system_message:
291 messages.insert(0, SystemMessage(content=system_message))
python/extensions/message_loop_prompts_after/_50_recall_memories.py
+1 -1
@@ -58,7 +58,7 @@ class RecallMemories(Extension):
58 query = await self.agent.call_utility_model(
59 system=system,
60 message=(
61 - loop_data.user_message.output_text() if loop_data.user_message else ""
61 + loop_data.user_message.output_text() if loop_data.user_message else "None"
62 ),
63 callback=log_callback,
64 )
python/extensions/message_loop_prompts_after/_51_recall_solutions.py
+1 -1
@@ -56,7 +56,7 @@ class RecallSolutions(Extension):
56
57 # call util llm to summarize conversation
58 query = await self.agent.call_utility_model(
59 - system=system, message=loop_data.user_message.output_text() if loop_data.user_message else "", callback=log_callback
59 + system=system, message=loop_data.user_message.output_text() if loop_data.user_message else "None", callback=log_callback
60 )
61
62 # get solutions database
python/extensions/monologue_start/_60_rename_chat.py
+3 -1
@@ -13,7 +13,9 @@ class RenameChat(Extension):
13 try:
14 # prepare history
15 history_text = self.agent.history.output_text()
16 - ctx_length = int(self.agent.config.utility_model.ctx_length * 0.3)
16 + ctx_length = min(
17 + int(self.agent.config.utility_model.ctx_length * 0.7), 5000
18 + )
19 history_text = tokens.trim_to_tokens(history_text, ctx_length, "start")
20 # prepare system and user prompt
21 system = self.agent.read_prompt("fw.rename_chat.sys.md")
python/helpers/shell_ssh.py
+1 -1
@@ -41,7 +41,7 @@ class SSHInteractiveSession:
41 allow_agent=False,
42 look_for_keys=False,
43 )
44 - self.shell = self.client.invoke_shell(width=160, height=48)
44 + self.shell = self.client.invoke_shell(width=80, height=40)
45 # self.shell.send(f'PS1="{SSHInteractiveSession.ps1_label}"'.encode())
46 # return
47 while True: # wait for end of initial output
python/tools/code_execution_tool.py
+32 -11
@@ -135,19 +135,22 @@ class CodeExecution(Tool):
135 async def execute_python_code(self, session: int, code: str, reset: bool = False):
136 escaped_code = shlex.quote(code)
137 command = f"ipython -c {escaped_code}"
138 - return await self.terminal_session(session, command, reset)
138 + prefix = "python> "+self.format_command_for_output(code)+"\n\n"
139 + return await self.terminal_session(session, command, reset, prefix)
140
141 async def execute_nodejs_code(self, session: int, code: str, reset: bool = False):
142 escaped_code = shlex.quote(code)
142 - command = f"node /exe/node_eval.js {escaped_code}"
143 - return await self.terminal_session(session, command, reset)
143 + command = f"node /exe/node_eval.js {escaped_code}"
144 + prefix = "node> "+self.format_command_for_output(code)+"\n\n"
145 + return await self.terminal_session(session, command, reset, prefix)
146
147 async def execute_terminal_command(
148 self, session: int, command: str, reset: bool = False
149 ):
148 - return await self.terminal_session(session, command, reset)
150 + prefix = "bash> "+self.format_command_for_output(command)+"\n\n"
151 + return await self.terminal_session(session, command, reset, prefix)
152
150 - async def terminal_session(self, session: int, command: str, reset: bool = False):
153 + async def terminal_session(self, session: int, command: str, reset: bool = False, prefix: str = ""):
154
155 await self.agent.handle_intervention() # wait for intervention and handle it, if paused
156 # try again on lost connection
@@ -181,7 +184,7 @@ class CodeExecution(Tool):
184 PrintStyle(
185 background_color="white", font_color="#1B4F72", bold=True
186 ).print(f"{self.agent.agent_name} code execution output")
184 - return await self.get_terminal_output(session)
187 + return await self.get_terminal_output(session=session, prefix=prefix)
188
189 except Exception as e:
190 if i == 1:
@@ -192,6 +195,19 @@ class CodeExecution(Tool):
195 else:
196 raise e
197
198 + def format_command_for_output(self, command: str):
199 + # truncate long commands
200 + short_cmd = command[:200]
201 + # normalize whitespace for cleaner output
202 + short_cmd = " ".join(short_cmd.split())
203 + # replace any sequence of ', ", or ` with a single '
204 + # short_cmd = re.sub(r"['\"`]+", "'", short_cmd) # no need anymore
205 + # final length
206 + short_cmd = truncate_text_string(short_cmd, 100)
207 + return f"{short_cmd}"
208 +
209 +
210 +
211 async def get_terminal_output(
212 self,
213 session=0,
@@ -201,6 +217,7 @@ class CodeExecution(Tool):
217 dialog_timeout=5, # potential dialog detection timeout
218 max_exec_timeout=180, # hard cap on total runtime
219 sleep_time=0.1,
220 + prefix=""
221 ):
222 # Common shell prompt regex patterns (add more as needed)
223 prompt_patterns = [
@@ -223,6 +240,10 @@ class CodeExecution(Tool):
240 truncated_output = ""
241 got_output = False
242
243 + # if prefix, log right away
244 + if prefix:
245 + self.log.update(content=prefix)
246 +
247 while True:
248 await asyncio.sleep(sleep_time)
249 full_output, partial_output = await self.state.shells[session].read_output(
@@ -238,7 +259,7 @@ class CodeExecution(Tool):
259 # full_output += partial_output # Append new output
260 truncated_output = self.fix_full_output(full_output)
261 heading = self.get_heading_from_output(truncated_output, 0)
241 - self.log.update(content=truncated_output, heading=heading)
262 + self.log.update(content=prefix + truncated_output, heading=heading)
263 last_output_time = now
264 got_output = True
265
@@ -270,7 +291,7 @@ class CodeExecution(Tool):
291 response = truncated_output + "\n\n" + response
292 PrintStyle.warning(sysinfo)
293 heading = self.get_heading_from_output(truncated_output, 0)
273 - self.log.update(content=response, heading=heading)
294 + self.log.update(content=prefix + response, heading=heading)
295 return response
296
297 # Waiting for first output
@@ -281,7 +302,7 @@ class CodeExecution(Tool):
302 )
303 response = self.agent.read_prompt("fw.code.info.md", info=sysinfo)
304 PrintStyle.warning(sysinfo)
284 - self.log.update(content=response)
305 + self.log.update(content=prefix + response)
306 return response
307 else:
308 # Waiting for more output after first output
@@ -294,7 +315,7 @@ class CodeExecution(Tool):
315 response = truncated_output + "\n\n" + response
316 PrintStyle.warning(sysinfo)
317 heading = self.get_heading_from_output(truncated_output, 0)
297 - self.log.update(content=response, heading=heading)
318 + self.log.update(content=prefix + response, heading=heading)
319 return response
320
321 # potential dialog detection
@@ -322,7 +343,7 @@ class CodeExecution(Tool):
343 heading = self.get_heading_from_output(
344 truncated_output, 0
345 )
325 - self.log.update(content=response, heading=heading)
346 + self.log.update(content=prefix + response, heading=heading)
347 return response
348
349 async def reset_terminal(self, session=0, reason: str | None = None):
webui/css/messages.css
+1 -1
@@ -162,7 +162,7 @@
162 display: block;
163 width: 100%;
164 overflow-x: auto;
165 - margin-bottom: 1em;
165 + padding-bottom: 1em;
166 }
167
168 .message-body .message-markdown-table-wrap table {
webui/js/messages.js
+6 -11
@@ -826,20 +826,15 @@ function convertPathsToLinks(str) {
826 }
827
828 function adjustMarkdownRender(element) {
829 - // find all tables in the element
830 - const tables = element.querySelectorAll("table");
829 + // find all tables and code blocks in the element
830 + const elements = element.querySelectorAll("table, code");
831
832 - // wrap each table with a div with class message-markdown-table-wrap
833 - tables.forEach((table) => {
834 - // create wrapper div
832 + // wrap each with a div with class message-markdown-table-wrap
833 + elements.forEach((el) => {
834 const wrapper = document.createElement("div");
835 wrapper.className = "message-markdown-table-wrap";
837 -
838 - // insert wrapper before table in the DOM
839 - table.parentNode.insertBefore(wrapper, table);
840 -
841 - // move table into wrapper
842 - wrapper.appendChild(table);
836 + el.parentNode.insertBefore(wrapper, el);
837 + wrapper.appendChild(el);
838 });
839 }
840