Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions packages/acp-ai-provider/examples/check-capabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* Check agent capabilities for multiple ACP agents
*
* Run: deno run -A packages/acp-ai-provider/examples/check-capabilities.ts
*/

import {
ClientSideConnection,
type InitializeResponse,
ndJsonStream,
PROTOCOL_VERSION,
} from "@agentclientprotocol/sdk";
import { type ChildProcess, spawn } from "node:child_process";
import process from "node:process";
import { Readable, Writable } from "node:stream";

interface AgentConfig {
name: string;
command: string;
args: string[];
}

const AGENTS: AgentConfig[] = [
{ name: "Claude Code ACP", command: "claude-code-acp", args: [] },
{ name: "Codex ACP", command: "codex-acp", args: [] },
{ name: "Gemini ACP", command: "gemini", args: ["--experimental-acp"] },
];

// Extended type to include agentInfo which is present in actual responses
interface ExtendedInitializeResponse extends InitializeResponse {
agentInfo?: {
name?: string;
title?: string;
version?: string;
};
}

async function checkAgent(config: AgentConfig): Promise<{
name: string;
success: boolean;
error?: string;
capabilities?: ExtendedInitializeResponse;
}> {
let agentProcess: ChildProcess | null = null;

try {
agentProcess = spawn(config.command, config.args, {
stdio: ["pipe", "pipe", "pipe"],
cwd: process.cwd(),
});

if (!agentProcess.stdin || !agentProcess.stdout) {
throw new Error("Failed to spawn process with stdio");
}

const input = Writable.toWeb(agentProcess.stdin) as WritableStream<
Uint8Array
>;
const output = Readable.toWeb(agentProcess.stdout) as ReadableStream<
Uint8Array
>;

const connection = new ClientSideConnection(
() => ({
sessionUpdate: () => Promise.resolve(),
requestPermission: () =>
Promise.resolve({
outcome: { outcome: "selected", optionId: "allow" },
}),
writeTextFile: () => {
throw new Error("Not implemented");
},
readTextFile: () => {
throw new Error("Not implemented");
},
}),
ndJsonStream(input, output),
);

// Add timeout
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout after 10s")), 10000)
);

const initResult = await Promise.race([
connection.initialize({
protocolVersion: PROTOCOL_VERSION,
clientCapabilities: {
fs: { readTextFile: false, writeTextFile: false },
terminal: false,
},
}),
timeoutPromise,
]);

return {
name: config.name,
success: true,
capabilities: initResult,
};
} catch (err) {
return {
name: config.name,
success: false,
error: err instanceof Error ? err.message : String(err),
};
} finally {
if (agentProcess) {
agentProcess.kill();
}
}
}

function formatBoolean(value: boolean | undefined): string {
if (value === true) return "✅";
if (value === false) return "❌";
return "—";
}

async function main() {
console.log("=== ACP Agent Capabilities Check ===\n");
console.log("Checking agents...\n");

const results = await Promise.all(AGENTS.map(checkAgent));

// Build table data
const tableData: Record<string, Record<string, string>> = {};

for (const result of results) {
if (!result.success) {
tableData[result.name] = {
Status: `❌ ${result.error}`,
Version: "—",
loadSession: "—",
Image: "—",
Audio: "—",
EmbeddedContext: "—",
"MCP HTTP": "—",
"MCP SSE": "—",
};
continue;
}

const caps = result.capabilities!;
const agentCaps = caps.agentCapabilities;
const promptCaps = agentCaps?.promptCapabilities;
const mcpCaps = agentCaps?.mcpCapabilities;

tableData[result.name] = {
Status: "✅ OK",
Version: caps.agentInfo?.version ?? "—",
loadSession: formatBoolean(agentCaps?.loadSession),
Image: formatBoolean(promptCaps?.image),
Audio: formatBoolean(promptCaps?.audio),
EmbeddedContext: formatBoolean(promptCaps?.embeddedContext),
"MCP HTTP": formatBoolean(mcpCaps?.http),
"MCP SSE": formatBoolean(mcpCaps?.sse),
};
}

console.log("=== Agent Capabilities ===\n");
console.table(tableData);
}

