teapot-coding-agent 0.23.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/adapter.js +198 -0
- package/dist/agent/agent.js +305 -20
- package/dist/agent/llm.js +33 -0
- package/dist/agent/skills.js +165 -32
- package/dist/agent/tools.js +632 -27
- package/dist/index.js +74 -27
- package/dist/log/events.js +96 -0
- package/dist/master.js +134 -12
- package/dist/model-meta.js +96 -36
- package/dist/server/api.js +42 -9
- package/package.json +1 -1
- package/public/assets/index-ClyCarLt.css +1 -0
- package/public/assets/{index-CNmd6GUS.js → index-sfdzqjpM.js} +20 -20
- package/public/assets/index-sfdzqjpM.js.map +1 -0
- package/public/assets/{shiki-NOpdigkY.js → shiki-D1TWrY4w.js} +2 -2
- package/public/assets/{shiki-NOpdigkY.js.map → shiki-D1TWrY4w.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CNmd6GUS.js.map +0 -1
- package/public/assets/index-QCwmIYsM.css +0 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { readEvents } from "../log/events.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const ACP_PROTOCOL_VERSION = 1;
|
|
4
|
+
/** JSON-RPC error codes we use (the standard set plus JSON-RPC's own). */
|
|
5
|
+
const PARSE_ERROR = -32700;
|
|
6
|
+
const METHOD_NOT_FOUND = -32601;
|
|
7
|
+
const INVALID_PARAMS = -32602;
|
|
8
|
+
const INTERNAL_ERROR = -32603;
|
|
9
|
+
/**
|
|
10
|
+
* Run the ACP adapter until stdin closes.
|
|
11
|
+
*
|
|
12
|
+
* Kept as a class rather than free functions so the session map and the pending
|
|
13
|
+
* request bookkeeping have somewhere to live, and so it is directly testable
|
|
14
|
+
* with an in-memory stream pair.
|
|
15
|
+
*/
|
|
16
|
+
export class AcpAdapter {
|
|
17
|
+
master;
|
|
18
|
+
input;
|
|
19
|
+
output;
|
|
20
|
+
defaultCwd;
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
/** id -> notification params, so a prompt turn can stream updates */
|
|
23
|
+
turnUpdates = new Map();
|
|
24
|
+
buffer = "";
|
|
25
|
+
nextSession = 1;
|
|
26
|
+
initialized = false;
|
|
27
|
+
constructor(opts) {
|
|
28
|
+
this.master = opts.master;
|
|
29
|
+
this.input = opts.input ?? process.stdin;
|
|
30
|
+
this.output = opts.output ?? process.stdout;
|
|
31
|
+
this.defaultCwd = opts.defaultCwd ?? process.cwd();
|
|
32
|
+
}
|
|
33
|
+
/** Attach to the stream. Resolves when stdin ends. */
|
|
34
|
+
listen() {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
this.input.on("data", (chunk) => this.onData(String(chunk)));
|
|
37
|
+
this.input.on("end", () => resolve());
|
|
38
|
+
this.input.on("error", () => resolve());
|
|
39
|
+
if (typeof this.input.resume === "function")
|
|
40
|
+
this.input.resume();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/* ---------- transport ---------- */
|
|
44
|
+
onData(chunk) {
|
|
45
|
+
this.buffer += chunk;
|
|
46
|
+
// NDJSON: one JSON object per line
|
|
47
|
+
let nl;
|
|
48
|
+
while ((nl = this.buffer.indexOf("\n")) >= 0) {
|
|
49
|
+
const line = this.buffer.slice(0, nl).trim();
|
|
50
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
51
|
+
if (line)
|
|
52
|
+
this.handleLine(line);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
handleLine(line) {
|
|
56
|
+
let msg;
|
|
57
|
+
try {
|
|
58
|
+
msg = JSON.parse(line);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// a parse error has no id to answer
|
|
62
|
+
this.send({ jsonrpc: "2.0", id: null, error: { code: PARSE_ERROR, message: "invalid JSON" } });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
void this.dispatch(msg);
|
|
66
|
+
}
|
|
67
|
+
send(res) {
|
|
68
|
+
this.output.write(JSON.stringify(res) + "\n");
|
|
69
|
+
}
|
|
70
|
+
notify(method, params) {
|
|
71
|
+
this.send({ jsonrpc: "2.0", method, params });
|
|
72
|
+
}
|
|
73
|
+
ok(id, result) {
|
|
74
|
+
this.send({ jsonrpc: "2.0", id, result });
|
|
75
|
+
}
|
|
76
|
+
fail(id, code, message) {
|
|
77
|
+
this.send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
78
|
+
}
|
|
79
|
+
/* ---------- dispatch ---------- */
|
|
80
|
+
async dispatch(msg) {
|
|
81
|
+
const { method, id, params } = msg;
|
|
82
|
+
const isNotification = id === undefined || id === null;
|
|
83
|
+
try {
|
|
84
|
+
switch (method) {
|
|
85
|
+
case "initialize":
|
|
86
|
+
return this.ok(id ?? null, this.initialize());
|
|
87
|
+
case "authenticate":
|
|
88
|
+
return this.ok(id ?? null, {});
|
|
89
|
+
case "session/new":
|
|
90
|
+
return this.ok(id ?? null, await this.newSession(params));
|
|
91
|
+
case "session/load":
|
|
92
|
+
return this.ok(id ?? null, await this.loadSession(params));
|
|
93
|
+
case "session/prompt":
|
|
94
|
+
return await this.prompt(id ?? null, params);
|
|
95
|
+
case "session/cancel":
|
|
96
|
+
this.cancel(params);
|
|
97
|
+
if (!isNotification)
|
|
98
|
+
return this.ok(id ?? null, {});
|
|
99
|
+
return;
|
|
100
|
+
default:
|
|
101
|
+
// notifications for unknown methods get NO response at all
|
|
102
|
+
if (isNotification)
|
|
103
|
+
return;
|
|
104
|
+
return this.fail(id ?? null, METHOD_NOT_FOUND, `unknown method: ${method}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (isNotification)
|
|
109
|
+
return; // never answer a notification
|
|
110
|
+
return this.fail(id ?? null, INTERNAL_ERROR, err.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
initialize() {
|
|
114
|
+
this.initialized = true;
|
|
115
|
+
return {
|
|
116
|
+
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
117
|
+
// Only capabilities we actually implement. Terminal/fs are deliberately
|
|
118
|
+
// absent: a client calls what we advertise, and a partial implementation
|
|
119
|
+
// would be worse than an honest omission.
|
|
120
|
+
agentCapabilities: { loadSession: true, promptCapabilities: { image: false, audio: false } },
|
|
121
|
+
authMethods: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/* ---------- sessions ---------- */
|
|
125
|
+
async newSession(params) {
|
|
126
|
+
const p = (params ?? {});
|
|
127
|
+
const cwd = p.cwd ? path.resolve(String(p.cwd)) : this.defaultCwd;
|
|
128
|
+
const id = `acp-${this.nextSession++}`;
|
|
129
|
+
const agent = await this.master.addAgent({ id, workspace: cwd }, { persist: false, fresh: true });
|
|
130
|
+
this.sessions.set(id, { id, cwd, agent });
|
|
131
|
+
return { sessionId: id };
|
|
132
|
+
}
|
|
133
|
+
async loadSession(params) {
|
|
134
|
+
const p = (params ?? {});
|
|
135
|
+
const id = String(p.sessionId ?? "");
|
|
136
|
+
const existing = this.sessions.get(id);
|
|
137
|
+
if (existing)
|
|
138
|
+
return null; // already live
|
|
139
|
+
const cwd = p.cwd ? path.resolve(String(p.cwd)) : this.defaultCwd;
|
|
140
|
+
// resuming means the agent's own log is restored, not a fresh session
|
|
141
|
+
const agent = await this.master.addAgent({ id, workspace: cwd }, { persist: false, fresh: false });
|
|
142
|
+
this.sessions.set(id, { id, cwd, agent });
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
cancel(params) {
|
|
146
|
+
const p = (params ?? {});
|
|
147
|
+
const s = p.sessionId ? this.sessions.get(String(p.sessionId)) : undefined;
|
|
148
|
+
s?.agent.stop("cancelled via ACP");
|
|
149
|
+
}
|
|
150
|
+
/* ---------- prompt turn ---------- */
|
|
151
|
+
async prompt(id, params) {
|
|
152
|
+
const p = (params ?? {});
|
|
153
|
+
const s = p.sessionId ? this.sessions.get(String(p.sessionId)) : undefined;
|
|
154
|
+
if (!s)
|
|
155
|
+
return this.fail(id, INVALID_PARAMS, `unknown session: ${String(p.sessionId)}`);
|
|
156
|
+
const text = (p.prompt ?? [])
|
|
157
|
+
.map((c) => String(c?.text ?? ""))
|
|
158
|
+
.join("\n")
|
|
159
|
+
.trim();
|
|
160
|
+
if (!text)
|
|
161
|
+
return this.fail(id, INVALID_PARAMS, "prompt must contain text");
|
|
162
|
+
this.turnUpdates.set(id, { sessionId: s.id });
|
|
163
|
+
try {
|
|
164
|
+
s.agent.enqueuePrompt(text, "user");
|
|
165
|
+
if (s.agent.status !== "running")
|
|
166
|
+
s.agent.start("acp prompt");
|
|
167
|
+
await s.agent.settled();
|
|
168
|
+
const stopReason = s.agent.goal.status === "done" ? "end_turn" : "end_turn";
|
|
169
|
+
return this.ok(id, { stopReason });
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
this.turnUpdates.delete(id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Stream a session's events to the client as session/update notifications. */
|
|
176
|
+
async drainOnce(sessionId, afterSeq = 0) {
|
|
177
|
+
const s = this.sessions.get(sessionId);
|
|
178
|
+
if (!s)
|
|
179
|
+
return afterSeq;
|
|
180
|
+
const events = await readEvents(s.agent.log.filePath).catch(() => []);
|
|
181
|
+
for (const e of events) {
|
|
182
|
+
if (e.seq <= afterSeq)
|
|
183
|
+
continue;
|
|
184
|
+
this.notify("session/update", {
|
|
185
|
+
sessionId,
|
|
186
|
+
update: {
|
|
187
|
+
sessionUpdate: e.type === "message" ? "agent_message_chunk" : "tool_call",
|
|
188
|
+
content: { type: "text", text: String(e.data?.content ?? "") },
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const max = events.reduce((m, e) => Math.max(m, e.seq), afterSeq);
|
|
193
|
+
return max;
|
|
194
|
+
}
|
|
195
|
+
get initialized_() {
|
|
196
|
+
return this.initialized;
|
|
197
|
+
}
|
|
198
|
+
}
|