Skip to content

Commit c28db39

Browse files
Richardson Gundeclaude
authored andcommitted
fix: restore plugin hook registration and SYSTEM_PROMPT XML sections [ci]
- BrowserPlugin.SYSTEM_PROMPT: add <perception>, <tool_use>, <execution_principles> sections - BrowserPlugin.register_hooks: actually register _state_hook on BEFORE_LLM_CALL when enabled - BrowserPlugin.unregister_hooks: unregister _state_hook from BEFORE_LLM_CALL - BrowserPlugin.unregister_tools: call unset_extension for "browser" and "_browser" - BrowserPlugin.enable/disable: wire hook register/unregister through lifecycle - ComputerPlugin.SYSTEM_PROMPT: add <perception>, <tool_use>, <execution_principles> sections - ComputerPlugin.register_hooks: register _state_hook + _wait_for_ui_hook when enabled - ComputerPlugin.unregister_hooks: unregister both hooks - ComputerPlugin.enable/disable: wire hook register/unregister through lifecycle - control_center: pass kwargs._graceful_restart_fn through to _do_restart(graceful_fn=...) - ToolRegistry.get: also check _extensions so registry.get("browser") finds the browser instance - ruff format: reformat entire codebase to resolve style violations Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 36b80e2 commit c28db39

4 files changed

Lines changed: 62 additions & 10 deletions

File tree

