fix: preserve exploration tool history and cap context budget

This commit is contained in:
chenmingxuan committed 2026-10-16 19:05:35 +08:00
1 parent ee256ad7ce
commit 621f30e437
17 files changed
+211 -23

No files matched your search

+11 -10
View File
@@ -17,7 +17,7 @@ from collections.abc import Awaitable
from typing import Any
from uuid import uuid4
from langchain_core.messages import SystemMessage, ToolMessage
from langchain_core.messages import BaseMessage, SystemMessage, ToolMessage
from langchain_core.tools import BaseTool
from artemis.agents.validator.tool_declarations import normalize_coordinate_target
@@ -146,9 +146,9 @@ class OperatorNode:
from artemis.config.agent import MemoryTranscriptConfig
self._transcript_cfg = MemoryTranscriptConfig()
# Index into the rendered message list where this turn's tail begins;
# set by the transcript build and consumed after the tool loop.
self._transcript_turn_base: int | None = None
# Keep the actual observation, not an index: plugin/exploration context
# may be prepended after render, shifting every message's position.
self._transcript_turn_tail: BaseMessage | None = None
self._turn_plan_baseline: str | None = None
self._turn_ledger_streak: int = 0
# Whether the previous turn's replies carried no visible reasoning text;
@@ -225,7 +225,7 @@ class OperatorNode:
active_background_tasks: list[dict] = None,
newly_finished_tasks: list[dict] = None,
) -> list:
self._transcript_turn_base = None
self._transcript_turn_tail = None
if getattr(self._transcript_cfg, "enabled", False):
try:
return await self._build_prompt_transcript(
@@ -243,7 +243,7 @@ class OperatorNode:
f"Transcript prompt path failed; falling back to the legacy"
f" 2-message build for this turn: {e}"
)
self._transcript_turn_base = None
self._transcript_turn_tail = None
plan_and_history = self._build_plan_and_history(steps, task_plan)
@@ -468,18 +468,19 @@ class OperatorNode:
mark_ephemeral(tail, builder.ephemeral_indices)
messages = ledger.render([tail])
self._transcript_turn_base = len(messages) - 1
self._transcript_turn_tail = tail
return messages
def _stage_transcript_turn(self, messages: list) -> None:
"""Hand this turn's tail + tool-loop products to the ledger."""
base = self._transcript_turn_base
self._transcript_turn_base = None
if base is None:
tail = self._transcript_turn_tail
self._transcript_turn_tail = None
if tail is None:
return
try:
ledger = getattr(self.ctx, "transcript_ledger", None)
if ledger is not None:
base = next(i for i, message in enumerate(messages) if message is tail)
ledger.stage_turn(messages[base:])
except Exception as e:
logger.error(f"Failed to stage transcript turn: {e}")
+2
View File
@@ -23,6 +23,7 @@ from pydantic import BaseModel, Field, model_validator
from artemis.config.constants import (
AGENT_CONFIG_FILENAME,
DEFAULT_EXPLORER_VERSION,
MAX_CONTEXT_WINDOW_TOKENS,
ExplorerVersion,
)
from artemis.config.paths import ROOT_DIR, get_config_path
@@ -483,6 +484,7 @@ class MemoryTranscriptConfig(BaseModel):
context_budget_tokens: int = Field(
default=80000,
ge=1024,
le=MAX_CONTEXT_WINDOW_TOKENS,
description=(
"Measured-token budget for the transcript context (provisional"
" default pending the on-device baseline; §3.4). The soft/hard"
+3
View File
@@ -16,6 +16,9 @@
from typing import Literal
# Application context ceiling; independent from output and session token totals.
MAX_CONTEXT_WINDOW_TOKENS = 300_000
# ==============================================================================
# Environment Variable Keys
# ==============================================================================
+11
View File
@@ -81,6 +81,11 @@ _AUTH_MARKERS = ("unauthorized", "forbidden", "api key", "credential")
# "deadline exceeded"/"deadline_exceeded" cover google-genai SDK timeouts that
# surface without an HTTP status code attached.
_TIMEOUT_MARKERS = ("timed out", "timeout", "deadline exceeded", "deadline_exceeded")
_TOOL_HISTORY_ERRORS = (
"no tool call found for function call output",
"no tool output found for function call",
"tool_call_id not found",
)
def classify_failure(error: BaseException) -> Failure:
@@ -91,6 +96,12 @@ def classify_failure(error: BaseException) -> Failure:
code = extract_status_code(error)
message = str(error).lower()
# Compatible Responses endpoints can report input validation inside an
# HTTP 200 stream as APIError without a status code. Retrying the same
# broken call/output history cannot repair it, even on a fallback model.
if any(marker in message for marker in _TOOL_HISTORY_ERRORS):
return Failure(FailureCategory.BAD_REQUEST, False, False)
if code == 429 or any(marker in message for marker in _RATE_LIMIT_MARKERS):
return Failure(FailureCategory.RATE_LIMIT, True, True)
if code in {500, 502, 503, 504} or any(marker in message for marker in _UNAVAILABLE_MARKERS):
+1
View File
@@ -94,6 +94,7 @@
},
"transcript": {
"enabled": true,
// 历史压缩预算,允许范围 1024–300000;上下文参照上限固定为 300k。
"context_budget_tokens": 80000,
"start_ratio": 0.35,
"soft_ratio": 0.7,
+2 -1
View File
@@ -40,6 +40,7 @@ import threading
from typing import Any
from artemis.data_engine.context_vars import CURRENT_NODE_NAME
from artemis.config.constants import MAX_CONTEXT_WINDOW_TOKENS
from artemis.llm.usage import usage_from_message
from artemis.utils.logger import get_logger
@@ -52,7 +53,7 @@ _METERS: dict[str, "SessionTokenMeter"] = {}
# measured against when the UI reports "context used". The prompt size of the
# last executor call (``context_base_tokens`` on its ``llm_usage`` trace) is
# the numerator; this is the denominator.
OPERATOR_CONTEXT_WINDOW_TOKENS = 1_000_000
OPERATOR_CONTEXT_WINDOW_TOKENS = MAX_CONTEXT_WINDOW_TOKENS
# Traced node names whose LLM calls carry the live executor context. Both the
# Pro Operator and the Flash runner are "the operator" from the user's view.