From 93de41343250d81c8e99eb6d9ba57111d232f4b3 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sat, 11 Jul 2026 23:22:25 +0800 Subject: [PATCH] feat(agent): expose request routing metadata through my --- nanobot/agent/tools/self.py | 19 +++++++++++++++ tests/agent/tools/test_self_tool.py | 36 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/nanobot/agent/tools/self.py b/nanobot/agent/tools/self.py index bfd1824c..5050d8d6 100644 --- a/nanobot/agent/tools/self.py +++ b/nanobot/agent/tools/self.py @@ -77,8 +77,11 @@ class MyTool(Tool): "exec_config", # inspect allowed (e.g. check sandbox), modify blocked "web_config", # inspect allowed (e.g. check enable), modify blocked "workspace_sandbox", # read-only view of workspace enforcement level + "request", # current message routing metadata }) + _REQUEST_FIELDS = ("channel", "chat_id", "sender_id") + _DENIED_ATTRS = frozenset({ "__class__", "__dict__", "__bases__", "__subclasses__", "__mro__", "__init__", "__new__", "__reduce__", "__getstate__", "__setstate__", @@ -141,6 +144,8 @@ class MyTool(Tool): "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" "Note: web_config and exec_config are readable but read-only.\n" "\n" "When to use:\n" @@ -173,6 +178,7 @@ class MyTool(Tool): "key": { "type": "string", "description": "Dot-path for check/set. Examples: 'max_iterations', 'workspace', 'provider_retry_mode'. " + "Use 'request.channel', 'request.chat_id', or 'request.sender_id' for current routing metadata. " "Use 'model_preset' to switch named model presets. For check without key, shows all config values.", }, "value": {"description": "New value (for set). Type must match target (int for max_iterations/context_window_tokens, str for model/model_preset)."}, @@ -340,6 +346,19 @@ class MyTool(Tool): def _inspect(self, key: str | None) -> str: if not key: return self._inspect_all() + if key == "request" or key.startswith("request."): + request_ctx = current_request_context() + if request_ctx is None: + return ToolResult.error("Error: current request context is unavailable") + if key == "request": + return self._format_value( + {field: getattr(request_ctx, field) for field in self._REQUEST_FIELDS}, + key, + ) + field = key.removeprefix("request.") + if field not in self._REQUEST_FIELDS: + return ToolResult.error(f"Error: '{key}' not found") + return self._format_value(getattr(request_ctx, field), key) if "." not in key: found, value = self._current_runtime_value(key) if found: diff --git a/tests/agent/tools/test_self_tool.py b/tests/agent/tools/test_self_tool.py index f2f2541f..01c18dde 100644 --- a/tests/agent/tools/test_self_tool.py +++ b/tests/agent/tools/test_self_tool.py @@ -1133,6 +1133,42 @@ class TestLastUsageInSummary: class TestRequestContext: + @pytest.mark.asyncio + async def test_check_exposes_current_routing_metadata_on_demand(self): + tool = _make_tool() + ctx = RequestContext( + channel="feishu", + chat_id="oc_abc123", + sender_id="ou_user456", + ) + + with request_context(ctx): + assert await tool.execute(action="check", key="request.channel") == ( + "request.channel: 'feishu'" + ) + assert await tool.execute(action="check", key="request.chat_id") == ( + "request.chat_id: 'oc_abc123'" + ) + assert await tool.execute(action="check", key="request.sender_id") == ( + "request.sender_id: 'ou_user456'" + ) + summary = await tool.execute(action="check") + + assert "oc_abc123" not in summary + assert "ou_user456" not in summary + + @pytest.mark.asyncio + async def test_request_routing_metadata_is_read_only(self): + tool = _make_tool() + + result = await tool.execute( + action="set", + key="request.chat_id", + value="replacement", + ) + + assert "read-only" in result + def test_audit_reads_bound_session(self): tool = _make_tool() ctx = RequestContext(