refactor(agent): remove loop iteration state (#5549)

* refactor(agent): remove loop iteration state

* docs(my): remove stale iteration guidance
This commit is contained in:
chengyongru
2026-08-26 15:57:47 +08:00
committed by GitHub
parent 4f6c0aedfa
commit 9d34fc5af2
10 changed files with 22 additions and 81 deletions
+3 -5
View File
@@ -4,11 +4,11 @@ Let the agent sense and adjust its own runtime state — like asking a coworker
## Why You Need It
Normal tools let the agent operate on the outside world (read/write files, search code). But the agent knows nothing about itself — it doesn't know which model it's running on, how many iterations are left, or how many tokens it has consumed.
Normal tools let the agent operate on the outside world (read/write files, search code). But the agent knows nothing about itself — it doesn't know which model it's running on, which workspace it can access, or which runtime limits apply.
My tool fills this gap. With it, the agent can:
- **Know who it is**: What model am I using? Where is my workspace? How many iterations remain?
- **Know who it is**: What model am I using? Where is my workspace? What is my per-turn iteration limit?
- **Adapt on the fly**: Complex task? Expand the context window. Simple chat? Switch to a faster model.
- **Remember across turns**: Store notes in your scratchpad that persist into the next conversation turn.
@@ -44,7 +44,6 @@ my(action="check")
# workspace: PosixPath('/tmp/workspace')
# provider_retry_mode: 'standard'
# max_tool_result_chars: 16000
# _current_iteration: 3
# _last_usage: {'prompt_tokens': 45000, 'completion_tokens': 8000}
# Note: prompt_tokens is cumulative across all turns, not current context window occupancy.
```
@@ -68,7 +67,7 @@ my(action="check", key="web_config.enable")
|----------|-----|
| "What model are you using?" | `check("model")` |
| "Which model preset is active?" | `check("model_preset")` |
| "How many more tool calls can you make?" | `check("max_iterations")` minus `check("_current_iteration")` |
| "What is the per-turn iteration limit?" | `check("max_iterations")` |
| "How many tokens has this conversation used?" | `check("_last_usage")` — cumulative across all turns |
| "Where is your working directory?" | `check("workspace")` |
| "Show me your full config" | `check()` |
@@ -205,7 +204,6 @@ Can be checked but not set:
| Subagent manager | `subagents` | Observable, but replacing breaks the system |
| Execution config | `exec_config` | Can check sandbox/enable status, cannot change it |
| Web config | `web_config` | Can check enable status, cannot change it |
| Iteration counter | `_current_iteration` | Updated by runner only |
### Sensitive field protection
-6
View File
@@ -201,10 +201,6 @@ class AgentLoop:
5. Sends responses back
"""
@property
def current_iteration(self) -> int:
return self._current_iteration
@property
def tool_names(self) -> list[str]:
return self.tools.tool_names
@@ -464,7 +460,6 @@ class AgentLoop:
if model_preset:
self.set_model_preset(model_preset, publish_update=False)
self._register_default_tools(provider_snapshot_loader=provider_snapshot_loader)
self._current_iteration: int = 0
self.commands = CommandRouter()
register_builtin_commands(self.commands)
@@ -1172,7 +1167,6 @@ class AgentLoop:
session_key=active_session_key,
workspace=effective_scope.project_path,
tool_hint_max_length=self.tool_hint_max_length,
on_iteration=lambda iteration: setattr(self, "_current_iteration", iteration),
registered_hook_factories=self._hook_factories,
turn_hook_factories=list(hook_factories or []),
registered_hooks=self._extra_hooks,
-4
View File
@@ -31,7 +31,6 @@ class AgentProgressHook(AgentHook):
*,
session_key: str | None = None,
tool_hint_max_length: int = 40,
on_iteration: Callable[[int], None] | None = None,
) -> None:
super().__init__(reraise=True)
self._on_progress = on_progress
@@ -39,7 +38,6 @@ class AgentProgressHook(AgentHook):
self._on_stream_end = on_stream_end
self._session_key = session_key
self._tool_hint_max_length = tool_hint_max_length
self._on_iteration = on_iteration
self._stream_buf = ""
self._think_extractor = IncrementalThinkExtractor()
self._reasoning_open = False
@@ -96,8 +94,6 @@ class AgentProgressHook(AgentHook):
self._think_extractor.reset()
async def before_iteration(self, context: AgentHookContext) -> None:
if self._on_iteration:
self._on_iteration(context.iteration)
logger.debug(
"Starting agent loop iteration {} for session {}",
context.iteration,
-9
View File
@@ -28,8 +28,6 @@ RUNTIME_SNAPSHOT_KEYS = frozenset({
"workspace",
"provider_retry_mode",
"max_tool_result_chars",
"current_iteration",
"_current_iteration",
"tool_names",
"web_config",
"exec_config",
@@ -59,7 +57,6 @@ class RuntimeSnapshot:
workspace: Path | str
provider_retry_mode: str
max_tool_result_chars: int
current_iteration: int
tool_names: list[str]
web_config: dict[str, object]
exec_config: dict[str, object]
@@ -77,8 +74,6 @@ class RuntimeSnapshot:
"workspace": self.workspace,
"provider_retry_mode": self.provider_retry_mode,
"max_tool_result_chars": self.max_tool_result_chars,
"current_iteration": self.current_iteration,
"_current_iteration": self.current_iteration,
"tool_names": self.tool_names,
"web_config": self.web_config,
"exec_config": self.exec_config,
@@ -141,9 +136,6 @@ class _RuntimeControlTarget(Protocol):
@property
def workspace(self) -> Path: ...
@property
def current_iteration(self) -> int: ...
@property
def tool_names(self) -> list[str]: ...
@@ -179,7 +171,6 @@ class AgentRuntimeControl:
),
provider_retry_mode=target.provider_retry_mode,
max_tool_result_chars=target.max_tool_result_chars,
current_iteration=target.current_iteration,
tool_names=list(target.tool_names),
web_config=_snapshot_web_config(target.web_config),
exec_config=_snapshot_exec_config(target.exec_config),
-5
View File
@@ -88,8 +88,6 @@ class MyTool(Tool):
READ_ONLY = frozenset({
"subagents", # observable but replacing it would break the system
"tool_names",
"current_iteration",
"_current_iteration", # updated by runner only
"exec_config", # inspect allowed (e.g. check sandbox), modify blocked
"web_config", # inspect allowed (e.g. check enable), modify blocked
"model_presets", # config-derived catalog; changes require config reload
@@ -152,8 +150,6 @@ class MyTool(Tool):
"(e.g. 'web_config.enable').\n"
"- set (key, value): change config or store notes in your scratchpad. "
"Scratchpad keys persist across turns but not restarts.\n"
"Key values: _current_iteration (current progress), "
"max_iterations - _current_iteration = remaining iterations.\n"
"Current routing metadata is available read-only via request.channel, "
"request.chat_id, and request.sender_id.\n"
"Use model_preset for session-scoped model or context changes; direct "
@@ -441,7 +437,6 @@ class MyTool(Tool):
"workspace",
"provider_retry_mode",
"max_tool_result_chars",
"_current_iteration",
"web_config",
"exec_config",
"subagents",
-2
View File
@@ -32,7 +32,6 @@ class AgentTurnHookSpec:
session_key: str | None = None
workspace: Path | None = None
tool_hint_max_length: int = 40
on_iteration: Callable[[int], None] | None = None
registered_hook_factories: list[AgentTurnHookFactory] = field(default_factory=list)
turn_hook_factories: list[AgentTurnHookFactory] = field(default_factory=list)
registered_hooks: list[AgentHook] = field(default_factory=list)
@@ -50,7 +49,6 @@ def build_agent_turn_hook(spec: AgentTurnHookSpec) -> AgentHook:
on_stream_end=spec.on_stream_end,
session_key=spec.session_key,
tool_hint_max_length=spec.tool_hint_max_length,
on_iteration=spec.on_iteration,
)
if spec.ephemeral and not spec.run_extra_hooks_for_ephemeral:
return progress_hook
+2 -2
View File
@@ -1,6 +1,6 @@
---
name: my
description: Inspect and optionally adjust the agent's runtime state. Use to check the current model or preset, context window, iteration progress and limits, workspace and tool configuration, subagent status, and request routing metadata such as channel, chat ID, and sender ID; diagnose unavailable capabilities; change allowed runtime settings; or store temporary session scratchpad values.
description: Inspect and optionally adjust the agent's runtime state. Use to check the current model or preset, context window and runtime limits, workspace and tool configuration, subagent status, and request routing metadata such as channel, chat ID, and sender ID; diagnose unavailable capabilities; change allowed runtime settings; or store temporary session scratchpad values.
---
# Self-Awareness
@@ -9,7 +9,7 @@ description: Inspect and optionally adjust the agent's runtime state. Use to che
1. **Identify the situation** from the categories below
2. **Call the my tool** with the appropriate action
3. **If set**, warn the user before changing impactful settings (model, iterations)
3. **If set**, warn the user before changing impactful settings such as the model or runtime limits
4. **For detailed examples**, read [references/examples.md](references/examples.md)
## When to check
+1 -18
View File
@@ -31,33 +31,18 @@ def test_turn_hook_context_preserves_legacy_positional_arguments(tmp_path) -> No
assert context.attributes == {}
@pytest.mark.asyncio
async def test_turn_hook_builder_runs_progress_hook_before_extra_hooks() -> None:
events: list[str] = []
hook = build_agent_turn_hook(AgentTurnHookSpec(
on_iteration=lambda iteration: events.append(f"progress:{iteration}"),
registered_hooks=[RecordingHook(events)],
))
await hook.before_iteration(AgentHookContext(iteration=2, messages=[]))
assert events == ["progress:2", "hook:2"]
@pytest.mark.asyncio
async def test_turn_hook_builder_runs_registered_hooks_before_turn_hooks() -> None:
events: list[str] = []
hook = build_agent_turn_hook(AgentTurnHookSpec(
on_iteration=lambda iteration: events.append(f"progress:{iteration}"),
registered_hooks=[RecordingHook(events, "registered")],
turn_hooks=[RecordingHook(events, "turn")],
))
await hook.before_iteration(AgentHookContext(iteration=2, messages=[]))
assert events == ["progress:2", "registered:2", "turn:2"]
assert events == ["registered:2", "turn:2"]
@pytest.mark.asyncio
@@ -75,7 +60,6 @@ async def test_turn_hook_builder_runs_factories_with_matching_registration_order
return _create
hook = build_agent_turn_hook(AgentTurnHookSpec(
on_iteration=lambda iteration: events.append(f"progress:{iteration}"),
channel="websocket",
chat_id="chat-1",
message_id="msg-1",
@@ -92,7 +76,6 @@ async def test_turn_hook_builder_runs_factories_with_matching_registration_order
await hook.before_iteration(AgentHookContext(iteration=2, messages=[]))
assert events == [
"progress:2",
"registered_factory:2",
"registered:2",
"turn_factory:2",
+16 -1
View File
@@ -57,7 +57,22 @@ def test_runtime_snapshot_has_exact_allowlist_and_redacts_secrets(tmp_path: Path
snapshot = _my_tool(loop)._runtime_control.snapshot()
values = snapshot.as_mapping()
assert frozenset(values) == RUNTIME_SNAPSHOT_KEYS
expected_snapshot_keys = frozenset({
"model",
"model_preset",
"model_presets",
"max_iterations",
"context_window_tokens",
"workspace",
"provider_retry_mode",
"max_tool_result_chars",
"tool_names",
"web_config",
"exec_config",
"subagents",
})
assert RUNTIME_SNAPSHOT_KEYS == expected_snapshot_keys
assert frozenset(values) == expected_snapshot_keys
assert RUNTIME_COMMAND_KEYS == frozenset({
"model",
"model_preset",
-29
View File
@@ -32,8 +32,6 @@ def _make_mock_loop(**overrides):
loop._start_time = 1000.0
loop.exec_config = ExecToolConfig()
loop.channels_config = MagicMock()
loop._current_iteration = 0
loop.current_iteration = loop._current_iteration
loop.provider_retry_mode = "standard"
loop.max_tool_result_chars = 16000
loop.model_preset = None
@@ -110,7 +108,6 @@ class TestInspectSummary:
assert "workspace" in result
assert "provider_retry_mode" in result
assert "max_tool_result_chars" in result
assert "_current_iteration" in result
# ---------------------------------------------------------------------------
@@ -1080,32 +1077,6 @@ class TestSecurityAttributeProtection:
assert result == "model_presets.fast.model: 'fast-model'"
# ---------------------------------------------------------------------------
# current iteration count (Fix #2)
# ---------------------------------------------------------------------------
class TestCurrentIteration:
@pytest.mark.asyncio
async def test_inspect_current_iteration(self):
tool = _make_tool()
result = await tool.execute(action="check", key="_current_iteration")
assert "0" in result
@pytest.mark.asyncio
async def test_current_iteration_in_summary(self):
tool = _make_tool()
result = await tool.execute(action="check")
assert "_current_iteration" in result
@pytest.mark.asyncio
async def test_modify_current_iteration_blocked(self):
"""_current_iteration is READ_ONLY — cannot be set manually."""
tool = _make_tool()
result = await tool.execute(action="set", key="_current_iteration", value=5)
assert "read-only" in result
# ---------------------------------------------------------------------------
# request context (audit session tracking)
# ---------------------------------------------------------------------------