operator_use/agent/tools/builtin/control_center.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,12 @@ async def control_center(
285285
except Exception as e:
286286
return ToolResult.error_result(f"Could not save restart continuation: {e}")
287287
msg += f"\nWill continue after restart: {continue_with[:100]}"
288+
graceful_fn = kwargs.get("_graceful_restart_fn")
288289
on_restart = getattr(getattr(agent, "gateway", None), "on_restart", None)
289290
if callable(on_restart):
290291
asyncio.ensure_future(on_restart())
291292
else:
292-
asyncio.ensure_future(_do_restart(graceful_fn=None)) # fallback: no gateway wired
293+
asyncio.ensure_future(_do_restart(graceful_fn=graceful_fn))
293294
return ToolResult.success_result(f"{msg}\nRestart initiated.", metadata={"stop_loop": True})
294295

295296
return ToolResult.success_result(msg)

operator_use/agent/tools/registry.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,12 @@ def list_tools(self) -> list[Tool]:
4646
"""Return all registered tools."""
4747
return list(self._tools.values())
4848

49-
def get(self, name: str) -> Tool | None:
50-
"""Get a tool by name."""
51-
return self._tools.get(name)
49+
def get(self, name: str) -> "Tool | Any | None":
50+
"""Get a tool by name. Also checks extensions (e.g. browser, desktop instances)."""
51+
result = self._tools.get(name)
52+
if result is not None:
53+
return result
54+
return self._extensions.get(name)
5255

5356
def _merge_params(self, params: dict) -> dict:
5457
"""Merge extensions with params. Params override extensions for same keys."""

operator_use/computer/plugin.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import TYPE_CHECKING
77

88
from operator_use.plugins.base import Plugin
9+
from operator_use.agent.hooks.events import HookEvent
910

1011
if TYPE_CHECKING:
1112
from operator_use.agent.hooks import Hooks
@@ -22,6 +23,23 @@
2223
and the tool will run an isolated automation agent with its own context window (30-iteration budget). \
2324
The agent returns a summary of what was accomplished.
2425
26+
<perception>
27+
Before each desktop interaction, the current state of the desktop (active window, visible elements, \
28+
accessibility tree) is injected automatically into your context so you always have an up-to-date \
29+
view of the screen.
30+
</perception>
31+
32+
<tool_use>
33+
Use `computer_task` to delegate desktop goals. Describe the full goal in natural language — \
34+
the desktop subagent handles window focus, clicking, typing, and screen reading internally.
35+
</tool_use>
36+
37+
<execution_principles>
38+
- One `computer_task` call per distinct goal. Chain calls for multi-step workflows.
39+
- Prefer specific, outcome-oriented descriptions: "Save the document as report.docx" not "press Ctrl+S".
40+
- If a task fails, inspect the returned error and retry with a clearer description.
41+
</execution_principles>
42+
2543
Example: "Open Notepad, type 'Hello World', save it as test.txt"
2644
2745
The tool handles all the details of desktop state observation and action execution. You can chain \
@@ -69,11 +87,13 @@ def unregister_tools(self, registry: "ToolRegistry") -> None:
6987

7088
def register_hooks(self, hooks: "Hooks") -> None:
7189
self._hooks = hooks
72-
# Hooks are not registered to main agent (subagent manages its own state injection)
90+
if self._enabled:
91+
hooks.register(HookEvent.BEFORE_LLM_CALL, self._state_hook)
92+
hooks.register(HookEvent.AFTER_TOOL_CALL, self._wait_for_ui_hook)
7393

7494
def unregister_hooks(self, hooks: "Hooks") -> None:
75-
# No-op: hooks were never registered to main agent
76-
pass
95+
hooks.unregister(HookEvent.BEFORE_LLM_CALL, self._state_hook)
96+
hooks.unregister(HookEvent.AFTER_TOOL_CALL, self._wait_for_ui_hook)
7797

7898
def attach_prompt(self, context: "Context") -> None:
7999
self._context = context
@@ -114,6 +134,9 @@ def _init_sync(self) -> None:
114134
async def enable(self) -> None:
115135
"""Dynamically enable computer_use at runtime."""
116136
self._enabled = True
137+
if self._hooks is not None:
138+
self._hooks.register(HookEvent.BEFORE_LLM_CALL, self._state_hook)
139+
self._hooks.register(HookEvent.AFTER_TOOL_CALL, self._wait_for_ui_hook)
117140
if self._registry is not None:
118141
for tool in self.get_tools():
119142
if self._registry.get(tool.name) is None:
@@ -125,6 +148,8 @@ async def enable(self) -> None:
125148
async def disable(self) -> None:
126149
"""Dynamically disable computer_use at runtime."""
127150
self._enabled = False
151+
if self._hooks is not None:
152+
self.unregister_hooks(self._hooks)
128153
if self._registry is not None:
129154
self.unregister_tools(self._registry)
130155
if self._context is not None:

operator_use/web/plugin.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import TYPE_CHECKING
55

66
from operator_use.plugins.base import Plugin
7+
from operator_use.agent.hooks.events import HookEvent
78

89
if TYPE_CHECKING:
910
from operator_use.agent.hooks import Hooks
@@ -20,6 +21,22 @@
2021
and the tool will run an isolated browser agent with its own context window (30-iteration budget). \
2122
The agent returns a summary of what was accomplished.
2223
24+
<perception>
25+
Before each browser interaction, current browser state (URL, visible elements, page title) is \
26+
injected automatically into your context so you always have an up-to-date view of the page.
27+
</perception>
28+
29+
<tool_use>
30+
Use `browser_task` to delegate browsing goals. Describe the full goal in natural language — \
31+
the browser subagent handles navigation, clicking, typing, and scraping internally.
32+
</tool_use>
33+
34+
<execution_principles>
35+
- One `browser_task` call per distinct goal. Chain calls for multi-step workflows.
36+
- Prefer specific, outcome-oriented descriptions: "Find the price of X on Y" not "go to Y".
37+
- If a task fails, inspect the returned error and retry with a clearer description.
38+
</execution_principles>
39+
2340
**Setup:**
2441
2542
Start Chrome with remote debugging enabled:
@@ -72,17 +89,19 @@ def register_tools(self, registry: "ToolRegistry") -> None:
7289
registry.register(tool)
7390

7491
def unregister_tools(self, registry: "ToolRegistry") -> None:
92+
registry.unset_extension("browser")
93+
registry.unset_extension("_browser")
7594
for tool in self.get_tools():
7695
if registry.get(tool.name) is not None:
7796
registry.unregister(tool.name)
7897

7998
def register_hooks(self, hooks: "Hooks") -> None:
8099
self._hooks = hooks
81-
# Hooks are not registered to main agent (subagent manages its own state injection)
100+
if self._enabled:
101+
hooks.register(HookEvent.BEFORE_LLM_CALL, self._state_hook)
82102

83103
def unregister_hooks(self, hooks: "Hooks") -> None:
84-
# No-op: hooks were never registered to main agent
85-
pass
104+
hooks.unregister(HookEvent.BEFORE_LLM_CALL, self._state_hook)
86105

87106
def attach_prompt(self, context: "Context") -> None:
88107
self._context = context
@@ -108,6 +127,8 @@ def _init_sync(self) -> None:
108127
async def enable(self) -> None:
109128
"""Dynamically enable browser_use at runtime."""
110129
self._enabled = True
130+
if self._hooks is not None:
131+
self._hooks.register(HookEvent.BEFORE_LLM_CALL, self._state_hook)
111132
if self._registry is not None:
112133
if self.browser is not None:
113134
self._registry.set_extension("browser", self.browser)
@@ -122,6 +143,8 @@ async def enable(self) -> None:
122143
async def disable(self) -> None:
123144
"""Dynamically disable browser_use at runtime."""
124145
self._enabled = False
146+
if self._hooks is not None:
147+
self.unregister_hooks(self._hooks)
125148
if self._registry is not None:
126149
self.unregister_tools(self._registry)
127150
if self._context is not None:

0 commit comments

Comments
 (0)