Repository navigation
Does the goal survive a handoff? Compaction boundaries in long multi-agent runs #8287
Replies: 5 comments 1 reply
|
Your diagnosis of multi-agent handoffs is spot-on: naive multi-agent architectures do not eliminate context pressure — they convert an observable in-memory compaction problem into a silent, distributed boundary problem. When Agent A summarizes its state for Agent B in natural language, it performs lossy compaction across a process boundary. If Agent A compacts mid-task, it summarizes a state it has already partially forgotten, and Agent B receives an incomplete contract with zero ability to inspect what was omitted. Here is how this is addressed in modern AutoGen architectures ( 1. Does AutoGen preserve goals verbatim, or do you implement it yourself?In standard To guarantee the goal survives every handoff and summarization, production implementations use two mechanisms: A. System Message Invariance (Protected Channel)In AutoGen, an agent's # Pass the verbatim goal and hard constraints directly into the system envelope
agent = AssistantAgent(
name="coder",
model_client=model_client,
system_message=(
f"GLOBAL GOAL (NON-EVICTABLE):\n{verbatim_goal}\n\n"
f"ACCEPTANCE CRITERIA:\n{chr(10).join(f'- {c}' for c in criteria)}\n\n"
"You are the execution specialist. Do not deviate from these constraints."
)
)B. Pinned Context TransformFor dynamic multi-turn tasks, you can register a custom from autogen_core.models import ModelContextTransform, LLMMessage
class PinnedGoalTransform(ModelContextTransform):
def __init__(self, pinned_goal: str):
self._pinned_message = UserMessage(content=f"[SYSTEM INVARIANT]\n{pinned_goal}", source="system")
async def transform(self, messages: list[LLMMessage]) -> list[LLMMessage]:
# Compact or truncate the body
compacted = await self.compact_body(messages[1:])
# Always re-anchor the pinned goal immediately after the system message
return [messages[0], self._pinned_message] + compacted2. Is the compaction event observable?Out-of-the-box AutoGen emits OpenTelemetry spans for agent messages and tool calls, but does not emit a native "dropped messages" diff. However, you can make compaction transparent by wrapping your compaction transformer with a differential audit logger: class ObservableCompactionTransform(ModelContextTransform):
def __init__(self, inner_transform: ModelContextTransform, logger):
self.inner = inner_transform
self.logger = logger
async def transform(self, messages: list[LLMMessage]) -> list[LLMMessage]:
original_ids = {id(m): m for m in messages}
transformed = await self.inner.transform(messages)
kept_ids = {id(m) for m in transformed}
dropped = [m for mid, m in original_ids.items() if mid not in kept_ids]
if dropped:
self.logger.warning("context_compaction_boundary", extra={
"input_count": len(messages),
"output_count": len(transformed),
"dropped_count": len(dropped),
"dropped_previews": [str(m.content)[:80] for m in dropped],
})
return transformedWhen an agent strays on turn 40, querying 3. The "Pointer-to-Trace" Pattern (Lazy Handoff)Instead of forcing Agent A to summarize 50k tokens into a lossy paragraph for Agent B, use By-Reference Handoffs. In this pattern:
# Agent B starts with a pristine context window (< 500 tokens)
handoff_payload = {
"root_goal": verbatim_goal,
"current_status": "Implemented auth endpoints; failing on test_jwt_expiry",
"trace_id": "run_98234_step_2"
}
# If Agent B is confused about why an earlier design decision was made:
# Agent B calls: retrieve_trace_detail(query="JWT expiration configuration", trace_id="run_98234_step_2")This decouples task progression from context bloat: Agent B receives a pristine working window without inheriting Agent A's noise, but retains on-demand, non-lossy access to raw history. |
|
|
On your question 1, we measured a small version of this, including a cost of pinning I have not seen mentioned. A constraint stated in chunk 1 of a chunked long task had to hold for 8 chunks. Three arms: no reminder, the constraint restated at the end of every chunk, and same-length neutral filler as a placebo. When the constraint dropped out of context after chunk 1, grok-4.5's adherence fell to 0.18 by the late chunks. The restated arm held 1.0 in every chunk. The filler arm collapsed like the baseline, so it is the goal text doing the work. When the constraint was still in context and we restated it anyway, a 7B model got worse, 0.15 with the restatement against 0.44 without. On grok the redundant reminder was harmless. That fits your reply about the system message eating a small window. So pin the goal verbatim, and on small models re-supply it when it has actually been dropped rather than every turn. This was 2 runs per arm with synthetic constraints (one was every sentence 7 words or fewer), so directional only. (We build agent memory tooling, or at least we are trying to. If we ever forget the goal mid-run, at least we will have measured it.) |
|
One API detail worth correcting for readers trying the code above: current For a pinned objective, put the compact invariant in Code paths: context interface and AssistantAgent request assembly. |
|
Your point that a handoff is a compaction nobody can see is the part I'd try to turn into a test. Before changing anything, I'd write a small canary check: seed the goal with two or three checkable constraints (e.g. "never touch files outside /app", "stop if cost exceeds X"), run the long task, and after each summarisation or handoff ask the receiving agent to restate those constraints and compare them to the originals. If you log the pre-compaction and post-compaction text as a pair, the check is a diff you can run in CI instead of something you find after a run drifts. I'd also replay the same long scenario 10-20 times and track how often the final outcome still satisfies the original acceptance criteria. Compaction loss is probabilistic, so a single passing run says little, but a pass rate that drops as run length grows points straight at the boundary. Asserting on the downstream actions (which tools were called, with what arguments) catches the "quietly restarted the plan" case better than reading the final answer, because repeated or re-derived tool calls show up as extra steps in the trace. |
Uh oh!
There was an error while loading. Please reload this page.
Disclosure up front: I build Grunz, a coding agent running open-weight models. Single-agent, not multi-agent, so treat this as a view from outside the paradigm rather than a critique from inside it. I'd genuinely like to know whether this is a solved problem here and I've just missed how.
The thing I keep running into
Long agent runs don't usually fail at a tool call. They fail at a compaction boundary.
When the context window fills, something has to decide what to discard. If the goal statement isn't preserved verbatim through that step, it gets summarised into vagueness. The model then reads back its own compacted notes, no longer knows precisely what it was doing, and quietly restarts the plan — frequently re-deriving work it already completed.
From the outside this reads as "the model forgot my instructions." But they weren't buried in the middle of a long context. They were compressed out. Nothing errors, so it doesn't show up as a failure — it shows up as a run that took three times as long and ended somewhere adjacent to where you wanted.
Why I'd expect multi-agent to make this harder, not easier
The intuitive pitch for multi-agent is that decomposition solves context pressure: each agent holds less, so nobody fills their window. My experience is that it relocates the problem rather than removing it, for two reasons.
1. Every handoff is a lossy compaction with extra steps. When agent A summarises its state for agent B, that's the same operation as a compaction — something decides what survives — except now it also crosses an agent boundary, so B has no access to what A dropped and no way to know it's missing. A compaction at least leaves the raw history recoverable in principle. A handoff generally doesn't.
2. The boundaries don't line up. An agent's context can fill mid-subtask, meaning it compacts, loses fidelity on its own assignment, and then hands off a summary of a plan it had already partially forgotten. The orchestrator sees a completed handoff and a plausible summary, so nothing looks wrong at the supervision layer.
Net effect in my experience: the group can be confidently, collectively wrong in a way a single agent with a full trace usually isn't — because every participant's view is locally coherent.
What I'd like to know from people who actually run these
Does AutoGen preserve the original goal and acceptance criteria verbatim across handoffs and summarisation steps, or is that something you implement yourself per-workflow? Pinning the goal verbatim through every compaction was the single highest-leverage change I made, and I'd expect it to matter more in a group chat, not less.
Is the compaction event itself observable? Teams log tool calls and messages, but I rarely see anyone logging what got dropped at a summarisation step. That's usually where a long run silently went wrong, and without it you can explain every individual message and still not explain the run.
Is there a known pattern for a handoff that carries a pointer to the full prior context rather than only a summary — so the receiving agent can pull back detail it discovers it needs, instead of working from a lossy snapshot?
Entirely possible the answer to all three is "yes, here's the doc," in which case I'd be glad to read it. I'd rather find out I'm wrong about this than keep telling people multi-agent makes it worse.
All reactions