File size: 6,434 Bytes
ee9fcd0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
"""Legacy conversation compatibility for the former Python Canvas UI tool.

New conversations define ``canvas_ui_control`` through the agent-server's
``client_tools`` JSON API. The distinct name avoids colliding with this
process-global legacy registration. This module remains importable because
persisted conversations store
``tool_module_qualnames = {"canvas_ui": "canvas_ui_tool"}`` and may contain
``CanvasUIAction`` / ``CanvasUIObservation`` events. Agent-server imports the
module before restoring those conversations so the legacy event kinds and tool
registration remain resolvable.

The server-side executor is a no-op that returns an acknowledgment. The actual
UI effect happens client-side: the frontend watches the WebSocket stream for
legacy ``canvas_ui`` and current ``canvas_ui_control`` ActionEvents and dispatches
the command.

The launchers also import this module at agent-server startup
(``--import-modules canvas_ui_tool``) so the builtin ``FinishTool`` registration
at the bottom runs before any conversation is created.
"""

from collections.abc import Sequence
from typing import Literal

from pydantic import Field

from openhands.sdk import Action, Observation, ToolDefinition
from openhands.sdk.tool import (
    FinishTool,
    ToolAnnotations,
    ToolExecutor,
    list_registered_tools,
    register_tool,
)


CanvasCommand = Literal["navigate_to_file", "open_tab", "show_preview"]
CanvasTab = Literal[
    "files", "browser", "vscode", "terminal", "planner", "tasklist"
]


class CanvasUIAction(Action):
    """Direct the Agent Canvas frontend to perform a UI action."""

    command: CanvasCommand = Field(description="UI command to dispatch.")
    path: str | None = Field(
        default=None,
        description=(
            "Workspace-relative file path. Required for navigate_to_file and "
            "show_preview; ignored otherwise."
        ),
    )
    tab: CanvasTab | None = Field(
        default=None,
        description=(
            "Tab to open. Required for open_tab; ignored otherwise. One of "
            "files, browser, vscode, terminal, planner, tasklist."
        ),
    )


class CanvasUIObservation(Observation):
    """Acknowledgment that the UI command was dispatched to the frontend."""


class CanvasUIExecutor(ToolExecutor[CanvasUIAction, CanvasUIObservation]):
    def __call__(
        self,
        action: CanvasUIAction,
        conversation=None,  # noqa: ARG002
    ) -> CanvasUIObservation:
        return CanvasUIObservation.from_text(
            f"UI command '{action.command}' dispatched to the Agent Canvas frontend."
        )


_CANVAS_UI_DESCRIPTION = """The user is interacting with you inside Agent Canvas β€” a web UI with a chat panel on the left and a tabbed right-side panel (files, terminal, browser, vscode, planner, tasklist). This tool lets you drive that right-side panel so the user sees what you just produced.

They will NOT see the files you wrote, the terminal output, or the browser
unless you call this tool to switch the right-side panel to the relevant
tab. Call this every time you finish work that produces something the user
should look at β€” don't rely on them noticing on their own.

When to call (pick the most specific option that matches your last action):

* You wrote or modified a single file (ANY language, ANY size β€” including
  small scripts like a hello-world bash file) β†’
    command="navigate_to_file", path=<workspace-relative path of that file>

* You generated an HTML page, image, SVG, PDF, markdown report, or other
  previewable artifact β†’
    command="show_preview", path=<that file>

* You finished editing multiple files in one logical step β†’
    command="open_tab", tab="files"
    (The Files tab automatically renders a diff view when the workspace has
    uncommitted git changes, which covers the "highlight changes" case.)

* You ran a long-running terminal command, or one whose output the user
  should inspect β†’
    command="open_tab", tab="terminal"

* You browsed to a URL the user should see β†’
    First call browser_get_state(include_screenshot=true) after your final
    browser interaction so Agent Canvas has a screenshot to display, then call
    command="open_tab", tab="browser"
    (browser_navigate alone only updates the URL; without browser_get_state,
    the Browser tab will open without a screenshot.)

Call this BEFORE writing your chat-message summary of the change, so the
artifact is visible while the user reads what you did. One canvas_ui call
per logical step is enough β€” don't repeat it for the same file or tab in
the same turn."""


class CanvasUITool(ToolDefinition[CanvasUIAction, CanvasUIObservation]):
    """Tool for controlling the Agent Canvas UI from the agent."""

    @classmethod
    def create(
        cls,
        conv_state=None,  # noqa: ARG003
        **params,  # noqa: ARG003
    ) -> Sequence["CanvasUITool"]:
        return [
            cls(
                description=_CANVAS_UI_DESCRIPTION,
                action_type=CanvasUIAction,
                observation_type=CanvasUIObservation,
                executor=CanvasUIExecutor(),
                annotations=ToolAnnotations(
                    readOnlyHint=True,
                    destructiveHint=False,
                    idempotentHint=True,
                    openWorldHint=False,
                ),
            )
        ]


# Persisted pre-client_tools conversations import this module by qualname before
# restoring their agent and events. Keep the registration until those records
# have a server-side migration path.
register_tool("canvas_ui", CanvasUITool)


# openhands-automation >= 1.9.0 preset entrypoints build their agent with
# get_default_agent(finish_tool_response_schema=TaskOutcome). That registers the
# SDK's builtin FinishTool only inside the entrypoint's own process and
# advertises it to the agent-server as `openhands.sdk.tool.builtins.finish` β€” a
# module that does not self-register β€” so the remote conversation every Agent
# Canvas automation run dispatches fails with "ToolDefinition 'FinishTool' is
# not registered". Registering the plain builtin here is enough: resolve_tool()
# strips `response_schema` before FinishTool.create() and re-applies it. Drop
# this once the SDK registers its builtins for remote conversations.
if FinishTool.__name__ not in list_registered_tools():
    register_tool(FinishTool.__name__, FinishTool)