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