225 lines
7.9 KiB
JavaScript
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
|