[BUG][PlannerGeneratorEvaluator] The entire shared state file is pasted into every agent call, O(steps² × retries) (#2053 follow-up)
Summary
Split out of #2053, which was closed by #2189 while this file was still outstanding. PlannerGeneratorEvaluator pastes its entire, ever-growing shared state file into every agent call as one user string. Of the structures listed in #2053 this is the worst offender for both context size and cost, and it is the only one with no open PR.
Sites
swarms/structs/planner_generator_evaluator.py
| Line | Caller | Expression |
|---|---|---|
:381-391 |
_run_planner |
--- SHARED STATE ---\n{shared_state}\n--- END SHARED STATE --- → planner_agent.run(task=...) |
:474-484 |
_negotiate_contract (generator) |
same |
:496-503 |
_negotiate_contract (evaluator) |
same, re-read after the generator appended |
:554-574 |
_execute_step |
same → generator_agent.run(task=...) |
:606-615 |
_evaluate_step |
same → evaluator_agent.run(task=...) |
None of the five calls passes messages=. self.conversation is cleared and seeded with the task at :812-815 but is otherwise not what the agents see; the shared-state file is.
Growth
_append_to_shared_state (:340-352) appends after the plan, after every contract proposal, after every contract review, after every generator output and after every evaluation, including retries. Each call then re-reads the whole file (:333-338) and pastes it in. The generator and evaluator are single instances created once (:309, :321) and reused for every step, so their own memory grows alongside the file.
Per step the prompt carries the full history of every previous step, so total tokens are O(steps² × retries). And because the pasted blob is different on every call, the cached prefix is never reused. This is exactly the structure where prompt caching would matter most.
Credit where due: all three agents are output_type="final" (:305, :317, :329), so what gets appended is an answer, not a transcript.
Fix
Two parts.
Send history as turns. Record each planner / generator / evaluator output into
self.conversationunder its agent name, and replace the--- SHARED STATE ---interpolation withfrom swarms.structs.context_utils import messages_for, split_last_turn prior, task = split_last_turn(messages_for(agent.agent_name, self.conversation)) agent.run(task=task, messages=prior)The file can stay as an on-disk audit log; it should stop being the prompt.
Scope what each call sees.
planner_worker_swarm.py(#2181) is the model: workers reset memory and receive only the dependency context they need. Here the generator on step N needs the plan, its contract, and the evaluator's feedback on its last attempt at step N, not every prior step's contract negotiation. That is the part that turns O(steps²) into O(steps).
Part 1 alone gives the cache a stable prefix. Part 2 is what makes long runs affordable.
Found at 7b709c95b.
Source: kyegomez/swarms