Files
paseo/packages/plugin/src/server/acp.ts
T
Mohamed Boudra b0c9de0c90 feat(plugins): first-class plugin providers — status, launch resolution, config overrides (#5707)
* feat(plugins): support provider status, launch resolution, and config overrides

* fix(plugins): defer override warnings until provider startup settles
2026-09-30 13:46:35 +01:00

124 lines
3.7 KiB
TypeScript

import type { JsonValue } from "@getpaseo/protocol/agent-types";
import type {
ProviderCatalog,
ProviderCommand,
ProviderConfigState,
ProviderNotice,
ProviderRegistration,
ProviderTimelineItem,
} from "./provider.js";
import { createAcpProviderConnection } from "./acp-internal/connection.js";
interface RunAcpProviderBaseOptions {
id: string;
label: string;
description?: string;
icon?: string;
acpOptions?: AcpOptions;
transformers?: readonly AcpTransformer[];
}
export type RunAcpProviderOptions = RunAcpProviderBaseOptions &
(
| { command: readonly [string, ...string[]]; connector?: never }
| { connector: AcpConnector; command?: never }
);
export type AcpStreamMessage =
| { jsonrpc: "2.0"; id: string | number | null; method: string; params?: unknown }
| { jsonrpc: "2.0"; method: string; params?: unknown }
| { jsonrpc: "2.0"; id: string | number | null; result: unknown }
| {
jsonrpc: "2.0";
id: string | number | null;
error: { code: number; message: string; data?: unknown };
};
export interface AcpStream {
writable: WritableStream<AcpStreamMessage>;
readable: ReadableStream<AcpStreamMessage>;
}
export type AcpConnector = () => AcpStream | Promise<AcpStream>;
export interface AcpOptions {
protocol?: "auto" | "v1";
startupTimeoutMs?: number;
waitForInitialCommands?: boolean;
initialCommandsTimeoutMs?: number;
}
export interface AcpContext {
sessionId: string;
}
export interface AcpConfigAccess {
read(): Promise<Readonly<Record<string, JsonValue>>>;
set(id: string, value: JsonValue): Promise<void>;
}
export interface AcpDiscoveryContext extends AcpContext {
config: AcpConfigAccess;
}
export interface AcpConfigureContext extends AcpContext {
config: AcpConfigAccess;
}
export type AcpConfigChange =
| { target: "model"; value: string | null }
| { target: "mode"; value: string | null }
| { target: "thinking"; value: string | null }
| { target: "setting"; id: string; value: JsonValue };
export interface AcpToolCallSnapshot {
id: string;
name?: string;
title: string;
kind?: string;
status: "pending" | "in_progress" | "completed" | "failed";
input: JsonValue;
output: JsonValue;
locations: readonly string[];
}
export type AcpVendorUpdate =
| { type: "commands"; commands: readonly ProviderCommand[] }
| { type: "config"; config: ProviderConfigState }
| { type: "timeline"; item: ProviderTimelineItem }
| { type: "notice"; notice: ProviderNotice };
export interface AcpTransformer {
discover?(
catalog: ProviderCatalog,
context: AcpDiscoveryContext,
): Promise<ProviderCatalog> | ProviderCatalog;
configure?(change: AcpConfigChange, context: AcpConfigureContext): Promise<"handled" | "pass">;
notification?(
notification: { method: string; params: JsonValue },
context: AcpContext,
): AcpVendorUpdate | readonly AcpVendorUpdate[] | null;
toolCall?(toolCall: AcpToolCallSnapshot, context: AcpContext): AcpToolCallSnapshot;
}
export function runAcpProvider(options: RunAcpProviderOptions): ProviderRegistration {
const hasCommand = options.command !== undefined;
const hasConnector = options.connector !== undefined;
if (hasCommand === hasConnector) {
throw new Error("ACP provider requires exactly one command or SDK connector");
}
if (options.command && (options.command.length === 0 || options.command[0].trim().length === 0)) {
throw new Error("ACP provider command must contain an executable");
}
return {
id: options.id,
label: options.label,
description: options.description,
icon: options.icon,
command: options.command,
connect(request) {
return createAcpProviderConnection(options, request);
},
};
}