main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});
156 changes: 156 additions & 0 deletions packages/acp-ai-provider/examples/load-session-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/**
* Test script to understand loadSession behavior
*
* This script tests how loadSession replays message history via sessionUpdate notifications.
*
* The ACP protocol specifies that when loadSession is called, the agent should:
* 1. Restore the session context and conversation history
* 2. Connect to the specified MCP servers
* 3. Stream the entire conversation history back to the client via notifications
*
* Run: deno run -A packages/acp-ai-provider/examples/load-session-test.ts
*/

import { createACPProvider } from "../src/provider.ts";
import { streamText } from "ai";
import process from "node:process";

async function main() {
console.log("=== Load Session History Replay Test ===\n");
console.log(
"This test demonstrates how loadSession captures replayed history.\n",
);

// Step 1: Create a new session and have a conversation
console.log("=== Step 1: Creating new session ===\n");

const provider1 = createACPProvider({
command: "claude-code-acp",
args: [],
session: {
cwd: process.cwd(),
mcpServers: [],
},
persistSession: true,
});

const session1 = await provider1.initSession();
console.log(`Session created: ${session1.sessionId}\n`);

// Step 2: Send a message
console.log("=== Step 2: Sending first message ===\n");
console.log('User: "My name is TestUser. Just say Hello TestUser."\n');

const { textStream: stream1 } = streamText({
model: provider1.languageModel(),
prompt: "My name is TestUser. Just say 'Hello TestUser' and nothing else.",
tools: provider1.tools,
});

console.log("Assistant: ");
for await (const chunk of stream1) {
process.stdout.write(chunk);
}
console.log("\n");

// Get the session ID for later
const sessionId = provider1.getSessionId();
console.log(`Session ID to resume: ${sessionId}\n`);

// Cleanup first provider
provider1.cleanup();
await new Promise((resolve) => setTimeout(resolve, 1000));

// Step 3: Create a new provider with existingSessionId to load the session
console.log("=== Step 3: Loading session with new provider ===\n");
console.log("Creating new provider with existingSessionId...\n");

const provider2 = createACPProvider({
command: "claude-code-acp",
args: [],
session: {
cwd: process.cwd(),
mcpServers: [],
},
existingSessionId: sessionId!,
persistSession: true,
});

// Initialize the session - this triggers loadSession
await provider2.initSession();

// Check what history was replayed
const replayedHistory = provider2.getReplayedHistory();
console.log(
`Collected ${replayedHistory.length} sessionUpdate notifications during loadSession.\n`,
);

if (replayedHistory.length > 0) {
console.log("History replay detected! Notification types received:");
const typeCounts: Record<string, number> = {};
for (const notification of replayedHistory) {
const type = notification.update.sessionUpdate;
typeCounts[type] = (typeCounts[type] || 0) + 1;
}
for (const [type, count] of Object.entries(typeCounts)) {
console.log(` - ${type}: ${count}`);
}

// Convert to AI SDK messages
const messages = provider2.getReplayedHistoryAsMessages();
console.log(`\nConverted to ${messages.length} AI SDK messages:`);
for (const msg of messages) {
console.log(` - role: ${msg.role}`);
if (typeof msg.content === "string") {
console.log(` content: "${msg.content.slice(0, 100)}..."`);
} else if (Array.isArray(msg.content)) {
console.log(` content: [${msg.content.length} parts]`);
}
}
} else {
console.log(
"No history was replayed via sessionUpdate notifications during loadSession.",
);
console.log(
"This means the agent handles history internally without notifying the client.",
);
console.log(
"The isFreshSession=false approach is correct - only send new user messages.",
);
}

// Step 4: Send another message to verify context is preserved
console.log("\n=== Step 4: Testing context preservation ===\n");
console.log('User: "What is my name?"\n');

const { textStream: stream2 } = streamText({
model: provider2.languageModel(),
prompt: "What is my name? Just say the name.",
tools: provider2.tools,
});

console.log("Assistant: ");
for await (const chunk of stream2) {
process.stdout.write(chunk);
}
console.log("\n");

// Cleanup
provider2.cleanup();

console.log("=== Test Complete ===\n");
console.log("Summary:");
console.log("- If history was replayed during loadSession, the client can");
console.log(
" use getReplayedHistory() or getReplayedHistoryAsMessages() to access it.",
);
console.log(
"- If no history was replayed, the agent handles history internally",
);
console.log(" and the current isFreshSession=false approach is correct.");
}

main().catch((err) => {
console.error("Error:", err);
process.exit(1);
});
Loading