Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions desktop/src/features/agents/lib/managedAgentControlActions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,33 @@ function agent(overrides = {}) {
};
}

test("relay-mesh agents cannot be manually started without a fresh target", async () => {
let called = false;
test("relay-mesh agents delegate start to the backend preflight", async () => {
const meshAgent = agent({
envVars: {
SPROUT_AGENT_PROVIDER: "openai",
OPENAI_COMPAT_BASE_URL: "http://127.0.0.1:9337/v1/",
},
});

let calledWith = null;
await startManagedAgentWithRules({
agent: meshAgent,
startManagedAgent: async (pubkey) => {
calledWith = pubkey;
},
});
assert.equal(calledWith, meshAgent.pubkey);

// Backend preflight failures (e.g. no live serve target) propagate as-is.
await assert.rejects(
startManagedAgentWithRules({
agent: agent({
envVars: {
SPROUT_AGENT_PROVIDER: "openai",
OPENAI_COMPAT_BASE_URL: "http://127.0.0.1:9337/v1/",
},
}),
agent: meshAgent,
startManagedAgent: async () => {
called = true;
throw new Error("no live serve target is available for this model");
},
}),
/Relay-mesh agents need a fresh serve target/,
/no live serve target/,
);
assert.equal(called, false);
});

test("ordinary local agents still start normally", async () => {
Expand Down
19 changes: 3 additions & 16 deletions desktop/src/features/agents/lib/managedAgentControlActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,27 +75,16 @@ export function resolveManagedAgentChannelId(
return matches.length === 1 ? matches[0].id : null;
}

function relayMeshAgentError(agent: ManagedAgent): string | null {
if (agent.backend.type !== "local") return null;
if (agent.envVars.SPROUT_AGENT_PROVIDER !== "openai") return null;
if (
agent.envVars.OPENAI_COMPAT_BASE_URL?.replace(/\/+$/, "") !==
"http://127.0.0.1:9337/v1"
) {
return null;
}
return "Relay-mesh agents need a fresh serve target before start. Create a new agent with Run on relay mesh selected.";
}

export async function startManagedAgentWithRules({
agent,
startManagedAgent,
}: {
agent: ManagedAgent;
startManagedAgent: StartManagedAgent;
}) {
const relayMeshError = relayMeshAgentError(agent);
if (relayMeshError) throw new Error(relayMeshError);
// Relay-mesh agents are no longer blocked here: the backend start preflight
// (ensure_relay_mesh_for_record) re-resolves a live serve target and dials
// it, failing with an actionable error when no peer serves the model.
await startManagedAgent(agent.pubkey);
}

Expand All @@ -108,8 +97,6 @@ export async function respawnManagedAgentWithRules({
startManagedAgent: StartManagedAgent;
stopManagedAgent: StopManagedAgent;
}) {
const relayMeshError = relayMeshAgentError(agent);
if (relayMeshError) throw new Error(relayMeshError);
if (agent.backend.type === "local" && isManagedAgentActive(agent)) {
await stopManagedAgent(agent.pubkey);
}
Expand Down
15 changes: 12 additions & 3 deletions desktop/src/testing/e2eBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4765,9 +4765,18 @@ async function handleStartManagedAgent(args: {
}): Promise<RawManagedAgent> {
const agent = getMockManagedAgent(args.pubkey);
if (isRelayMeshManagedAgent(agent)) {
throw new Error(
"relay mesh agents cannot be started from saved state because the selected serve target is not persisted. Create a new agent with Run on relay mesh selected to refresh the target for http://127.0.0.1:9337/v1.",
);
// Model the backend start preflight (ensure_relay_mesh_for_record): a
// saved relay-mesh agent re-resolves a live serve target for its model
// and only fails when no peer currently serves it.
const modelId = agent.env_vars?.OPENAI_COMPAT_MODEL;
const hasLiveTarget =
mockMeshState.admitted &&
mockMeshState.models.some((model) => model.id === modelId);
if (!hasLiveTarget) {
throw new Error(
"relay mesh agents cannot be started from saved state because no live serve target is available for this model. Start serving on a mesh peer, or create a new agent with Run on relay mesh selected to refresh the target for http://127.0.0.1:9337/v1.",
);
}
}

const now = new Date().toISOString();
Expand Down
26 changes: 19 additions & 7 deletions desktop/tests/e2e/mesh-compute.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ test("a non-member cannot enable relay-mesh — membership is the gate", async (
expect(seq).not.toContain("create_managed_agent");
});

test("saved relay-mesh agents require a fresh serve target before manual start", async ({
test("saved relay-mesh agents restart via the backend serve-target preflight", async ({
page,
}) => {
await gotoApp(page);
Expand Down Expand Up @@ -330,18 +330,30 @@ test("saved relay-mesh agents require a fresh serve target before manual start",
.toContain("stop_managed_agent");
await expect(row).toContainText("stopped");

const before = (await commands(page)).length;
// With a live serve target for the model, manual restart goes through:
// the backend preflight re-resolves the target and the agent starts.
await openManagedAgentActions(page, pubkey);
await page.getByRole("menuitem", { name: "Spawn" }).click();
await expect
.poll(async () => await commands(page))
.toContain("start_managed_agent");
await expect(row).toContainText("running");

await openManagedAgentActions(page, pubkey);
await page.getByRole("menuitem", { name: "Stop" }).click();
await expect(row).toContainText("stopped");

// Without a live serve target, the backend preflight rejects the start
// with an actionable error, surfaced as a toast; the agent stays stopped.
await setMesh(page, { models: [] });
await openManagedAgentActions(page, pubkey);
await page.getByRole("menuitem", { name: "Spawn" }).click();

await expect(
page
.locator("[data-sonner-toast]")
.filter({ hasText: "Relay-mesh agents need a fresh serve target" }),
.filter({ hasText: "no live serve target is available" }),
).toBeVisible();
expect((await commands(page)).slice(before)).not.toContain(
"start_managed_agent",
);
await expect(row).toContainText("stopped");

await expect(
Expand All @@ -360,6 +372,6 @@ test("saved relay-mesh agents require a fresh serve target before manual start",
return err instanceof Error ? err.message : String(err);
}
}, pubkey),
).resolves.toContain("selected serve target is not persisted");
).resolves.toContain("no live serve target is available");
await expect(row).toContainText("stopped");
});