1 #!/usr/bin/env node
2 // A minimal Agent Client Protocol agent used by the smoke test.
3 // Reads newline-delimited JSON-RPC 2.0 from stdin and writes the same on stdout.
4 // Implements just enough of the protocol to exercise the extension's client:
5 // - initialize → returns protocolVersion + agent capabilities
6 // - session/new → returns a session id
7 // - session/prompt → streams two agent_message_chunk updates, then a
8 // tool_call update, then resolves with stopReason
9 // - session/cancel → no-op (notification)
10 // Also exercises inbound agent→client calls: fs/read_text_file before responding.
11
12 import readline from "node:readline";
13
14 const rl = readline.createInterface({ input: process.stdin });
15
16 let sessionCounter = 0;
17 let nextId = 1;
18 const pending = new Map();
19
20 function send(obj) {
21 process.stdout.write(JSON.stringify(obj) + "\n");
22 }
23
24 function notify(method, params) {
25 send({ jsonrpc: "2.0", method, params });
26 }
27
28 function request(method, params) {
29 const id = nextId++;
30 return new Promise((resolve, reject) => {
31 pending.set(id, { resolve, reject });
32 send({ jsonrpc: "2.0", id, method, params });
33 });
34 }
35
36 async function handlePrompt(params) {
37 const { sessionId, prompt } = params;
38 const userText = (prompt?.[0]?.text ?? "").trim();
39
40 // Demonstrate an inbound agent→client request (fs/read_text_file).
41 try {
42 await request("fs/read_text_file", { path: "package.json" });
43 } catch {
44 // ignore — smoke test cares about the round-trip, not the contents
45 }
46
47 notify("session/update", {
48 sessionId,
49 update: {
50 sessionUpdate: "agent_message_chunk",
51 content: { type: "text", text: `echo: ${userText}` }
52 }
53 });
54
55 notify("session/update", {
56 sessionId,
57 update: {
58 sessionUpdate: "agent_message_chunk",
59 content: { type: "text", text: " (done)" }
60 }
61 });
62
63 notify("session/update", {
64 sessionId,
65 update: {
66 sessionUpdate: "tool_call",
67 toolCallId: "tc_1",
68 title: "noop_tool",
69 status: "completed"
70 }
71 });
72
73 return { stopReason: "end_turn" };
74 }
75
76 const requestHandlers = {
77 initialize: () => ({
78 protocolVersion: 1,
79 agentCapabilities: { promptCapabilities: { embeddedContext: true } }
80 }),
81 "session/new": () => {
82 sessionCounter += 1;
83 return { sessionId: `sess_${sessionCounter}` };
84 },
85 "session/prompt": handlePrompt
86 };
87
88 const notificationHandlers = {
89 "session/cancel": () => {
90 // accepted; no-op for the smoke test
91 }
92 };
93
94 rl.on("line", async (line) => {
95 const text = line.trim();
96 if (!text) {
97 return;
98 }
99 let msg;
100 try {
101 msg = JSON.parse(text);
102 } catch {
103 return;
104 }
105
106 // Response to one of our outbound requests
107 if (msg.id !== undefined && (msg.result !== undefined || msg.error !== undefined)) {
108 const pendingEntry = pending.get(msg.id);
109 if (pendingEntry) {
110 pending.delete(msg.id);
111 if (msg.error) {
112 pendingEntry.reject(new Error(msg.error.message));
113 } else {
114 pendingEntry.resolve(msg.result);
115 }
116 }
117 return;
118 }
119
120 // Inbound request
121 if (msg.id !== undefined && msg.method) {
122 const handler = requestHandlers[msg.method];
123 if (!handler) {
124 send({
125 jsonrpc: "2.0",
126 id: msg.id,
127 error: { code: -32601, message: `Method not found: ${msg.method}` }
128 });
129 return;
130 }
131 try {
132 const result = await handler(msg.params);
133 send({ jsonrpc: "2.0", id: msg.id, result });
134 } catch (err) {
135 send({
136 jsonrpc: "2.0",
137 id: msg.id,
138 error: { code: -32603, message: err.message ?? "Internal error" }
139 });
140 }
141 return;
142 }
143
144 // Inbound notification
145 if (msg.method) {
146 notificationHandlers[msg.method]?.(msg.params);
147 }
148 });