mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-06 12:03:07 +08:00
chore: bump SDK deps (software-agent-sdk 1.44.0, automation 1.9.0, extensions 0.19.0) (#16968)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -249,6 +249,7 @@ you are running inside of — NOT the automation backend.
|
|||||||
- `POST /admin/trajectory/register` — register a named trajectory (JSON body: `{name, turns}` where each turn is `{tool_call: {name, arguments}}` or `{text: "..."}`)
|
- `POST /admin/trajectory/register` — register a named trajectory (JSON body: `{name, turns}` where each turn is `{tool_call: {name, arguments}}` or `{text: "..."}`)
|
||||||
- `POST /admin/trajectory/activate` — activate a previously registered trajectory
|
- `POST /admin/trajectory/activate` — activate a previously registered trajectory
|
||||||
- `GET /admin/requests` — return the list of all `/v1/chat/completions` request bodies captured since the last reset (used by the image-upload test to verify the image was forwarded to the LLM)
|
- `GET /admin/requests` — return the list of all `/v1/chat/completions` request bodies captured since the last reset (used by the image-upload test to verify the image was forwarded to the LLM)
|
||||||
|
- Profile pre-flight ping: agent-server ≥ 1.43 sends a 1-token `ping` completion whenever an LLM profile is saved (`POST /api/profiles/{name}/validate`, 30 s budget in the canvas). The mock answers it with a canned `pong` instead of feeding it to `TestLLM`, so it neither consumes a scripted turn nor 500s-and-retries past the canvas timeout when the trajectory is exhausted; it is also left out of the `/admin/requests` history.
|
||||||
- **Real automation backend**: The automation test uses the production automation backend (started by `bin/agent-canvas.mjs`), NOT a mock server. Terminal `curl` commands from the agent hit the automation API through the ingress proxy at the test's `BACKEND_URL` (default `http://localhost:18300`). Auth uses the `X-Session-API-Key` header matching the stack's session key.
|
- **Real automation backend**: The automation test uses the production automation backend (started by `bin/agent-canvas.mjs`), NOT a mock server. Terminal `curl` commands from the agent hit the automation API through the ingress proxy at the test's `BACKEND_URL` (default `http://localhost:18300`). Auth uses the `X-Session-API-Key` header matching the stack's session key.
|
||||||
- **Test helpers** (`tests/e2e/mock-llm/utils/mock-llm-helpers.ts`): Exports `registerTrajectory()`, `activateTrajectory()`, `resetMockLLM()`, `ensureMockLLMProfile()`, `getMockLLMRequests()` (fetches captured completion bodies from `GET /admin/requests`), `IMAGE_REPLY_TOKEN` + `MINIMAL_PNG_BASE64` (constants for the image-upload spec), ACP helpers (`configureAcpAgent()`, `verifyAcpAgentSettings()`, `resetToOpenHandsAgent()`, `ACP_REPLY_TOKEN`, `MOCK_ACP_SERVER_PATH`), and more.
|
- **Test helpers** (`tests/e2e/mock-llm/utils/mock-llm-helpers.ts`): Exports `registerTrajectory()`, `activateTrajectory()`, `resetMockLLM()`, `ensureMockLLMProfile()`, `getMockLLMRequests()` (fetches captured completion bodies from `GET /admin/requests`), `IMAGE_REPLY_TOKEN` + `MINIMAL_PNG_BASE64` (constants for the image-upload spec), ACP helpers (`configureAcpAgent()`, `verifyAcpAgentSettings()`, `resetToOpenHandsAgent()`, `ACP_REPLY_TOKEN`, `MOCK_ACP_SERVER_PATH`), and more.
|
||||||
- **Padding response for internal LLM call**: The agent-server makes an internal LLM call (condenser/skill-analysis) before the agent's main loop starts when skills are activated. This consumes one trajectory response. Automation tests prepend a throwaway `{ text: "" }` response as padding. The conversation test does NOT need this because its user message doesn't trigger skill activation.
|
- **Padding response for internal LLM call**: The agent-server makes an internal LLM call (condenser/skill-analysis) before the agent's main loop starts when skills are activated. This consumes one trajectory response. Automation tests prepend a throwaway `{ text: "" }` response as padding. The conversation test does NOT need this because its user message doesn't trigger skill activation.
|
||||||
@@ -578,13 +579,14 @@ When adding code that needs a new string, decide up front which rule it falls un
|
|||||||
- `scripts/dev-safe.mjs` uses `uvx` for temporary agent-server installation — no permanent `uv tool install` needed. Environment variables (highest precedence first):
|
- `scripts/dev-safe.mjs` uses `uvx` for temporary agent-server installation — no permanent `uv tool install` needed. Environment variables (highest precedence first):
|
||||||
- `OH_AGENT_SERVER_LOCAL_PATH` — absolute path to a local `software-agent-sdk` checkout. Runs the local checkout via `uvx` with `--with-editable` for `openhands-sdk`/`openhands-tools`/`openhands-workspace` and `--reinstall` for `openhands-agent-server`, so SDK edits are picked up on restart. Highest precedence.
|
- `OH_AGENT_SERVER_LOCAL_PATH` — absolute path to a local `software-agent-sdk` checkout. Runs the local checkout via `uvx` with `--with-editable` for `openhands-sdk`/`openhands-tools`/`openhands-workspace` and `--reinstall` for `openhands-agent-server`, so SDK edits are picked up on restart. Highest precedence.
|
||||||
- `OH_AGENT_SERVER_GIT_REF` — git commit SHA or branch name (takes precedence over version)
|
- `OH_AGENT_SERVER_GIT_REF` — git commit SHA or branch name (takes precedence over version)
|
||||||
- `OH_AGENT_SERVER_VERSION` — specific PyPI version (e.g., "1.42.1")
|
- `OH_AGENT_SERVER_VERSION` — specific PyPI version (e.g., "1.44.0")
|
||||||
- `OH_SECRET_KEY` — secret key for settings encryption; auto-generated and persisted to `~/.openhands/agent-canvas/secret-key.txt` on first run (same file Docker uses), ensuring dev mode and Docker share the same key when both mount the same `~/.openhands` directory. Override with the env var to pin a specific key.
|
- `OH_SECRET_KEY` — secret key for settings encryption; auto-generated and persisted to `~/.openhands/agent-canvas/secret-key.txt` on first run (same file Docker uses), ensuring dev mode and Docker share the same key when both mount the same `~/.openhands` directory. Override with the env var to pin a specific key.
|
||||||
- `SESSION_API_KEY` / `OH_SESSION_API_KEYS_0` / `VITE_SESSION_API_KEY` — session API key for agent-server authentication; auto-generated using `crypto.randomBytes(32)` if not set, passed to both agent-server (`OH_SESSION_API_KEYS_0`) and frontend (`VITE_SESSION_API_KEY`)
|
- `SESSION_API_KEY` / `OH_SESSION_API_KEYS_0` / `VITE_SESSION_API_KEY` — session API key for agent-server authentication; auto-generated using `crypto.randomBytes(32)` if not set, passed to both agent-server (`OH_SESSION_API_KEYS_0`) and frontend (`VITE_SESSION_API_KEY`)
|
||||||
- Default: released PyPI version `1.42.1` for agent-server SDK libraries
|
- Default: released PyPI version `1.44.0` for agent-server SDK libraries
|
||||||
|
|
||||||
- Security: launchers generate and persist a 64-character session API key at `~/.openhands/agent-canvas/session-api-key.txt` unless overridden. The agent-server and automation backend share that session key. `OH_SECRET_KEY` protects settings encryption and is persisted separately at `~/.openhands/agent-canvas/secret-key.txt`.
|
- Security: launchers generate and persist a 64-character session API key at `~/.openhands/agent-canvas/session-api-key.txt` unless overridden. The agent-server and automation backend share that session key. `OH_SECRET_KEY` protects settings encryption and is persisted separately at `~/.openhands/agent-canvas/secret-key.txt`.
|
||||||
- `scripts/dev-safe.mjs` should fail fast if `uvx` cannot be spawned (for example missing PATH entries).
|
- `scripts/dev-safe.mjs` should fail fast if `uvx` cannot be spawned (for example missing PATH entries).
|
||||||
|
- `tools/` holds Python modules the agent-server can import: `buildAgentServerEnv` exposes the directory through `OH_EXTRA_PYTHON_PATH` (Docker: `/opt/agent-canvas/tools`, see `docker/entrypoint.sh`). `tools/canvas_ui_tool.py` is imported at startup via `--import-modules canvas_ui_tool` (appended by `buildAgentServerCommand` and by both launch lines in `docker/entrypoint.sh`), not only lazily from persisted conversation metadata: besides the legacy `canvas_ui` registration it registers the SDK's builtin `FinishTool`, so `openhands-automation` ≥ 1.9.0 presets — which dispatch remote conversations with `finish_tool_response_schema=TaskOutcome` and advertise the tool as the non-self-registering `openhands.sdk.tool.builtins.finish` — do not fail every run with `ToolDefinition 'FinishTool' is not registered`. Remove that registration once the SDK registers builtins for remote conversations.
|
||||||
- `npm run dev` runs the full local stack via `uvx` (agent-server + automation backend + Vite dev server + ingress proxy) with no Docker dependency. `npm run dev:static` does the same but serves a production build of the frontend instead of the Vite dev server.
|
- `npm run dev` runs the full local stack via `uvx` (agent-server + automation backend + Vite dev server + ingress proxy) with no Docker dependency. `npm run dev:static` does the same but serves a production build of the frontend instead of the Vite dev server.
|
||||||
- `scripts/dev-with-automation.mjs` runs the full stack: agent-server, automation backend (both via uvx), frontend server, and ingress proxy. It defaults to Vite when run directly, supports `--static` for an existing build, and supports `--dynamic` so wrappers that default static can opt back into Vite. Uses a standalone ingress proxy (`scripts/ingress.mjs`) to route traffic:
|
- `scripts/dev-with-automation.mjs` runs the full stack: agent-server, automation backend (both via uvx), frontend server, and ingress proxy. It defaults to Vite when run directly, supports `--static` for an existing build, and supports `--dynamic` so wrappers that default static can opt back into Vite. Uses a standalone ingress proxy (`scripts/ingress.mjs`) to route traffic:
|
||||||
- Keep `SIGINT`, `SIGTERM`, and `SIGHUP` wired through the coordinated shutdown handler. Services run in detached process groups on POSIX, so cleanup must use `signalProcessTree()` rather than signaling only the direct child; regression coverage lives in `__tests__/scripts/dev-with-automation.test.ts`.
|
- Keep `SIGINT`, `SIGTERM`, and `SIGHUP` wired through the coordinated shutdown handler. Services run in detached process groups on POSIX, so cleanup must use `signalProcessTree()` rather than signaling only the direct child; regression coverage lives in `__tests__/scripts/dev-with-automation.test.ts`.
|
||||||
|
|||||||
@@ -71,8 +71,13 @@ describe("RecommendedAutomationsRail", () => {
|
|||||||
"news-digest",
|
"news-digest",
|
||||||
"slack-standup-digest",
|
"slack-standup-digest",
|
||||||
"linear-triage-assistant",
|
"linear-triage-assistant",
|
||||||
|
"linear-issue-to-github-pr",
|
||||||
|
"linear-issue-to-gitlab-mr",
|
||||||
|
"linear-issue-to-bitbucket-pr",
|
||||||
"jira-issue-to-pr",
|
"jira-issue-to-pr",
|
||||||
|
"jira-issue-to-gitlab-mr",
|
||||||
"research-brief-writer",
|
"research-brief-writer",
|
||||||
|
"jira-issue-to-bitbucket-pr",
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
screen.getByText(I18nKey.RECOMMENDED_AUTOMATIONS$SECTION_LABEL),
|
screen.getByText(I18nKey.RECOMMENDED_AUTOMATIONS$SECTION_LABEL),
|
||||||
@@ -125,6 +130,11 @@ describe("RecommendedAutomationsRail", () => {
|
|||||||
{ name: "Linear issue triage assistant" },
|
{ name: "Linear issue triage assistant" },
|
||||||
{ name: "Jira issue to GitHub PR" },
|
{ name: "Jira issue to GitHub PR" },
|
||||||
{ name: "Research brief writer" },
|
{ name: "Research brief writer" },
|
||||||
|
{ name: "Linear issue to GitHub PR" },
|
||||||
|
{ name: "Linear issue to GitLab MR" },
|
||||||
|
{ name: "Linear issue to Bitbucket PR" },
|
||||||
|
{ name: "Jira issue to GitLab MR" },
|
||||||
|
{ name: "Jira issue to Bitbucket PR" },
|
||||||
]}
|
]}
|
||||||
onSelect={vi.fn()}
|
onSelect={vi.fn()}
|
||||||
/>,
|
/>,
|
||||||
|
|||||||
@@ -213,8 +213,14 @@ describe("recommended automations", () => {
|
|||||||
"github-repo-monitor",
|
"github-repo-monitor",
|
||||||
"slack-standup-digest",
|
"slack-standup-digest",
|
||||||
"linear-triage-assistant",
|
"linear-triage-assistant",
|
||||||
|
"linear-issue-to-github-pr",
|
||||||
|
"linear-issue-to-gitlab-mr",
|
||||||
|
"linear-issue-to-bitbucket-pr",
|
||||||
"jira-issue-to-pr",
|
"jira-issue-to-pr",
|
||||||
|
"qa-changes",
|
||||||
|
"jira-issue-to-gitlab-mr",
|
||||||
"research-brief-writer",
|
"research-brief-writer",
|
||||||
|
"jira-issue-to-bitbucket-pr",
|
||||||
"upstream-fork-sync",
|
"upstream-fork-sync",
|
||||||
"incident-retrospective-drafter",
|
"incident-retrospective-drafter",
|
||||||
]);
|
]);
|
||||||
@@ -240,7 +246,7 @@ describe("recommended automations", () => {
|
|||||||
expect(betaHeading).toHaveTextContent(
|
expect(betaHeading).toHaveTextContent(
|
||||||
I18nKey.RECOMMENDED_AUTOMATIONS$BETA_LABEL,
|
I18nKey.RECOMMENDED_AUTOMATIONS$BETA_LABEL,
|
||||||
);
|
);
|
||||||
expect(within(betaHeading).getByText("7")).toBeInTheDocument();
|
expect(within(betaHeading).getByText("13")).toBeInTheDocument();
|
||||||
|
|
||||||
const betaSection = screen.getByTestId(
|
const betaSection = screen.getByTestId(
|
||||||
"recommended-automations-beta-section",
|
"recommended-automations-beta-section",
|
||||||
|
|||||||
@@ -468,20 +468,22 @@ describe("buildAgentServerCommand", () => {
|
|||||||
// Defaults to the released PyPI version with all SDK packages pinned to same version
|
// Defaults to the released PyPI version with all SDK packages pinned to same version
|
||||||
expect(cmd.args).toEqual([
|
expect(cmd.args).toEqual([
|
||||||
"--from",
|
"--from",
|
||||||
"openhands-agent-server==1.42.1",
|
"openhands-agent-server==1.44.0",
|
||||||
"--with",
|
"--with",
|
||||||
"openhands-sdk==1.42.1",
|
"openhands-sdk==1.44.0",
|
||||||
"--with",
|
"--with",
|
||||||
"openhands-tools==1.42.1",
|
"openhands-tools==1.44.0",
|
||||||
"--with",
|
"--with",
|
||||||
"openhands-workspace==1.42.1",
|
"openhands-workspace==1.44.0",
|
||||||
"--with",
|
"--with",
|
||||||
"agent-client-protocol<0.11",
|
"agent-client-protocol<0.11",
|
||||||
"--with",
|
"--with",
|
||||||
"posthog>=6,<7",
|
"posthog>=6,<7",
|
||||||
"agent-server",
|
"agent-server",
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
]);
|
]);
|
||||||
expect(cmd.source).toBe("PyPI (1.42.1, default)");
|
expect(cmd.source).toBe("PyPI (1.44.0, default)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses specific PyPI version when OH_AGENT_SERVER_VERSION is set with all packages pinned", () => {
|
it("uses specific PyPI version when OH_AGENT_SERVER_VERSION is set with all packages pinned", () => {
|
||||||
@@ -504,6 +506,8 @@ describe("buildAgentServerCommand", () => {
|
|||||||
"--with",
|
"--with",
|
||||||
"posthog>=6,<7",
|
"posthog>=6,<7",
|
||||||
"agent-server",
|
"agent-server",
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
]);
|
]);
|
||||||
expect(cmd.source).toBe("PyPI (1.18.0)");
|
expect(cmd.source).toBe("PyPI (1.18.0)");
|
||||||
});
|
});
|
||||||
@@ -527,6 +531,8 @@ describe("buildAgentServerCommand", () => {
|
|||||||
"--with",
|
"--with",
|
||||||
"posthog>=6,<7",
|
"posthog>=6,<7",
|
||||||
"agent-server",
|
"agent-server",
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
]);
|
]);
|
||||||
expect(cmd.source).toBe("git (feature-branch)");
|
expect(cmd.source).toBe("git (feature-branch)");
|
||||||
});
|
});
|
||||||
@@ -548,6 +554,8 @@ describe("buildAgentServerCommand", () => {
|
|||||||
"--with",
|
"--with",
|
||||||
"posthog>=6,<7",
|
"posthog>=6,<7",
|
||||||
"agent-server",
|
"agent-server",
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
]);
|
]);
|
||||||
expect(cmd.source).toBe("git (abc1234)");
|
expect(cmd.source).toBe("git (abc1234)");
|
||||||
});
|
});
|
||||||
@@ -584,6 +592,8 @@ describe("buildAgentServerCommand", () => {
|
|||||||
"--with",
|
"--with",
|
||||||
"posthog>=6,<7",
|
"posthog>=6,<7",
|
||||||
"agent-server",
|
"agent-server",
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
]);
|
]);
|
||||||
expect(cmd.source).toBe(`local (${sdk})`);
|
expect(cmd.source).toBe(`local (${sdk})`);
|
||||||
});
|
});
|
||||||
@@ -604,6 +614,27 @@ describe("buildAgentServerCommand", () => {
|
|||||||
expect(cmd.args).not.toContain("openhands-agent-server==1.18.0");
|
expect(cmd.args).not.toContain("openhands-agent-server==1.18.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("passes --import-modules to the agent-server, after the executable, in every source mode", () => {
|
||||||
|
// The flag must sit after "agent-server" so uvx hands it to the server
|
||||||
|
// instead of parsing it itself. tools/canvas_ui_tool.py documents why the
|
||||||
|
// module has to be imported before any conversation is created.
|
||||||
|
const variants = [
|
||||||
|
{},
|
||||||
|
{ OH_AGENT_SERVER_VERSION: "1.18.0" },
|
||||||
|
{ OH_AGENT_SERVER_GIT_REF: "feature-branch" },
|
||||||
|
{ OH_AGENT_SERVER_LOCAL_PATH: "/abs/path/to/software-agent-sdk" },
|
||||||
|
];
|
||||||
|
for (const env of variants) {
|
||||||
|
const { args } = buildAgentServerCommand(env);
|
||||||
|
const executable = args.indexOf("agent-server");
|
||||||
|
expect(executable).toBeGreaterThan(-1);
|
||||||
|
expect(args.slice(executable + 1)).toEqual([
|
||||||
|
"--import-modules",
|
||||||
|
"canvas_ui_tool",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects relative OH_AGENT_SERVER_LOCAL_PATH", () => {
|
it("rejects relative OH_AGENT_SERVER_LOCAL_PATH", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
buildAgentServerCommand({
|
buildAgentServerCommand({
|
||||||
|
|||||||
@@ -70,14 +70,21 @@ describe("recommended automation rail", () => {
|
|||||||
expect(conversationIds).toEqual([
|
expect(conversationIds).toEqual([
|
||||||
"slack-standup-digest",
|
"slack-standup-digest",
|
||||||
"linear-triage-assistant",
|
"linear-triage-assistant",
|
||||||
|
"linear-issue-to-github-pr",
|
||||||
|
"linear-issue-to-gitlab-mr",
|
||||||
|
"linear-issue-to-bitbucket-pr",
|
||||||
"jira-issue-to-pr",
|
"jira-issue-to-pr",
|
||||||
|
"jira-issue-to-gitlab-mr",
|
||||||
"research-brief-writer",
|
"research-brief-writer",
|
||||||
|
"jira-issue-to-bitbucket-pr",
|
||||||
]);
|
]);
|
||||||
expect(conversationIds).not.toContain("upstream-fork-sync");
|
expect(conversationIds).not.toContain("upstream-fork-sync");
|
||||||
expect(conversationIds).not.toContain("incident-retrospective-drafter");
|
expect(conversationIds).not.toContain("incident-retrospective-drafter");
|
||||||
// Beta as of extensions 0.18.0, and set up by a host form rather than a
|
// Beta as of extensions 0.18.0, and set up by a host form rather than a
|
||||||
// conversation, so it is in neither group.
|
// conversation, so it is in neither group.
|
||||||
expect(conversationIds).not.toContain("github-repo-monitor");
|
expect(conversationIds).not.toContain("github-repo-monitor");
|
||||||
|
// Same for `qa-changes` (extensions 0.19.0): host form, so neither group.
|
||||||
|
expect(conversationIds).not.toContain("qa-changes");
|
||||||
expect(isConversationLaunchAutomation(slackStandup)).toBe(true);
|
expect(isConversationLaunchAutomation(slackStandup)).toBe(true);
|
||||||
expect(isConversationLaunchAutomation(upstreamFork)).toBe(false);
|
expect(isConversationLaunchAutomation(upstreamFork)).toBe(false);
|
||||||
expect(SETUP_REGISTRY.findById(upstreamFork.id)).not.toBeNull();
|
expect(SETUP_REGISTRY.findById(upstreamFork.id)).not.toBeNull();
|
||||||
@@ -94,6 +101,11 @@ describe("recommended automation rail", () => {
|
|||||||
{ name: "Linear issue triage assistant" },
|
{ name: "Linear issue triage assistant" },
|
||||||
{ name: "Jira issue to GitHub PR" },
|
{ name: "Jira issue to GitHub PR" },
|
||||||
{ name: "Research brief writer" },
|
{ name: "Research brief writer" },
|
||||||
|
{ name: "Linear issue to GitHub PR" },
|
||||||
|
{ name: "Linear issue to GitLab MR" },
|
||||||
|
{ name: "Linear issue to Bitbucket PR" },
|
||||||
|
{ name: "Jira issue to GitLab MR" },
|
||||||
|
{ name: "Jira issue to Bitbucket PR" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(flattenRecommendedRailGroups(groups)).toEqual([]);
|
expect(flattenRecommendedRailGroups(groups)).toEqual([]);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"_comment": "Single source of truth for version pins, ports, paths, and defaults shared across the npm and Docker install paths. Read by scripts/dev-safe.mjs, scripts/dev-with-automation.mjs, docker/entrypoint.sh (via generated defaults.env), and .github/workflows/docker.yml.",
|
"_comment": "Single source of truth for version pins, ports, paths, and defaults shared across the npm and Docker install paths. Read by scripts/dev-safe.mjs, scripts/dev-with-automation.mjs, docker/entrypoint.sh (via generated defaults.env), and .github/workflows/docker.yml.",
|
||||||
"versions": {
|
"versions": {
|
||||||
"agentServer": "1.42.1",
|
"agentServer": "1.44.0",
|
||||||
"agentCanvas": "1.15.0",
|
"agentCanvas": "1.15.0",
|
||||||
"automation": "1.8.0"
|
"automation": "1.9.0"
|
||||||
},
|
},
|
||||||
"compatibility": {
|
"compatibility": {
|
||||||
"minimumAgentServer": "1.28.0"
|
"minimumAgentServer": "1.28.0"
|
||||||
|
|||||||
@@ -268,7 +268,11 @@ export AUTOMATION_AGENT_SERVER_URL="${AUTOMATION_AGENT_SERVER_URL:-http://127.0.
|
|||||||
|
|
||||||
# Keep the legacy canvas_ui_tool module importable when the agent-server restores
|
# Keep the legacy canvas_ui_tool module importable when the agent-server restores
|
||||||
# conversations whose persisted metadata still references its module qualname.
|
# conversations whose persisted metadata still references its module qualname.
|
||||||
|
# It is also imported at startup below (--import-modules) so its builtin
|
||||||
|
# FinishTool registration lets automation runs resolve the tool on their
|
||||||
|
# remote conversations (see the note at the bottom of tools/canvas_ui_tool.py).
|
||||||
export OH_EXTRA_PYTHON_PATH="${OH_EXTRA_PYTHON_PATH:-/opt/agent-canvas/tools}"
|
export OH_EXTRA_PYTHON_PATH="${OH_EXTRA_PYTHON_PATH:-/opt/agent-canvas/tools}"
|
||||||
|
AGENT_SERVER_IMPORT_MODULES="canvas_ui_tool"
|
||||||
|
|
||||||
# Track child PIDs so we can clean up on exit.
|
# Track child PIDs so we can clean up on exit.
|
||||||
PIDS=()
|
PIDS=()
|
||||||
@@ -288,10 +292,12 @@ log "Starting agent-server on port $AGENT_SERVER_PORT..."
|
|||||||
|
|
||||||
if command -v openhands-agent-server >/dev/null 2>&1; then
|
if command -v openhands-agent-server >/dev/null 2>&1; then
|
||||||
# Binary build (production image)
|
# Binary build (production image)
|
||||||
openhands-agent-server --port "$AGENT_SERVER_PORT" &
|
openhands-agent-server --port "$AGENT_SERVER_PORT" \
|
||||||
|
--import-modules "$AGENT_SERVER_IMPORT_MODULES" &
|
||||||
elif [ -x /agent-server/.venv/bin/python ]; then
|
elif [ -x /agent-server/.venv/bin/python ]; then
|
||||||
# Source build (development image)
|
# Source build (development image)
|
||||||
/agent-server/.venv/bin/python -m openhands.agent_server --port "$AGENT_SERVER_PORT" &
|
/agent-server/.venv/bin/python -m openhands.agent_server --port "$AGENT_SERVER_PORT" \
|
||||||
|
--import-modules "$AGENT_SERVER_IMPORT_MODULES" &
|
||||||
else
|
else
|
||||||
log_error "Cannot find agent-server binary or source venv."
|
log_error "Cannot find agent-server binary or source venv."
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
Generated
+8
-8
@@ -13,8 +13,8 @@
|
|||||||
"@heroui/react": "2.8.10",
|
"@heroui/react": "2.8.10",
|
||||||
"@microlink/react-json-view": "1.31.25",
|
"@microlink/react-json-view": "1.31.25",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@openhands/extensions": "0.18.0",
|
"@openhands/extensions": "0.19.0",
|
||||||
"@openhands/typescript-client": "1.38.1",
|
"@openhands/typescript-client": "1.39.0",
|
||||||
"@react-router/node": "7.18.2",
|
"@react-router/node": "7.18.2",
|
||||||
"@react-router/serve": "7.18.2",
|
"@react-router/serve": "7.18.2",
|
||||||
"@tailwindcss/vite": "4.3.3",
|
"@tailwindcss/vite": "4.3.3",
|
||||||
@@ -4164,9 +4164,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@openhands/extensions": {
|
"node_modules/@openhands/extensions": {
|
||||||
"version": "0.18.0",
|
"version": "0.19.0",
|
||||||
"resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@openhands/extensions/-/extensions-0.19.0.tgz",
|
||||||
"integrity": "sha512-0cbVueh8wk5OLq+kbEgI5k1sFUaNgahmxXwvOR5+4fpxwc2YnVivdyDzxjZ+SiagUmYi/EgADFVa/5X38e9BzQ==",
|
"integrity": "sha512-nT0jTxvVUmiCcKjg2lLYAUUAYFFFbMivZuCjj8qxOIXs6yePVANEpt8HkLe5ViCEweRsbIokfDwcnjQDzVhQ6w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.20.0"
|
"node": ">=18.20.0"
|
||||||
@@ -4178,9 +4178,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@openhands/typescript-client": {
|
"node_modules/@openhands/typescript-client": {
|
||||||
"version": "1.38.1",
|
"version": "1.39.0",
|
||||||
"resolved": "https://registry.npmjs.org/@openhands/typescript-client/-/typescript-client-1.38.1.tgz",
|
"resolved": "https://registry.npmjs.org/@openhands/typescript-client/-/typescript-client-1.39.0.tgz",
|
||||||
"integrity": "sha512-KYpcmgL85uKZ0pvEQAxuwfj0M/8Qq+c6Q2bhaPxpwGayXkDeVWoUxIxomly4vdAqhYseK2pUJiHVoCc81gcf7Q==",
|
"integrity": "sha512-AXRR9bXYYsT87NvkJCB2sLBxAjoGwt6MIQED4g5EufpXaUvY21BDyuDjNpsVtGl6WFnjbvTK1kFytkbxAe0rvw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@openrouter/sdk": "^1.2.11",
|
"@openrouter/sdk": "^1.2.11",
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
"@heroui/react": "2.8.10",
|
"@heroui/react": "2.8.10",
|
||||||
"@microlink/react-json-view": "1.31.25",
|
"@microlink/react-json-view": "1.31.25",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@openhands/extensions": "0.18.0",
|
"@openhands/extensions": "0.19.0",
|
||||||
"@openhands/typescript-client": "1.38.1",
|
"@openhands/typescript-client": "1.39.0",
|
||||||
"@react-router/node": "7.18.2",
|
"@react-router/node": "7.18.2",
|
||||||
"@react-router/serve": "7.18.2",
|
"@react-router/serve": "7.18.2",
|
||||||
"@tailwindcss/vite": "4.3.3",
|
"@tailwindcss/vite": "4.3.3",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node scripts/check-sdk-version-sync.mjs
|
* node scripts/check-sdk-version-sync.mjs
|
||||||
* EXPECTED_SDK_VERSION=1.42.1 node scripts/check-sdk-version-sync.mjs
|
* EXPECTED_SDK_VERSION=1.44.0 node scripts/check-sdk-version-sync.mjs
|
||||||
* node scripts/check-sdk-version-sync.mjs --check-pypi
|
* node scripts/check-sdk-version-sync.mjs --check-pypi
|
||||||
*
|
*
|
||||||
* Environment variables:
|
* Environment variables:
|
||||||
@@ -78,7 +78,7 @@ Triggering from other repos:
|
|||||||
-H "Authorization: token \$GITHUB_TOKEN" \\
|
-H "Authorization: token \$GITHUB_TOKEN" \\
|
||||||
-H "Accept: application/vnd.github.v3+json" \\
|
-H "Accept: application/vnd.github.v3+json" \\
|
||||||
https://api.github.com/repos/OpenHands/OpenHands/dispatches \\
|
https://api.github.com/repos/OpenHands/OpenHands/dispatches \\
|
||||||
-d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.42.1"}}'
|
-d '{"event_type": "sdk-version-check", "client_payload": {"version": "1.44.0"}}'
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -260,9 +260,9 @@ async function fetchPyPIDependencies(packageName, version) {
|
|||||||
* Parse PyPI requires_dist array and extract SDK package versions
|
* Parse PyPI requires_dist array and extract SDK package versions
|
||||||
*
|
*
|
||||||
* PyPI returns dependencies in PEP 508 format like:
|
* PyPI returns dependencies in PEP 508 format like:
|
||||||
* "openhands-sdk>=1.42.1,<2.0.0"
|
* "openhands-sdk>=1.44.0,<2.0.0"
|
||||||
* "openhands-tools==1.42.1"
|
* "openhands-tools==1.44.0"
|
||||||
* "openhands-workspace (>=1.42.1)"
|
* "openhands-workspace (>=1.44.0)"
|
||||||
*/
|
*/
|
||||||
function parseSdkVersionsFromRequiresDist(requiresDist) {
|
function parseSdkVersionsFromRequiresDist(requiresDist) {
|
||||||
const versions = {};
|
const versions = {};
|
||||||
@@ -276,7 +276,7 @@ function parseSdkVersionsFromRequiresDist(requiresDist) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract the version number - look for patterns like:
|
// Extract the version number - look for patterns like:
|
||||||
// ">=1.42.1", "==1.42.1", "(>=1.42.1)", "~=1.42.1"
|
// ">=1.44.0", "==1.44.0", "(>=1.44.0)", "~=1.44.0"
|
||||||
// After the package name and before any comma or closing paren
|
// After the package name and before any comma or closing paren
|
||||||
const versionPattern = /[><=~!]+\s*([0-9]+(?:\.[0-9]+)*)/;
|
const versionPattern = /[><=~!]+\s*([0-9]+(?:\.[0-9]+)*)/;
|
||||||
const match = dep.match(versionPattern);
|
const match = dep.match(versionPattern);
|
||||||
|
|||||||
+15
-1
@@ -401,6 +401,16 @@ export function validateFrontendDependencies(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modules the agent-server imports at startup (`--import-modules`). They are
|
||||||
|
* resolved from `tools/`, which `buildAgentServerEnv` exposes through
|
||||||
|
* OH_EXTRA_PYTHON_PATH. Importing `canvas_ui_tool` eagerly registers the SDK's
|
||||||
|
* builtin FinishTool so automation presets (openhands-automation >= 1.9.0) can
|
||||||
|
* resolve it on the remote conversations they dispatch — see the note at the
|
||||||
|
* bottom of tools/canvas_ui_tool.py.
|
||||||
|
*/
|
||||||
|
export const AGENT_SERVER_IMPORT_MODULES = "canvas_ui_tool";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the uvx command and arguments for running agent-server.
|
* Build the uvx command and arguments for running agent-server.
|
||||||
*
|
*
|
||||||
@@ -411,7 +421,7 @@ export function validateFrontendDependencies(
|
|||||||
* edits are picked up without a manual reinstall. The agent-server itself
|
* edits are picked up without a manual reinstall. The agent-server itself
|
||||||
* is rebuilt from local source on each invocation (--reinstall).
|
* is rebuilt from local source on each invocation (--reinstall).
|
||||||
* - OH_AGENT_SERVER_GIT_REF: Git commit SHA or branch name
|
* - OH_AGENT_SERVER_GIT_REF: Git commit SHA or branch name
|
||||||
* - OH_AGENT_SERVER_VERSION: Specific PyPI version (e.g., "1.42.1")
|
* - OH_AGENT_SERVER_VERSION: Specific PyPI version (e.g., "1.44.0")
|
||||||
*
|
*
|
||||||
* If none are set, defaults to the released version specified by
|
* If none are set, defaults to the released version specified by
|
||||||
* DEFAULT_AGENT_SERVER_VERSION. Set OH_AGENT_SERVER_GIT_REF to use a
|
* DEFAULT_AGENT_SERVER_VERSION. Set OH_AGENT_SERVER_GIT_REF to use a
|
||||||
@@ -516,6 +526,10 @@ export function buildAgentServerCommand(env = process.env) {
|
|||||||
source = `PyPI (${DEFAULT_AGENT_SERVER_VERSION}, default)`;
|
source = `PyPI (${DEFAULT_AGENT_SERVER_VERSION}, default)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Everything after the executable name is an agent-server CLI argument.
|
||||||
|
// Import the registration module before any conversation is created.
|
||||||
|
uvxArgs.push("--import-modules", AGENT_SERVER_IMPORT_MODULES);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
command: "uvx",
|
command: "uvx",
|
||||||
args: uvxArgs,
|
args: uvxArgs,
|
||||||
|
|||||||
@@ -156,23 +156,54 @@ async function listAutomationRuns(
|
|||||||
return resp.json();
|
return resp.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AutomationRunRecord {
|
||||||
|
id: string;
|
||||||
|
status: string;
|
||||||
|
conversation_id: string | null;
|
||||||
|
error_detail?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Statuses a run never leaves (see RunStatus in openhands-automation). */
|
||||||
|
const TERMINAL_RUN_STATUSES = new Set([
|
||||||
|
"COMPLETED",
|
||||||
|
"FAILED",
|
||||||
|
"CANCELLED",
|
||||||
|
"SKIPPED",
|
||||||
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Poll until a run reaches the expected status or times out.
|
* Poll until a run reaches the expected status or times out.
|
||||||
|
*
|
||||||
|
* Fails fast, quoting the backend's `error_detail`, once every run has ended
|
||||||
|
* in some other terminal status — polling on would only surface a timeout
|
||||||
|
* and hide why the run actually stopped.
|
||||||
*/
|
*/
|
||||||
async function waitForRunStatus(
|
async function waitForRunStatus(
|
||||||
request: import("@playwright/test").APIRequestContext,
|
request: import("@playwright/test").APIRequestContext,
|
||||||
automationId: string,
|
automationId: string,
|
||||||
expectedStatus: string,
|
expectedStatus: string,
|
||||||
timeoutMs = 30_000,
|
timeoutMs = 30_000,
|
||||||
) {
|
): Promise<AutomationRunRecord> {
|
||||||
const deadline = Date.now() + timeoutMs;
|
const deadline = Date.now() + timeoutMs;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const data = await listAutomationRuns(request, automationId);
|
const data = await listAutomationRuns(request, automationId);
|
||||||
const runs = data.runs ?? data.items ?? [];
|
const runs: AutomationRunRecord[] = data.runs ?? data.items ?? [];
|
||||||
const match = runs.find(
|
const match = runs.find((r) => r.status === expectedStatus);
|
||||||
(r: { status: string }) => r.status === expectedStatus,
|
|
||||||
);
|
|
||||||
if (match) return match;
|
if (match) return match;
|
||||||
|
if (
|
||||||
|
runs.length > 0 &&
|
||||||
|
runs.every((r) => TERMINAL_RUN_STATUSES.has(r.status))
|
||||||
|
) {
|
||||||
|
const summary = runs
|
||||||
|
.map(
|
||||||
|
(r) =>
|
||||||
|
`${r.id}=${r.status}${r.error_detail ? ` (${r.error_detail})` : ""}`,
|
||||||
|
)
|
||||||
|
.join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`No run reached "${expectedStatus}"; every run already ended: ${summary}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
await new Promise((r) => setTimeout(r, 1_000));
|
await new Promise((r) => setTimeout(r, 1_000));
|
||||||
}
|
}
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ from openhands.sdk.testing import TestLLM, TestLLMExhaustedError
|
|||||||
BASH_TOKEN = "MOCK_LLM_E2E_BASH_OK"
|
BASH_TOKEN = "MOCK_LLM_E2E_BASH_OK"
|
||||||
REPLY_TOKEN = "MOCK_LLM_E2E_REPLY_OK"
|
REPLY_TOKEN = "MOCK_LLM_E2E_REPLY_OK"
|
||||||
|
|
||||||
|
# The user turn the agent-server sends when it pre-flights a saved LLM profile
|
||||||
|
# (POST /api/profiles/{name}/validate, agent-server >= 1.43). Matched by
|
||||||
|
# _is_preflight_ping() so the check never touches the scripted trajectory.
|
||||||
|
PREFLIGHT_PING_TEXT = "ping"
|
||||||
|
|
||||||
# SDK exception → (HTTP status, OpenAI error type)
|
# SDK exception → (HTTP status, OpenAI error type)
|
||||||
ERROR_MAP: dict[type, tuple[int, str]] = {
|
ERROR_MAP: dict[type, tuple[int, str]] = {
|
||||||
LLMAuthenticationError: (401, "invalid_api_key"),
|
LLMAuthenticationError: (401, "invalid_api_key"),
|
||||||
@@ -162,6 +167,23 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|||||||
length = int(self.headers.get("Content-Length", 0))
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
body = json.loads(self.rfile.read(length)) if length else {}
|
body = json.loads(self.rfile.read(length)) if length else {}
|
||||||
|
|
||||||
|
# ── Profile pre-flight ping ──
|
||||||
|
# Saving a profile against agent-server >= 1.43 fires a 1-token "ping"
|
||||||
|
# completion through the submitted config, and the canvas waits at
|
||||||
|
# most 30 s for the verdict. Answer it here instead of feeding it to
|
||||||
|
# TestLLM: it would otherwise consume a scripted turn, and once the
|
||||||
|
# trajectory is exhausted the 500 below makes the SDK retry with
|
||||||
|
# backoff until the canvas gives up — leaving the profile editor stuck
|
||||||
|
# on "Validating...". It stays out of the request history, which tests
|
||||||
|
# read for the conversation's own completions.
|
||||||
|
if _is_preflight_ping(body):
|
||||||
|
raw = _preflight_pong(body.get("model"))
|
||||||
|
if body.get("stream"):
|
||||||
|
self._send_streaming(raw)
|
||||||
|
else:
|
||||||
|
self._send_json(200, raw)
|
||||||
|
return
|
||||||
|
|
||||||
# Append to request history for test verification.
|
# Append to request history for test verification.
|
||||||
# Tests can GET /admin/requests to confirm image content was included.
|
# Tests can GET /admin/requests to confirm image content was included.
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -338,6 +360,53 @@ class MockLLMHandler(BaseHTTPRequestHandler):
|
|||||||
print(f"[mock-llm] {args[0]}", file=sys.stderr, flush=True)
|
print(f"[mock-llm] {args[0]}", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_preflight_ping(body: dict) -> bool:
|
||||||
|
"""Match the agent-server's profile pre-flight check.
|
||||||
|
|
||||||
|
It is a single user message saying "ping" with ``max_tokens=1`` (see
|
||||||
|
``profiles_router.validate_profile`` in openhands-agent-server). The text
|
||||||
|
arrives either as a plain string or as OpenAI content parts.
|
||||||
|
"""
|
||||||
|
if body.get("max_tokens") != 1:
|
||||||
|
return False
|
||||||
|
messages = body.get("messages")
|
||||||
|
if not isinstance(messages, list) or len(messages) != 1:
|
||||||
|
return False
|
||||||
|
message = messages[0]
|
||||||
|
if not isinstance(message, dict) or message.get("role") != "user":
|
||||||
|
return False
|
||||||
|
content = message.get("content")
|
||||||
|
if isinstance(content, list):
|
||||||
|
content = "".join(
|
||||||
|
part.get("text", "")
|
||||||
|
for part in content
|
||||||
|
if isinstance(part, dict) and part.get("type") == "text"
|
||||||
|
)
|
||||||
|
return isinstance(content, str) and content.strip() == PREFLIGHT_PING_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
def _preflight_pong(model: str | None) -> dict:
|
||||||
|
"""Minimal OpenAI-style completion for the pre-flight ping.
|
||||||
|
|
||||||
|
Uses the raw shape ``_send_streaming`` also understands, so the reply works
|
||||||
|
whether or not the caller asked for streaming.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"id": "chatcmpl-mock-preflight",
|
||||||
|
"object": "chat.completion",
|
||||||
|
"created": int(time.time()),
|
||||||
|
"model": model or "mock-preflight",
|
||||||
|
"choices": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"message": {"role": "assistant", "content": "pong"},
|
||||||
|
"finish_reason": "stop",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _parse_trajectory_turns(raw_turns: list[dict]) -> list[Message | Exception]:
|
def _parse_trajectory_turns(raw_turns: list[dict]) -> list[Message | Exception]:
|
||||||
"""Convert JSON turn descriptors into Message objects.
|
"""Convert JSON turn descriptors into Message objects.
|
||||||
|
|
||||||
|
|||||||
@@ -410,6 +410,21 @@ test.describe("OpenHands provider hidden base_url preservation", () => {
|
|||||||
// Advanced view. The value becomes hidden after switching to Basic, but it
|
// Advanced view. The value becomes hidden after switching to Basic, but it
|
||||||
// is still part of the profile unless the model changes. ──
|
// is still part of the profile unless the model changes. ──
|
||||||
await routeSessionApiKey(page);
|
await routeSessionApiKey(page);
|
||||||
|
// agent-server >= 1.43 pre-flights every profile save with a 1-token
|
||||||
|
// completion through the submitted config. CUSTOM_BASE_URL is a
|
||||||
|
// placeholder host by design — this test is about the value surviving a
|
||||||
|
// Basic-view re-save, not about reaching it — so answer the check from
|
||||||
|
// the browser instead of letting the SDK retry an unreachable host past
|
||||||
|
// the canvas's 30 s validation budget. Registered after
|
||||||
|
// routeSessionApiKey(): Playwright routes are LIFO, and that handler
|
||||||
|
// `continue()`s every backend request it sees first.
|
||||||
|
await page.route("**/api/profiles/*/validate", (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ valid: true, error: null }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
await ensureMockLLMAgentProfile(page.request);
|
await ensureMockLLMAgentProfile(page.request);
|
||||||
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
|
await page.goto("/settings/llm", { waitUntil: "domcontentloaded" });
|
||||||
await dismissAnalyticsModal(page);
|
await dismissAnalyticsModal(page);
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ 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
|
UI effect happens client-side: the frontend watches the WebSocket stream for
|
||||||
legacy ``canvas_ui`` and current ``canvas_ui_control`` ActionEvents and dispatches
|
legacy ``canvas_ui`` and current ``canvas_ui_control`` ActionEvents and dispatches
|
||||||
the command.
|
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 collections.abc import Sequence
|
||||||
@@ -22,8 +26,10 @@ from pydantic import Field
|
|||||||
|
|
||||||
from openhands.sdk import Action, Observation, ToolDefinition
|
from openhands.sdk import Action, Observation, ToolDefinition
|
||||||
from openhands.sdk.tool import (
|
from openhands.sdk.tool import (
|
||||||
|
FinishTool,
|
||||||
ToolAnnotations,
|
ToolAnnotations,
|
||||||
ToolExecutor,
|
ToolExecutor,
|
||||||
|
list_registered_tools,
|
||||||
register_tool,
|
register_tool,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,3 +143,16 @@ class CanvasUITool(ToolDefinition[CanvasUIAction, CanvasUIObservation]):
|
|||||||
# restoring their agent and events. Keep the registration until those records
|
# restoring their agent and events. Keep the registration until those records
|
||||||
# have a server-side migration path.
|
# have a server-side migration path.
|
||||||
register_tool("canvas_ui", CanvasUITool)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user