copperhead 0.9.0 → 0.11.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/NOTICE +1 -1
- package/README.md +47 -8
- package/dist/agent/context.js +2 -0
- package/dist/agent/context.js.map +1 -0
- package/dist/agent/dock-renderer.js +2 -2
- package/dist/agent/dock-renderer.js.map +1 -1
- package/dist/agent/envelope.js +105 -0
- package/dist/agent/envelope.js.map +1 -0
- package/dist/agent/filetools.js +24 -1
- package/dist/agent/filetools.js.map +1 -1
- package/dist/agent/ledger.js +24 -0
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/loop.js +63 -72
- package/dist/agent/loop.js.map +1 -1
- package/dist/agent/prompts.js +4 -3
- package/dist/agent/prompts.js.map +1 -1
- package/dist/agent/providers/claude-code.js +17 -1
- package/dist/agent/providers/claude-code.js.map +1 -1
- package/dist/agent/providers/codex.js +84 -39
- package/dist/agent/providers/codex.js.map +1 -1
- package/dist/agent/providers/tool-protocol.js +21 -0
- package/dist/agent/providers/tool-protocol.js.map +1 -1
- package/dist/agent/recovery.js +186 -15
- package/dist/agent/recovery.js.map +1 -1
- package/dist/agent/registry.js +49 -0
- package/dist/agent/registry.js.map +1 -0
- package/dist/agent/render.js +2 -2
- package/dist/agent/render.js.map +1 -1
- package/dist/agent/theme.js +10 -5
- package/dist/agent/theme.js.map +1 -1
- package/dist/agent/tools.js +100 -586
- package/dist/agent/tools.js.map +1 -1
- package/dist/agent/transcript.js +2 -0
- package/dist/agent/transcript.js.map +1 -1
- package/dist/capabilities/define.js +35 -0
- package/dist/capabilities/define.js.map +1 -0
- package/dist/capabilities/handlers.js +744 -0
- package/dist/capabilities/handlers.js.map +1 -0
- package/dist/capabilities/helpers.js +39 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/index.js +50 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/skills/generate-report.js +23 -0
- package/dist/capabilities/skills/generate-report.js.map +1 -0
- package/dist/cli.js +158 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/check.js +33 -1
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/create.js +181 -26
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/doctor.js +83 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/skill.js +109 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/sync.js +3 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/config.js +19 -6
- package/dist/config.js.map +1 -1
- package/dist/kicad/bootstrap.js +24 -3
- package/dist/kicad/bootstrap.js.map +1 -1
- package/dist/kicad/cli.js +106 -18
- package/dist/kicad/cli.js.map +1 -1
- package/dist/kicad/dossier.js +207 -0
- package/dist/kicad/dossier.js.map +1 -0
- package/dist/kicad/draft/draft.js +135 -0
- package/dist/kicad/draft/draft.js.map +1 -0
- package/dist/kicad/draft/engine.js +5310 -0
- package/dist/kicad/draft/engine.js.map +1 -0
- package/dist/kicad/draft/ir.js +368 -0
- package/dist/kicad/draft/ir.js.map +1 -0
- package/dist/kicad/draft/symsource.js +504 -0
- package/dist/kicad/draft/symsource.js.map +1 -0
- package/dist/kicad/emit.js +220 -0
- package/dist/kicad/emit.js.map +1 -0
- package/dist/kicad/fab.js +13 -0
- package/dist/kicad/fab.js.map +1 -1
- package/dist/kicad/legibility.js +608 -0
- package/dist/kicad/legibility.js.map +1 -0
- package/dist/kicad/score.js +431 -0
- package/dist/kicad/score.js.map +1 -0
- package/dist/kicad/sexp.js +265 -6
- package/dist/kicad/sexp.js.map +1 -1
- package/dist/kicad/symlib.js +346 -16
- package/dist/kicad/symlib.js.map +1 -1
- package/dist/mcp/server.js +485 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/memory/bom-table.js +75 -39
- package/dist/memory/bom-table.js.map +1 -1
- package/dist/memory/scaffold.js +14 -1
- package/dist/memory/scaffold.js.map +1 -1
- package/dist/util/redact.js +6 -0
- package/dist/util/redact.js.map +1 -1
- package/package.json +13 -8
- package/src/agent/context.ts +35 -0
- package/src/agent/dock-renderer.ts +3 -2
- package/src/agent/envelope.ts +124 -0
- package/src/agent/filetools.ts +26 -1
- package/src/agent/ledger.ts +24 -0
- package/src/agent/loop.ts +73 -78
- package/src/agent/prompts.ts +4 -3
- package/src/agent/providers/claude-code.ts +22 -1
- package/src/agent/providers/codex.ts +91 -42
- package/src/agent/providers/tool-protocol.ts +22 -0
- package/src/agent/recovery.ts +183 -13
- package/src/agent/registry.ts +58 -0
- package/src/agent/render.ts +4 -3
- package/src/agent/theme.ts +15 -5
- package/src/agent/tools.ts +124 -628
- package/src/agent/transcript.ts +6 -0
- package/src/agent/types.ts +10 -5
- package/src/capabilities/define.ts +88 -0
- package/src/capabilities/handlers.ts +769 -0
- package/src/capabilities/helpers.ts +37 -0
- package/src/capabilities/index.ts +53 -0
- package/src/capabilities/skills/generate-report.ts +25 -0
- package/src/cli.ts +154 -0
- package/src/commands/check.ts +51 -1
- package/src/commands/create.ts +183 -21
- package/src/commands/doctor.ts +85 -6
- package/src/commands/skill.ts +127 -0
- package/src/commands/sync.ts +5 -3
- package/src/config.ts +56 -8
- package/src/kicad/bootstrap.ts +24 -3
- package/src/kicad/cli.ts +129 -18
- package/src/kicad/dossier.ts +217 -0
- package/src/kicad/draft/draft.ts +173 -0
- package/src/kicad/draft/engine.ts +5274 -0
- package/src/kicad/draft/ir.ts +416 -0
- package/src/kicad/draft/symsource.ts +549 -0
- package/src/kicad/emit.ts +300 -0
- package/src/kicad/fab.ts +15 -0
- package/src/kicad/legibility.ts +695 -0
- package/src/kicad/score.ts +502 -0
- package/src/kicad/sexp.ts +346 -6
- package/src/kicad/symlib.ts +364 -18
- package/src/mcp/server.ts +560 -0
- package/src/memory/bom-table.ts +85 -38
- package/src/memory/scaffold.ts +14 -1
- package/src/util/redact.ts +6 -0
- package/dist/memory/synap.js +0 -152
- package/dist/memory/synap.js.map +0 -1
- package/src/memory/synap.ts +0 -217
package/src/agent/recovery.ts
CHANGED
|
@@ -2,11 +2,25 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import type { Msg, Provider } from './types.js';
|
|
5
|
+
import { resolveLibrarySymbol, searchInstalledSymbols, symbolSearchDirs, listInstalledLibraries } from '../kicad/symlib.js';
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* How many times a hung provider turn is retried before the run gives up. One
|
|
9
|
+
* policy, two turn loops: the main agent loop and the nested skill sub-run.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_TURN_TIMEOUTS = 3;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when a provider turn trips its watchdog. `idle`: no response or
|
|
15
|
+
* progress for `ms`, so the call looks hung. `max`: still running at the hard
|
|
16
|
+
* cap however much progress it reported, so the turn is too large, not hung.
|
|
17
|
+
*/
|
|
7
18
|
export class TurnTimeoutError extends Error {
|
|
8
|
-
constructor(
|
|
9
|
-
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly ms: number,
|
|
21
|
+
public readonly kind: 'idle' | 'max' = 'idle',
|
|
22
|
+
) {
|
|
23
|
+
super(kind === 'max' ? `turn still running after ${ms}ms (hard cap)` : `turn exceeded ${ms}ms without responding`);
|
|
10
24
|
this.name = 'TurnTimeoutError';
|
|
11
25
|
}
|
|
12
26
|
}
|
|
@@ -23,18 +37,81 @@ export async function withTimeout<T>(
|
|
|
23
37
|
ms: number,
|
|
24
38
|
onTimeout?: () => void | Promise<void>,
|
|
25
39
|
): Promise<T> {
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
return withWatchdog(() => fn(), { idleMs: ms, ...(onTimeout ? { onTimeout } : {}) });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The turn watchdog: `withTimeout` with the deadline measured from the last sign
|
|
45
|
+
* of progress instead of from the start. `fn` receives an `activity` callback and
|
|
46
|
+
* each call restarts the `idleMs` deadline, so a long turn that keeps producing
|
|
47
|
+
* output is never mistaken for a hung one, while a call that reports nothing gets
|
|
48
|
+
* `idleMs` as a plain whole-call deadline (unchanged for providers that cannot
|
|
49
|
+
* stream). `maxMs` is a hard cap activity does not extend, so a turn that streams
|
|
50
|
+
* forever still ends. The cap bounds a call that is producing output, so it only
|
|
51
|
+
* trips once `fn` has reported progress (a silent call is judged by `idleMs`
|
|
52
|
+
* alone, exactly as before the cap existed), and it is never shorter than
|
|
53
|
+
* `idleMs` (a lower cap would cut off a turn the idle deadline alone allows).
|
|
54
|
+
* `<= 0` (or non-finite) disables either limit; with both disabled this just
|
|
55
|
+
* awaits `fn`.
|
|
56
|
+
*/
|
|
57
|
+
export async function withWatchdog<T>(
|
|
58
|
+
fn: (activity: () => void) => Promise<T>,
|
|
59
|
+
opts: { idleMs: number; maxMs?: number; onTimeout?: () => void | Promise<void> },
|
|
60
|
+
): Promise<T> {
|
|
61
|
+
const idleOn = Number.isFinite(opts.idleMs) && opts.idleMs > 0;
|
|
62
|
+
const maxMs = opts.maxMs ?? 0;
|
|
63
|
+
const maxOn = Number.isFinite(maxMs) && maxMs > 0;
|
|
64
|
+
if (!idleOn && !maxOn) return fn(() => {});
|
|
65
|
+
const capMs = idleOn ? Math.max(maxMs, opts.idleMs) : maxMs;
|
|
66
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
67
|
+
let maxTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
// Set once the race is decided either way, so a late activity() from an
|
|
69
|
+
// abandoned call cannot re-arm a timer after the watchdog is done.
|
|
70
|
+
let settled = false;
|
|
71
|
+
// Whether fn has reported progress yet, and whether the cap came due while it
|
|
72
|
+
// had not (see the cap timer below).
|
|
73
|
+
let progressed = false;
|
|
74
|
+
let capPassed = false;
|
|
75
|
+
let trip!: (err: TurnTimeoutError) => void;
|
|
28
76
|
const timeout = new Promise<never>((_, reject) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
77
|
+
trip = (err) => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(idleTimer);
|
|
81
|
+
clearTimeout(maxTimer);
|
|
82
|
+
// Reject before tearing down: onTimeout (provider.close()) may reject the
|
|
83
|
+
// in-flight call synchronously, and the race must settle on the timeout,
|
|
84
|
+
// not on that teardown error, or the caller never sees TurnTimeoutError.
|
|
85
|
+
reject(err);
|
|
86
|
+
void Promise.resolve(opts.onTimeout?.()).catch(() => {});
|
|
87
|
+
};
|
|
33
88
|
});
|
|
89
|
+
const armIdle = (): void => {
|
|
90
|
+
clearTimeout(idleTimer);
|
|
91
|
+
idleTimer = setTimeout(() => trip(new TurnTimeoutError(opts.idleMs, 'idle')), opts.idleMs);
|
|
92
|
+
};
|
|
93
|
+
const activity = (): void => {
|
|
94
|
+
if (settled) return;
|
|
95
|
+
progressed = true;
|
|
96
|
+
if (capPassed) return trip(new TurnTimeoutError(capMs, 'max'));
|
|
97
|
+
if (idleOn) armIdle();
|
|
98
|
+
};
|
|
99
|
+
if (idleOn) armIdle();
|
|
100
|
+
if (maxOn) {
|
|
101
|
+
maxTimer = setTimeout(() => {
|
|
102
|
+
// The cap bounds a turn that is producing output. A call that has reported
|
|
103
|
+
// nothing yet stays under the idle deadline alone; if it starts reporting
|
|
104
|
+
// progress later, the cap applies at that moment.
|
|
105
|
+
if (progressed) trip(new TurnTimeoutError(capMs, 'max'));
|
|
106
|
+
else capPassed = true;
|
|
107
|
+
}, capMs);
|
|
108
|
+
}
|
|
34
109
|
try {
|
|
35
|
-
return await Promise.race([fn(), timeout]);
|
|
110
|
+
return await Promise.race([fn(activity), timeout]);
|
|
36
111
|
} finally {
|
|
37
|
-
|
|
112
|
+
settled = true;
|
|
113
|
+
clearTimeout(idleTimer);
|
|
114
|
+
clearTimeout(maxTimer);
|
|
38
115
|
}
|
|
39
116
|
}
|
|
40
117
|
|
|
@@ -116,6 +193,91 @@ export async function transcriptExcerpt(transcriptDir: string, maxChars = 4000):
|
|
|
116
193
|
return joined.length > maxChars ? joined.slice(joined.length - maxChars) : joined;
|
|
117
194
|
}
|
|
118
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Deterministically re-probe every lib_id named in a failure narrative against
|
|
198
|
+
* the installed libraries, so the diagnostician judges symbol-availability
|
|
199
|
+
* claims from machine facts instead of the agent's prose. An agent that has
|
|
200
|
+
* been dead-ended by wrong library nicknames concludes — and records — that
|
|
201
|
+
* whole libraries are absent when they are installed; a refusal built on that
|
|
202
|
+
* premise reads exactly like a genuine environmental gap, and the one thing
|
|
203
|
+
* that distinguishes them is re-checking the named lib_ids, which costs no
|
|
204
|
+
* LLM turn. Never throws; on any probe error it reports what it could.
|
|
205
|
+
*/
|
|
206
|
+
export async function symbolAvailabilityFacts(text: string, dirs?: string[], cap = 8): Promise<string> {
|
|
207
|
+
const ids: string[] = [];
|
|
208
|
+
// A library nickname is its `.kicad_sym` filename stem, so it can carry `-`
|
|
209
|
+
// and `.` as well as `_` (`Custom-Parts`, `MyCorp.RF`) — a nickname the regex
|
|
210
|
+
// truncates is probed as the wrong lib_id and reported absent, which is the
|
|
211
|
+
// false negative this whole fact block exists to prevent. Separators are
|
|
212
|
+
// interior only, so a trailing sentence period is not swallowed.
|
|
213
|
+
for (const m of text.matchAll(/\b([A-Za-z0-9_](?:[A-Za-z0-9_.-]*[A-Za-z0-9_])?):([A-Za-z0-9][A-Za-z0-9_.+-]*)/g)) {
|
|
214
|
+
const lib = m[1]!;
|
|
215
|
+
const name = m[2]!;
|
|
216
|
+
// Require letters on both sides: drops file:line refs ("create.ts:311"),
|
|
217
|
+
// times and bare numbers. Engine-generated power symbols are not library
|
|
218
|
+
// facts.
|
|
219
|
+
if (!/[A-Za-z]/.test(lib) || !/[A-Za-z]/.test(name) || lib === 'copperhead_power') continue;
|
|
220
|
+
const libId = `${lib}:${name}`;
|
|
221
|
+
if (!ids.includes(libId)) ids.push(libId);
|
|
222
|
+
}
|
|
223
|
+
if (!ids.length) return '';
|
|
224
|
+
// Collection is unbounded but probing is capped, so a probe-heavy transcript
|
|
225
|
+
// stays cheap. The overflow is named rather than dropped: the supervisor is
|
|
226
|
+
// told these facts are ground truth, and silently probing 8 of 30 lib_ids
|
|
227
|
+
// would let it read "unprobed" as "absent".
|
|
228
|
+
const probed = ids.slice(0, cap);
|
|
229
|
+
const unprobed = ids.slice(cap);
|
|
230
|
+
let searchDirs: string[];
|
|
231
|
+
try {
|
|
232
|
+
searchDirs = dirs ?? (await symbolSearchDirs());
|
|
233
|
+
} catch {
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
if (!searchDirs.length) return '';
|
|
237
|
+
// A directory with no readable library means nothing was checked: emitting
|
|
238
|
+
// "not installed" lines as ground truth from that state is the exact false
|
|
239
|
+
// absence this block exists to prevent (same guard as bomSymbolDossier).
|
|
240
|
+
if (!(await listInstalledLibraries(searchDirs)).size) return '';
|
|
241
|
+
const lines: string[] = [];
|
|
242
|
+
for (const libId of probed) {
|
|
243
|
+
const name = libId.slice(libId.indexOf(':') + 1);
|
|
244
|
+
try {
|
|
245
|
+
const r = await resolveLibrarySymbol(libId, searchDirs);
|
|
246
|
+
if (r.status === 'ok') {
|
|
247
|
+
lines.push(`- ${libId}: RESOLVES on this machine (${r.pins.length} pins)`);
|
|
248
|
+
} else if (r.status === 'found-elsewhere') {
|
|
249
|
+
// The resolver already located the part under another lib_id; saying
|
|
250
|
+
// "not installed" here would be the exact false absence claim this
|
|
251
|
+
// block exists to prevent.
|
|
252
|
+
lines.push(`- ${libId}: not at that lib_id, but installed as: ${r.libIds.slice(0, 4).join(', ')}`);
|
|
253
|
+
} else {
|
|
254
|
+
const elsewhere = await searchInstalledSymbols(name, searchDirs, 4);
|
|
255
|
+
const inThat =
|
|
256
|
+
r.status === 'no-symbol' && r.candidates.length
|
|
257
|
+
? ` (closest in that library: ${r.candidates.slice(0, 4).join(', ')})`
|
|
258
|
+
: '';
|
|
259
|
+
const where = elsewhere.length
|
|
260
|
+
? `; installed as: ${elsewhere.join(', ')}`
|
|
261
|
+
: `; no installed symbol matches "${name}" in any library`;
|
|
262
|
+
lines.push(
|
|
263
|
+
r.status === 'no-symbol'
|
|
264
|
+
? `- ${libId}: not in that library${inThat}${where}`
|
|
265
|
+
: `- ${libId}: no library of that nickname is installed${where}`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
// a single unreadable library must not sink the fact block
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (!lines.length) return '';
|
|
273
|
+
if (unprobed.length) {
|
|
274
|
+
lines.push(
|
|
275
|
+
`- NOT RE-PROBED (probe limit ${cap}): ${unprobed.join(', ')} — these were named in the text but not checked, so nothing above says whether they exist.`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return lines.join('\n');
|
|
279
|
+
}
|
|
280
|
+
|
|
119
281
|
/**
|
|
120
282
|
* Ask the model whether a failed/incomplete stage is worth retrying, and if so
|
|
121
283
|
* how. Uses a fresh, tool-less provider turn (the same saved-login backend the
|
|
@@ -132,6 +294,9 @@ export async function diagnoseStageFailure(
|
|
|
132
294
|
excerpt: string;
|
|
133
295
|
attempt: number;
|
|
134
296
|
maxAttempts: number;
|
|
297
|
+
/** Deterministic re-probe results for lib_ids named in the failure/excerpt
|
|
298
|
+
* (`symbolAvailabilityFacts`); authoritative over the transcript's claims. */
|
|
299
|
+
symbolFacts?: string;
|
|
135
300
|
},
|
|
136
301
|
): Promise<StageDiagnosis> {
|
|
137
302
|
const system =
|
|
@@ -145,10 +310,15 @@ export async function diagnoseStageFailure(
|
|
|
145
310
|
`Failure: ${input.failure}\n` +
|
|
146
311
|
`This was attempt ${input.attempt} of ${input.maxAttempts}.\n\n` +
|
|
147
312
|
`Recent transcript (most recent last):\n${input.excerpt}\n\n` +
|
|
313
|
+
(input.symbolFacts
|
|
314
|
+
? `Machine-verified symbol facts — a deterministic re-probe of the lib_ids named above, run just now against this machine's installed KiCad libraries. Each line reported below is ground truth and overrides anything the transcript claims about that symbol's availability. Coverage may be partial: a lib_id listed as NOT RE-PROBED, or absent from this block entirely, is unknown, never confirmed absent.\n${input.symbolFacts}\n\n`
|
|
315
|
+
: '') +
|
|
148
316
|
'Reply with ONLY a JSON object, no prose:\n' +
|
|
149
317
|
'{"verdict":"retry"|"abort","reason":"<one sentence>","guidance":"<if retry: concrete, specific instructions to prepend to the next attempt so it avoids this failure; otherwise empty>"}\n' +
|
|
150
|
-
'- "retry" if the failure looks transient or fixable with clearer instructions (a dropped or locked tool call, an empty/no-op edit, a skipped step, a
|
|
151
|
-
'- "
|
|
318
|
+
'- "retry" if the failure looks transient or fixable with clearer instructions (a dropped or locked tool call, an empty/no-op edit, a skipped step, a hung provider call that timed out, a formatting slip).\n' +
|
|
319
|
+
'- a turn stopped at the hard time cap while still producing output (turnMaxMs, "too large, not hung") is NOT transient: repeating it hits the cap again. Retry only with guidance that splits that work into several smaller edits (e.g. one part or one sheet section per edit).\n' +
|
|
320
|
+
'- "abort" if repeating the same attempt will not help and a human should look (missing inputs, a genuine dead-end, or the same failure already seen on a prior attempt).\n' +
|
|
321
|
+
'- an agent\'s claim that a symbol or library is absent is NOT evidence: agents dead-ended by wrong library nicknames routinely conclude whole libraries are missing. If the machine-verified facts contradict the failure\'s premise (a cited-absent lib_id RESOLVES, or the part is installed under another library), the verdict is "retry", with guidance quoting the correct lib_ids.';
|
|
152
322
|
const messages: Msg[] = [
|
|
153
323
|
{ role: 'system', content: system },
|
|
154
324
|
{ role: 'user', content: user },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { catalog } from '../capabilities/index.js';
|
|
2
|
+
import type { CatalogEntry, CatalogSkill } from '../capabilities/define.js';
|
|
3
|
+
import type { RunContext } from './context.js';
|
|
4
|
+
import { PROTOCOL_VERSION } from './envelope.js';
|
|
5
|
+
|
|
6
|
+
export { PROTOCOL_VERSION };
|
|
7
|
+
|
|
8
|
+
export class ToolRegistry {
|
|
9
|
+
private readonly byName = new Map<string, CatalogEntry>();
|
|
10
|
+
|
|
11
|
+
constructor(entries: CatalogEntry[]) {
|
|
12
|
+
for (const source of entries) {
|
|
13
|
+
const e: CatalogEntry = { ...source };
|
|
14
|
+
if (this.byName.has(e.name)) throw new Error(`duplicate catalog name "${e.name}"`);
|
|
15
|
+
this.byName.set(e.name, e);
|
|
16
|
+
}
|
|
17
|
+
for (const e of this.byName.values()) {
|
|
18
|
+
if (e.kind === 'skill' && !e.gateProvided) {
|
|
19
|
+
const conj = (ctx: RunContext) => this.conjunction(e.tools, ctx);
|
|
20
|
+
e.gate = conj;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get protocolVersion(): number {
|
|
26
|
+
return PROTOCOL_VERSION;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get(name: string): CatalogEntry | undefined {
|
|
30
|
+
return this.byName.get(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
all(): CatalogEntry[] {
|
|
34
|
+
return [...this.byName.values()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
skills(): CatalogSkill[] {
|
|
38
|
+
return this.all().filter((e): e is CatalogSkill => e.kind === 'skill');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when every named tool exists and its gate is open. */
|
|
42
|
+
conjunction(toolNames: string[], ctx: RunContext): boolean {
|
|
43
|
+
return toolNames.every((n) => {
|
|
44
|
+
const t = this.byName.get(n);
|
|
45
|
+
return t?.kind === 'tool' && t.gate(ctx);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Entries whose gate is open. Skills also require every declared tool's gate (D3). */
|
|
50
|
+
list(ctx: RunContext): CatalogEntry[] {
|
|
51
|
+
return this.all().filter((e) => {
|
|
52
|
+
if (e.kind === 'skill' && (e.tools.includes('finish') || !this.conjunction(e.tools, ctx))) return false;
|
|
53
|
+
return e.gate(ctx);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const registry = new ToolRegistry(catalog);
|
package/src/agent/render.ts
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { copper, dim, setColorEnabled, styleOutcome, toolLine, warn } from './theme.js';
|
|
12
|
+
import type { ViewHint } from './envelope.js';
|
|
12
13
|
|
|
13
14
|
export interface ProgressRenderer {
|
|
14
15
|
log(line: string): void;
|
|
15
16
|
/** Called at the start of each turn with cumulative token totals so far. */
|
|
16
17
|
turnStart(turn: number, maxTurns: number, tokensIn: number, tokensOut: number): void;
|
|
17
|
-
toolResult(name: string, firstLine: string): void;
|
|
18
|
+
toolResult(name: string, firstLine: string, ok?: boolean, viewHint?: ViewHint): void;
|
|
18
19
|
/** Busy text while a provider call is in flight; null when idle. */
|
|
19
20
|
status(text: string | null): void;
|
|
20
21
|
/**
|
|
@@ -175,8 +176,8 @@ export class InteractiveRenderer implements ProgressRenderer {
|
|
|
175
176
|
this.redraw();
|
|
176
177
|
}
|
|
177
178
|
|
|
178
|
-
toolResult(name: string, firstLine: string): void {
|
|
179
|
-
this.log(toolLine(name, firstLine));
|
|
179
|
+
toolResult(name: string, firstLine: string, ok?: boolean, viewHint?: ViewHint): void {
|
|
180
|
+
this.log(toolLine(name, firstLine, ok, viewHint));
|
|
180
181
|
}
|
|
181
182
|
|
|
182
183
|
status(text: string | null): void {
|
package/src/agent/theme.ts
CHANGED
|
@@ -59,11 +59,21 @@ export function stageLine(name: string, detail: string, kind: 'info' | 'ok' | 'w
|
|
|
59
59
|
return `${label} ${body}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Tool-result scrollback line: glyph from envelope `ok`, not a regex on prose.
|
|
64
|
+
* The envelope `viewHint` styles the name: mutations (they changed the repo)
|
|
65
|
+
* read copper, exports light copper, queries/diagnostics stay dim. Plain mode
|
|
66
|
+
* is unaffected: every painter no-ops while color is off.
|
|
67
|
+
*/
|
|
68
|
+
export function toolLine(
|
|
69
|
+
name: string,
|
|
70
|
+
firstLine: string,
|
|
71
|
+
succeeded = false,
|
|
72
|
+
viewHint?: 'diagnostic' | 'mutation' | 'query' | 'export',
|
|
73
|
+
): string {
|
|
74
|
+
const glyph = succeeded ? ok('✓') : copper('▸');
|
|
75
|
+
const label = viewHint === 'mutation' ? copper(name) : viewHint === 'export' ? copperLight(name) : dim(name);
|
|
76
|
+
return ` ${glyph} ${label} ${firstLine}`;
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/** Color the final outcome line from its exit-path token. */
|