diff --git a/docs/my-tool.md b/docs/my-tool.md index e21466d1b..2e2ad3779 100644 --- a/docs/my-tool.md +++ b/docs/my-tool.md @@ -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 diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 27056abeb..789508dc1 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -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, diff --git a/nanobot/agent/progress_hook.py b/nanobot/agent/progress_hook.py index 0fa04748d..c6134ab35 100644 --- a/nanobot/agent/progress_hook.py +++ b/nanobot/agent/progress_hook.py @@ -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, diff --git a/nanobot/agent/tools/runtime_control.py b/nanobot/agent/tools/runtime_control.py index 066c4aa71..3e442db9f 100644 --- a/nanobot/agent/tools/runtime_control.py +++ b/nanobot/agent/tools/runtime_control.py @@ -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), diff --git a/nanobot/agent/tools/self.py b/nanobot/agent/tools/self.py index 87946f29f..cbac4315e 100644 --- a/nanobot/agent/tools/self.py +++ b/nanobot/agent/tools/self.py @@ -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", diff --git a/nanobot/agent/turn_hooks.py b/nanobot/agent/turn_hooks.py index 5f398e9f7..0cbf92f0d 100644 --- a/nanobot/agent/turn_hooks.py +++ b/nanobot/agent/turn_hooks.py @@ -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 diff --git a/nanobot/skills/my/SKILL.md b/nanobot/skills/my/SKILL.md index 539c0fd1e..87293caea 100644 --- a/nanobot/skills/my/SKILL.md +++ b/nanobot/skills/my/SKILL.md @@ -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 diff --git a/tests/agent/test_turn_hooks.py b/tests/agent/test_turn_hooks.py index 4372b2523..56d8347f8 100644 --- a/tests/agent/test_turn_hooks.py +++ b/tests/agent/test_turn_hooks.py @@ -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", diff --git a/tests/agent/tools/test_runtime_control.py b/tests/agent/tools/test_runtime_control.py index 984934287..28dafec11 100644 --- a/tests/agent/tools/test_runtime_control.py +++ b/tests/agent/tools/test_runtime_control.py @@ -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", diff --git a/tests/agent/tools/test_self_tool.py b/tests/agent/tools/test_self_tool.py index 256d441f4..632a01738 100644 --- a/tests/agent/tools/test_self_tool.py +++ b/tests/agent/tools/test_self_tool.py @@ -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) # ---------------------------------------------------------------------------