feat(agents): let a custom agent speak ACP (#3863)

Co-authored-by: Joao Manso <X363081@bcpcorp.net>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
This commit is contained in:
João Manso
2026-09-23 11:01:32 +01:00
committed by GitHub
parent f8708da1e0
commit f2d52f1826
55 changed files with 2303 additions and 274 deletions
+9
View File
@@ -640,6 +640,15 @@ export class ApiClient {
return this.request("GET", "/api/v1/agents/available");
}
/** Removes a custom agent by slug, so a spec that creates one leaves the
* worker's agent list as it found it. Missing is not an error. */
async deleteCustomAgentByName(name: string): Promise<void> {
const { agents } = await this.listAgents();
const agent = agents.find((candidate) => candidate.name === name);
if (!agent) return;
await this.request("DELETE", `/api/v1/agents/${agent.id}`);
}
async deleteAgentProfile(profileId: string, force?: boolean): Promise<void> {
const qs = force ? "?force=true" : "";
await this.request("DELETE", `/api/v1/agent-profiles/${profileId}${qs}`);
@@ -0,0 +1,39 @@
import { test, expect } from "../../fixtures/test-base";
const AGENT_NAME = "E2E Acp Agent";
const AGENT_SLUG = "e2e-acp-agent";
test.describe("Custom ACP agent", () => {
test.afterEach(async ({ apiClient }) => {
await apiClient.deleteCustomAgentByName(AGENT_SLUG);
});
test("registers a custom agent kandev drives over ACP", async ({ testPage, apiClient }) => {
await testPage.goto("/settings/agents");
await testPage.getByTestId("new-agent-button").click();
const dialog = testPage.getByRole("dialog", { name: "Add custom agent" });
await expect(dialog).toBeVisible();
await expect(dialog.getByTestId("mcp-strategy-select")).toBeVisible();
await dialog.getByTestId("agent-protocol-select").click();
await testPage.getByRole("listbox").getByRole("option").filter({ hasText: /^ACP/ }).click();
// Resolved MCP servers reach an ACP agent through session/new, so the
// passthrough strategy picker is not offered for it.
await expect(dialog.getByTestId("mcp-strategy-select")).toHaveCount(0);
await dialog.getByLabel("Display Name").fill(AGENT_NAME);
await dialog.getByLabel("Command").fill(`${AGENT_SLUG} --acp`);
await dialog.getByRole("button", { name: "Create" }).click();
await expect(testPage.getByTestId(`agent-group-${AGENT_SLUG}`)).toBeVisible();
await expect(testPage.getByTestId(`agent-mcp-strategy-${AGENT_SLUG}`)).toHaveCount(0);
// The stored definition is what a restart replays, so it has to say ACP
// rather than leaving the agent to come back as a terminal one.
const { agents } = await apiClient.listAgents();
const created = agents.find((agent) => agent.name === AGENT_SLUG);
expect(created?.tui_config?.protocol).toBe("acp");
});
});
@@ -11,7 +11,7 @@ test.describe("Custom TUI MCP strategy selector", () => {
await testPage.goto("/settings/agents");
await testPage.getByTestId("new-agent-button").click();
const dialog = testPage.getByRole("dialog", { name: "Add TUI Agent" });
const dialog = testPage.getByRole("dialog", { name: "Add custom agent" });
await expect(dialog).toBeVisible();
const trigger = dialog.getByTestId("mcp-strategy-select");
@@ -0,0 +1,42 @@
import { test, expect } from "../../fixtures/test-base";
const AGENT_NAME = "Mobile ACP Agent";
const AGENT_SLUG = "mobile-acp-agent";
test.describe("Custom ACP agent on mobile", () => {
test.afterEach(async ({ apiClient }) => {
await apiClient.deleteCustomAgentByName(AGENT_SLUG);
});
test("selects ACP and keeps the protocol controls usable", async ({ testPage, apiClient }) => {
await testPage.goto("/settings/agents");
await testPage.getByTestId("new-agent-button").tap();
const dialog = testPage.getByRole("dialog", { name: "Add custom agent" });
await expect(dialog).toBeVisible();
const protocol = dialog.getByTestId("agent-protocol-select");
await expect
.poll(async () => (await protocol.boundingBox())?.height ?? 0)
.toBeGreaterThanOrEqual(44);
await protocol.tap();
const listbox = testPage.getByRole("listbox");
const acpOption = listbox.getByRole("option", { name: "ACP" });
await expect(acpOption).toBeVisible();
await expect
.poll(async () => (await acpOption.boundingBox())?.height ?? 0)
.toBeGreaterThanOrEqual(44);
await acpOption.tap();
await expect(dialog.getByTestId("mcp-strategy-select")).toHaveCount(0);
await dialog.getByLabel("Display Name").fill(AGENT_NAME);
await dialog.getByLabel("Command").fill(`${AGENT_SLUG} --acp`);
await dialog.getByRole("button", { name: "Create" }).tap();
await expect(testPage.getByTestId(`agent-group-${AGENT_SLUG}`)).toBeVisible();
const { agents } = await apiClient.listAgents();
const created = agents.find((agent) => agent.name === AGENT_SLUG);
expect(created?.tui_config?.protocol).toBe("acp");
});
});
@@ -10,7 +10,7 @@ test.describe("Custom TUI MCP strategy selector on mobile", () => {
await testPage.goto("/settings/agents");
await testPage.getByTestId("new-agent-button").tap();
const dialog = testPage.getByRole("dialog", { name: "Add TUI Agent" });
const dialog = testPage.getByRole("dialog", { name: "Add custom agent" });
await expect(dialog).toBeVisible();
const trigger = dialog.getByTestId("mcp-strategy-select");