| 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 | }); |