copperhead 0.7.0 → 0.8.0
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/README.md +36 -4
- package/dist/agent/animate.js +76 -0
- package/dist/agent/animate.js.map +1 -0
- package/dist/agent/box.js +89 -0
- package/dist/agent/box.js.map +1 -0
- package/dist/agent/dock-renderer.js +173 -0
- package/dist/agent/dock-renderer.js.map +1 -0
- package/dist/agent/logo.js +21 -0
- package/dist/agent/logo.js.map +1 -0
- package/dist/agent/loop.js +15 -5
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/providers/claude-code.js +1 -212
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/cursor.js +317 -0
- package/dist/agent/providers/cursor.js.map +1 -0
- package/dist/agent/providers/tool-protocol.js +205 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -0
- package/dist/agent/render.js +32 -15
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/runmeta.js +4 -5
- package/dist/agent/runmeta.js.map +1 -1
- package/dist/agent/theme.js +84 -0
- package/dist/agent/theme.js.map +1 -0
- package/dist/cli.js +134 -13
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.js +41 -32
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/demo.js +146 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/doctor.js +240 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/repl-inspect.js +342 -0
- package/dist/commands/repl-inspect.js.map +1 -0
- package/dist/commands/repl.js +618 -0
- package/dist/commands/repl.js.map +1 -0
- package/dist/config.js +5 -2
- package/dist/config.js.map +1 -1
- package/dist/kicad/cli.js +126 -6
- package/dist/kicad/cli.js.map +1 -1
- package/dist/util/cli-args.js +35 -0
- package/dist/util/cli-args.js.map +1 -0
- package/dist/util/dock.js +155 -0
- package/dist/util/dock.js.map +1 -0
- package/dist/util/git.js +129 -4
- package/dist/util/git.js.map +1 -1
- package/dist/util/live-prompt.js +542 -0
- package/dist/util/live-prompt.js.map +1 -0
- package/dist/util/paths.js +9 -0
- package/dist/util/paths.js.map +1 -1
- package/dist/util/select.js +172 -0
- package/dist/util/select.js.map +1 -0
- package/package.json +3 -2
- package/src/agent/animate.ts +90 -0
- package/src/agent/box.ts +99 -0
- package/src/agent/dock-renderer.ts +181 -0
- package/src/agent/logo.ts +23 -0
- package/src/agent/loop.ts +15 -5
- package/src/agent/providers/claude-code.ts +2 -216
- package/src/agent/providers/cursor.ts +364 -0
- package/src/agent/providers/tool-protocol.ts +212 -0
- package/src/agent/render.ts +33 -16
- package/src/agent/runmeta.ts +6 -7
- package/src/agent/theme.ts +91 -0
- package/src/cli.ts +139 -15
- package/src/commands/create.ts +81 -30
- package/src/commands/demo.ts +184 -0
- package/src/commands/doctor.ts +289 -0
- package/src/commands/repl-inspect.ts +353 -0
- package/src/commands/repl.ts +685 -0
- package/src/config.ts +6 -3
- package/src/kicad/cli.ts +132 -7
- package/src/layout/claude-ui-layout.md +72 -0
- package/src/layout/repl-ui-layout.md +139 -0
- package/src/util/cli-args.ts +42 -0
- package/src/util/dock.ts +161 -0
- package/src/util/git.ts +140 -4
- package/src/util/live-prompt.ts +595 -0
- package/src/util/paths.ts +10 -0
- package/src/util/select.ts +192 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { mkdtemp, rm, utimes } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { parseToolCalls, renderConversation, renderDelta, renderToolProtocol } from './tool-protocol.js';
|
|
6
|
+
const NATIVE_MUTATION_TYPES = new Set([
|
|
7
|
+
'tool_call',
|
|
8
|
+
'tool_use',
|
|
9
|
+
'tool-call',
|
|
10
|
+
'shell',
|
|
11
|
+
'write',
|
|
12
|
+
'edit',
|
|
13
|
+
'apply_patch',
|
|
14
|
+
'file_change',
|
|
15
|
+
'mcp_tool',
|
|
16
|
+
]);
|
|
17
|
+
/** Subtype tokens that indicate native execution (whole-token match). */
|
|
18
|
+
const NATIVE_SUBTYPE_RE = /(^|_)(tool|shell|write|edit|patch|mutation)(_|$)/;
|
|
19
|
+
export class CursorProvider {
|
|
20
|
+
model;
|
|
21
|
+
runFn;
|
|
22
|
+
sessionResume;
|
|
23
|
+
name = 'cursor';
|
|
24
|
+
callSeq = 0;
|
|
25
|
+
cwdPromise;
|
|
26
|
+
sessionId;
|
|
27
|
+
sentCount = 0;
|
|
28
|
+
inFlight = new Set();
|
|
29
|
+
constructor(model, runFn = defaultCursorRun,
|
|
30
|
+
/**
|
|
31
|
+
* Opt-in: resume one CLI session across turns and send only new messages.
|
|
32
|
+
* OFF by default and mutually exclusive with the response cache — mixing
|
|
33
|
+
* them desyncs the resumed session. `makeProvider` enables it only when
|
|
34
|
+
* the cache is off.
|
|
35
|
+
*/
|
|
36
|
+
sessionResume = false) {
|
|
37
|
+
this.model = model;
|
|
38
|
+
this.runFn = runFn;
|
|
39
|
+
this.sessionResume = sessionResume;
|
|
40
|
+
}
|
|
41
|
+
async chat(messages, tools, opts = {}) {
|
|
42
|
+
const system = messages
|
|
43
|
+
.filter((m) => m.role === 'system')
|
|
44
|
+
.map((m) => m.content)
|
|
45
|
+
.join('\n\n');
|
|
46
|
+
const systemPrompt = [system, renderToolProtocol(tools)].filter(Boolean).join('\n\n');
|
|
47
|
+
const resume = this.sessionResume ? this.sessionId : undefined;
|
|
48
|
+
const prompt = resume ? renderDelta(messages, this.sentCount) : renderConversation(messages);
|
|
49
|
+
const catalog = new Set(tools.map((t) => t.name));
|
|
50
|
+
const workspace = await this.ensureWorkspace();
|
|
51
|
+
const aborter = new AbortController();
|
|
52
|
+
this.inFlight.add(aborter);
|
|
53
|
+
let inputTokens = 0;
|
|
54
|
+
let outputTokens = 0;
|
|
55
|
+
let text = null;
|
|
56
|
+
try {
|
|
57
|
+
const result = await this.runFn({
|
|
58
|
+
prompt,
|
|
59
|
+
systemPrompt,
|
|
60
|
+
workspace,
|
|
61
|
+
...(this.model ? { model: this.model } : {}),
|
|
62
|
+
...(resume ? { resume } : {}),
|
|
63
|
+
signal: aborter.signal,
|
|
64
|
+
env: subprocessEnv(),
|
|
65
|
+
});
|
|
66
|
+
text = result.text;
|
|
67
|
+
if (this.sessionResume && result.sessionId)
|
|
68
|
+
this.sessionId = result.sessionId;
|
|
69
|
+
inputTokens = result.usage.inputTokens;
|
|
70
|
+
outputTokens = result.usage.outputTokens;
|
|
71
|
+
opts.onStream?.(text.length);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
if (isAuthError(err))
|
|
75
|
+
throw new Error(authHint(err.message));
|
|
76
|
+
throw enhanceCliError(err);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
this.inFlight.delete(aborter);
|
|
80
|
+
}
|
|
81
|
+
// Only advance the high-water mark when resume is on (same as claude-code).
|
|
82
|
+
if (this.sessionResume)
|
|
83
|
+
this.sentCount = messages.length;
|
|
84
|
+
const parsed = parseToolCalls(text, () => `cur-${++this.callSeq}`, catalog);
|
|
85
|
+
return {
|
|
86
|
+
text: parsed.text,
|
|
87
|
+
toolCalls: parsed.toolCalls,
|
|
88
|
+
usage: { inputTokens, outputTokens },
|
|
89
|
+
nudge: parsed.nudge,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async close() {
|
|
93
|
+
for (const aborter of this.inFlight) {
|
|
94
|
+
try {
|
|
95
|
+
aborter.abort();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// best effort
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
this.inFlight.clear();
|
|
102
|
+
const pending = this.cwdPromise;
|
|
103
|
+
this.cwdPromise = undefined;
|
|
104
|
+
if (!pending)
|
|
105
|
+
return;
|
|
106
|
+
try {
|
|
107
|
+
await rm(await pending, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// best effort
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async ensureWorkspace() {
|
|
114
|
+
if (!this.cwdPromise)
|
|
115
|
+
this.cwdPromise = mkdtemp(path.join(os.tmpdir(), 'copperhead-cursor-'));
|
|
116
|
+
const cwd = await this.cwdPromise;
|
|
117
|
+
const now = new Date();
|
|
118
|
+
await utimes(cwd, now, now).catch(() => { });
|
|
119
|
+
return cwd;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Minimal env passed to the Cursor CLI subprocess (saved login via `agent login`). */
|
|
123
|
+
const CURSOR_SUBPROCESS_ENV_KEYS = [
|
|
124
|
+
'PATH',
|
|
125
|
+
'HOME',
|
|
126
|
+
'USER',
|
|
127
|
+
'LOGNAME',
|
|
128
|
+
'SHELL',
|
|
129
|
+
'TMPDIR',
|
|
130
|
+
'TEMP',
|
|
131
|
+
'TMP',
|
|
132
|
+
'LANG',
|
|
133
|
+
'LC_ALL',
|
|
134
|
+
'LC_CTYPE',
|
|
135
|
+
'LC_MESSAGES',
|
|
136
|
+
'TERM',
|
|
137
|
+
'XDG_CONFIG_HOME',
|
|
138
|
+
'XDG_DATA_HOME',
|
|
139
|
+
'XDG_CACHE_HOME',
|
|
140
|
+
'XDG_RUNTIME_DIR',
|
|
141
|
+
// Windows home / login-config location (`USERPROFILE\.cursor\cli-config.json`)
|
|
142
|
+
'USERPROFILE',
|
|
143
|
+
'HOMEDRIVE',
|
|
144
|
+
'HOMEPATH',
|
|
145
|
+
'SystemRoot',
|
|
146
|
+
'ComSpec',
|
|
147
|
+
'APPDATA',
|
|
148
|
+
'LOCALAPPDATA',
|
|
149
|
+
];
|
|
150
|
+
/** Build an allowlisted env for the Cursor Agent subprocess (no API keys or unrelated secrets). */
|
|
151
|
+
export function subprocessEnv() {
|
|
152
|
+
const env = {};
|
|
153
|
+
for (const key of CURSOR_SUBPROCESS_ENV_KEYS) {
|
|
154
|
+
const value = process.env[key];
|
|
155
|
+
if (value !== undefined)
|
|
156
|
+
env[key] = value;
|
|
157
|
+
}
|
|
158
|
+
return env;
|
|
159
|
+
}
|
|
160
|
+
/** Parse `--print --output-format json` stdout into assistant text and session id. */
|
|
161
|
+
export function parseCursorStdout(stdout) {
|
|
162
|
+
const trimmed = stdout.trim();
|
|
163
|
+
let text = '';
|
|
164
|
+
let sessionId;
|
|
165
|
+
let sawResult = false;
|
|
166
|
+
// Prefer a single pretty-printed JSON object (whole buffer) before NDJSON lines.
|
|
167
|
+
if (trimmed) {
|
|
168
|
+
try {
|
|
169
|
+
const whole = JSON.parse(trimmed);
|
|
170
|
+
const extracted = extractResultFields(whole);
|
|
171
|
+
if (extracted) {
|
|
172
|
+
return {
|
|
173
|
+
text: extracted.text,
|
|
174
|
+
sessionId: extracted.sessionId,
|
|
175
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
if (isCursorHardFail(err))
|
|
181
|
+
throw err;
|
|
182
|
+
// fall through to line-based parse
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const lines = trimmed
|
|
186
|
+
.split('\n')
|
|
187
|
+
.map((l) => l.trim())
|
|
188
|
+
.filter(Boolean);
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
let obj;
|
|
191
|
+
try {
|
|
192
|
+
obj = JSON.parse(line);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const extracted = extractResultFields(obj);
|
|
198
|
+
if (extracted) {
|
|
199
|
+
text = extracted.text;
|
|
200
|
+
sessionId = extracted.sessionId ?? sessionId;
|
|
201
|
+
sawResult = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!sawResult && !text && lines.length) {
|
|
205
|
+
// Fallback: last parseable JSON line with a string result field
|
|
206
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
207
|
+
try {
|
|
208
|
+
const obj = JSON.parse(lines[i]);
|
|
209
|
+
if (typeof obj.result === 'string') {
|
|
210
|
+
assertNoNativeMutation(obj);
|
|
211
|
+
text = obj.result;
|
|
212
|
+
if (typeof obj.session_id === 'string')
|
|
213
|
+
sessionId = obj.session_id;
|
|
214
|
+
sawResult = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
if (isCursorHardFail(err))
|
|
220
|
+
throw err;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!sawResult && trimmed) {
|
|
226
|
+
throw new Error(`cursor: could not parse Cursor Agent output as JSON — raw stdout: ${trimmed.slice(0, 500)}`);
|
|
227
|
+
}
|
|
228
|
+
// Official Cursor JSON schema does not expose token usage; callers see zeros.
|
|
229
|
+
return { text, sessionId, usage: { inputTokens: 0, outputTokens: 0 } };
|
|
230
|
+
}
|
|
231
|
+
function extractResultFields(obj) {
|
|
232
|
+
assertNoNativeMutation(obj);
|
|
233
|
+
const type = typeof obj.type === 'string' ? obj.type.toLowerCase() : '';
|
|
234
|
+
if (type === 'result' || typeof obj.result === 'string') {
|
|
235
|
+
if (obj.is_error === true) {
|
|
236
|
+
throw new Error(typeof obj.result === 'string' ? obj.result : 'Cursor Agent returned an error result');
|
|
237
|
+
}
|
|
238
|
+
if (typeof obj.result === 'string') {
|
|
239
|
+
return {
|
|
240
|
+
text: obj.result,
|
|
241
|
+
...(typeof obj.session_id === 'string' ? { sessionId: obj.session_id } : {}),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
function isCursorHardFail(err) {
|
|
248
|
+
return (err instanceof Error &&
|
|
249
|
+
(err.message.includes('reasoning-only invariant') ||
|
|
250
|
+
err.message.includes('Cursor Agent returned an error') ||
|
|
251
|
+
err.message.startsWith('cursor:')));
|
|
252
|
+
}
|
|
253
|
+
function assertNoNativeMutation(obj) {
|
|
254
|
+
const type = typeof obj.type === 'string' ? obj.type.toLowerCase() : '';
|
|
255
|
+
if (NATIVE_MUTATION_TYPES.has(type)) {
|
|
256
|
+
throw new Error(`cursor: Cursor Agent emitted native tool event "${obj.type}" — reasoning-only invariant violated. Refusing to continue.`);
|
|
257
|
+
}
|
|
258
|
+
const subtype = typeof obj.subtype === 'string' ? obj.subtype.toLowerCase() : '';
|
|
259
|
+
if (subtype && NATIVE_SUBTYPE_RE.test(subtype) && type !== 'result') {
|
|
260
|
+
throw new Error(`cursor: Cursor Agent output subtype "${obj.subtype}" (line type "${obj.type ?? ''}") suggests native execution — reasoning-only invariant violated.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** Default subprocess runner: invokes `agent` (or `COPPERHEAD_CURSOR_PATH`) in plan mode. */
|
|
264
|
+
export async function defaultCursorRun(args) {
|
|
265
|
+
const bin = process.env.COPPERHEAD_CURSOR_PATH || 'agent';
|
|
266
|
+
const fullPrompt = [args.systemPrompt, args.prompt].filter(Boolean).join('\n\n---\n\n');
|
|
267
|
+
// Prompt goes on stdin — a single argv element caps at ~128 KiB on Linux
|
|
268
|
+
// (MAX_ARG_STRLEN); real .kicad_pcb reads routinely exceed that.
|
|
269
|
+
const cmdArgs = [
|
|
270
|
+
'--print',
|
|
271
|
+
'--output-format',
|
|
272
|
+
'json',
|
|
273
|
+
'--mode',
|
|
274
|
+
'plan',
|
|
275
|
+
'--trust',
|
|
276
|
+
'--sandbox',
|
|
277
|
+
'enabled',
|
|
278
|
+
'--workspace',
|
|
279
|
+
args.workspace,
|
|
280
|
+
];
|
|
281
|
+
if (args.model)
|
|
282
|
+
cmdArgs.push('--model', args.model);
|
|
283
|
+
if (args.resume)
|
|
284
|
+
cmdArgs.push('--resume', args.resume);
|
|
285
|
+
const { stdout } = await execa(bin, cmdArgs, {
|
|
286
|
+
input: fullPrompt,
|
|
287
|
+
env: args.env ?? subprocessEnv(),
|
|
288
|
+
cancelSignal: args.signal,
|
|
289
|
+
reject: true,
|
|
290
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
291
|
+
});
|
|
292
|
+
return parseCursorStdout(stdout);
|
|
293
|
+
}
|
|
294
|
+
function isAuthError(err) {
|
|
295
|
+
// Subprocess failures use exitCode, not HTTP status. Only treat real HTTP
|
|
296
|
+
// status fields as 401/403; otherwise match the CLI's auth message.
|
|
297
|
+
const status = err?.status ??
|
|
298
|
+
err?.statusCode;
|
|
299
|
+
if (status === 401 || status === 403)
|
|
300
|
+
return true;
|
|
301
|
+
const m = (err?.message ?? '').toLowerCase();
|
|
302
|
+
return /unauthenticat|unauthoriz|not logged in|please log in|login required|agent login/.test(m);
|
|
303
|
+
}
|
|
304
|
+
function authHint(detail) {
|
|
305
|
+
return ('cursor is not authenticated: run `agent login` and verify with `agent status`. ' +
|
|
306
|
+
`Set COPPERHEAD_CURSOR_PATH if the CLI is not on PATH (original error: ${detail})`);
|
|
307
|
+
}
|
|
308
|
+
function enhanceCliError(err) {
|
|
309
|
+
const original = err;
|
|
310
|
+
if (original.code === 'ENOENT') {
|
|
311
|
+
return new Error('Cursor Agent CLI not found on PATH. Install Cursor Agent or set COPPERHEAD_CURSOR_PATH to the `agent` binary.', { cause: err });
|
|
312
|
+
}
|
|
313
|
+
if (original.message?.includes('reasoning-only invariant'))
|
|
314
|
+
return original;
|
|
315
|
+
return new Error(`Cursor CLI provider failed: ${original.message}`, { cause: err });
|
|
316
|
+
}
|
|
317
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../../src/agent/providers/cursor.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export function renderToolProtocol(tools) {
|
|
2
|
+
if (!tools.length)
|
|
3
|
+
return '';
|
|
4
|
+
const lines = [
|
|
5
|
+
'# Tool protocol',
|
|
6
|
+
'',
|
|
7
|
+
'You are the reasoning half of a tool-driven workflow; you cannot run anything yourself.',
|
|
8
|
+
'To take an action, reply with EXACTLY ONE JSON object and nothing else, wrapped in a',
|
|
9
|
+
'```json fenced code block:',
|
|
10
|
+
'',
|
|
11
|
+
'```json',
|
|
12
|
+
'{"tool": "<tool_name>", "args": { ... }}',
|
|
13
|
+
'```',
|
|
14
|
+
'',
|
|
15
|
+
'Use only the tools listed below, with `args` matching the tool\'s JSON Schema. If you have',
|
|
16
|
+
'no tool to call and only want to say something, reply with plain prose and no JSON block.',
|
|
17
|
+
'',
|
|
18
|
+
'## Available tools',
|
|
19
|
+
];
|
|
20
|
+
for (const t of tools) {
|
|
21
|
+
lines.push('', `### ${t.name}`, t.description, `Parameters (JSON Schema): ${JSON.stringify(t.parameters)}`);
|
|
22
|
+
}
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
/** Delta prompt for a resumed CLI session: new user lines and tool results only. */
|
|
26
|
+
export function renderDelta(messages, from) {
|
|
27
|
+
const idToName = new Map();
|
|
28
|
+
for (const m of messages) {
|
|
29
|
+
if (m.role === 'assistant')
|
|
30
|
+
for (const call of m.toolCalls ?? [])
|
|
31
|
+
idToName.set(call.id, call.name);
|
|
32
|
+
}
|
|
33
|
+
const parts = [];
|
|
34
|
+
for (const m of messages.slice(Math.max(0, from))) {
|
|
35
|
+
if (m.role === 'user') {
|
|
36
|
+
parts.push(`[user]\n${m.content}`);
|
|
37
|
+
}
|
|
38
|
+
else if (m.role === 'tool') {
|
|
39
|
+
const name = idToName.get(m.toolCallId) ?? m.toolCallId;
|
|
40
|
+
parts.push(`[result of ${name}]\n${m.content}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parts.join('\n\n');
|
|
44
|
+
}
|
|
45
|
+
export function renderConversation(messages) {
|
|
46
|
+
const idToName = new Map();
|
|
47
|
+
const parts = [];
|
|
48
|
+
for (const m of messages) {
|
|
49
|
+
if (m.role === 'system')
|
|
50
|
+
continue;
|
|
51
|
+
if (m.role === 'user') {
|
|
52
|
+
parts.push(`[user]\n${m.content}`);
|
|
53
|
+
}
|
|
54
|
+
else if (m.role === 'assistant') {
|
|
55
|
+
if (m.content)
|
|
56
|
+
parts.push(`[assistant]\n${m.content}`);
|
|
57
|
+
for (const call of m.toolCalls ?? []) {
|
|
58
|
+
idToName.set(call.id, call.name);
|
|
59
|
+
parts.push(`[assistant tool call]\n\`\`\`json\n${JSON.stringify({ tool: call.name, args: call.args })}\n\`\`\``);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const name = idToName.get(m.toolCallId) ?? m.toolCallId;
|
|
64
|
+
parts.push(`[result of ${name}]\n${m.content}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return parts.join('\n\n');
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Detect a malformed-but-intended tool call in a turn that dispatched none
|
|
71
|
+
* (#I10). The signature is machine-recognizable: the text contains
|
|
72
|
+
* `"tool":"<name>"` naming a tool in the current catalog, yet nothing parsed.
|
|
73
|
+
* That is the exact case where the tolerant extractor's silence misleads the
|
|
74
|
+
* model — the JSON was near-miss malformed (a brace short, or the outer object
|
|
75
|
+
* split so only an inner `{args}` with no `tool` key balanced), not the tool
|
|
76
|
+
* being broken. Returns a one-line steer to re-emit it, or undefined when the
|
|
77
|
+
* absence of a call is genuine (plain prose, no tool named).
|
|
78
|
+
*/
|
|
79
|
+
function detectMalformedCall(text, catalog) {
|
|
80
|
+
const re = /"tool"\s*:\s*"([^"]+)"/g;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = re.exec(text)) !== null) {
|
|
83
|
+
const name = m[1];
|
|
84
|
+
if (catalog.has(name)) {
|
|
85
|
+
return (`A tool call for "${name}" looks malformed — it named the tool but did not parse as ` +
|
|
86
|
+
'valid JSON (likely unbalanced braces or a missing closing brace), so no call ran. ' +
|
|
87
|
+
'Re-emit it as exactly one complete JSON object: {"tool": "...", "args": { ... }}.');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract tool-call JSON from the model's reply. Tolerant by design (D1):
|
|
94
|
+
* unparseable output is returned as plain text with no tool calls rather than
|
|
95
|
+
* throwing, so a non-conforming turn degrades to the loop's stall/nudge path.
|
|
96
|
+
* A parsed block only counts as a tool call when its name is in the current
|
|
97
|
+
* turn's catalog (`availableTools(ctx)`): a hallucinated or locked tool name is
|
|
98
|
+
* left as prose so the loop nudges, rather than dispatching a bogus call.
|
|
99
|
+
*/
|
|
100
|
+
export function parseToolCalls(text, nextId, catalog) {
|
|
101
|
+
if (!text)
|
|
102
|
+
return { text: null, toolCalls: [] };
|
|
103
|
+
const toolCalls = [];
|
|
104
|
+
const matched = [];
|
|
105
|
+
// Extract tool calls by scanning for complete JSON objects, NOT by matching
|
|
106
|
+
// ``` fences. A tool call's `content`/`args` can hold a full markdown doc that
|
|
107
|
+
// itself contains ``` code fences; a fence regex truncates the JSON at the
|
|
108
|
+
// first inner fence, JSON.parse fails, and the call is silently dropped (the
|
|
109
|
+
// model then assumes it wrote a file it never did). The brace scan is
|
|
110
|
+
// string-aware, so braces and backticks inside JSON string values are ignored.
|
|
111
|
+
let searchFrom = 0;
|
|
112
|
+
while (searchFrom < text.length) {
|
|
113
|
+
const braceAt = text.indexOf('{', searchFrom);
|
|
114
|
+
if (braceAt < 0)
|
|
115
|
+
break;
|
|
116
|
+
const span = scanJsonObject(text, braceAt);
|
|
117
|
+
if (!span) {
|
|
118
|
+
// Unbalanced '{' (stray brace in prose): retry from the next candidate so
|
|
119
|
+
// one bad brace can't hide a well-formed call later in the reply.
|
|
120
|
+
searchFrom = braceAt + 1;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const call = toToolCall(text.slice(span.start, span.end), nextId, catalog);
|
|
124
|
+
if (call) {
|
|
125
|
+
toolCalls.push(call);
|
|
126
|
+
matched.push([span.start, span.end]);
|
|
127
|
+
}
|
|
128
|
+
searchFrom = span.end;
|
|
129
|
+
}
|
|
130
|
+
if (!toolCalls.length) {
|
|
131
|
+
// No call dispatched — but did the model clearly *intend* one? A fenced
|
|
132
|
+
// ```json block that names a catalog tool yet produced zero calls is a
|
|
133
|
+
// malformed near-miss (unbalanced braces, a missing `}`, or an inner object
|
|
134
|
+
// with no `tool` key). Silently dropping it gives the model no signal, so it
|
|
135
|
+
// misreads "no result" as "this tool is broken" and can bake that false
|
|
136
|
+
// conclusion into a committed summary (#I10). Surface a nudge instead.
|
|
137
|
+
return { text: text.trim() ? text : null, toolCalls, nudge: detectMalformedCall(text, catalog) };
|
|
138
|
+
}
|
|
139
|
+
// Prose is whatever survives once the tool-call objects (and any now-empty
|
|
140
|
+
// ```json fences around them) are removed.
|
|
141
|
+
let prose = '';
|
|
142
|
+
let cursor = 0;
|
|
143
|
+
for (const [start, end] of matched) {
|
|
144
|
+
prose += text.slice(cursor, start);
|
|
145
|
+
cursor = end;
|
|
146
|
+
}
|
|
147
|
+
prose += text.slice(cursor);
|
|
148
|
+
prose = prose.replace(/```(?:json)?\s*```/gi, '').replace(/```(?:json)?\s*$/gi, '').trim();
|
|
149
|
+
return { text: prose.length ? prose : null, toolCalls };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Find the first complete, brace-balanced JSON object at or after `from`,
|
|
153
|
+
* respecting JSON string quoting/escaping so braces or backticks inside string
|
|
154
|
+
* values do not end the scan. Returns its `[start, end)` bounds or null.
|
|
155
|
+
*/
|
|
156
|
+
function scanJsonObject(text, from) {
|
|
157
|
+
const start = text.indexOf('{', from);
|
|
158
|
+
if (start < 0)
|
|
159
|
+
return null;
|
|
160
|
+
let depth = 0;
|
|
161
|
+
let inStr = false;
|
|
162
|
+
let esc = false;
|
|
163
|
+
for (let i = start; i < text.length; i++) {
|
|
164
|
+
const ch = text[i];
|
|
165
|
+
if (inStr) {
|
|
166
|
+
if (esc)
|
|
167
|
+
esc = false;
|
|
168
|
+
else if (ch === '\\')
|
|
169
|
+
esc = true;
|
|
170
|
+
else if (ch === '"')
|
|
171
|
+
inStr = false;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (ch === '"')
|
|
175
|
+
inStr = true;
|
|
176
|
+
else if (ch === '{')
|
|
177
|
+
depth++;
|
|
178
|
+
else if (ch === '}' && --depth === 0)
|
|
179
|
+
return { start, end: i + 1 };
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function toToolCall(raw, nextId, catalog) {
|
|
184
|
+
if (!raw)
|
|
185
|
+
return null;
|
|
186
|
+
let obj;
|
|
187
|
+
try {
|
|
188
|
+
obj = JSON.parse(raw.trim());
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
if (!obj || typeof obj !== 'object')
|
|
194
|
+
return null;
|
|
195
|
+
const rec = obj;
|
|
196
|
+
if (typeof rec.tool !== 'string')
|
|
197
|
+
return null;
|
|
198
|
+
// Only accept names the turn actually advertised. An empty catalog means the
|
|
199
|
+
// turn offered no tools, so nothing parses as a call.
|
|
200
|
+
if (!catalog.has(rec.tool))
|
|
201
|
+
return null;
|
|
202
|
+
const args = rec.args && typeof rec.args === 'object' ? rec.args : {};
|
|
203
|
+
return { id: nextId(), name: rec.tool, args };
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=tool-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-protocol.js","sourceRoot":"","sources":["../../../src/agent/providers/tool-protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/render.js
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* once at startup: interactive (TTY, no --json/--plain) pins a status line to
|
|
4
4
|
* the bottom of the terminal and redraws it in place; plain emits line-oriented
|
|
5
5
|
* output with zero ANSI escapes — the mode CI, pipes, and tests see.
|
|
6
|
+
*
|
|
7
|
+
* Interactive mode adds subtle SGR chrome (copper accent, dim secondary text).
|
|
8
|
+
* Plain mode never emits color (AC-8.9).
|
|
6
9
|
*/
|
|
10
|
+
import { copper, dim, setColorEnabled, styleOutcome, toolLine, warn } from './theme.js';
|
|
7
11
|
/** Compact token count: 850 -> "850", 12300 -> "12.3k". */
|
|
8
12
|
export function fmtTokens(n) {
|
|
9
13
|
return n < 1000 ? String(n) : `${(n / 1000).toFixed(1)}k`;
|
|
@@ -72,20 +76,30 @@ export class InteractiveRenderer {
|
|
|
72
76
|
process.on('SIGINT', this.onSigint);
|
|
73
77
|
}
|
|
74
78
|
statusText() {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
`${fmtTokens(this.tokensIn)} in / ${fmtTokens(this.tokensOut)} out`,
|
|
78
|
-
fmtDuration(Date.now() - this.startMs),
|
|
79
|
-
];
|
|
80
|
-
if (this.busy) {
|
|
81
|
-
// Fold streamed-output volume into the busy segment so a large turn's
|
|
82
|
-
// status line visibly grows — a hung one stays frozen (5.1).
|
|
83
|
-
parts.push(this.streamedChars ? `${this.busy} ~${fmtTokens(this.streamedChars)} ch` : this.busy);
|
|
84
|
-
}
|
|
79
|
+
// Raw (uncolored) segments once; both the colored line and the
|
|
80
|
+
// narrow-terminal fallback are assembled from these.
|
|
85
81
|
const spinner = this.busy ? FRAMES[this.frame % FRAMES.length] : '·';
|
|
86
|
-
const
|
|
82
|
+
const turn = `turn ${this.turn}/${this.maxTurns}`;
|
|
83
|
+
const tokens = `${fmtTokens(this.tokensIn)} in / ${fmtTokens(this.tokensOut)} out`;
|
|
84
|
+
const elapsed = fmtDuration(Date.now() - this.startMs);
|
|
85
|
+
// Fold streamed-output volume into the busy segment so a large turn's
|
|
86
|
+
// status line visibly grows — a hung one stays frozen (5.1).
|
|
87
|
+
const busy = this.busy
|
|
88
|
+
? this.streamedChars
|
|
89
|
+
? `${this.busy} ~${fmtTokens(this.streamedChars)} ch`
|
|
90
|
+
: this.busy
|
|
91
|
+
: undefined;
|
|
92
|
+
const parts = [turn, dim(tokens), dim(elapsed), ...(busy ? [warn(busy)] : [])];
|
|
93
|
+
const line = `${this.busy ? copper(spinner) : dim(spinner)} ${parts.join(dim(' · '))}`;
|
|
94
|
+
// Truncate by visible length roughly: strip SGR when measuring so color
|
|
95
|
+
// codes don't eat the column budget and clip the readable text early.
|
|
87
96
|
const width = this.out.columns ?? 80;
|
|
88
|
-
|
|
97
|
+
const visible = line.replace(/\x1b\[[0-9;]*m/g, '');
|
|
98
|
+
if (visible.length <= width)
|
|
99
|
+
return line;
|
|
100
|
+
// Fall back to an uncolored truncated line when the terminal is too narrow.
|
|
101
|
+
const plain = [spinner, turn, tokens, elapsed, ...(busy ? [busy] : [])].join(' · ');
|
|
102
|
+
return plain.length > width ? plain.slice(0, width - 1) : plain;
|
|
89
103
|
}
|
|
90
104
|
redraw() {
|
|
91
105
|
if (this.idle)
|
|
@@ -126,7 +140,7 @@ export class InteractiveRenderer {
|
|
|
126
140
|
this.redraw();
|
|
127
141
|
}
|
|
128
142
|
toolResult(name, firstLine) {
|
|
129
|
-
this.log(
|
|
143
|
+
this.log(toolLine(name, firstLine));
|
|
130
144
|
}
|
|
131
145
|
status(text) {
|
|
132
146
|
this.busy = text;
|
|
@@ -145,7 +159,7 @@ export class InteractiveRenderer {
|
|
|
145
159
|
finish(line) {
|
|
146
160
|
if (this.statusShown)
|
|
147
161
|
this.out.write(CLEAR_LINE);
|
|
148
|
-
this.out.write(line + '\n');
|
|
162
|
+
this.out.write(styleOutcome(line) + '\n');
|
|
149
163
|
this.suspend();
|
|
150
164
|
}
|
|
151
165
|
/** Release the status line (stop the spinner, restore the cursor) but stay usable. */
|
|
@@ -176,9 +190,12 @@ export class InteractiveRenderer {
|
|
|
176
190
|
* (AC-2.4): the only thing a --json invocation writes to stdout is its JSON.
|
|
177
191
|
*/
|
|
178
192
|
export function makeRenderer(opts) {
|
|
193
|
+
const interactive = !opts.json && !opts.plain && Boolean(process.stdout.isTTY);
|
|
194
|
+
// Color tracks the interactive path so --plain / pipes stay zero-ANSI (AC-8.9).
|
|
195
|
+
setColorEnabled(interactive && !process.env.NO_COLOR);
|
|
179
196
|
if (opts.json)
|
|
180
197
|
return plainRenderer((line) => console.error(line));
|
|
181
|
-
if (
|
|
198
|
+
if (interactive)
|
|
182
199
|
return new InteractiveRenderer();
|
|
183
200
|
return plainRenderer((line) => console.log(line));
|
|
184
201
|
}
|