From f31032b80154bb7b032e4e65c05c4d319235fa1f Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 14 Jul 2026 23:42:55 +0800 Subject: [PATCH] core/agent+cli: wire max_context_chars and re-raise CancelledError Propagate context budget from [agent] config; preserve cancel through tool invoke for mid-turn interruption. --- CLAUDE.md | 6 +++--- src/plyngent/agent/chat.py | 6 +++++- src/plyngent/agent/tools.py | 2 ++ src/plyngent/cli/editor.py | 1 + src/plyngent/cli/state.py | 1 + src/plyngent/config/models.py | 1 + tests/test_config/test_agent_section.py | 3 +++ 7 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8ffd59c..32b7703 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,10 +54,10 @@ Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init, default local user, se - **`ChatClient`** Protocol for `chat_completions`. - **`@tool` / `ToolRegistry`**: decorator infers JSON Schema from type hints; execute tools by name. -- **`run_chat_loop`**: multi-round tool loop; default **streaming** text deltas + stream tool-call merge; parallel tools; tool-result char budget; optional `on_limit`. +- **`run_chat_loop`**: multi-round tool loop; default **streaming** text deltas + stream tool-call merge; parallel tools; tool-result char budget; soft context compact on request; cooperative cancel points; optional `on_limit`. - **`ChatAgent`**: optional `MemoryStore` (persist on success only); `stream`; system prompt; `pending_retry_text` + `retry()`. -- Events: text_delta, assistant_message, tool_call/result, max_rounds, **error**, **cancelled**. -- Config ``[agent]``: `system_prompt`, `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`. +- Events: text_delta, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`). +- Config ``[agent]``: `system_prompt`, `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_chars`. ### Tools (`tools/`) diff --git a/src/plyngent/agent/chat.py b/src/plyngent/agent/chat.py index 02571bd..ac035ef 100644 --- a/src/plyngent/agent/chat.py +++ b/src/plyngent/agent/chat.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from plyngent.lmproto.openai_compatible.model import SystemChatMessage, UserChatMessage -from .budget import DEFAULT_TOOL_RESULT_MAX_CHARS +from .budget import DEFAULT_CONTEXT_MAX_CHARS, DEFAULT_TOOL_RESULT_MAX_CHARS from .loop import DEFAULT_MAX_ROUNDS, run_chat_loop if TYPE_CHECKING: @@ -35,6 +35,7 @@ class ChatAgent: system_prompt: str | None max_tool_result_chars: int parallel_tools: bool + max_context_chars: int messages: list[AnyChatMessage] pending_retry_text: str | None @@ -54,6 +55,7 @@ class ChatAgent: system_prompt: str | None = None, max_tool_result_chars: int = DEFAULT_TOOL_RESULT_MAX_CHARS, parallel_tools: bool = True, + max_context_chars: int = DEFAULT_CONTEXT_MAX_CHARS, ) -> None: self.client = client self.model = model @@ -67,6 +69,7 @@ class ChatAgent: self.system_prompt = system_prompt self.max_tool_result_chars = max_tool_result_chars self.parallel_tools = parallel_tools + self.max_context_chars = max_context_chars self.messages = list(messages) if messages is not None else [] self.pending_retry_text = None self._ensure_system_prompt() @@ -125,6 +128,7 @@ class ChatAgent: stream=self.stream, max_tool_result_chars=self.max_tool_result_chars, parallel_tools=self.parallel_tools, + max_context_chars=self.max_context_chars, ): yield event completed = True diff --git a/src/plyngent/agent/tools.py b/src/plyngent/agent/tools.py index 025b6e7..786c119 100644 --- a/src/plyngent/agent/tools.py +++ b/src/plyngent/agent/tools.py @@ -193,6 +193,8 @@ class ToolRegistry: result = definition.handler(**args) if inspect.isawaitable(result): result = await result + except asyncio.CancelledError: + raise except TypeError as exc: return f"error: invalid tool arguments: {exc}" except Exception as exc: # noqa: BLE001 — surface tool failures to the model diff --git a/src/plyngent/cli/editor.py b/src/plyngent/cli/editor.py index 5b071be..71e9452 100644 --- a/src/plyngent/cli/editor.py +++ b/src/plyngent/cli/editor.py @@ -28,6 +28,7 @@ _MINIMAL_CONFIG = """\ # parallel_tools = true # confirm_destructive = true # path_denylist = ["/secrets/", ".ssh/"] +# max_context_chars = 200000 # [providers.example] # preset = "openai-compatible" diff --git a/src/plyngent/cli/state.py b/src/plyngent/cli/state.py index b0dcfa7..39a5d60 100644 --- a/src/plyngent/cli/state.py +++ b/src/plyngent/cli/state.py @@ -69,6 +69,7 @@ class ReplState: system_prompt=system_prompt, max_tool_result_chars=agent_cfg.max_tool_result_chars, parallel_tools=agent_cfg.parallel_tools, + max_context_chars=agent_cfg.max_context_chars, ) def rebuild_client(self) -> None: diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index caa34b2..ef4ad6f 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -18,6 +18,7 @@ class AgentConfig(Struct, omit_defaults=True): parallel_tools: bool = True confirm_destructive: bool = True path_denylist: list[str] = field(default_factory=list) + max_context_chars: int = 200_000 class ModelConfig(Struct, omit_defaults=True): diff --git a/tests/test_config/test_agent_section.py b/tests/test_config/test_agent_section.py index 06f7707..961e59d 100644 --- a/tests/test_config/test_agent_section.py +++ b/tests/test_config/test_agent_section.py @@ -17,6 +17,7 @@ def test_agent_section_defaults(tmp_path: Path) -> None: assert store.agent_config.parallel_tools is True assert store.agent_config.confirm_destructive is True assert store.agent_config.path_denylist == [] + assert store.agent_config.max_context_chars == 200_000 # noqa: PLR2004 def test_agent_section_parse(tmp_path: Path) -> None: @@ -29,6 +30,7 @@ max_tool_result_chars = 100 parallel_tools = false confirm_destructive = false path_denylist = ["/secrets/", ".ssh/"] +max_context_chars = 5000 """, encoding="utf-8", ) @@ -38,3 +40,4 @@ path_denylist = ["/secrets/", ".ssh/"] assert store.agent_config.parallel_tools is False assert store.agent_config.confirm_destructive is False assert store.agent_config.path_denylist == ["/secrets/", ".ssh/"] + assert store.agent_config.max_context_chars == 5000 # noqa: PLR2004