feat(FN-986): add agent stop/start CLI commands and extension tools

- Add `fn agent stop <id>` and `fn agent start <id>` CLI commands for pausing/resuming agents
- Add `kb_agent_stop` and `kb_agent_start` tools to the pi extension for in-session agent control
- Validate state transitions using AGENT_VALID_TRANSITIONS before applying changes
- Export AgentStore from @fusion/core public API
- Add comprehensive tests for agent CLI commands (210 lines)
- Add changeset for @gsxdsm/fusion patch release
This commit is contained in:
gsxdsm
2026-04-05 16:15:22 -07:00
parent 11dfcaf7e2
commit f6bd237f46
11 changed files with 498 additions and 0 deletions

View File

@@ -1519,6 +1519,132 @@ export default function kbExtension(pi: ExtensionAPI) {
},
});
// ── kb_agent_stop ─────────────────────────────────────────────────
pi.registerTool({
name: "kb_agent_stop",
label: "KB: Stop Agent",
description:
"Stop a running agent — pauses its execution. " +
"Transitions the agent from running/active to paused state.",
promptSnippet: "Stop (pause) a running Fusion agent",
promptGuidelines: [
"Use to pause an agent that is currently running or active",
"Stopped agents can be resumed with kb_agent_start",
"Agents in 'idle', 'error', or 'terminated' state cannot be stopped",
],
parameters: Type.Object({
id: Type.String({ description: "Agent ID to stop (e.g., agent-abc123)" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { AgentStore, AGENT_VALID_TRANSITIONS } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: ctx.cwd + "/.fusion" });
await agentStore.init();
const agent = await agentStore.getAgent(params.id);
if (!agent) {
return {
content: [{ type: "text", text: `Agent ${params.id} not found` }],
isError: true,
details: { error: "Agent not found" },
};
}
if (agent.state === "paused") {
return {
content: [{ type: "text", text: `Agent ${params.id} is already paused` }],
details: { agentId: params.id, state: agent.state },
};
}
const validTargets = AGENT_VALID_TRANSITIONS[agent.state];
if (!validTargets.includes("paused")) {
return {
content: [
{
type: "text",
text: `Cannot stop agent ${params.id} — current state '${agent.state}' cannot transition to 'paused'. Valid transitions: ${validTargets.join(", ")}`,
},
],
isError: true,
details: { agentId: params.id, currentState: agent.state, validTargets },
};
}
await agentStore.updateAgentState(params.id, "paused");
return {
content: [{ type: "text", text: `Stopped ${params.id}` }],
details: { agentId: params.id, previousState: agent.state, newState: "paused" },
};
},
});
// ── kb_agent_start ────────────────────────────────────────────────
pi.registerTool({
name: "kb_agent_start",
label: "KB: Start Agent",
description:
"Start a stopped agent — resumes its execution. " +
"Transitions the agent from paused to active state.",
promptSnippet: "Start (resume) a stopped Fusion agent",
promptGuidelines: [
"Use to resume an agent that has been paused",
"Only agents in 'paused' state can be started",
"Agents in 'idle' or 'error' state cannot be started — use reset instead",
],
parameters: Type.Object({
id: Type.String({ description: "Agent ID to start (e.g., agent-abc123)" }),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const { AgentStore, AGENT_VALID_TRANSITIONS } = await import("@fusion/core");
const agentStore = new AgentStore({ rootDir: ctx.cwd + "/.fusion" });
await agentStore.init();
const agent = await agentStore.getAgent(params.id);
if (!agent) {
return {
content: [{ type: "text", text: `Agent ${params.id} not found` }],
isError: true,
details: { error: "Agent not found" },
};
}
if (agent.state === "active" || agent.state === "running") {
return {
content: [{ type: "text", text: `Agent ${params.id} is already running (${agent.state})` }],
details: { agentId: params.id, state: agent.state },
};
}
const validTargets = AGENT_VALID_TRANSITIONS[agent.state];
if (!validTargets.includes("active")) {
return {
content: [
{
type: "text",
text: `Cannot start agent ${params.id} — current state '${agent.state}' cannot transition to 'active'. Valid transitions: ${validTargets.join(", ")}`,
},
],
isError: true,
details: { agentId: params.id, currentState: agent.state, validTargets },
};
}
await agentStore.updateAgentState(params.id, "active");
return {
content: [{ type: "text", text: `Started ${params.id}` }],
details: { agentId: params.id, previousState: agent.state, newState: "active" },
};
},
});
// ── /fn command — start the dashboard + engine ───────────────────
let dashboardProcess: ChildProcess | null = null;