mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 08:13:11 +03:00
refactor(agent): remove loop iteration state (#5549)
* refactor(agent): remove loop iteration state * docs(my): remove stale iteration guidance
This commit is contained in:
+3
-5
@@ -4,11 +4,11 @@ Let the agent sense and adjust its own runtime state — like asking a coworker
|
|||||||
|
|
||||||
## Why You Need It
|
## 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:
|
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.
|
- **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.
|
- **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')
|
# workspace: PosixPath('/tmp/workspace')
|
||||||
# provider_retry_mode: 'standard'
|
# provider_retry_mode: 'standard'
|
||||||
# max_tool_result_chars: 16000
|
# max_tool_result_chars: 16000
|
||||||
# _current_iteration: 3
|
|
||||||
# _last_usage: {'prompt_tokens': 45000, 'completion_tokens': 8000}
|
# _last_usage: {'prompt_tokens': 45000, 'completion_tokens': 8000}
|
||||||
# Note: prompt_tokens is cumulative across all turns, not current context window occupancy.
|
# 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")` |
|
| "What model are you using?" | `check("model")` |
|
||||||
| "Which model preset is active?" | `check("model_preset")` |
|
| "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 |
|
| "How many tokens has this conversation used?" | `check("_last_usage")` — cumulative across all turns |
|
||||||
| "Where is your working directory?" | `check("workspace")` |
|
| "Where is your working directory?" | `check("workspace")` |
|
||||||
| "Show me your full config" | `check()` |
|
| "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 |
|
| Subagent manager | `subagents` | Observable, but replacing breaks the system |
|
||||||
| Execution config | `exec_config` | Can check sandbox/enable status, cannot change it |
|
| Execution config | `exec_config` | Can check sandbox/enable status, cannot change it |
|
||||||
| Web config | `web_config` | Can check 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
|
### Sensitive field protection
|
||||||
|
|
||||||
|
|||||||
@@ -201,10 +201,6 @@ class AgentLoop:
|
|||||||
5. Sends responses back
|
5. Sends responses back
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@property
|
|
||||||
def current_iteration(self) -> int:
|
|
||||||
return self._current_iteration
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tool_names(self) -> list[str]:
|
def tool_names(self) -> list[str]:
|
||||||
return self.tools.tool_names
|
return self.tools.tool_names
|
||||||
@@ -464,7 +460,6 @@ class AgentLoop:
|
|||||||
if model_preset:
|
if model_preset:
|
||||||
self.set_model_preset(model_preset, publish_update=False)
|
self.set_model_preset(model_preset, publish_update=False)
|
||||||
self._register_default_tools(provider_snapshot_loader=provider_snapshot_loader)
|
self._register_default_tools(provider_snapshot_loader=provider_snapshot_loader)
|
||||||
self._current_iteration: int = 0
|
|
||||||
self.commands = CommandRouter()
|
self.commands = CommandRouter()
|
||||||
register_builtin_commands(self.commands)
|
register_builtin_commands(self.commands)
|
||||||
|
|
||||||
@@ -1172,7 +1167,6 @@ class AgentLoop:
|
|||||||
session_key=active_session_key,
|
session_key=active_session_key,
|
||||||
workspace=effective_scope.project_path,
|
workspace=effective_scope.project_path,
|
||||||
tool_hint_max_length=self.tool_hint_max_length,
|
tool_hint_max_length=self.tool_hint_max_length,
|
||||||
on_iteration=lambda iteration: setattr(self, "_current_iteration", iteration),
|
|
||||||
registered_hook_factories=self._hook_factories,
|
registered_hook_factories=self._hook_factories,
|
||||||
turn_hook_factories=list(hook_factories or []),
|
turn_hook_factories=list(hook_factories or []),
|
||||||
registered_hooks=self._extra_hooks,
|
registered_hooks=self._extra_hooks,
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class AgentProgressHook(AgentHook):
|
|||||||
*,
|
*,
|
||||||
session_key: str | None = None,
|
session_key: str | None = None,
|
||||||
tool_hint_max_length: int = 40,
|
tool_hint_max_length: int = 40,
|
||||||
on_iteration: Callable[[int], None] | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(reraise=True)
|
super().__init__(reraise=True)
|
||||||
self._on_progress = on_progress
|
self._on_progress = on_progress
|
||||||
@@ -39,7 +38,6 @@ class AgentProgressHook(AgentHook):
|
|||||||
self._on_stream_end = on_stream_end
|
self._on_stream_end = on_stream_end
|
||||||
self._session_key = session_key
|
self._session_key = session_key
|
||||||
self._tool_hint_max_length = tool_hint_max_length
|
self._tool_hint_max_length = tool_hint_max_length
|
||||||
self._on_iteration = on_iteration
|
|
||||||
self._stream_buf = ""
|
self._stream_buf = ""
|
||||||
self._think_extractor = IncrementalThinkExtractor()
|
self._think_extractor = IncrementalThinkExtractor()
|
||||||
self._reasoning_open = False
|
self._reasoning_open = False
|
||||||
@@ -96,8 +94,6 @@ class AgentProgressHook(AgentHook):
|
|||||||
self._think_extractor.reset()
|
self._think_extractor.reset()
|
||||||
|
|
||||||
async def before_iteration(self, context: AgentHookContext) -> None:
|
async def before_iteration(self, context: AgentHookContext) -> None:
|
||||||
if self._on_iteration:
|
|
||||||
self._on_iteration(context.iteration)
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Starting agent loop iteration {} for session {}",
|
"Starting agent loop iteration {} for session {}",
|
||||||
context.iteration,
|
context.iteration,
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ RUNTIME_SNAPSHOT_KEYS = frozenset({
|
|||||||
"workspace",
|
"workspace",
|
||||||
"provider_retry_mode",
|
"provider_retry_mode",
|
||||||
"max_tool_result_chars",
|
"max_tool_result_chars",
|
||||||
"current_iteration",
|
|
||||||
"_current_iteration",
|
|
||||||
"tool_names",
|
"tool_names",
|
||||||
"web_config",
|
"web_config",
|
||||||
"exec_config",
|
"exec_config",
|
||||||
@@ -59,7 +57,6 @@ class RuntimeSnapshot:
|
|||||||
workspace: Path | str
|
workspace: Path | str
|
||||||
provider_retry_mode: str
|
provider_retry_mode: str
|
||||||
max_tool_result_chars: int
|
max_tool_result_chars: int
|
||||||
current_iteration: int
|
|
||||||
tool_names: list[str]
|
tool_names: list[str]
|
||||||
web_config: dict[str, object]
|
web_config: dict[str, object]
|
||||||
exec_config: dict[str, object]
|
exec_config: dict[str, object]
|
||||||
@@ -77,8 +74,6 @@ class RuntimeSnapshot:
|
|||||||
"workspace": self.workspace,
|
"workspace": self.workspace,
|
||||||
"provider_retry_mode": self.provider_retry_mode,
|
"provider_retry_mode": self.provider_retry_mode,
|
||||||
"max_tool_result_chars": self.max_tool_result_chars,
|
"max_tool_result_chars": self.max_tool_result_chars,
|
||||||
"current_iteration": self.current_iteration,
|
|
||||||
"_current_iteration": self.current_iteration,
|
|
||||||
"tool_names": self.tool_names,
|
"tool_names": self.tool_names,
|
||||||
"web_config": self.web_config,
|
"web_config": self.web_config,
|
||||||
"exec_config": self.exec_config,
|
"exec_config": self.exec_config,
|
||||||
@@ -141,9 +136,6 @@ class _RuntimeControlTarget(Protocol):
|
|||||||
@property
|
@property
|
||||||
def workspace(self) -> Path: ...
|
def workspace(self) -> Path: ...
|
||||||
|
|
||||||
@property
|
|
||||||
def current_iteration(self) -> int: ...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tool_names(self) -> list[str]: ...
|
def tool_names(self) -> list[str]: ...
|
||||||
|
|
||||||
@@ -179,7 +171,6 @@ class AgentRuntimeControl:
|
|||||||
),
|
),
|
||||||
provider_retry_mode=target.provider_retry_mode,
|
provider_retry_mode=target.provider_retry_mode,
|
||||||
max_tool_result_chars=target.max_tool_result_chars,
|
max_tool_result_chars=target.max_tool_result_chars,
|
||||||
current_iteration=target.current_iteration,
|
|
||||||
tool_names=list(target.tool_names),
|
tool_names=list(target.tool_names),
|
||||||
web_config=_snapshot_web_config(target.web_config),
|
web_config=_snapshot_web_config(target.web_config),
|
||||||
exec_config=_snapshot_exec_config(target.exec_config),
|
exec_config=_snapshot_exec_config(target.exec_config),
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ class MyTool(Tool):
|
|||||||
READ_ONLY = frozenset({
|
READ_ONLY = frozenset({
|
||||||
"subagents", # observable but replacing it would break the system
|
"subagents", # observable but replacing it would break the system
|
||||||
"tool_names",
|
"tool_names",
|
||||||
"current_iteration",
|
|
||||||
"_current_iteration", # updated by runner only
|
|
||||||
"exec_config", # inspect allowed (e.g. check sandbox), modify blocked
|
"exec_config", # inspect allowed (e.g. check sandbox), modify blocked
|
||||||
"web_config", # inspect allowed (e.g. check enable), modify blocked
|
"web_config", # inspect allowed (e.g. check enable), modify blocked
|
||||||
"model_presets", # config-derived catalog; changes require config reload
|
"model_presets", # config-derived catalog; changes require config reload
|
||||||
@@ -152,8 +150,6 @@ class MyTool(Tool):
|
|||||||
"(e.g. 'web_config.enable').\n"
|
"(e.g. 'web_config.enable').\n"
|
||||||
"- set (key, value): change config or store notes in your scratchpad. "
|
"- set (key, value): change config or store notes in your scratchpad. "
|
||||||
"Scratchpad keys persist across turns but not restarts.\n"
|
"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, "
|
"Current routing metadata is available read-only via request.channel, "
|
||||||
"request.chat_id, and request.sender_id.\n"
|
"request.chat_id, and request.sender_id.\n"
|
||||||
"Use model_preset for session-scoped model or context changes; direct "
|
"Use model_preset for session-scoped model or context changes; direct "
|
||||||
@@ -441,7 +437,6 @@ class MyTool(Tool):
|
|||||||
"workspace",
|
"workspace",
|
||||||
"provider_retry_mode",
|
"provider_retry_mode",
|
||||||
"max_tool_result_chars",
|
"max_tool_result_chars",
|
||||||
"_current_iteration",
|
|
||||||
"web_config",
|
"web_config",
|
||||||
"exec_config",
|
"exec_config",
|
||||||
"subagents",
|
"subagents",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ class AgentTurnHookSpec:
|
|||||||
session_key: str | None = None
|
session_key: str | None = None
|
||||||
workspace: Path | None = None
|
workspace: Path | None = None
|
||||||
tool_hint_max_length: int = 40
|
tool_hint_max_length: int = 40
|
||||||
on_iteration: Callable[[int], None] | None = None
|
|
||||||
registered_hook_factories: list[AgentTurnHookFactory] = field(default_factory=list)
|
registered_hook_factories: list[AgentTurnHookFactory] = field(default_factory=list)
|
||||||
turn_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)
|
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,
|
on_stream_end=spec.on_stream_end,
|
||||||
session_key=spec.session_key,
|
session_key=spec.session_key,
|
||||||
tool_hint_max_length=spec.tool_hint_max_length,
|
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:
|
if spec.ephemeral and not spec.run_extra_hooks_for_ephemeral:
|
||||||
return progress_hook
|
return progress_hook
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: my
|
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
|
# 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
|
1. **Identify the situation** from the categories below
|
||||||
2. **Call the my tool** with the appropriate action
|
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)
|
4. **For detailed examples**, read [references/examples.md](references/examples.md)
|
||||||
|
|
||||||
## When to check
|
## When to check
|
||||||
|
|||||||
@@ -31,33 +31,18 @@ def test_turn_hook_context_preserves_legacy_positional_arguments(tmp_path) -> No
|
|||||||
assert context.attributes == {}
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_turn_hook_builder_runs_registered_hooks_before_turn_hooks() -> None:
|
async def test_turn_hook_builder_runs_registered_hooks_before_turn_hooks() -> None:
|
||||||
events: list[str] = []
|
events: list[str] = []
|
||||||
|
|
||||||
hook = build_agent_turn_hook(AgentTurnHookSpec(
|
hook = build_agent_turn_hook(AgentTurnHookSpec(
|
||||||
on_iteration=lambda iteration: events.append(f"progress:{iteration}"),
|
|
||||||
registered_hooks=[RecordingHook(events, "registered")],
|
registered_hooks=[RecordingHook(events, "registered")],
|
||||||
turn_hooks=[RecordingHook(events, "turn")],
|
turn_hooks=[RecordingHook(events, "turn")],
|
||||||
))
|
))
|
||||||
|
|
||||||
await hook.before_iteration(AgentHookContext(iteration=2, messages=[]))
|
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
|
@pytest.mark.asyncio
|
||||||
@@ -75,7 +60,6 @@ async def test_turn_hook_builder_runs_factories_with_matching_registration_order
|
|||||||
return _create
|
return _create
|
||||||
|
|
||||||
hook = build_agent_turn_hook(AgentTurnHookSpec(
|
hook = build_agent_turn_hook(AgentTurnHookSpec(
|
||||||
on_iteration=lambda iteration: events.append(f"progress:{iteration}"),
|
|
||||||
channel="websocket",
|
channel="websocket",
|
||||||
chat_id="chat-1",
|
chat_id="chat-1",
|
||||||
message_id="msg-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=[]))
|
await hook.before_iteration(AgentHookContext(iteration=2, messages=[]))
|
||||||
|
|
||||||
assert events == [
|
assert events == [
|
||||||
"progress:2",
|
|
||||||
"registered_factory:2",
|
"registered_factory:2",
|
||||||
"registered:2",
|
"registered:2",
|
||||||
"turn_factory:2",
|
"turn_factory:2",
|
||||||
|
|||||||
@@ -57,7 +57,22 @@ def test_runtime_snapshot_has_exact_allowlist_and_redacts_secrets(tmp_path: Path
|
|||||||
snapshot = _my_tool(loop)._runtime_control.snapshot()
|
snapshot = _my_tool(loop)._runtime_control.snapshot()
|
||||||
values = snapshot.as_mapping()
|
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({
|
assert RUNTIME_COMMAND_KEYS == frozenset({
|
||||||
"model",
|
"model",
|
||||||
"model_preset",
|
"model_preset",
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ def _make_mock_loop(**overrides):
|
|||||||
loop._start_time = 1000.0
|
loop._start_time = 1000.0
|
||||||
loop.exec_config = ExecToolConfig()
|
loop.exec_config = ExecToolConfig()
|
||||||
loop.channels_config = MagicMock()
|
loop.channels_config = MagicMock()
|
||||||
loop._current_iteration = 0
|
|
||||||
loop.current_iteration = loop._current_iteration
|
|
||||||
loop.provider_retry_mode = "standard"
|
loop.provider_retry_mode = "standard"
|
||||||
loop.max_tool_result_chars = 16000
|
loop.max_tool_result_chars = 16000
|
||||||
loop.model_preset = None
|
loop.model_preset = None
|
||||||
@@ -110,7 +108,6 @@ class TestInspectSummary:
|
|||||||
assert "workspace" in result
|
assert "workspace" in result
|
||||||
assert "provider_retry_mode" in result
|
assert "provider_retry_mode" in result
|
||||||
assert "max_tool_result_chars" 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'"
|
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)
|
# request context (audit session tracking)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user