aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-28 10:56:26 +0200
committerDanilo M. <danix@danix.xyz>2026-09-28 10:56:26 +0200
commit7a6190becf90364c6f31f401ccda745e4745081c (patch)
tree8058b650696457dc701f2bf2f78bcff5c5da0b99
parent48211368c01599d1c173d6e3347674750976db47 (diff)
downloaddesktop-assistant-7a6190becf90364c6f31f401ccda745e4745081c.tar.gz
desktop-assistant-7a6190becf90364c6f31f401ccda745e4745081c.zip
Keep tool calls in history, with placeholder results
History kept only what was said, so after a few factual answers the model saw no tool behind them and invented the next one: asked for pending updates it made up counts without calling the tool (1/10 on a replayed session). Keeping each turn's tool calls brings it back to 10/10. Their results are replaced by a placeholder, which scores the same and keeps the context close to what it was; a follow-up on an old result calls the tool again. The window is now the last 10 whole turns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS.md22
-rw-r--r--assistant.py18
2 files changed, 35 insertions, 5 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 6e66526..9966849 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -479,6 +479,28 @@ of 2000-character results ~4k), so the 20-message window and the
`stop processing: n_tokens = N, truncated = 0` per request, but it is
shared with every router client, so it cannot be attributed to the
assistant. `truncated = 1` there would mean an overflow.
+- 2026-09-28: history now keeps tool calls with placeholder results (see
+ "Tool calls in history"), window 10 whole turns. Each past call adds
+ ~50 tokens, so the worst case goes from ~9k to ~10k (10 turns of 3
+ calls). Full results kept would
+ have been ~22k.
+
+### Tool calls in history (2026-09-28)
+
+"Ci sono aggiornamenti da installare?" after four exchanges (time, mail,
+calc, calendar) got "12 updates, 3 security fixes, a kernel bump" with
+no tool call: invented, the system was up to date. History kept only
+what was said, so the model saw factual answers with no tool behind them
+and copied that. Replayed against the live model, 10 runs each: that
+history 1/10 called `updates`, empty history 10/10; a system prompt
+line saying earlier answers came from hidden tool calls 0/10 (worse);
+the same history with its tool calls kept 10/10, whether results were
+kept whole, cut to 200 characters, or replaced by a placeholder "(shown
+to the user at the time)" (**chosen**, cheapest). With the placeholder, a
+follow-up on an old result ("chi ha mandato la terza email?") calls the
+tool again, 10/10, no invented answer; costs one extra tool call. The
+"never guess" rule alone does not hold once history shows tool-less
+answers.
### Barge-in (2026-09-26)
diff --git a/assistant.py b/assistant.py
index 991dfa0..2a84cd6 100644
--- a/assistant.py
+++ b/assistant.py
@@ -548,15 +548,17 @@ def reply(history, q, mine):
log(f"you ({lang}, {time.perf_counter() - t:.2f}s): {text}")
emit({"type": "state", "state": "thinking"})
question = {"role": "user", "content": text}
- # ponytail: last 20 messages, summarise if long sessions need memory
# The model has no clock: without the date it makes one up for tools.
# Also stamped on the question itself, which history does not keep: with
# the time only in the system prompt, "what time is it?" asked again an
# hour later copied the old answer from history. Stamping every message
# in history fixed that but lost tool arguments (account filter 0/6).
now = time.strftime("%A %d %B %Y, %H:%M")
- msgs = ([{"role": "system", "content": f"{CFG['system']} It is {now}."}] + history[-20:]
+ # ponytail: last 10 turns, summarise if long sessions need memory
+ msgs = ([{"role": "system", "content": f"{CFG['system']} It is {now}."}]
+ + [m for past in history[-10:] for m in past]
+ [{"role": "user", "content": f"[{now}] {text}"}])
+ first = len(msgs) # this turn's tool messages start here
t, said, voice, announced = time.perf_counter(), [], lang, False
def say(s):
@@ -607,9 +609,15 @@ def reply(history, q, mine):
log("interrupted")
return
q.put((mine, None))
- # Tool calls stay inside the turn: history keeps only what was said. A cut
- # off exchange is dropped whole, so user and assistant still alternate.
- history += [question, {"role": "assistant", "content": " ".join(said)}]
+ # History keeps the tool calls: with only what was said, the model saw
+ # facts answered without tools and invented the next ones (no tool call
+ # 9/10 on a replayed session, 0/10 with the calls kept). Results are
+ # replaced by a placeholder, for context: same 10/10, and a follow-up on
+ # an old result calls the tool again (10/10). A cut off exchange is
+ # dropped whole, so user and assistant still alternate.
+ past = [{**m, "content": "(shown to the user at the time)"} if m["role"] == "tool" else m
+ for m in msgs[first:]]
+ history.append([question] + past + [{"role": "assistant", "content": " ".join(said)}])
log("assistant:", " ".join(said))