ucode-agent 1.26.1 → 1.27.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 -7
- package/package.json +1 -1
- package/skills/build-app/DIGEST.md +94 -0
- package/skills/build-app/SKILL.md +222 -181
- package/skills/ui-ux/DIGEST.md +135 -0
- package/src/core/context.js +164 -151
- package/src/core/loop.js +209 -39
- package/src/core/provider.js +874 -868
- package/src/core/skills.js +189 -165
- package/src/core/window.js +27 -2
- package/src/tools/blocks.js +117 -27
- package/src/tools/index.js +71 -23
- package/src/tools/scaffold.js +104 -14
- package/src/ui/plain.js +4 -2
- package/src/ui/screen.js +14 -5
- package/src/ui/theme.js +100 -24
- package/templates/blocks/plain/filter-bar.js +133 -0
- package/templates/blocks/plain/item-list.js +249 -0
- package/templates/blocks/plain/modal.js +141 -0
- package/templates/blocks/plain/store.js +93 -0
- package/templates/blocks/plain/theme-toggle.js +116 -0
- package/templates/blocks/plain/toast.js +107 -0
- package/templates/plain-html/styles.css +4 -0
package/src/core/loop.js
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
tools, runTool, describe, setRoot, setConfirm, PARALLEL_SAFE, WRITES, FILE_WRITES,
|
|
30
30
|
} from '../tools/index.js';
|
|
31
|
-
import { projectMap, loadMemory, remember, MEMORY_FILE } from './context.js';
|
|
31
|
+
import { projectMap, loadMemory, hasCode, remember, MEMORY_FILE } from './context.js';
|
|
32
32
|
import { autoUpdate } from './updater.js';
|
|
33
33
|
import { closeBrowser, forgetReviews } from '../tools/browser.js';
|
|
34
34
|
import {
|
|
@@ -146,6 +146,10 @@ const TRACE_FILE = process.env.UCODE_TRACE
|
|
|
146
146
|
const THIN_RESULTS = new Set([
|
|
147
147
|
'read_file', 'read_files', 'grep', 'glob', 'list_dir', 'run_command', 'run_commands',
|
|
148
148
|
'look_at_app', 'web_search', 'edit_file', 'multi_edit', 'edit_files',
|
|
149
|
+
// create_app hands back the starter's files in full so they are never read.
|
|
150
|
+
// That is worth a round trip once and nothing at all after the next few
|
|
151
|
+
// steps, by which point they are on disk like any other file.
|
|
152
|
+
'create_app',
|
|
149
153
|
]);
|
|
150
154
|
|
|
151
155
|
/** Replace long strings in old tool arguments with a note of their size. */
|
|
@@ -396,10 +400,93 @@ function workerPrompt({ cwd, name, memory, skills, map }) {
|
|
|
396
400
|
/** The files a writing tool call touches. */
|
|
397
401
|
function pathsOf(call) {
|
|
398
402
|
const a = call.args ?? {};
|
|
399
|
-
|
|
403
|
+
// create_app writes the app in the same call it scaffolds it, so those
|
|
404
|
+
// files are changes like any other: they are checked, and they are counted.
|
|
405
|
+
if (call.name === 'batch_write' || call.name === 'edit_files' || call.name === 'create_app') {
|
|
406
|
+
return (a.files ?? []).map((f) => f?.path).filter(Boolean);
|
|
407
|
+
}
|
|
400
408
|
return a.path ? [a.path] : [];
|
|
401
409
|
}
|
|
402
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Is everything that changed part of a page that has nothing to run?
|
|
413
|
+
*
|
|
414
|
+
* The verify nudge exists so an app is not handed over untested. It costs a
|
|
415
|
+
* whole round trip, so it has to be about the thing that was built: a folder
|
|
416
|
+
* with an index.html and no package.json cannot be started or tested, and its
|
|
417
|
+
* files were parsed on the spot as they were written. Running the surrounding
|
|
418
|
+
* repository's `npm test` against it proves nothing and takes half a minute.
|
|
419
|
+
*/
|
|
420
|
+
async function pagesOnly(paths, root) {
|
|
421
|
+
const folders = new Map();
|
|
422
|
+
for (const rel of paths) {
|
|
423
|
+
const dir = String(rel).replace(/\\/g, '/').split('/')[0];
|
|
424
|
+
if (!dir || dir.includes('.')) return false; // a file at the root belongs to the project
|
|
425
|
+
if (!folders.has(dir)) {
|
|
426
|
+
folders.set(dir, (await exists(path.join(root, dir, 'index.html'))) &&
|
|
427
|
+
!(await exists(path.join(root, dir, 'package.json'))));
|
|
428
|
+
}
|
|
429
|
+
if (!folders.get(dir)) return false;
|
|
430
|
+
}
|
|
431
|
+
return folders.size > 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Reads the file paths out of a tool call's arguments while they stream.
|
|
436
|
+
*
|
|
437
|
+
* The arguments of a call that writes an app are tens of kilobytes of JSON
|
|
438
|
+
* arriving over a minute or more. Only the paths are wanted, they appear in
|
|
439
|
+
* the order the files are written, and each one is complete long before the
|
|
440
|
+
* content that follows it — so a plain scan for the next `"path": "..."`
|
|
441
|
+
* says exactly where the model has got to.
|
|
442
|
+
*
|
|
443
|
+
* Scanning resumes from where it left off rather than re-reading the whole
|
|
444
|
+
* string on every delta, which is the difference between a few thousand
|
|
445
|
+
* characters of work and a few million over one call.
|
|
446
|
+
*/
|
|
447
|
+
const PATH_IN_ARGS = /"path"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
448
|
+
|
|
449
|
+
export class Writing {
|
|
450
|
+
constructor() {
|
|
451
|
+
this.at = new Map(); // call index -> how far it has been scanned
|
|
452
|
+
this.count = new Map(); // call index -> files named so far
|
|
453
|
+
this.last = null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The line to show, or null when nothing new has been named. */
|
|
457
|
+
seen(index, name, args) {
|
|
458
|
+
if (!WRITES.has(name)) return null;
|
|
459
|
+
const from = this.at.get(index) ?? 0;
|
|
460
|
+
if (args.length <= from) return null;
|
|
461
|
+
|
|
462
|
+
// Overlap by the length of the longest thing a path match can straddle,
|
|
463
|
+
// so a path split across two deltas is not missed.
|
|
464
|
+
const window = args.slice(Math.max(0, from - 512));
|
|
465
|
+
this.at.set(index, args.length);
|
|
466
|
+
|
|
467
|
+
let found = null;
|
|
468
|
+
let extra = 0;
|
|
469
|
+
PATH_IN_ARGS.lastIndex = 0;
|
|
470
|
+
for (const match of window.matchAll(PATH_IN_ARGS)) {
|
|
471
|
+
if (match[1] === this.last) continue;
|
|
472
|
+
found = match[1];
|
|
473
|
+
extra++;
|
|
474
|
+
}
|
|
475
|
+
if (!found) return null;
|
|
476
|
+
|
|
477
|
+
const total = (this.count.get(index) ?? 0) + extra;
|
|
478
|
+
this.count.set(index, total);
|
|
479
|
+
this.last = found;
|
|
480
|
+
return `writing ${shortenPath(found, 42)}${total > 1 ? ` · ${total} files so far` : ''}`;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Tools that only mean anything once there is code in the folder. */
|
|
485
|
+
const LOOKUP_TOOLS = new Set(['find_symbol', 'outline', 'rename_symbol', 'type_of']);
|
|
486
|
+
|
|
487
|
+
/** A request that plainly wants the internet keeps web_search in a new project. */
|
|
488
|
+
const WANTS_WEB = /\b(?:search|google|web|online|internet|latest|current|news|docs|documentation|api reference|look up|find out about)\b/i;
|
|
489
|
+
|
|
403
490
|
const exists = (p) => access(p).then(() => true, () => false);
|
|
404
491
|
|
|
405
492
|
/** Skills reach the model as one extra tool, so bodies load only when wanted. */
|
|
@@ -460,8 +547,11 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
460
547
|
'something is incomplete, say which part and why.',
|
|
461
548
|
'',
|
|
462
549
|
'BE FAST. Every tool call is a round trip, and round trips are nearly all of the',
|
|
463
|
-
'time a build takes. So:
|
|
464
|
-
'
|
|
550
|
+
'time a build takes. So: a new app is ONE create_app call with its files passed',
|
|
551
|
+
'in — the starter and the whole app together, not a scaffold and then a write.',
|
|
552
|
+
'Its starter files come back inside that result, so there is nothing to read',
|
|
553
|
+
'afterwards. Everything else goes in ONE batch_write rather than a write_file',
|
|
554
|
+
'per file. Read every file you need in ONE read_files. Never read a',
|
|
465
555
|
'file you just wrote, and never read one back after edit_file — the result',
|
|
466
556
|
'already contains it. Do not re-check work the checks have already reported on.',
|
|
467
557
|
'Fast is not sloppy: it is the same work with the waiting taken out.',
|
|
@@ -482,37 +572,45 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
482
572
|
' - One line when you move between the big pieces of work: "The layout is done,',
|
|
483
573
|
' now the animations."',
|
|
484
574
|
' - One line at the end saying what it does and how to try it.',
|
|
485
|
-
'That closing line is ONE OR TWO SENTENCES
|
|
486
|
-
'list, never ticks or bullets walking through
|
|
487
|
-
'
|
|
488
|
-
'
|
|
489
|
-
'is
|
|
575
|
+
'That closing line is ONE OR TWO SENTENCES, and SIX LINES IS THE HARD CEILING.',
|
|
576
|
+
'Never a checklist, never a feature list, never ticks or bullets walking through',
|
|
577
|
+
'the request item by item, never a list of the files you touched - the user',
|
|
578
|
+
'watched them go past. "Tide is built - open tide/index.html, or serve the folder',
|
|
579
|
+
'and visit it." Anything longer is a status report nobody asked for, and it is',
|
|
580
|
+
'the last thing on screen, so it is what the whole session looks like. ucode cuts',
|
|
581
|
+
'the closing message to eight lines before it is drawn, so anything past that is',
|
|
582
|
+
'written for nobody: say the one thing that matters and stop.',
|
|
490
583
|
'That is all. A line before every tool call is not narration, it is noise: the',
|
|
491
584
|
'steps already show on screen, and repeating them in words buries the few',
|
|
492
585
|
'sentences worth reading.',
|
|
493
586
|
'',
|
|
494
|
-
'YOUR FIRST WORDS, BEFORE ANY TOOL CALL, EVERY TIME. Nothing else appears on',
|
|
495
|
-
'screen while you work, so if you say nothing the user watches a blank page.',
|
|
496
|
-
'',
|
|
497
|
-
'Open with one plain sentence naming the thing and the shape of it:',
|
|
498
|
-
' I will build Tide for you - a tasks app in a single HTML file, dark with a',
|
|
499
|
-
' teal accent.',
|
|
500
|
-
'Then one line whenever you start a new piece of the work: Now the filter row.',
|
|
501
|
-
'Then one line at the end saying it is done.',
|
|
502
|
-
'',
|
|
503
|
-
'Plain words only. No methodology, no "component-based approach", no',
|
|
504
|
-
'"structured design", no listing the qualities the work will have. Never',
|
|
505
|
-
'"I need to", never "Let me", never a plan of which files you will touch, never',
|
|
506
|
-
'the request repeated back. Say it the way you would to someone watching over',
|
|
507
|
-
'your shoulder, who can already see the screen.',
|
|
587
|
+
'YOUR FIRST WORDS, BEFORE ANY TOOL CALL, EVERY TIME. Nothing else appears on',
|
|
588
|
+
'screen while you work, so if you say nothing the user watches a blank page.',
|
|
589
|
+
'',
|
|
590
|
+
'Open with one plain sentence naming the thing and the shape of it:',
|
|
591
|
+
' I will build Tide for you - a tasks app in a single HTML file, dark with a',
|
|
592
|
+
' teal accent.',
|
|
593
|
+
'Then one line whenever you start a new piece of the work: Now the filter row.',
|
|
594
|
+
'Then one line at the end saying it is done.',
|
|
595
|
+
'',
|
|
596
|
+
'Plain words only. No methodology, no "component-based approach", no',
|
|
597
|
+
'"structured design", no listing the qualities the work will have. Never',
|
|
598
|
+
'"I need to", never "Let me", never a plan of which files you will touch, never',
|
|
599
|
+
'the request repeated back. Say it the way you would to someone watching over',
|
|
600
|
+
'your shoulder, who can already see the screen.',
|
|
508
601
|
'',
|
|
509
602
|
|
|
510
603
|
'',
|
|
511
604
|
'Before you guess at an API, ask: type_of gives the exact signature from the',
|
|
512
605
|
'TypeScript this project has installed, and find_symbol says where something is declared without',
|
|
513
606
|
'reading five files to find it. Rename with rename_symbol rather than edit_file — a',
|
|
514
|
-
'find-and-replace that matches too much is the most common broken edit.
|
|
515
|
-
'
|
|
607
|
+
'find-and-replace that matches too much is the most common broken edit.',
|
|
608
|
+
'',
|
|
609
|
+
'CALL add_block BEFORE WRITING A LIST, A FILTER ROW, A STORE, A DIALOG, A TOAST, A',
|
|
610
|
+
'THEME TOGGLE, A TABLE OR AN EMPTY STATE BY HAND. It has all of those, written for',
|
|
611
|
+
'whichever starter this app uses, keyboard and empty states included, and each one',
|
|
612
|
+
'is a hundred lines you do not have to type. Typing is the slowest part of a build:',
|
|
613
|
+
'a page assembled from blocks is finished minutes before the same page typed out.',
|
|
516
614
|
'',
|
|
517
615
|
...(mode === 'plan' ? [
|
|
518
616
|
'You are in PLAN MODE. Reading, searching and research are available; every tool',
|
|
@@ -654,6 +752,7 @@ export class Agent {
|
|
|
654
752
|
this.session = newSession(cwd, model());
|
|
655
753
|
this.working = [];
|
|
656
754
|
this.loaded = new Set();
|
|
755
|
+
this.short = new Set(); // loaded as a digest, so load_skill can still fetch the whole thing
|
|
657
756
|
this.abort = null;
|
|
658
757
|
this.busy = false;
|
|
659
758
|
this.check = null;
|
|
@@ -927,9 +1026,14 @@ export class Agent {
|
|
|
927
1026
|
autoLoad(input) {
|
|
928
1027
|
for (const skill of autoLoadFor(this.skills, input)) {
|
|
929
1028
|
if (this.loaded.has(skill.name)) continue;
|
|
1029
|
+
// The short form where the skill has one. Everything loaded here is
|
|
1030
|
+
// re-read by the provider on every step of the build, so the depth is
|
|
1031
|
+
// left behind load_skill and the rules come now.
|
|
1032
|
+
const message = skillMessage(skill, { automatic: true, short: true });
|
|
930
1033
|
this.loaded.add(skill.name);
|
|
931
|
-
this.
|
|
932
|
-
this.
|
|
1034
|
+
if (message.short) this.short.add(skill.name);
|
|
1035
|
+
this.push(message);
|
|
1036
|
+
this.ui.note(`${skill.name} skill loaded for this${message.short ? ' (short form)' : ''}`);
|
|
933
1037
|
}
|
|
934
1038
|
}
|
|
935
1039
|
|
|
@@ -950,6 +1054,10 @@ export class Agent {
|
|
|
950
1054
|
projectMap(this.cwd).catch(() => ''),
|
|
951
1055
|
loadMemory(this.cwd).catch(() => ''),
|
|
952
1056
|
]);
|
|
1057
|
+
// Nothing to look up in an empty folder, so those tools do not go out with
|
|
1058
|
+
// the request. Decided per turn: the moment there is code, they are back.
|
|
1059
|
+
this.fresh = !hasCode(this.map);
|
|
1060
|
+
this.wantsWeb = WANTS_WEB.test(input);
|
|
953
1061
|
await this.persist();
|
|
954
1062
|
|
|
955
1063
|
// A busy model was swapped for a fallback earlier; after a few minutes the
|
|
@@ -1003,16 +1111,28 @@ export class Agent {
|
|
|
1003
1111
|
}
|
|
1004
1112
|
}
|
|
1005
1113
|
|
|
1006
|
-
/**
|
|
1114
|
+
/**
|
|
1115
|
+
* The tools the model may see, given the mode and what is in the folder.
|
|
1116
|
+
*
|
|
1117
|
+
* A new project has nothing to look up: no symbol to find, nothing to
|
|
1118
|
+
* rename, no types to ask about, and — unless the request says otherwise —
|
|
1119
|
+
* nothing to search the web for. Their schemas are eight hundred tokens the
|
|
1120
|
+
* provider re-reads on every step of the build, and they are also five more
|
|
1121
|
+
* wrong turns available to a model deciding what to do next.
|
|
1122
|
+
*/
|
|
1007
1123
|
toolsNow() {
|
|
1008
1124
|
const all = [...tools, loadSkillTool, planTool, delegateTool];
|
|
1009
|
-
|
|
1010
|
-
|
|
1125
|
+
const live = this.fresh
|
|
1126
|
+
? all.filter((t) => !LOOKUP_TOOLS.has(t.name) && !(t.name === 'web_search' && !this.wantsWeb))
|
|
1127
|
+
: all;
|
|
1128
|
+
if (this.ui.mode !== 'plan') return live;
|
|
1129
|
+
return live.filter((t) => !WRITES.has(t.name));
|
|
1011
1130
|
}
|
|
1012
1131
|
|
|
1013
1132
|
/** Model, tools, model, until it answers with prose. */
|
|
1014
1133
|
async run() {
|
|
1015
1134
|
const available = this.toolsNow();
|
|
1135
|
+
this.offering = new Set(available.map((t) => t.name));
|
|
1016
1136
|
let argRetries = 0;
|
|
1017
1137
|
let continuations = 0;
|
|
1018
1138
|
let askedToVerify = false;
|
|
@@ -1060,6 +1180,16 @@ export class Agent {
|
|
|
1060
1180
|
this.early.set(call.id, this.execute(call));
|
|
1061
1181
|
}
|
|
1062
1182
|
};
|
|
1183
|
+
// A whole app is one tool call whose arguments take a minute or two
|
|
1184
|
+
// to arrive, and nothing can be started until they have. What can
|
|
1185
|
+
// happen is saying where it has got to: the files are named in the
|
|
1186
|
+
// order they are written, so the spinner names the one being
|
|
1187
|
+
// written now instead of sitting on "working" for ninety seconds.
|
|
1188
|
+
const writing = new Writing();
|
|
1189
|
+
opts.onToolArgs = ({ index, name, args }) => {
|
|
1190
|
+
const at = writing.seen(index, name, args);
|
|
1191
|
+
if (at) this.ui.updateSpinner(at);
|
|
1192
|
+
};
|
|
1063
1193
|
}
|
|
1064
1194
|
|
|
1065
1195
|
var asked = Date.now();
|
|
@@ -1144,9 +1274,12 @@ export class Agent {
|
|
|
1144
1274
|
// Text that turns out to be narration ahead of a tool call folds into a
|
|
1145
1275
|
// status line instead — that is where the live commentary comes from.
|
|
1146
1276
|
const narrating = reply.toolCalls.length > 0;
|
|
1147
|
-
|
|
1277
|
+
// No tool calls left and something was actually done: this is the closing
|
|
1278
|
+
// message, the last thing on screen, and it gets cut to eight lines.
|
|
1279
|
+
const closing = !narrating && (this.touched.size > 0 || this.ranSomething);
|
|
1280
|
+
if (streaming) this.ui.streamEnd({ asNarration: narrating, closing });
|
|
1148
1281
|
else if (reply.text && narrating && isLabel(reply.text)) this.ui.narrate(reply.text);
|
|
1149
|
-
else if (reply.text) this.ui.assistant(reply.text);
|
|
1282
|
+
else if (reply.text) this.ui.assistant(reply.text, { closing });
|
|
1150
1283
|
|
|
1151
1284
|
if (reply.toolCalls.length === 0) {
|
|
1152
1285
|
// The answer stopped at the provider's output cap rather than at the
|
|
@@ -1184,8 +1317,14 @@ export class Agent {
|
|
|
1184
1317
|
}
|
|
1185
1318
|
}
|
|
1186
1319
|
|
|
1187
|
-
// It changed code and never ran anything. Send it back once
|
|
1188
|
-
|
|
1320
|
+
// It changed code and never ran anything. Send it back once — but
|
|
1321
|
+
// only if this project's check would actually exercise what changed.
|
|
1322
|
+
// A page and a stylesheet in a repo that happens to have `npm test`
|
|
1323
|
+
// was costing a whole round trip to run someone else's unit tests
|
|
1324
|
+
// against a file they have never heard of, when the page's own script
|
|
1325
|
+
// was parsed locally a moment ago.
|
|
1326
|
+
if (this.touched.size && !this.ranSomething && this.check && !askedToVerify &&
|
|
1327
|
+
!(await pagesOnly(this.touched, this.cwd))) {
|
|
1189
1328
|
askedToVerify = true;
|
|
1190
1329
|
if (reply.text) this.push({ role: 'assistant', content: reply.text });
|
|
1191
1330
|
this.push({
|
|
@@ -1221,7 +1360,12 @@ export class Agent {
|
|
|
1221
1360
|
continue;
|
|
1222
1361
|
}
|
|
1223
1362
|
|
|
1224
|
-
|
|
1363
|
+
// An assistant message with no content and no tool calls is not a turn,
|
|
1364
|
+
// it is a hole. Providers reject the whole conversation as malformed
|
|
1365
|
+
// once one is in it — which showed up as HTTP 400 on every request
|
|
1366
|
+
// after the model went quiet, with the conversation at 0% full and
|
|
1367
|
+
// "usually an oversized conversation" printed underneath it.
|
|
1368
|
+
if (reply.text?.trim()) this.push({ role: 'assistant', content: reply.text });
|
|
1225
1369
|
if (!reply.text?.trim()) {
|
|
1226
1370
|
// Silence after being asked to speak is not a finished turn. Saying
|
|
1227
1371
|
// "Done" here is the worst thing available: the user believes it,
|
|
@@ -1491,6 +1635,20 @@ export class Agent {
|
|
|
1491
1635
|
});
|
|
1492
1636
|
}
|
|
1493
1637
|
|
|
1638
|
+
// A tool that was not offered does not run, whatever the model calls it.
|
|
1639
|
+
// The list it is sent is the list that exists: plan mode withholds every
|
|
1640
|
+
// tool that writes, and a new project is not sent the ones that look code
|
|
1641
|
+
// up — and a model naming one from memory was, until this check, executing
|
|
1642
|
+
// it. A withheld tool has to be refused, not just left out of the menu.
|
|
1643
|
+
if (!this.offering?.has(call.name)) {
|
|
1644
|
+
throw new ToolFailure({
|
|
1645
|
+
kind: 'no_such_tool',
|
|
1646
|
+
attempted: `calling ${call.name}`,
|
|
1647
|
+
failed: `${call.name} is not available${this.ui.mode === 'plan' ? ' in plan mode' : ' in this project'}.`,
|
|
1648
|
+
fix: `Use one of the tools you were given: ${[...this.offering ?? []].join(', ')}.`,
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1494
1652
|
if (call.name === 'load_skill') return this.loadSkill(call.args?.name);
|
|
1495
1653
|
if (call.name === 'update_plan') return this.updatePlan(call.args?.items);
|
|
1496
1654
|
if (call.name === 'delegate') return this.delegate(call.args?.tasks);
|
|
@@ -1604,9 +1762,11 @@ export class Agent {
|
|
|
1604
1762
|
async runWorker(task, index) {
|
|
1605
1763
|
const name = clip(String(task.name || `worker ${index + 1}`).trim(), 16);
|
|
1606
1764
|
const touched = new Set();
|
|
1765
|
+
// A worker gets what the lead has, digest included: its prompt is re-read
|
|
1766
|
+
// on every step it takes, the same as the lead's.
|
|
1607
1767
|
const skills = this.skills
|
|
1608
1768
|
.filter((s) => this.loaded.has(s.name))
|
|
1609
|
-
.map((s) => `--- ${s.name} ---\n${s.body}`)
|
|
1769
|
+
.map((s) => `--- ${s.name} ---\n${this.short.has(s.name) && s.digest ? s.digest : s.body}`)
|
|
1610
1770
|
.join('\n\n');
|
|
1611
1771
|
const messages = [
|
|
1612
1772
|
{ role: 'system', content: workerPrompt({ cwd: this.cwd, name, memory: this.memory, skills, map: this.map }) },
|
|
@@ -1840,11 +2000,14 @@ export class Agent {
|
|
|
1840
2000
|
});
|
|
1841
2001
|
}
|
|
1842
2002
|
|
|
1843
|
-
|
|
2003
|
+
// Loaded in full already: nothing to do. Loaded as a digest: this is the
|
|
2004
|
+
// model asking for the depth, which is exactly what the digest points at.
|
|
2005
|
+
if (this.loaded.has(skill.name) && !this.short.has(skill.name)) {
|
|
1844
2006
|
return { content: `The "${skill.name}" skill is already loaded above. Follow it.`, summary: 'already loaded' };
|
|
1845
2007
|
}
|
|
1846
2008
|
|
|
1847
2009
|
this.loaded.add(skill.name);
|
|
2010
|
+
this.short.delete(skill.name);
|
|
1848
2011
|
this.push(skillMessage(skill));
|
|
1849
2012
|
return {
|
|
1850
2013
|
content: `Loaded "${skill.name}". Its instructions are in your context now — follow them.`,
|
|
@@ -1882,7 +2045,11 @@ export class Agent {
|
|
|
1882
2045
|
{ role: 'user', content: forSummary(older) },
|
|
1883
2046
|
],
|
|
1884
2047
|
[],
|
|
1885
|
-
|
|
2048
|
+
// Always the quick model, whatever the user picked for the work
|
|
2049
|
+
// itself. This is a mechanical restatement in the middle of a
|
|
2050
|
+
// build the user is waiting on; a reasoning model would think
|
|
2051
|
+
// about it for half a minute and produce the same paragraph.
|
|
2052
|
+
{ signal: this.abort?.signal, temperature: 0, model: DEFAULT_MODEL },
|
|
1886
2053
|
);
|
|
1887
2054
|
return reply.text;
|
|
1888
2055
|
},
|
|
@@ -2208,6 +2375,7 @@ ${out.content}` });
|
|
|
2208
2375
|
this.session = loaded;
|
|
2209
2376
|
this.working = [...loaded.messages];
|
|
2210
2377
|
this.loaded = new Set(loaded.messages.filter((m) => m.skill).map((m) => m.skill));
|
|
2378
|
+
this.short = new Set(loaded.messages.filter((m) => m.skill && m.short).map((m) => m.skill));
|
|
2211
2379
|
if (loaded.model && MODELS[loaded.model]) setModel(loaded.model);
|
|
2212
2380
|
return true;
|
|
2213
2381
|
} catch (err) {
|
|
@@ -2258,6 +2426,7 @@ ${out.content}` });
|
|
|
2258
2426
|
this.session = newSession(this.cwd, model());
|
|
2259
2427
|
this.working = [];
|
|
2260
2428
|
this.loaded = new Set();
|
|
2429
|
+
this.short = new Set(); // loaded as a digest, so load_skill can still fetch the whole thing
|
|
2261
2430
|
this.showHeader();
|
|
2262
2431
|
}
|
|
2263
2432
|
|
|
@@ -2278,8 +2447,9 @@ ${out.content}` });
|
|
|
2278
2447
|
this.ui.blank();
|
|
2279
2448
|
for (const s of this.skills) {
|
|
2280
2449
|
const live = this.loaded.has(s.name);
|
|
2450
|
+
const how = live && this.short.has(s.name) ? dim(' · short form') : '';
|
|
2281
2451
|
const auto = s.triggers.length ? dim(' · loads itself') : '';
|
|
2282
|
-
this.ui.write(` ${live ? blue('●') : dim('○')} ${blue(s.name)}${auto}`);
|
|
2452
|
+
this.ui.write(` ${live ? blue('●') : dim('○')} ${blue(s.name)}${how}${auto}`);
|
|
2283
2453
|
this.ui.write(` ${dim(s.description)}`);
|
|
2284
2454
|
}
|
|
2285
2455
|
this.ui.blank();
|