Spaces:
Running on Zero
add: Truncate conversation history to a fixed token budget
Browse filesThe 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>
|
@@ -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 |
|