Add diagnostics for generic ACP providers (#955)
This commit is contained in:
@@ -366,6 +366,14 @@ Required fields for ACP providers:
|
|||||||
- `label`
|
- `label`
|
||||||
- `command` — the command to spawn the agent process (must support ACP over stdio)
|
- `command` — the command to spawn the agent process (must support ACP over stdio)
|
||||||
|
|
||||||
|
### Generic ACP diagnostics
|
||||||
|
|
||||||
|
Paseo diagnostics for `extends: "acp"` providers report the configured command, resolved launcher binary, version output, ACP `initialize`, ACP `session/new`, model count, modes, and final status.
|
||||||
|
|
||||||
|
For package-runner commands such as `npx -y @google/gemini-cli --acp`, the version probe keeps the package spec and runs `npx -y @google/gemini-cli --version`. This diagnoses the actual agent package instead of only proving that `npx` exists.
|
||||||
|
|
||||||
|
ACP probes use short timeouts and browser-suppression environment variables so agents that enter an auth/browser flow fail as a diagnostic error instead of hanging the provider screen.
|
||||||
|
|
||||||
### Example: Google Gemini CLI
|
### Example: Google Gemini CLI
|
||||||
|
|
||||||
[Gemini CLI](https://github.com/google-gemini/gemini-cli) supports ACP via the `--acp` flag.
|
[Gemini CLI](https://github.com/google-gemini/gemini-cli) supports ACP via the `--acp` flag.
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const mockState = vi.hoisted(() => {
|
|||||||
genericAcp: [] as Array<{
|
genericAcp: [] as Array<{
|
||||||
command: string[];
|
command: string[];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
providerId?: string;
|
||||||
|
label?: string;
|
||||||
}>,
|
}>,
|
||||||
},
|
},
|
||||||
isCommandAvailable: vi.fn(async (_command: string) => false),
|
isCommandAvailable: vi.fn(async (_command: string) => false),
|
||||||
@@ -241,7 +243,12 @@ vi.mock("./providers/generic-acp-agent.js", () => ({
|
|||||||
readonly provider = "acp";
|
readonly provider = "acp";
|
||||||
readonly runtimeSettings?: unknown;
|
readonly runtimeSettings?: unknown;
|
||||||
|
|
||||||
constructor(options: { command: string[]; env?: Record<string, string> }) {
|
constructor(options: {
|
||||||
|
command: string[];
|
||||||
|
env?: Record<string, string>;
|
||||||
|
providerId?: string;
|
||||||
|
label?: string;
|
||||||
|
}) {
|
||||||
this.runtimeSettings = {
|
this.runtimeSettings = {
|
||||||
command: {
|
command: {
|
||||||
mode: "replace",
|
mode: "replace",
|
||||||
@@ -252,6 +259,8 @@ vi.mock("./providers/generic-acp-agent.js", () => ({
|
|||||||
mockState.constructorArgs.genericAcp.push({
|
mockState.constructorArgs.genericAcp.push({
|
||||||
command: options.command,
|
command: options.command,
|
||||||
env: options.env,
|
env: options.env,
|
||||||
|
providerId: options.providerId,
|
||||||
|
label: options.label,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,12 +396,16 @@ test("new provider extending acp uses GenericACPAgentClient", () => {
|
|||||||
env: {
|
env: {
|
||||||
ACP_TOKEN: "secret",
|
ACP_TOKEN: "secret",
|
||||||
},
|
},
|
||||||
|
providerId: "my-agent",
|
||||||
|
label: "My Agent",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: ["my-agent", "--acp"],
|
command: ["my-agent", "--acp"],
|
||||||
env: {
|
env: {
|
||||||
ACP_TOKEN: "secret",
|
ACP_TOKEN: "secret",
|
||||||
},
|
},
|
||||||
|
providerId: "my-agent",
|
||||||
|
label: "My Agent",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -517,6 +517,8 @@ function addDerivedProviders(
|
|||||||
logger,
|
logger,
|
||||||
command,
|
command,
|
||||||
env: override.env,
|
env: override.env,
|
||||||
|
providerId,
|
||||||
|
label: override.label ?? providerId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -713,7 +713,10 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async spawnProcess(launchEnv?: Record<string, string>): Promise<SpawnedACPProcess> {
|
protected async spawnProcess(
|
||||||
|
launchEnv?: Record<string, string>,
|
||||||
|
options?: { initializeTimeoutMs?: number },
|
||||||
|
): Promise<SpawnedACPProcess> {
|
||||||
const { command, args } = await this.resolveLaunchCommand();
|
const { command, args } = await this.resolveLaunchCommand();
|
||||||
const child = spawnProcess(command, args, {
|
const child = spawnProcess(command, args, {
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
@@ -743,14 +746,35 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
{ logger: this.logger, provider: this.provider },
|
{ logger: this.logger, provider: this.provider },
|
||||||
);
|
);
|
||||||
const connection = new ClientSideConnection(() => this.buildProbeClient(), stream);
|
const connection = new ClientSideConnection(() => this.buildProbeClient(), stream);
|
||||||
const initialize = await Promise.race([
|
|
||||||
connection.initialize({
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
protocolVersion: PROTOCOL_VERSION,
|
const initializeTimeoutPromise = options?.initializeTimeoutMs
|
||||||
clientCapabilities: ACP_CLIENT_CAPABILITIES,
|
? new Promise<never>((_, reject) => {
|
||||||
clientInfo: { name: "Paseo", version: "dev" },
|
timeout = setTimeout(() => {
|
||||||
}),
|
reject(new Error(`ACP initialize timed out after ${options.initializeTimeoutMs}ms`));
|
||||||
spawnErrorPromise,
|
}, options.initializeTimeoutMs);
|
||||||
]);
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let initialize: InitializeResponse;
|
||||||
|
try {
|
||||||
|
initialize = await Promise.race([
|
||||||
|
connection.initialize({
|
||||||
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
|
clientCapabilities: ACP_CLIENT_CAPABILITIES,
|
||||||
|
clientInfo: { name: "Paseo", version: "dev" },
|
||||||
|
}),
|
||||||
|
spawnErrorPromise,
|
||||||
|
...(initializeTimeoutPromise ? [initializeTimeoutPromise] : []),
|
||||||
|
]);
|
||||||
|
} catch (error) {
|
||||||
|
await terminateChildProcess(child, 2_000);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { child, connection, initialize };
|
return { child, connection, initialize };
|
||||||
}
|
}
|
||||||
@@ -782,8 +806,7 @@ export class ACPAgentClient implements AgentClient {
|
|||||||
// No active session to close here; ignore capability.
|
// No active session to close here; ignore capability.
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
probe.child.kill("SIGTERM");
|
await terminateChildProcess(probe.child, 2_000);
|
||||||
await waitForChildExit(probe.child, 2_000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2706,3 +2729,14 @@ async function waitForChildExit(
|
|||||||
child.kill("SIGKILL");
|
child.kill("SIGKILL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function terminateChildProcess(
|
||||||
|
child: ChildProcessWithoutNullStreams,
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<void> {
|
||||||
|
child.kill("SIGTERM");
|
||||||
|
child.stdin.destroy();
|
||||||
|
child.stdout.destroy();
|
||||||
|
child.stderr.destroy();
|
||||||
|
await waitForChildExit(child, timeoutMs);
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
|
import { buildVersionProbeCommand, GenericACPAgentClient } from "./generic-acp-agent.js";
|
||||||
|
import type { SpawnedACPProcess } from "./acp-agent.js";
|
||||||
|
|
||||||
|
describe("GenericACPAgentClient diagnostics", () => {
|
||||||
|
test("probes npx-backed agent packages instead of npx itself", () => {
|
||||||
|
expect(buildVersionProbeCommand(["npx", "-y", "@google/gemini-cli@0.41.1", "--acp"])).toEqual({
|
||||||
|
command: "npx",
|
||||||
|
args: ["-y", "@google/gemini-cli@0.41.1", "--version"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(buildVersionProbeCommand(["pnpm", "dlx", "@agent/foo@1.2.3", "--acp"])).toEqual({
|
||||||
|
command: "pnpm",
|
||||||
|
args: ["dlx", "@agent/foo@1.2.3", "--version"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports command, binary, ACP initialize, session, models, and modes", async () => {
|
||||||
|
class TestGenericACPAgentClient extends GenericACPAgentClient {
|
||||||
|
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
|
||||||
|
return {
|
||||||
|
child: { kill: vi.fn(), exitCode: 0, signalCode: null, once: vi.fn() },
|
||||||
|
initialize: {
|
||||||
|
protocolVersion: 1,
|
||||||
|
agentInfo: { name: "Cursor Agent", version: "2026.05.09" },
|
||||||
|
agentCapabilities: {},
|
||||||
|
},
|
||||||
|
connection: {
|
||||||
|
newSession: vi.fn().mockResolvedValue({
|
||||||
|
sessionId: "session-1",
|
||||||
|
models: {
|
||||||
|
currentModelId: "composer-2[fast=true]",
|
||||||
|
availableModels: [
|
||||||
|
{
|
||||||
|
modelId: "composer-2[fast=true]",
|
||||||
|
name: "Composer 2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
modes: {
|
||||||
|
currentModeId: "ask",
|
||||||
|
availableModes: [
|
||||||
|
{ id: "agent", name: "Agent" },
|
||||||
|
{ id: "ask", name: "Ask" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
configOptions: [],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
} as SpawnedACPProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async closeProbe(): Promise<void> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new TestGenericACPAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
command: [process.execPath, "acp"],
|
||||||
|
providerId: "cursor",
|
||||||
|
label: "Cursor",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { diagnostic } = await client.getDiagnostic();
|
||||||
|
|
||||||
|
expect(diagnostic).toContain("Cursor (ACP)");
|
||||||
|
expect(diagnostic).toContain("Provider ID: cursor");
|
||||||
|
expect(diagnostic).toContain(`Configured command: ${process.execPath} acp`);
|
||||||
|
expect(diagnostic).toContain(`Launcher binary: ${process.execPath}`);
|
||||||
|
expect(diagnostic).toContain(`Version command: ${process.execPath} --version`);
|
||||||
|
expect(diagnostic).toContain("ACP initialize: ok (protocol 1, Cursor Agent 2026.05.09)");
|
||||||
|
expect(diagnostic).toContain("ACP session/new: ok (session-1)");
|
||||||
|
expect(diagnostic).toContain("Models: 1");
|
||||||
|
expect(diagnostic).toContain("Modes: Agent, Ask");
|
||||||
|
expect(diagnostic).toContain("Status: Available");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reports ACP probe failures instead of falling back to no diagnostic", async () => {
|
||||||
|
class FailingGenericACPAgentClient extends GenericACPAgentClient {
|
||||||
|
protected override async spawnProcess(): Promise<SpawnedACPProcess> {
|
||||||
|
throw new Error("initialize timed out");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = new FailingGenericACPAgentClient({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
command: [process.execPath, "acp"],
|
||||||
|
providerId: "cursor",
|
||||||
|
label: "Cursor",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { diagnostic } = await client.getDiagnostic();
|
||||||
|
|
||||||
|
expect(diagnostic).toContain("Cursor (ACP)");
|
||||||
|
expect(diagnostic).toContain("ACP initialize: Error - initialize timed out");
|
||||||
|
expect(diagnostic).toContain("Status: Error (ACP probe failed: initialize timed out)");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,16 +1,32 @@
|
|||||||
|
import { homedir } from "node:os";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
import { isCommandAvailable } from "../../../utils/executable.js";
|
import { findExecutable, isCommandAvailable } from "../../../utils/executable.js";
|
||||||
import { ACPAgentClient } from "./acp-agent.js";
|
import { execCommand } from "../../../utils/spawn.js";
|
||||||
|
import { createProviderEnvSpec } from "../provider-launch-config.js";
|
||||||
|
import { ACPAgentClient, type SessionStateResponse } from "./acp-agent.js";
|
||||||
|
import {
|
||||||
|
formatDiagnosticStatus,
|
||||||
|
formatProviderDiagnostic,
|
||||||
|
formatProviderDiagnosticError,
|
||||||
|
toDiagnosticErrorMessage,
|
||||||
|
} from "./diagnostic-utils.js";
|
||||||
|
|
||||||
|
const ACP_DIAGNOSTIC_INITIALIZE_TIMEOUT_MS = 8_000;
|
||||||
|
const ACP_DIAGNOSTIC_SESSION_TIMEOUT_MS = 8_000;
|
||||||
|
|
||||||
interface GenericACPAgentClientOptions {
|
interface GenericACPAgentClientOptions {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
command: [string, ...string[]];
|
command: [string, ...string[]];
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
|
providerId?: string;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GenericACPAgentClient extends ACPAgentClient {
|
export class GenericACPAgentClient extends ACPAgentClient {
|
||||||
private readonly command: [string, ...string[]];
|
private readonly command: [string, ...string[]];
|
||||||
|
private readonly providerId?: string;
|
||||||
|
private readonly label?: string;
|
||||||
|
|
||||||
constructor(options: GenericACPAgentClientOptions) {
|
constructor(options: GenericACPAgentClientOptions) {
|
||||||
super({
|
super({
|
||||||
@@ -23,6 +39,8 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.command = options.command;
|
this.command = options.command;
|
||||||
|
this.providerId = options.providerId;
|
||||||
|
this.label = options.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> {
|
protected override async resolveLaunchCommand(): Promise<{ command: string; args: string[] }> {
|
||||||
@@ -35,4 +53,247 @@ export class GenericACPAgentClient extends ACPAgentClient {
|
|||||||
override async isAvailable(): Promise<boolean> {
|
override async isAvailable(): Promise<boolean> {
|
||||||
return isCommandAvailable(this.command[0]);
|
return isCommandAvailable(this.command[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getDiagnostic(): Promise<{ diagnostic: string }> {
|
||||||
|
const providerName = formatProviderName(this.label, this.providerId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resolvedBinary = await findExecutable(this.command[0]);
|
||||||
|
const available = resolvedBinary !== null;
|
||||||
|
const versionProbe = buildVersionProbeCommand(this.command);
|
||||||
|
const probeResult = available
|
||||||
|
? await this.runDiagnosticACPProbe()
|
||||||
|
: {
|
||||||
|
status: formatDiagnosticStatus(false),
|
||||||
|
initialize: "Not checked",
|
||||||
|
session: "Not checked",
|
||||||
|
models: "Not checked",
|
||||||
|
modes: "Not checked",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
diagnostic: formatProviderDiagnostic(providerName, [
|
||||||
|
{ label: "Provider ID", value: this.providerId ?? "unknown" },
|
||||||
|
{ label: "Configured command", value: this.command.join(" ") },
|
||||||
|
{ label: "Launcher binary", value: resolvedBinary ?? "not found" },
|
||||||
|
{
|
||||||
|
label: "Version command",
|
||||||
|
value: formatCommand(versionProbe.command, versionProbe.args),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Version",
|
||||||
|
value: resolvedBinary
|
||||||
|
? await resolveCommandVersion(versionProbe, this.runtimeSettings?.env)
|
||||||
|
: "unknown",
|
||||||
|
},
|
||||||
|
{ label: "ACP initialize", value: probeResult.initialize },
|
||||||
|
{ label: "ACP session/new", value: probeResult.session },
|
||||||
|
{ label: "Models", value: probeResult.models },
|
||||||
|
{ label: "Modes", value: probeResult.modes },
|
||||||
|
{ label: "Status", value: probeResult.status },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
diagnostic: formatProviderDiagnosticError(providerName, error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runDiagnosticACPProbe(): Promise<ACPDiagnosticProbeResult> {
|
||||||
|
let initializeValue = "Not checked";
|
||||||
|
let sessionValue = "Not checked";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const probe = await this.spawnProcess(
|
||||||
|
{
|
||||||
|
NO_BROWSER: "true",
|
||||||
|
NO_OPEN_BROWSER: "1",
|
||||||
|
GEMINI_CLI_NO_BROWSER: "true",
|
||||||
|
CI: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
initializeTimeoutMs: ACP_DIAGNOSTIC_INITIALIZE_TIMEOUT_MS,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
initializeValue = formatInitializeResult(probe.initialize);
|
||||||
|
const response = await withTimeout(
|
||||||
|
probe.connection.newSession({
|
||||||
|
cwd: homedir(),
|
||||||
|
mcpServers: [],
|
||||||
|
}),
|
||||||
|
ACP_DIAGNOSTIC_SESSION_TIMEOUT_MS,
|
||||||
|
"ACP session/new",
|
||||||
|
);
|
||||||
|
sessionValue = response.sessionId ? `ok (${response.sessionId})` : "ok";
|
||||||
|
return {
|
||||||
|
status: formatDiagnosticStatus(true),
|
||||||
|
initialize: initializeValue,
|
||||||
|
session: sessionValue,
|
||||||
|
...summarizeSessionState(this.transformSessionResponse(response)),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await this.closeProbe(probe);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: formatDiagnosticStatus(true, {
|
||||||
|
source: "ACP probe",
|
||||||
|
cause: error,
|
||||||
|
}),
|
||||||
|
initialize: formatProbeError(initializeValue, error),
|
||||||
|
session:
|
||||||
|
initializeValue === "Not checked" ? "Not checked" : formatProbeError(sessionValue, error),
|
||||||
|
models: "Not checked",
|
||||||
|
modes: "Not checked",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ACPDiagnosticProbeResult {
|
||||||
|
status: string;
|
||||||
|
initialize: string;
|
||||||
|
session: string;
|
||||||
|
models: string;
|
||||||
|
modes: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommandInvocation {
|
||||||
|
command: string;
|
||||||
|
args: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProviderName(label: string | undefined, providerId: string | undefined): string {
|
||||||
|
if (label) {
|
||||||
|
return `${label} (ACP)`;
|
||||||
|
}
|
||||||
|
if (providerId) {
|
||||||
|
return `${providerId} (ACP)`;
|
||||||
|
}
|
||||||
|
return "Custom ACP";
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCommand(command: string, args: string[]): string {
|
||||||
|
return [command, ...args].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildVersionProbeCommand(command: [string, ...string[]]): CommandInvocation {
|
||||||
|
const [launcher, ...args] = command;
|
||||||
|
if (isPackageRunner(launcher)) {
|
||||||
|
return {
|
||||||
|
command: launcher,
|
||||||
|
args: [...takePackageRunnerPrefix(args), "--version"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
command: launcher,
|
||||||
|
args: ["--version"],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPackageRunner(command: string): boolean {
|
||||||
|
return ["npx", "bunx", "pnpm", "uvx"].includes(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function takePackageRunnerPrefix(args: string[]): string[] {
|
||||||
|
if (args.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (args[0] === "dlx") {
|
||||||
|
return ["dlx", ...takePackageSpecPrefix(args.slice(1))];
|
||||||
|
}
|
||||||
|
return takePackageSpecPrefix(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function takePackageSpecPrefix(args: string[]): string[] {
|
||||||
|
const prefix: string[] = [];
|
||||||
|
for (let index = 0; index < args.length; index += 1) {
|
||||||
|
const arg = args[index];
|
||||||
|
prefix.push(arg);
|
||||||
|
if (arg === "--package" || arg === "-p") {
|
||||||
|
if (args[index + 1]) {
|
||||||
|
prefix.push(args[index + 1]);
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!arg.startsWith("-")) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatInitializeResult(initialize: {
|
||||||
|
protocolVersion: number;
|
||||||
|
agentInfo?: unknown;
|
||||||
|
}): string {
|
||||||
|
const agentInfo = isAgentInfo(initialize.agentInfo)
|
||||||
|
? `${initialize.agentInfo.name}${initialize.agentInfo.version ? ` ${initialize.agentInfo.version}` : ""}`
|
||||||
|
: "ok";
|
||||||
|
return `ok (protocol ${initialize.protocolVersion}, ${agentInfo})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAgentInfo(value: unknown): value is { name: string; version?: string } {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"name" in value &&
|
||||||
|
typeof Reflect.get(value, "name") === "string"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeSessionState(
|
||||||
|
response: SessionStateResponse,
|
||||||
|
): Pick<ACPDiagnosticProbeResult, "models" | "modes"> {
|
||||||
|
const models = response.models?.availableModels ?? [];
|
||||||
|
const modes = response.modes?.availableModes ?? [];
|
||||||
|
return {
|
||||||
|
models: `${models.length}`,
|
||||||
|
modes:
|
||||||
|
modes.length > 0 ? modes.map((mode) => mode.name || mode.id).join(", ") : "none reported",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatProbeError(currentValue: string, error: unknown): string {
|
||||||
|
if (currentValue !== "Not checked") {
|
||||||
|
return currentValue;
|
||||||
|
}
|
||||||
|
return `Error - ${toDiagnosticErrorMessage(error)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCommandVersion(
|
||||||
|
invocation: CommandInvocation,
|
||||||
|
env: Record<string, string> | undefined,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execCommand(invocation.command, invocation.args, {
|
||||||
|
...createProviderEnvSpec({ runtimeSettings: { env } }),
|
||||||
|
timeout: 5_000,
|
||||||
|
});
|
||||||
|
return stdout.trim() || stderr.trim() || "unknown";
|
||||||
|
} catch (error) {
|
||||||
|
return `error: ${toDiagnosticErrorMessage(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
try {
|
||||||
|
return await Promise.race([
|
||||||
|
promise,
|
||||||
|
new Promise<never>((_, reject) => {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
||||||
|
}, timeoutMs);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
} finally {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user