Hi — thanks for keeping smolagents small and readable; it made this easy to trace.
I want to flag a cost characteristic that isn't spelled out in the docs and that surprises people
on longer tool-loop runs: input tokens grow quadratically with the number of steps, because the
full accumulated memory is re-sent to the model on every action step.
The mechanism, in v1.26.0:
write_memory_to_messages walks all of self.memory.steps and appends each step's messages
(agents.py L758-770).
memory.steps only ever grows within a run — it's a list initialised empty and appended to,
cleared only by reset()
(memory.py L230,
L232-234).
- The action loop calls it with no
summary_mode, so the full history replays each step
(agents.py L1284);
summary_mode=True is used only for planning
(agents.py L684).
- The default step ceiling is
max_steps: int = 20
(agents.py L300).
So if P = system-prompt tokens and s = tokens each completed step adds to memory (thought +
code + tool observation), cumulative input tokens over an n-step run are:
Σ (k=1..n) [ P + (k-1)·s ] = n·P + s · n(n-1)/2
The n(n-1)/2 term is quadratic. Priced naively as "n steps ≈ n × one step", the history you pay
for is n · s; the actual history replayed is n(n-1)/2 · s — e.g. 9.5× more replay at the
default max_steps=20 (190 vs 20). It bites hardest when a tool returns a large observation (a web
page chunk, a file, a dataframe), since that payload is then re-sent on every later step.
To be clear, this is a reasonable default — the model needs context — and smolagents is more honest
about it than most: every ActionStep already carries token_usage
(memory.py L63),
so summing step.token_usage.input_tokens across agent.memory.steps after a run shows the curve
directly. The gap is just that it isn't documented, and there's no in-tree example of bounding it.
What I'd suggest, using the hooks you already ship: step_callbacks is first-class
(agents.py L282/L304,
wired at L416),
and because memory.steps is a plain list the caller owns, a callback can cap how much history
survives into the next write_memory_to_messages — e.g. keep the last N ActionSteps and collapse
older observations to a short marker. That turns the input curve from quadratic back toward linear
for a bounded recall trade-off.
Happy to open a small PR if you're open to it — either:
- a short docs note ("Controlling context cost on long runs") showing the
token_usage
summation and a step_callbacks trimming example, and/or
- an optional helper (e.g. a
keep_last_n_action_steps callback factory) so the mitigation is
one import rather than hand-rolled.
Would a docs-only PR, a helper, or neither fit best with where you want the memory API to go?
Thanks!
Hi — thanks for keeping smolagents small and readable; it made this easy to trace.
I want to flag a cost characteristic that isn't spelled out in the docs and that surprises people
on longer tool-loop runs: input tokens grow quadratically with the number of steps, because the
full accumulated memory is re-sent to the model on every action step.
The mechanism, in
v1.26.0:write_memory_to_messageswalks all ofself.memory.stepsand appends each step's messages(agents.py L758-770).
memory.stepsonly ever grows within a run — it's a list initialised empty and appended to,cleared only by
reset()(memory.py L230,
L232-234).
summary_mode, so the full history replays each step(agents.py L1284);
summary_mode=Trueis used only for planning(agents.py L684).
max_steps: int = 20(agents.py L300).
So if
P= system-prompt tokens ands= tokens each completed step adds to memory (thought +code + tool observation), cumulative input tokens over an
n-step run are:The
n(n-1)/2term is quadratic. Priced naively as "n steps ≈ n × one step", the history you payfor is
n · s; the actual history replayed isn(n-1)/2 · s— e.g. 9.5× more replay at thedefault
max_steps=20(190 vs 20). It bites hardest when a tool returns a large observation (a webpage chunk, a file, a dataframe), since that payload is then re-sent on every later step.
To be clear, this is a reasonable default — the model needs context — and smolagents is more honest
about it than most: every
ActionStepalready carriestoken_usage(memory.py L63),
so summing
step.token_usage.input_tokensacrossagent.memory.stepsafter a run shows the curvedirectly. The gap is just that it isn't documented, and there's no in-tree example of bounding it.
What I'd suggest, using the hooks you already ship:
step_callbacksis first-class(agents.py L282/L304,
wired at L416),
and because
memory.stepsis a plain list the caller owns, a callback can cap how much historysurvives into the next
write_memory_to_messages— e.g. keep the last NActionSteps and collapseolder
observationsto a short marker. That turns the input curve from quadratic back toward linearfor a bounded recall trade-off.
Happy to open a small PR if you're open to it — either:
token_usagesummation and a
step_callbackstrimming example, and/orkeep_last_n_action_stepscallback factory) so the mitigation isone import rather than hand-rolled.
Would a docs-only PR, a helper, or neither fit best with where you want the memory API to go?
Thanks!