mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-29 04:32:57 +00:00
+1








264f6e1cca
Makes OMP a first-class provider: import keeps the session model, replay matches live rendering, /compact waits for OMP, Auto thinking, working multi-select and Other answers, mid-turn steering, fallback model and Fast mode, archived agents open without starting omp, profile-aware import, crash recovery and approval-mode changes, readable tool-call rows, one bubble per /skill, blocked and abandoned todos, OMP notifications, hidden todo reminders on reload, and session MCP servers for schedules and Hub runs. Fixes #2727, fixes #2857, fixes #4000, fixes #4790, fixes #5354, fixes #5099 Co-authored-by: Abhishek Gupta <183943363+abhi-wan-kenobi@users.noreply.github.com> Co-authored-by: Jegor Kitskerkin <jegor.kitskerkin@gmail.com> Co-authored-by: Kh05ifr4nD <meandSSH0219@gmail.com> Co-authored-by: Nguyen Quang Minh <135627235+n24q02m@users.noreply.github.com> Co-authored-by: Omer Cohen <639682+omercnet@users.noreply.github.com> Co-authored-by: Thomas Van <minhthong@gmail.com> Co-authored-by: allo <alloevil@hotmail.com> Co-authored-by: gray-graff <gray.graff@gmail.com> Co-authored-by: kasrakhosravi <kasra@goldsky.com> Co-authored-by: viet-initx <viet@initx.io>
321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
import type { Logger } from "pino";
|
|
import { z } from "zod";
|
|
|
|
import type { ProviderRuntimeSettings } from "../../provider-launch-config.js";
|
|
import {
|
|
JSONL_RPC_NO_TIMEOUT,
|
|
JsonlRpcProcess,
|
|
type JsonlRpcLaunch,
|
|
} from "../jsonl-rpc-process.js";
|
|
import { establishOmpProtocol } from "./protocol-session.js";
|
|
import {
|
|
buildOmpLaunch,
|
|
type OmpRuntime,
|
|
type OmpRuntimeLaunch,
|
|
type OmpRuntimeSession,
|
|
type OmpStartSessionInput,
|
|
} from "./runtime.js";
|
|
import {
|
|
OmpBranchMessagesResultSchema,
|
|
OmpBranchResultSchema,
|
|
OmpCommandsResultSchema,
|
|
OmpHostToolsResultSchema,
|
|
OmpMessagesResultSchema,
|
|
OmpModelSchema,
|
|
OmpModelsResultSchema,
|
|
OmpPromptAckSchema,
|
|
OmpRpcCommandSchema,
|
|
OmpRuntimeEventSchema,
|
|
OmpSessionStateSchema,
|
|
OmpSessionStatsSchema,
|
|
type OmpThinkingLevel,
|
|
type OmpAgentMessage,
|
|
type OmpModel,
|
|
type OmpPromptAck,
|
|
type OmpRpcCommand,
|
|
type OmpRpcHostToolDefinition,
|
|
type OmpRpcHostToolResult,
|
|
type OmpRpcHostToolUpdate,
|
|
type OmpRpcSlashCommand,
|
|
type OmpRuntimeEvent,
|
|
type OmpSessionState,
|
|
type OmpSessionStats,
|
|
type OmpSubagentSubscriptionLevel,
|
|
} from "./rpc-types.js";
|
|
|
|
const DEFAULT_OMP_COMMAND: [string, ...string[]] = [process.env.OMP_COMMAND ?? "omp"];
|
|
const DEFAULT_COMMANDS_RPC_NAME = "get_available_commands";
|
|
|
|
export interface OmpCliRuntimeOptions {
|
|
logger: Logger;
|
|
runtimeSettings?: ProviderRuntimeSettings;
|
|
command?: [string, ...string[]];
|
|
commandsRpcName?: "get_available_commands";
|
|
readyTimeoutMs?: number;
|
|
requestTimeoutMs?: number;
|
|
spawnProcess?: (launch: OmpRuntimeLaunch) => ChildProcessWithoutNullStreams;
|
|
}
|
|
|
|
export class OmpCliRuntime implements OmpRuntime {
|
|
private readonly command: [string, ...string[]];
|
|
private readonly commandsRpcName: "get_available_commands";
|
|
private readonly spawnProcess?: (launch: OmpRuntimeLaunch) => ChildProcessWithoutNullStreams;
|
|
|
|
constructor(private readonly options: OmpCliRuntimeOptions) {
|
|
this.command = options.command ?? DEFAULT_OMP_COMMAND;
|
|
this.commandsRpcName = options.commandsRpcName ?? DEFAULT_COMMANDS_RPC_NAME;
|
|
this.spawnProcess = options.spawnProcess;
|
|
}
|
|
|
|
async startSession(input: OmpStartSessionInput): Promise<OmpRuntimeSession> {
|
|
const launch = buildOmpLaunch({
|
|
command: this.command,
|
|
runtimeSettings: this.options.runtimeSettings,
|
|
session: input,
|
|
});
|
|
const [command, ...args] = launch.argv;
|
|
const processLaunch: JsonlRpcLaunch = {
|
|
command,
|
|
args,
|
|
cwd: launch.cwd,
|
|
env: launch.env,
|
|
};
|
|
const spawn = this.spawnProcess;
|
|
const processOptions = {
|
|
launch: processLaunch,
|
|
logger: this.options.logger,
|
|
diagnosticName: "OMP RPC",
|
|
defaultRequestTimeoutMs: this.options.requestTimeoutMs,
|
|
...(spawn ? { spawn: () => spawn(launch) } : {}),
|
|
};
|
|
const process = new JsonlRpcProcess(processOptions);
|
|
const handleAbort = () => void process.close(input.signal?.reason).catch(() => undefined);
|
|
input.signal?.addEventListener("abort", handleAbort, { once: true });
|
|
try {
|
|
await establishOmpProtocol(process, this.options.logger, {
|
|
readyTimeoutMs: this.options.readyTimeoutMs,
|
|
requestTimeoutMs: this.options.requestTimeoutMs,
|
|
});
|
|
input.signal?.throwIfAborted();
|
|
return new OmpCliRuntimeSession(process, this.commandsRpcName);
|
|
} catch (error) {
|
|
const startupError = error instanceof Error ? error : new Error(String(error));
|
|
await process.close(startupError);
|
|
throw startupError;
|
|
} finally {
|
|
input.signal?.removeEventListener("abort", handleAbort);
|
|
}
|
|
}
|
|
}
|
|
|
|
class OmpCliRuntimeSession implements OmpRuntimeSession {
|
|
private readonly subscribers = new Set<(event: OmpRuntimeEvent) => void>();
|
|
activeBranchEntryId?: string;
|
|
|
|
constructor(
|
|
private readonly process: JsonlRpcProcess,
|
|
private readonly commandsRpcName: "get_available_commands",
|
|
) {
|
|
process.onMessage((message) => {
|
|
const event = OmpRuntimeEventSchema.safeParse(message);
|
|
if (event.success) {
|
|
this.emit(event.data);
|
|
}
|
|
});
|
|
process.onExit(({ error }) => {
|
|
this.emit({ type: "process_exit", error: error.message });
|
|
});
|
|
}
|
|
|
|
onEvent(callback: (event: OmpRuntimeEvent) => void): () => void {
|
|
this.subscribers.add(callback);
|
|
return () => {
|
|
this.subscribers.delete(callback);
|
|
};
|
|
}
|
|
|
|
async prompt(
|
|
message: string,
|
|
images?: Array<{ type: "image"; data: string; mimeType: string }>,
|
|
): Promise<OmpPromptAck> {
|
|
const { id: requestId, promise } = this.process.startRequest({
|
|
type: "prompt",
|
|
message,
|
|
...(images?.length ? { images } : {}),
|
|
});
|
|
const ack = OmpPromptAckSchema.parse(await promise) ?? {};
|
|
return { requestId, ...ack };
|
|
}
|
|
|
|
async compact(customInstructions?: string): Promise<void> {
|
|
await this.request(
|
|
{
|
|
type: "compact",
|
|
...(customInstructions ? { customInstructions } : {}),
|
|
},
|
|
JSONL_RPC_NO_TIMEOUT,
|
|
);
|
|
}
|
|
|
|
async setAutoCompaction(enabled: boolean): Promise<void> {
|
|
await this.request({ type: "set_auto_compaction", enabled });
|
|
}
|
|
|
|
async abort(): Promise<void> {
|
|
await this.requestStopWork({ type: "abort" });
|
|
}
|
|
|
|
async getState(): Promise<OmpSessionState> {
|
|
return OmpSessionStateSchema.parse(await this.request({ type: "get_state" }));
|
|
}
|
|
|
|
async setFastMode(enabled: boolean): Promise<{ enabled: boolean; active: boolean }> {
|
|
const result = await this.request({ type: "set_fast_mode", enabled });
|
|
return z.object({ enabled: z.boolean(), active: z.boolean() }).parse(result);
|
|
}
|
|
|
|
async getMessages(): Promise<OmpAgentMessage[]> {
|
|
const data = OmpMessagesResultSchema.parse(await this.request({ type: "get_messages" }));
|
|
return data.messages ?? [];
|
|
}
|
|
|
|
async getAvailableModels(timeoutMs?: number | null): Promise<OmpModel[]> {
|
|
const data = OmpModelsResultSchema.parse(
|
|
await this.request({ type: "get_available_models" }, timeoutMs),
|
|
);
|
|
return data.models ?? [];
|
|
}
|
|
|
|
async setModel(provider: string, modelId: string): Promise<OmpModel> {
|
|
return OmpModelSchema.parse(await this.request({ type: "set_model", provider, modelId }));
|
|
}
|
|
|
|
async setThinkingLevel(level: OmpThinkingLevel): Promise<void> {
|
|
await this.request({ type: "set_thinking_level", level });
|
|
}
|
|
|
|
async getSessionStats(): Promise<OmpSessionStats> {
|
|
// COMPAT(ompGetStateFallback): added in v0.1.105 — older OMP binaries
|
|
// lack the `get_session_stats` RPC command; fall back to extracting
|
|
// context window usage from `get_state`. Remove after 2027-01-10 once the
|
|
// supported OMP floor includes `get_session_stats`.
|
|
let stats: OmpSessionStats | undefined;
|
|
try {
|
|
stats = OmpSessionStatsSchema.parse(await this.request({ type: "get_session_stats" }));
|
|
} catch {
|
|
// get_session_stats not supported by this binary — will try get_state below
|
|
}
|
|
if (stats?.tokens == null && stats?.cost == null && stats?.contextUsage == null) {
|
|
try {
|
|
const state = OmpSessionStateSchema.parse(await this.request({ type: "get_state" }));
|
|
const ctx = state.contextUsage;
|
|
if (ctx) {
|
|
return {
|
|
contextUsage: {
|
|
tokens: typeof ctx.tokens === "number" ? ctx.tokens : undefined,
|
|
contextWindow: typeof ctx.contextWindow === "number" ? ctx.contextWindow : undefined,
|
|
},
|
|
};
|
|
}
|
|
} catch {
|
|
// get_state also failed — nothing we can do
|
|
}
|
|
}
|
|
return stats ?? {};
|
|
}
|
|
|
|
async getCommands(): Promise<OmpRpcSlashCommand[]> {
|
|
const data = OmpCommandsResultSchema.parse(await this.request({ type: this.commandsRpcName }));
|
|
return data.commands ?? [];
|
|
}
|
|
|
|
async setSubagentSubscription(level: OmpSubagentSubscriptionLevel): Promise<void> {
|
|
await this.request({ type: "set_subagent_subscription", level });
|
|
}
|
|
|
|
async setHostTools(tools: OmpRpcHostToolDefinition[]): Promise<string[]> {
|
|
const data = OmpHostToolsResultSchema.parse(
|
|
await this.request({ type: "set_host_tools", tools }),
|
|
);
|
|
return data.toolNames ?? [];
|
|
}
|
|
|
|
sendHostToolResult(result: OmpRpcHostToolResult): void {
|
|
this.process.send({ ...result });
|
|
}
|
|
|
|
sendHostToolUpdate(update: OmpRpcHostToolUpdate): void {
|
|
this.process.send({ ...update });
|
|
}
|
|
|
|
async branch(entryId: string): Promise<{ text: string }> {
|
|
const data = OmpBranchResultSchema.parse(await this.request({ type: "branch", entryId }));
|
|
if (data.cancelled === true) {
|
|
throw new Error("OMP branch was cancelled");
|
|
}
|
|
if (typeof data.text !== "string") {
|
|
throw new Error("OMP branch response did not include restored prompt text");
|
|
}
|
|
this.activeBranchEntryId = entryId;
|
|
return { text: data.text };
|
|
}
|
|
|
|
async getBranchMessages(): Promise<Array<{ entryId: string; text: string }>> {
|
|
const data = OmpBranchMessagesResultSchema.parse(
|
|
await this.request({ type: "get_branch_messages" }),
|
|
);
|
|
return data.messages ?? [];
|
|
}
|
|
|
|
async steer(
|
|
message: string,
|
|
images?: Array<{ type: "image"; data: string; mimeType: string }>,
|
|
): Promise<void> {
|
|
await this.request({ type: "steer", message, ...(images?.length ? { images } : {}) });
|
|
}
|
|
|
|
followUp(
|
|
message: string,
|
|
images?: Array<{ type: "image"; data: string; mimeType: string }>,
|
|
): void {
|
|
this.process.send({ type: "follow_up", message, ...(images?.length ? { images } : {}) });
|
|
}
|
|
|
|
async handoff(customInstructions?: string): Promise<void> {
|
|
await this.request({
|
|
type: "handoff",
|
|
...(customInstructions ? { customInstructions } : {}),
|
|
});
|
|
}
|
|
|
|
respondToExtensionUiRequest(
|
|
id: string,
|
|
response: { value?: string; confirmed?: boolean; cancelled?: boolean },
|
|
): void {
|
|
this.process.send({ type: "extension_ui_response", id, ...response });
|
|
}
|
|
|
|
cancelExtensionUiRequest(id: string): void {
|
|
this.respondToExtensionUiRequest(id, { cancelled: true });
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
await this.process.close(new Error("OMP RPC session is closed"));
|
|
}
|
|
|
|
private request(command: OmpRpcCommand, timeoutMs?: number | null): Promise<unknown> {
|
|
return this.process.request(OmpRpcCommandSchema.parse(command), timeoutMs);
|
|
}
|
|
|
|
private requestStopWork(command: OmpRpcCommand): Promise<void> {
|
|
return this.process.requestStopWork(OmpRpcCommandSchema.parse(command));
|
|
}
|
|
|
|
private emit(event: OmpRuntimeEvent): void {
|
|
for (const subscriber of this.subscribers) {
|
|
subscriber(event);
|
|
}
|
|
}
|
|
}
|