fix(desktop): allow restarting saved relay-mesh agents from the UI (#956)

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
tlongwell-block
2026-06-10 16:56:02 -04:00
committed by GitHub
parent 60c8c5036a
commit 510009c11d
4 changed files with 55 additions and 37 deletions

View File

@@ -39,23 +39,33 @@ function agent(overrides = {}) {
}; };
} }
test("relay-mesh agents cannot be manually started without a fresh target", async () => { test("relay-mesh agents delegate start to the backend preflight", async () => {
let called = false; const meshAgent = agent({
await assert.rejects(
startManagedAgentWithRules({
agent: agent({
envVars: { envVars: {
SPROUT_AGENT_PROVIDER: "openai", SPROUT_AGENT_PROVIDER: "openai",
OPENAI_COMPAT_BASE_URL: "http://127.0.0.1:9337/v1/", 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: meshAgent,
startManagedAgent: async () => { 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 () => { test("ordinary local agents still start normally", async () => {

View File

@@ -75,18 +75,6 @@ export function resolveManagedAgentChannelId(
return matches.length === 1 ? matches[0].id : null; 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({ export async function startManagedAgentWithRules({
agent, agent,
startManagedAgent, startManagedAgent,
@@ -94,8 +82,9 @@ export async function startManagedAgentWithRules({
agent: ManagedAgent; agent: ManagedAgent;
startManagedAgent: StartManagedAgent; startManagedAgent: StartManagedAgent;
}) { }) {
const relayMeshError = relayMeshAgentError(agent); // Relay-mesh agents are no longer blocked here: the backend start preflight
if (relayMeshError) throw new Error(relayMeshError); // (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); await startManagedAgent(agent.pubkey);
} }
@@ -108,8 +97,6 @@ export async function respawnManagedAgentWithRules({
startManagedAgent: StartManagedAgent; startManagedAgent: StartManagedAgent;
stopManagedAgent: StopManagedAgent; stopManagedAgent: StopManagedAgent;
}) { }) {
const relayMeshError = relayMeshAgentError(agent);
if (relayMeshError) throw new Error(relayMeshError);
if (agent.backend.type === "local" && isManagedAgentActive(agent)) { if (agent.backend.type === "local" && isManagedAgentActive(agent)) {
await stopManagedAgent(agent.pubkey); await stopManagedAgent(agent.pubkey);
} }

View File

@@ -4765,10 +4765,19 @@ async function handleStartManagedAgent(args: {
}): Promise<RawManagedAgent> { }): Promise<RawManagedAgent> {
const agent = getMockManagedAgent(args.pubkey); const agent = getMockManagedAgent(args.pubkey);
if (isRelayMeshManagedAgent(agent)) { if (isRelayMeshManagedAgent(agent)) {
// 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( 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.", "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(); const now = new Date().toISOString();
if (agent.backend.type === "provider") { if (agent.backend.type === "provider") {

View File

@@ -278,7 +278,7 @@ test("a non-member cannot enable relay-mesh — membership is the gate", async (
expect(seq).not.toContain("create_managed_agent"); 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, page,
}) => { }) => {
await gotoApp(page); await gotoApp(page);
@@ -330,18 +330,30 @@ test("saved relay-mesh agents require a fresh serve target before manual start",
.toContain("stop_managed_agent"); .toContain("stop_managed_agent");
await expect(row).toContainText("stopped"); 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 openManagedAgentActions(page, pubkey);
await page.getByRole("menuitem", { name: "Spawn" }).click(); await page.getByRole("menuitem", { name: "Spawn" }).click();
await expect( await expect(
page page
.locator("[data-sonner-toast]") .locator("[data-sonner-toast]")
.filter({ hasText: "Relay-mesh agents need a fresh serve target" }), .filter({ hasText: "no live serve target is available" }),
).toBeVisible(); ).toBeVisible();
expect((await commands(page)).slice(before)).not.toContain(
"start_managed_agent",
);
await expect(row).toContainText("stopped"); await expect(row).toContainText("stopped");
await expect( await expect(
@@ -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); return err instanceof Error ? err.message : String(err);
} }
}, pubkey), }, pubkey),
).resolves.toContain("selected serve target is not persisted"); ).resolves.toContain("no live serve target is available");
await expect(row).toContainText("stopped"); await expect(row).toContainText("stopped");
}); });