From 44577e048cd67e62df387ef9b759fe5451f873e6 Mon Sep 17 00:00:00 2001 From: doria <93405247+dr-frmr@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:17:24 -0500 Subject: [PATCH] feat: add message-search fallback in dialectic tools for short sessions (#362) --- .../documentation/introduction/overview.mdx | 4 +-- src/utils/agent_tools.py | 35 +++++++++++++++++-- src/utils/clients.py | 20 +++++++++++ 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/docs/v3/documentation/introduction/overview.mdx b/docs/v3/documentation/introduction/overview.mdx index 10ce0775..53cc2d48 100644 --- a/docs/v3/documentation/introduction/overview.mdx +++ b/docs/v3/documentation/introduction/overview.mdx @@ -65,7 +65,7 @@ Honcho has four storage primitives that work together: - **Workspaces** - Top-level containers that isolate different applications or environments - **Peers** - Any entity that persists but changes over time (users, agents, objects, and more) - **Sessions** - Interaction threads between peers with temporal boundaries -- **Messages** - Units of data that trigger reasoning (conversations, events, activity, documents, and more) +- **Messages** - Units of data that trigger reasoning (conversations, events, activity, documents, and more) When you write messages to Honcho, they're stored and processed in the background. Custom reasoning models perform formal logical [_reasoning_](/v3/documentation/core-concepts/reasoning) to generate conclusions about each peer. These conclusions are stored as [_representations_](/v3/documentation/core-concepts/representation) that you can query to provide rich context for your agents. @@ -100,4 +100,4 @@ Welcome to Honcho. We're excited to have you at the frontier of AI with us 🫡. Learn how Honcho reasons about data to build memory - \ No newline at end of file + diff --git a/src/utils/agent_tools.py b/src/utils/agent_tools.py index 691249cb..2e8cf8e5 100644 --- a/src/utils/agent_tools.py +++ b/src/utils/agent_tools.py @@ -1129,7 +1129,34 @@ async def _handle_search_memory(ctx: ToolContext, tool_input: dict[str, Any]) -> mem = Representation.from_documents(documents) total_count = mem.len() if total_count == 0: - return f"No observations found for query '{tool_input['query']}'" + # fallback behavior: if the memory is *empty*, that means we're quite + # early in a workspace/peer/session -- in order to give good answers in + # this stage, and be efficient with tool calls, and make sure the model + # doesn't short-circuit and think there's nothing here, we + # automatically search the message history for relevant information. + query = tool_input["query"] + if ctx.agent_type == "dialectic": + limit = min(tool_input.get("top_k", 20), 20) + snippets = await crud.search_messages( + ctx.db, + workspace_name=ctx.workspace_name, + session_name=ctx.session_name, + query=query, + limit=limit, + context_window=0, + ) + if snippets: + message_output = _format_message_snippets( + snippets, f"for query '{query}'" + ) + return ( + f"No observations yet. Message search results:\n\n{message_output}" + ) + return ( + f"No observations found for query '{query}', and no messages found in " + "history. Try a different phrasing or use grep_messages for exact text." + ) + return f"No observations found for query '{query}'" mem_str = mem.str_with_ids() if ctx.include_observation_ids else str(mem) return f"Found {total_count} observations for query '{tool_input['query']}':\n\n{mem_str}" @@ -1696,12 +1723,14 @@ async def create_tool_executor( Returns: String result describing what was done """ - logger.info(f"[tool call] {tool_name}") + logger.info(f"[tool call] {tool_name} {tool_input}") try: handler = _TOOL_HANDLERS.get(tool_name) if handler: - return await handler(ctx, tool_input) + result = await handler(ctx, tool_input) + logger.info(f"[tool result] {tool_name} {result}") + return result return f"Unknown tool: {tool_name}" except ValueError as e: diff --git a/src/utils/clients.py b/src/utils/clients.py index 5ce820f7..ae1beadd 100644 --- a/src/utils/clients.py +++ b/src/utils/clients.py @@ -706,6 +706,7 @@ async def _execute_tool_loop( total_output_tokens = 0 total_cache_creation_tokens = 0 total_cache_read_tokens = 0 + empty_response_retries = 0 # Track effective tool_choice - switches from "required" to "auto" after first iteration effective_tool_choice = tool_choice @@ -778,6 +779,25 @@ async def _execute_tool_loop( # No tool calls, return final response logger.debug("No tool calls in response, finishing") + if ( + isinstance(response.content, str) + and not response.content.strip() + and empty_response_retries < 1 + and iteration < max_tool_iterations - 1 + ): + empty_response_retries += 1 + conversation_messages.append( + { + "role": "user", + "content": ( + "Your last response was empty. Provide a concise answer " + "to the original query using the available context." + ), + } + ) + iteration += 1 + continue + if stream_final: # Stream the final response with metadata from tool execution stream = _stream_final_response(