ucode-agent 1.13.0 → 1.15.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/package.json +1 -1
- package/src/core/loop.js +43 -10
- package/src/ui/screen.js +91 -23
package/package.json
CHANGED
package/src/core/loop.js
CHANGED
|
@@ -434,12 +434,41 @@ function systemPrompt({ cwd, skills, mode, check, map, memory }) {
|
|
|
434
434
|
'',
|
|
435
435
|
'## How to work',
|
|
436
436
|
'',
|
|
437
|
-
'
|
|
438
|
-
'
|
|
439
|
-
'
|
|
440
|
-
'
|
|
441
|
-
'
|
|
442
|
-
'
|
|
437
|
+
'Before building anything, turn the request into a list of what it must do — every',
|
|
438
|
+
'feature named, and the ones any user would expect whether or not they were named',
|
|
439
|
+
'(an empty state, an error state, the keyboard doing the obvious thing, working on',
|
|
440
|
+
'a phone). Keep it as the plan. Build against it, then go through it one item at a',
|
|
441
|
+
'time before you say a word about being finished. Most of what gets missed was',
|
|
442
|
+
'never written down.',
|
|
443
|
+
'',
|
|
444
|
+
'Do not put code in your reply. Not a snippet, not "here is the key part", not a',
|
|
445
|
+
'summary of the file. It is already in the file and the user can open it; pasting',
|
|
446
|
+
'it again buries the one or two sentences that actually matter. Say what it does',
|
|
447
|
+
'and what to try.',
|
|
448
|
+
'',
|
|
449
|
+
'Do not claim it is done while anything is still running or unchecked. "I have',
|
|
450
|
+
'built it" said before the build finishes is worse than saying nothing: the user',
|
|
451
|
+
'believes you, looks, and finds it broken. Finish, check, then say so — and if',
|
|
452
|
+
'something is incomplete, say which part and why.',
|
|
453
|
+
'',
|
|
454
|
+
'FIRST, EVERY TIME: one short line saying what you are about to do, then the tool',
|
|
455
|
+
'calls. Never open a turn with a tool call and no words. "Right, the HTML',
|
|
456
|
+
'structure first." / "Now the state and the render loop." / "That is the layout',
|
|
457
|
+
'done - onto the animations." / "Let me see what is there." One sentence, your own',
|
|
458
|
+
'voice, before the actions - not after them, not instead of them, not a',
|
|
459
|
+
'restatement of the request. The user watches this scroll past, and without those',
|
|
460
|
+
'lines it is a list of file operations they cannot read intent from.',
|
|
461
|
+
'',
|
|
462
|
+
'FIRST, EVERY TIME: write one short line saying what you are about to do, then',
|
|
463
|
+
'make the tool calls. Never open a turn with a tool call and no words. Examples:',
|
|
464
|
+
'"Right, the HTML structure first." / "Now the state and the render loop." /',
|
|
465
|
+
'"That is the layout done - onto the animations." / "Let me see what is there."',
|
|
466
|
+
'One sentence, your own voice, before the actions - not after them, not instead',
|
|
467
|
+
'of them, and not a restatement of what was asked. The user is watching this',
|
|
468
|
+
'scroll past; without those lines it is a list of file operations and they cannot',
|
|
469
|
+
'tell what you are building. This matters as much as the code.',
|
|
470
|
+
'',
|
|
471
|
+
|
|
443
472
|
'',
|
|
444
473
|
'Before you guess at an API, ask: type_of gives the exact signature from the',
|
|
445
474
|
'TypeScript this project has installed, and find_symbol says where something is declared without',
|
|
@@ -974,7 +1003,7 @@ export class Agent {
|
|
|
974
1003
|
};
|
|
975
1004
|
// Only a real terminal has somewhere to stream into.
|
|
976
1005
|
if (this.full) {
|
|
977
|
-
opts.onThinking = () => this.ui.thinkingDelta();
|
|
1006
|
+
opts.onThinking = (delta) => this.ui.thinkingDelta(delta);
|
|
978
1007
|
opts.onText = (delta) => {
|
|
979
1008
|
if (!streaming) {
|
|
980
1009
|
streaming = true;
|
|
@@ -1370,9 +1399,13 @@ export class Agent {
|
|
|
1370
1399
|
reportFailure(call, err) {
|
|
1371
1400
|
if (!(err instanceof ToolFailure)) throw err;
|
|
1372
1401
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1402
|
+
// Bad arguments are the model talking to itself. "old_string and new_string
|
|
1403
|
+
// are identical" is a correction it will make on the next step, and it means
|
|
1404
|
+
// nothing to whoever is watching except that something went wrong. It goes
|
|
1405
|
+
// to the model, which can act on it, and not to the screen. A refusal the
|
|
1406
|
+
// user made, and anything that actually failed, still shows.
|
|
1407
|
+
if (err instanceof Declined) this.ui.toolFailed('declined');
|
|
1408
|
+
else if (err.kind !== 'bad_args') this.ui.toolFailed(`${err.kind}: ${err.failed}`);
|
|
1376
1409
|
this.push({
|
|
1377
1410
|
role: 'tool',
|
|
1378
1411
|
toolCallId: call.id,
|
package/src/ui/screen.js
CHANGED
|
@@ -297,40 +297,66 @@ export class Screen {
|
|
|
297
297
|
// U+25CF, not U+23FA: the latter carries emoji presentation, which Windows
|
|
298
298
|
// Terminal draws as a white circle on a blue tile.
|
|
299
299
|
const kind = groupKind(label);
|
|
300
|
-
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// rather than adding another almost-identical one beneath it.
|
|
308
|
-
if (run && run.kind === kind && gap <= 1) {
|
|
309
|
-
if (gap === 1) this.lines.pop(); // its single result line, now counted
|
|
300
|
+
// One line per kind of work for as long as the model is working on one
|
|
301
|
+
// thing. Reading, writing and reading again used to draw six lines that
|
|
302
|
+
// said three things; now the "Reading files" line it already has is the
|
|
303
|
+
// one that counts up, wherever it sits.
|
|
304
|
+
const run = (this.segment ??= new Map()).get(kind);
|
|
305
|
+
|
|
306
|
+
if (run && this.lines[run.at] !== undefined) {
|
|
310
307
|
run.count++;
|
|
311
308
|
run.label = label;
|
|
312
309
|
run.targets.push(groupTarget(label));
|
|
313
|
-
this.
|
|
310
|
+
this.run = run;
|
|
311
|
+
this.paintRun({ live: true });
|
|
314
312
|
} else {
|
|
315
313
|
this.push(`${narrationMark()} ${narration(asLabel(label))}`);
|
|
316
314
|
this.run = {
|
|
317
315
|
kind, count: 1, at: this.lines.length - 1, label,
|
|
318
316
|
targets: [groupTarget(label)], added: 0, removed: 0,
|
|
319
317
|
};
|
|
318
|
+
this.segment.set(kind, this.run);
|
|
319
|
+
this.paintRun({ live: true });
|
|
320
320
|
}
|
|
321
321
|
this.updateSpinner(label);
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
-
/**
|
|
325
|
-
|
|
324
|
+
/**
|
|
325
|
+
* The model speaking — or a plan, or a failure — ends the segment.
|
|
326
|
+
*
|
|
327
|
+
* Up to that point a kind of work keeps one line and counts up on it. After
|
|
328
|
+
* it, the next read is a new piece of work and deserves its own line, which
|
|
329
|
+
* is what makes the transcript read as a sequence of things done rather
|
|
330
|
+
* than a set of running totals.
|
|
331
|
+
*/
|
|
332
|
+
endRun() {
|
|
333
|
+
if (this.run) this.paintRun({ live: false });
|
|
334
|
+
this.run = null;
|
|
335
|
+
this.segment = new Map();
|
|
336
|
+
}
|
|
326
337
|
|
|
327
338
|
/** Redraw the run's single line from what it has accumulated. */
|
|
328
|
-
|
|
339
|
+
/**
|
|
340
|
+
* Redraw the run's single line from what it has accumulated.
|
|
341
|
+
*
|
|
342
|
+
* While its step is still running the text shimmers, which is the only
|
|
343
|
+
* thing on screen saying "this is happening now" once the per-step result
|
|
344
|
+
* lines are gone. It settles to plain dim the moment the step finishes, so
|
|
345
|
+
* the finished ones above stay quiet.
|
|
346
|
+
*/
|
|
347
|
+
paintRun({ live = this.run?.live } = {}) {
|
|
329
348
|
if (!this.run) return;
|
|
330
|
-
|
|
349
|
+
const text = asLabel(runLine(this.run));
|
|
350
|
+
this.run.live = live;
|
|
351
|
+
this.lines[this.run.at] = `${narrationMark()} ${live ? shimmer(text, this.tick * FRAME_MS) : narration(text)}`;
|
|
331
352
|
this.render();
|
|
332
353
|
}
|
|
333
354
|
|
|
355
|
+
/** Let the line in flight animate, one frame per tick. */
|
|
356
|
+
paintLiveRun() {
|
|
357
|
+
if (this.run?.live && this.lines[this.run.at] !== undefined) this.paintRun({ live: true });
|
|
358
|
+
}
|
|
359
|
+
|
|
334
360
|
/**
|
|
335
361
|
* A change, as its two numbers.
|
|
336
362
|
*
|
|
@@ -361,7 +387,10 @@ export class Screen {
|
|
|
361
387
|
* already names the step, and a change adds its numbers to that same line.
|
|
362
388
|
* Only a failure earns a line of its own.
|
|
363
389
|
*/
|
|
364
|
-
toolResult() {
|
|
390
|
+
toolResult() {
|
|
391
|
+
// The step is over: the line stops moving and joins the quiet ones above.
|
|
392
|
+
if (this.run) this.paintRun({ live: false });
|
|
393
|
+
}
|
|
365
394
|
|
|
366
395
|
toolFailed(summary) {
|
|
367
396
|
// A failure is never folded away.
|
|
@@ -503,17 +532,54 @@ export class Screen {
|
|
|
503
532
|
// than silence. The spinner counts the seconds so the wait is visibly alive,
|
|
504
533
|
// and the transcript gets one line afterwards saying how long it took.
|
|
505
534
|
|
|
506
|
-
|
|
535
|
+
/**
|
|
536
|
+
* The model's reasoning, live, one line at a time.
|
|
537
|
+
*
|
|
538
|
+
* Models reach for a tool before they say anything, so the first words of a
|
|
539
|
+
* step were arriving a minute after it began — while the reasoning channel
|
|
540
|
+
* had been streaming words the whole time and we were throwing them away to
|
|
541
|
+
* keep a timer. Its latest sentence now shows on one line that rewrites
|
|
542
|
+
* itself, which is something to read from the first second.
|
|
543
|
+
*
|
|
544
|
+
* It is scaffolding, not the answer: it shimmers while it is live and it is
|
|
545
|
+
* taken off the screen the moment the real reply starts.
|
|
546
|
+
*/
|
|
547
|
+
thinkingDelta(text = '') {
|
|
507
548
|
if (this.thoughtSince === undefined) this.thoughtSince = Date.now();
|
|
549
|
+
if (!text) return;
|
|
550
|
+
|
|
551
|
+
this.thought = ((this.thought ?? '') + text).slice(-2000);
|
|
552
|
+
// The last sentence it has finished, or what it has written of the next.
|
|
553
|
+
const parts = this.thought.split(/(?<=[.!?])\s+/).filter((p) => p.trim());
|
|
554
|
+
const latest = (parts[parts.length - 1] ?? '').replace(/\s+/g, ' ').trim();
|
|
555
|
+
if (!latest) return;
|
|
556
|
+
|
|
557
|
+
const line = ` ${shimmer(clip(latest, Math.max(20, this.width() - 6)), this.tick * FRAME_MS)}`;
|
|
558
|
+
if (this.thinkAt === undefined || this.lines[this.thinkAt] === undefined) {
|
|
559
|
+
this.thinkAt = this.lines.length;
|
|
560
|
+
this.push(line);
|
|
561
|
+
} else {
|
|
562
|
+
this.lines[this.thinkAt] = line;
|
|
563
|
+
this.render();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** Keep the live thought moving between deltas. */
|
|
568
|
+
paintLiveThought() {
|
|
569
|
+
if (this.thinkAt !== undefined && this.lines[this.thinkAt] !== undefined) this.thinkingDelta('');
|
|
508
570
|
}
|
|
509
571
|
|
|
510
572
|
thinkingEnd() {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
573
|
+
// The thought was the wait; once there is a reply it has nothing to add,
|
|
574
|
+
// so it comes off the screen rather than settling into the transcript.
|
|
575
|
+
if (this.thinkAt !== undefined) {
|
|
576
|
+
this.lines.splice(this.thinkAt, 1);
|
|
577
|
+
if (this.run && this.run.at > this.thinkAt) this.run.at--;
|
|
578
|
+
for (const run of this.segment?.values() ?? []) if (run.at > this.thinkAt) run.at--;
|
|
579
|
+
this.thinkAt = undefined;
|
|
580
|
+
this.render();
|
|
581
|
+
}
|
|
582
|
+
this.thought = '';
|
|
517
583
|
this.thoughtSince = undefined;
|
|
518
584
|
}
|
|
519
585
|
|
|
@@ -856,6 +922,8 @@ export class Screen {
|
|
|
856
922
|
if (this.spinTimer) return;
|
|
857
923
|
this.spinTimer = setInterval(() => {
|
|
858
924
|
this.tick++;
|
|
925
|
+
this.paintLiveRun();
|
|
926
|
+
this.paintLiveThought();
|
|
859
927
|
this.paintStatus();
|
|
860
928
|
}, FRAME_MS);
|
|
861
929
|
this.spinTimer.unref?.();
|