atakan Claude Sonnet 5 commited on
Commit
5d9e9cd
·
1 Parent(s): 6902397

add: Truncate conversation history to a fixed token budget

Browse files

The web frontend resends the entire growing conversation on every turn with
no client-side trimming (web/app.js), and nothing on the backend capped it
either: a long chat would eventually exceed the GGUF backend's n_ctx=16384
and start erroring outright, or just keep getting slower with no ceiling at
all on the PyTorch/MLX backends -- neither is a graceful "forgets old
context" degradation, both are failure modes.

_truncate_history keeps the most recent turns that fit within 6000 tokens
(well under 16384, leaving headroom for the system prompt, RAG-injected
passages, tool schemas, and the current turn's own multi-step tool loop),
dropping the oldest turns first and never leaving a leading assistant turn
with no preceding user message.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Files changed (1) hide show
  1. controlai_agent/orchestrator.py +45 -0
controlai_agent/orchestrator.py CHANGED
@@ -549,6 +549,49 @@ def _fabrication_refusal_note(refusals: list[str]) -> str:
549
  )
550
 
551
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  class ControlAIAgent:
553
  """Universal Control Engineering Agent supporting GGUF, Ollama C++, Apple MLX, and PyTorch."""
554
 
@@ -881,6 +924,7 @@ class ControlAIAgent:
881
  verbose: bool = False,
882
  ) -> AgentResult:
883
  """Execute a complete agent interaction loop synchronously."""
 
884
  messages: list[dict[str, Any]] = []
885
  effective_sys = self._get_grounded_instruction(user_prompt, system_instruction)
886
 
@@ -1053,6 +1097,7 @@ class ControlAIAgent:
1053
  max_tokens_per_step: int = 2500,
1054
  ) -> Generator[dict[str, Any], None, None]:
1055
  """Stream token-by-token generation and tool execution events with zero JSON leakage."""
 
1056
  messages: list[dict[str, Any]] = []
1057
  effective_sys = self._get_grounded_instruction(user_prompt, system_instruction)
1058
 
 
549
  )
550
 
551
 
552
+ # The web frontend resends the entire growing conversation on every turn
553
+ # (web/app.js) with no client-side trimming, and nothing here capped it
554
+ # either: a long chat would eventually exceed the GGUF backend's
555
+ # n_ctx=16384 and error outright, or just keep getting slower with no
556
+ # ceiling at all on the PyTorch/MLX backends. Budget is deliberately well
557
+ # under 16384 to leave headroom for the system prompt, injected RAG
558
+ # passages, the full tool-schema catalog, and the current turn's own
559
+ # multi-step tool loop -- all of which share the same context window.
560
+ MAX_HISTORY_TOKENS = 6000
561
+
562
+
563
+ def _truncate_history(
564
+ history: list[dict[str, Any]] | None, tokenizer: Any, max_tokens: int = MAX_HISTORY_TOKENS
565
+ ) -> list[dict[str, Any]] | None:
566
+ """Keep the most recent turns of `history` that fit within max_tokens,
567
+ dropping the oldest first so a long conversation degrades (forgets early
568
+ turns) instead of failing outright."""
569
+ if not history:
570
+ return history
571
+
572
+ kept: list[dict[str, Any]] = []
573
+ total = 0
574
+ for item in reversed(history):
575
+ content = item.get("content", "")
576
+ if not isinstance(content, str) or not content:
577
+ continue
578
+ try:
579
+ n = len(tokenizer.encode(content))
580
+ except Exception:
581
+ n = len(content) // 4 # rough fallback if the tokenizer call itself fails
582
+ if kept and total + n > max_tokens:
583
+ break
584
+ kept.append(item)
585
+ total += n
586
+
587
+ kept.reverse()
588
+ # A leading assistant turn with no preceding user turn would read as a
589
+ # reply to nothing -- drop it.
590
+ while kept and kept[0].get("role") != "user":
591
+ kept.pop(0)
592
+ return kept
593
+
594
+
595
  class ControlAIAgent:
596
  """Universal Control Engineering Agent supporting GGUF, Ollama C++, Apple MLX, and PyTorch."""
597
 
 
924
  verbose: bool = False,
925
  ) -> AgentResult:
926
  """Execute a complete agent interaction loop synchronously."""
927
+ history = _truncate_history(history, self.hf_tokenizer)
928
  messages: list[dict[str, Any]] = []
929
  effective_sys = self._get_grounded_instruction(user_prompt, system_instruction)
930
 
 
1097
  max_tokens_per_step: int = 2500,
1098
  ) -> Generator[dict[str, Any], None, None]:
1099
  """Stream token-by-token generation and tool execution events with zero JSON leakage."""
1100
+ history = _truncate_history(history, self.hf_tokenizer)
1101
  messages: list[dict[str, Any]] = []
1102
  effective_sys = self._get_grounded_instruction(user_prompt, system_instruction)
1103