The Patch That Silenced the Echo: Suppressing Duplicate Agent Responses in an LLM-Driven Fleet Manager
Introduction
In the course of building a fully autonomous LLM-driven fleet management agent for a distributed GPU proving infrastructure, a subtle but persistent usability problem emerged: the agent appeared to respond twice to every stimulus. Users saw "double responses" — two assistant messages per agent run, cluttering the UI and wasting precious tokens in the LLM context window. The root cause was a pattern inherent to how language models interact with tools: the model would produce a natural-language narration before invoking tools, and then produce a second final answer after the tools returned. Both messages were faithfully stored in the conversation database and fed back into the prompt on subsequent runs, effectively doubling the cost of every agent cycle while making the UI feel broken.
Message 4969 is the moment this problem was surgically corrected. It is a single apply_patch call that modifies the conversation-to-prompt building loop in the agent's Python code, implementing the suppression of intermediate assistant messages. This message is small in size but large in consequence — it represents the culmination of a diagnostic chain that traced a user-facing annoyance to a deep architectural pattern in how LLM agents generate and store messages.
The Message
The message reads, in its entirety:
[assistant] Now edit build loop to skip intermediate assistant messages. [apply_patch] {"patchText":" Begin Patch\n Update File: /tmp/czk/cmd/vast-manager/agent/vast_agent.py\n@@\n- n_msgs = len(messages)\n- for i, m in enumerate(messages):\n- llm_messages.append(db_msg_to_openai(m, msgs_from_end=n_msgs - i))\n+ # Suppress duplicate intermediate assistant respons... Success. Updated the following files: M cmd/vast-manager/agent/vast_agent.py
The patch text is truncated in the message display, but the intent is clear: the old code iterated over every message in the conversation and appended it to the LLM prompt. The new code adds logic to identify and skip "intermediate" assistant messages — those that are not the final message in their run.
Why This Message Was Written: The Double-Response Problem
The "double responses" issue was first raised by the user, who noticed that the agent seemed to reply twice to the same stimulus. This was not an illusion. The agent's LLM, when given a task that required tool calls, would follow a characteristic pattern: first, it would produce a natural-language reasoning message explaining what it planned to do (e.g., "I'll check the fleet status and scale up if needed..."), then it would issue tool calls, and after the tools returned, it would produce a second message summarizing the results and announcing the verdict. Both messages were stored in the conversation database with role='assistant' and the same run_id.
This pattern is a natural consequence of how LLMs are trained and prompted. The model is conditioned to "think aloud" before acting. The system prompt explicitly encourages this: the agent is told to reason step by step. But the conversation storage system treated every LLM output as an equal citizen — there was no distinction between a preliminary "thinking" message and a final answer.
The consequences were twofold. First, the UI conversation view showed two assistant bubbles per run, making the agent appear verbose and confusing. Second, and more critically, the LLM context-building loop — the code that assembles the conversation history into the prompt for the next run — was feeding both messages back into the model. This meant the LLM was reading its own preliminary reasoning as if it were a separate historical event, wasting context window space and potentially confusing the model about what had actually been decided.
The Diagnostic Chain
Message 4969 did not happen in isolation. It was the second patch in a two-patch sequence, and the product of an extensive investigation that spanned multiple rounds of debugging.
The investigation began with the user's report of "double responses" ([msg 4960]: "continue investigating the double responses"). The assistant initially suspected a race condition — perhaps the systemd timer and the path unit were both triggering the agent simultaneously, producing parallel runs. This was a reasonable hypothesis: the agent had two activation mechanisms (a 5-minute timer and an event-driven path unit), and they could theoretically fire close together.
The assistant spent several messages investigating this angle. It checked systemd logs, examined run timestamps, and confirmed that parallel activations were indeed happening — but the file lock (implemented in an earlier round) ensured that only one run actually executed; the other exited with "Another agent instance is already running — exiting." So parallel activations were not the cause of duplicate responses.
The assistant then examined the conversation database directly ([msg 4963]), dumping the last 30 messages with their run IDs and timestamps. This revealed the true pattern: within a single run, there were two assistant messages — one before the tool calls and one after. The user was seeing a single run produce two outputs.
With the root cause identified, the fix required two coordinated changes. First, the system prompt needed a rule telling the model to suppress narrative text before tool calls — to save the full explanation for the final response. This was applied in [msg 4968]. Second, the conversation-to-prompt building loop needed to actively filter out intermediate assistant messages, keeping only the last assistant message per run. This is what message 4969 implements.
Input Knowledge Required
To understand this message, one must grasp several layers of context:
- The agent architecture: The fleet management agent is a Python script (
vast_agent.py) that runs periodically (via systemd timer and path unit). Each run fetches the conversation history from a Go backend API, builds an LLM prompt from it, calls the LLM, processes tool calls, and stores the results back in the conversation database. - The conversation model: Messages in the database have a
role(user/assistant/tool), arun_ididentifying which agent cycle produced them, and anid. The LLM prompt is constructed by iterating over all messages and converting them to OpenAI-format messages. - The "intermediate assistant message" pattern: When an LLM is given tools, it may produce a response with
content(text) and then issuetool_calls. The OpenAI API represents this as a single assistant message. However, in this agent's implementation, the assistant's text response and tool calls were being stored as separate messages — or the model was being called twice per run (once for reasoning, once for final answer). The exact mechanism is less important than the observable effect: two assistant messages per run. - The existing compaction logic: The code already had a mechanism to compact stale long tool outputs (>300 chars, >5 messages from end). The new suppression logic would build on this same infrastructure.
How the Decision Was Made
The decision to suppress intermediate assistant messages was reached through a process of elimination. The assistant considered and ruled out several alternative explanations:
- Parallel systemd activations: Ruled out because the file lock prevented concurrent execution.
- Notification re-processing: Ruled out because notifications were being marked as consumed.
- Race conditions in the trigger mechanism: Ruled out because the lock worked correctly. Once the true cause was identified, the fix was straightforward in concept but required careful implementation. The key insight was that the "last assistant message per run" is the authoritative one — it contains the final verdict and any explanations. Earlier assistant messages in the same run are preliminary and should be excluded from both the LLM prompt and the UI display. The assistant chose to implement the fix in two layers: 1. Prompt engineering ([msg 4968]): Add a rule telling the model to avoid pre-tool narration. This is a soft fix — it relies on the model's compliance and can be ignored. 2. Hard filtering ([msg 4969]): Modify the code to actively skip intermediate messages. This is a hard fix — it enforces the behavior regardless of what the model does. This two-layer approach is characteristic of robust agent engineering: the prompt guides the model toward good behavior, while the code enforces correctness regardless of model compliance.
The Implementation
The patch replaces a simple iterative loop with a filtered one. The old code:
n_msgs = len(messages)
for i, m in enumerate(messages):
llm_messages.append(db_msg_to_openai(m, msgs_from_end=n_msgs - i))
This loop iterated over every message, computing msgs_from_end (a parameter used for compaction — messages closer to the end are less likely to be compacted). Every message — user, assistant, tool — was included.
The new code adds logic to identify the last assistant message per run and skip earlier ones. The exact implementation is truncated in the message display, but the pattern is standard: build a dictionary mapping run_id to the maximum message id for assistant messages, then skip any assistant message whose id is not the maximum for its run_id.
This is a deceptively simple change with significant implications. By removing intermediate messages, the patch:
- Reduces token usage: Each run now contributes at most one assistant message to the prompt, not two. Over dozens of runs, this saves thousands of tokens.
- Eliminates confusion: The LLM no longer sees its own preliminary reasoning as a separate historical event. It sees only the final decision and explanation.
- Improves UI clarity: The conversation view shows one response per run, matching user expectations.
Assumptions and Potential Pitfalls
The patch makes several assumptions:
- The last assistant message per run is the most informative: This is generally true — the final message contains the verdict and any explanation. But if the model produces a final message that is terse (e.g., just "Done.") while the intermediate message contained detailed reasoning, the suppression could lose valuable information.
- All intermediate messages are safe to discard: The patch assumes that earlier assistant messages in the same run are purely preliminary and contain no information that isn't also present (or superseded) in the final message. This is a reasonable assumption for the current agent design, where the model is explicitly told to save explanations for the final response.
- The run_id is reliable: The suppression logic depends on
run_idbeing correctly assigned and consistent. If a bug caused messages from the same run to have differentrun_idvalues, the suppression would fail. Conversely, if messages from different runs somehow shared arun_id, the suppression could incorrectly keep only one. - Tool outputs are not affected: The patch only suppresses assistant messages. Tool outputs (which contain the results of tool calls) are still included. This is correct — tool outputs provide factual data that the model needs to see.
Output Knowledge Created
This message created a concrete, tested change to the agent's codebase. The immediate output is a modified vast_agent.py file with the new suppression logic. But the broader output is:
- A documented pattern for handling LLM multi-message runs: The approach of keeping only the last assistant message per run can be applied to any agent that produces multiple messages per cycle.
- A verified fix for the "double responses" bug: The subsequent deployment ([msg 4972]) and user feedback confirm that the fix resolved the issue.
- A template for UI-side suppression: The same pattern was later applied to the UI conversation view ([msg 4971]), ensuring consistency between what the LLM sees and what the user sees.
The Thinking Process
The assistant's reasoning, visible in the preceding messages, shows a methodical approach to debugging. It did not jump to conclusions. It formulated hypotheses (parallel activations, notification re-processing, duplicate runs) and tested each one against the data. When the logs showed that parallel activations were handled correctly by the lock, it moved on. When the conversation dump revealed two assistant messages per run, it pivoted to the correct root cause.
The assistant also demonstrated an understanding of the trade-offs involved. It chose to implement both a prompt-level fix (soft, guiding) and a code-level fix (hard, enforcing). This dual approach is a成熟 pattern in LLM application development: the prompt shapes the model's behavior, but the code provides a safety net.
Conclusion
Message 4969 is a small patch with outsized impact. It fixed a user-facing bug that made the agent appear broken, reduced token consumption in the LLM prompt, and established a pattern for handling multi-message runs that would be applied across the entire system. More importantly, it exemplifies the kind of meticulous, evidence-driven debugging that is essential when building autonomous LLM agents. The "double responses" were not a superficial UI glitch — they were a symptom of a deeper architectural pattern where the agent's natural conversational style (thinking aloud before acting) collided with the system's assumption that each run produces exactly one message. By understanding this pattern and designing a fix that addresses both the symptom and the root cause, the assistant transformed a frustrating user experience into a clean, predictable interaction.