Files
fusion/plugins/fusion-plugin-cursor-runtime/src/runtime-adapter.ts
gsxdsm b6efd89ae5 FN-9097: add Cursor CLI execution runtime
Add a supervised Cursor CLI runtime for routed Cursor model sessions.

- Stream Cursor prompt output into Fusion session callbacks with resume support.
- Support Windows Cursor shims, inactivity-based supervision, and cursor-agent to cursor fallback execution.
- Route Cursor CLI models through the bundled plugin and document the runtime contract.

Files changed:
 .changeset/fn-9097-cursor-cli-runtime.md           |   7 ++
 docs/cursor-cli-contract.md                        |  21 ++++
 docs/settings-reference.md                         |   4 +-
 .../cli-runtime-routing-conformance.test.ts        |  11 +-
 .../engine/src/agents/agent-session-helpers.ts     |   6 +-
 packages/engine/src/agents/cli-provider-routing.ts |  27 ++---
 plugins/fusion-plugin-cursor-runtime/README.md     |  21 ++++
 plugins/fusion-plugin-cursor-runtime/package.json  |   1 +
 .../src/__tests__/cli-spawn.test.ts                |  25 ++++-
 .../src/__tests__/prompt-transport.test.ts         |  58 ++++++++++
 .../src/__tests__/runtime-adapter.test.ts          |  44 +++++---
 .../src/__tests__/stream-parser.test.ts            |  12 +++
 .../fusion-plugin-cursor-runtime/src/cli-spawn.ts  |  57 +++++++++-
 plugins/fusion-plugin-cursor-runtime/src/index.ts  |   4 +-
 .../src/prompt-transport.ts                        | 120 +++++++++++++++++++++
 .../src/runtime-adapter.ts                         |  59 ++++++----
 .../src/stream-parser.ts                           |  43 ++++++++
 plugins/fusion-plugin-cursor-runtime/src/types.ts  |  18 ++--
 pnpm-lock.yaml                                     |   3 +
 19 files changed, 458 insertions(+), 83 deletions(-)

Fusion-Task-Id: FN-9097

Fusion-Task-Lineage: a4eed861-6059-4528-9f80-3426f5ccad58

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-08-15 08:51:35 -07:00

45 lines
3.4 KiB
TypeScript

import { launchCursorPrompt } from "./prompt-transport.js";
import type { AgentRuntime, AgentRuntimeOptions, AgentSessionResult, CursorStreamSession } from "./types.js";
function context(options: AgentRuntimeOptions): string {
const skills = Array.isArray(options.skills) ? options.skills.filter((value) => typeof value === "string" && value.trim()) : [];
return ["Fusion runtime context:", `- Tool mode: ${options.tools ?? "readonly"}`, skills.length ? `- Requested skills: ${skills.join(", ")}` : ""].filter(Boolean).join("\n");
}
/*
FNXC:CursorCli 2026-08-15-15:16:
Cursor has no system-prompt flag, so Fusion fuses system context only into the first stdin prompt.
The stored cwd and tools are immutable session authority: later turns cannot turn a review session into --force.
*/
export class CursorRuntimeAdapter implements AgentRuntime {
readonly id = "cursor";
readonly name = "Cursor Runtime";
constructor(private readonly settings?: Record<string, unknown>) {}
async createSession(options: AgentRuntimeOptions): Promise<AgentSessionResult> {
const messages: unknown[] = [];
const session: CursorStreamSession = { model: options.defaultModelId?.replace(/^cursor-cli\//, "") ?? "auto", systemPrompt: options.systemPrompt, messages, state: { messages }, sessionId: "", cwd: options.cwd, tools: options.tools, callbacks: { onText: options.onText, onThinking: options.onThinking, onToolStart: options.onToolStart, onToolEnd: options.onToolEnd }, fusedSystemPrompt: [options.systemPrompt?.trim(), context(options)].filter(Boolean).join("\n\n"), disposed: false, dispose: () => {
/* FNXC:CursorCli 2026-08-15-15:32: Disposing a Fusion session must abort its live autonomous Cursor turn so prompt-transport performs supervised process-tree teardown. */
session.disposed = true;
session.activeAbortController?.abort();
} };
return { session, sessionFile: undefined };
}
async promptWithFallback(session: CursorStreamSession, prompt: string, _options?: unknown): Promise<void> {
if (session.disposed) throw new Error("Cursor session is disposed.");
const priorId = session.sessionId;
const first = !priorId;
const sent = first ? `${session.fusedSystemPrompt}\n\nUser request:\n${prompt}` : prompt;
const emitted = new Set<string>();
const controller = new AbortController();
session.activeAbortController = controller;
try {
const outcome = await launchCursorPrompt({ binary: typeof this.settings?.cursorCliBinaryPath === "string" ? this.settings.cursorCliBinaryPath : undefined, model: session.model, cwd: session.cwd, tools: session.tools, prompt: sent, resumeId: priorId || undefined, signal: controller.signal, onThinking: session.callbacks.onThinking, onToolStart: session.callbacks.onToolStart, onToolEnd: session.callbacks.onToolEnd, onText: (text) => { if (!emitted.has(text)) { emitted.add(text); session.callbacks.onText?.(text); } } });
session.sessionId = outcome.sessionId ?? session.sessionId;
session.messages.push({ role: "user", content: prompt }, { role: "assistant", content: outcome.text });
} catch (error) { session.sessionId = priorId; throw error; } finally {
if (session.activeAbortController === controller) session.activeAbortController = undefined;
}
}
describeModel(session: CursorStreamSession): string { return `cursor-cli/${(session.model || "auto").replace(/^cursor-cli\//, "")}`; }
}