This repository has been archived on 2026-03-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

225 lines
7.9 KiB
JavaScript

#!/usr/bin/env node
import * as acp from "../acp.js";
import { Readable, Writable } from "node:stream";
class ExampleAgent {
connection;
sessions;
constructor(connection) {
this.connection = connection;
this.sessions = new Map();
}
async initialize(_params) {
return {
protocolVersion: acp.PROTOCOL_VERSION,
agentCapabilities: {
loadSession: false,
},
};
}
async newSession(_params) {
const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
this.sessions.set(sessionId, {
pendingPrompt: null,
});
return {
sessionId,
};
}
async authenticate(_params) {
// No auth needed - return empty response
return {};
}
async setSessionMode(_params) {
// Session mode changes not implemented in this example
return {};
}
async prompt(params) {
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session ${params.sessionId} not found`);
}
session.pendingPrompt?.abort();
session.pendingPrompt = new AbortController();
try {
await this.simulateTurn(params.sessionId, session.pendingPrompt.signal);
}
catch (err) {
if (session.pendingPrompt.signal.aborted) {
return { stopReason: "cancelled" };
}
throw err;
}
session.pendingPrompt = null;
return {
stopReason: "end_turn",
};
}
async simulateTurn(sessionId, abortSignal) {
// Send initial text chunk
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: "I'll help you with that. Let me start by reading some files to understand the current situation.",
},
},
});
await this.simulateModelInteraction(abortSignal);
// Send a tool call that doesn't need permission
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: "call_1",
title: "Reading project files",
kind: "read",
status: "pending",
locations: [{ path: "/project/README.md" }],
rawInput: { path: "/project/README.md" },
},
});
await this.simulateModelInteraction(abortSignal);
// Update tool call to completed
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "call_1",
status: "completed",
content: [
{
type: "content",
content: {
type: "text",
text: "# My Project\n\nThis is a sample project...",
},
},
],
rawOutput: { content: "# My Project\n\nThis is a sample project..." },
},
});
await this.simulateModelInteraction(abortSignal);
// Send more text
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: " Now I understand the project structure. I need to make some changes to improve it.",
},
},
});
await this.simulateModelInteraction(abortSignal);
// Send a tool call that DOES need permission
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call",
toolCallId: "call_2",
title: "Modifying critical configuration file",
kind: "edit",
status: "pending",
locations: [{ path: "/project/config.json" }],
rawInput: {
path: "/project/config.json",
content: '{"database": {"host": "new-host"}}',
},
},
});
// Request permission for the sensitive operation
const permissionResponse = await this.connection.requestPermission({
sessionId,
toolCall: {
toolCallId: "call_2",
title: "Modifying critical configuration file",
kind: "edit",
status: "pending",
locations: [{ path: "/home/user/project/config.json" }],
rawInput: {
path: "/home/user/project/config.json",
content: '{"database": {"host": "new-host"}}',
},
},
options: [
{
kind: "allow_once",
name: "Allow this change",
optionId: "allow",
},
{
kind: "reject_once",
name: "Skip this change",
optionId: "reject",
},
],
});
if (permissionResponse.outcome.outcome === "cancelled") {
return;
}
switch (permissionResponse.outcome.optionId) {
case "allow": {
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "tool_call_update",
toolCallId: "call_2",
status: "completed",
rawOutput: { success: true, message: "Configuration updated" },
},
});
await this.simulateModelInteraction(abortSignal);
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: " Perfect! I've successfully updated the configuration. The changes have been applied.",
},
},
});
break;
}
case "reject": {
await this.simulateModelInteraction(abortSignal);
await this.connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: "agent_message_chunk",
content: {
type: "text",
text: " I understand you prefer not to make that change. I'll skip the configuration update.",
},
},
});
break;
}
default:
throw new Error(`Unexpected permission outcome ${permissionResponse.outcome}`);
}
}
simulateModelInteraction(abortSignal) {
return new Promise((resolve, reject) => setTimeout(() => {
// In a real agent, you'd pass this abort signal to the LLM client
if (abortSignal.aborted) {
reject();
}
else {
resolve();
}
}, 1000));
}
async cancel(params) {
this.sessions.get(params.sessionId)?.pendingPrompt?.abort();
}
}
const input = Writable.toWeb(process.stdout);
const output = Readable.toWeb(process.stdin);
const stream = acp.ndJsonStream(input, output);
new acp.AgentSideConnection((conn) => new ExampleAgent(conn), stream);
//# sourceMappingURL=agent.js.map