teapot-coding-agent 0.26.3 → 0.26.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.js +67 -7
- package/dist/agent/tools.js +44 -0
- package/dist/server/api.js +9 -0
- package/package.json +1 -1
- package/public/assets/{index-B6nvBQLn.js → index-CfHZpzkw.js} +22 -22
- package/public/assets/index-CfHZpzkw.js.map +1 -0
- package/public/assets/{index-CP1dmlDT.css → index-UpRths52.css} +1 -1
- package/public/assets/{shiki-DPtj9Srk.js → shiki-Cy1X9klC.js} +2 -2
- package/public/assets/{shiki-DPtj9Srk.js.map → shiki-Cy1X9klC.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-B6nvBQLn.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -89,14 +89,25 @@ Most session state is not injected into prompts — fetch it with tools instead:
|
|
|
89
89
|
turn right after the call (a short "reported." is fine).
|
|
90
90
|
- Be frugal: prefer small precise edits, avoid runaway loops.`;
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
* content (`llm.ts`).
|
|
92
|
+
* A message whose ENTIRE text is parenthesised is an acknowledgement, not work.
|
|
94
93
|
*
|
|
95
|
-
* #94:
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
* #94: the first version of this listed three literals — `(tool call)`,
|
|
95
|
+
* `(no content)`, `(no output)` — and caught 2 of the 10 forms actually reaching
|
|
96
|
+
* a parent. Measured across every session log, the rest are harness
|
|
97
|
+
* acknowledgements written by `answerMeta` and the meta-tool handlers:
|
|
98
|
+
*
|
|
99
|
+
* (goal complete) (round ended) (tool try) (no sub-agents)
|
|
100
|
+
* (tool_call) (nothing running to stop) (no result recorded)
|
|
101
|
+
* (todo.md is empty …) (decisions.md is empty …)
|
|
102
|
+
*
|
|
103
|
+
* So it matches the SHAPE — a whole line wrapped in parentheses — rather than an
|
|
104
|
+
* enumeration that is stale the moment another ack is added.
|
|
105
|
+
*
|
|
106
|
+
* The trade: a genuine message that happens to be entirely parenthesised would
|
|
107
|
+
* also be dropped. That is vanishingly rare in this feed, and the alternative —
|
|
108
|
+
* an enumeration — was measured to be wrong 80% of the time. If it ever matters,
|
|
109
|
+
* the ack should be marked structurally rather than by its punctuation.
|
|
110
|
+
*/ export const PLACEHOLDER_TEXT = /^\(.*\)$/;
|
|
100
111
|
/**
|
|
101
112
|
* Harness acknowledgements — meta-lines about the run itself rather than the
|
|
102
113
|
* work ("decision recorded to decisions.md", "progress recorded", …).
|
|
@@ -1018,6 +1029,29 @@ export class Agent {
|
|
|
1018
1029
|
// Bounded like the rest of the snapshot, and ids are opaque, so this
|
|
1019
1030
|
// cannot carry prompt text.
|
|
1020
1031
|
pendingPromptIds: this.pendingPrompts.map((p) => p.id).filter((x) => !!x).slice(0, 64),
|
|
1032
|
+
// #87: the QUEUE itself, not just its ids. The UI's echo rows are
|
|
1033
|
+
// memory-only (like liveByAgent and timelineCache), so after a reload the
|
|
1034
|
+
// queue survives on the server while the UI has nothing to render — the
|
|
1035
|
+
// queued message then appears only as its log row, i.e. already sent.
|
|
1036
|
+
//
|
|
1037
|
+
// Ids alone cannot fix that: they identify a prompt, they do not carry its
|
|
1038
|
+
// text, so the UI cannot rebuild an echo from them. This is the queue the
|
|
1039
|
+
// operator is looking at, already on screen in the ⏳ badge, so shipping it
|
|
1040
|
+
// is not new exposure — it is the same data in a shape the timeline can
|
|
1041
|
+
// use.
|
|
1042
|
+
//
|
|
1043
|
+
// `at` is the enqueue time so the echo keeps a stable position and id
|
|
1044
|
+
// across rebuilds; `sent` mirrors the echo flag the UI already tracks.
|
|
1045
|
+
pendingPromptQueue: this.pendingPrompts
|
|
1046
|
+
.filter((p) => !!p.id)
|
|
1047
|
+
.slice(0, 64)
|
|
1048
|
+
.map((p, i) => ({
|
|
1049
|
+
id: p.id,
|
|
1050
|
+
text: p.text.slice(0, 8000),
|
|
1051
|
+
source: p.source,
|
|
1052
|
+
at: Date.now() - (this.pendingPrompts.length - i),
|
|
1053
|
+
...(p.images?.length ? { images: p.images.slice(0, 8) } : {}),
|
|
1054
|
+
})),
|
|
1021
1055
|
todo: this.todo.slice(0, 32_000), // match set_todo's cap — no silent truncation
|
|
1022
1056
|
parent: this.opts.parent,
|
|
1023
1057
|
awaiting: this.awaitingUser,
|
|
@@ -1252,6 +1286,8 @@ export class Agent {
|
|
|
1252
1286
|
}
|
|
1253
1287
|
/** monotonic counter: a start() bumps it, so a pending stop status write loses */
|
|
1254
1288
|
stopSeq = 0;
|
|
1289
|
+
/** #95: set when this round already injected a harness progress request */
|
|
1290
|
+
progressRequestedThisRound = false;
|
|
1255
1291
|
setStatus(s, reason = "") {
|
|
1256
1292
|
// Update the field FIRST. log.append fires onEvent synchronously, and the
|
|
1257
1293
|
// master's child-event hook wakes parked wait_children callers — those
|
|
@@ -1349,6 +1385,22 @@ export class Agent {
|
|
|
1349
1385
|
await this.sleepInterruptible(this.opts.continueDelayMs);
|
|
1350
1386
|
if (this.stopRequested)
|
|
1351
1387
|
break;
|
|
1388
|
+
// #95: do NOT pair this with a progress request. Both fire at the turn
|
|
1389
|
+
// boundary and both inject a user message, so the model received two
|
|
1390
|
+
// harness instructions back to back — "Continue working toward the
|
|
1391
|
+
// current goal" immediately followed by "Please give a brief progress
|
|
1392
|
+
// report now", which reads as the harness talking to itself.
|
|
1393
|
+
//
|
|
1394
|
+
// The progress request already re-enters the loop, so it IS the
|
|
1395
|
+
// continuation: suppressing the nudge when one is pending costs nothing
|
|
1396
|
+
// and leaves exactly one instruction per boundary.
|
|
1397
|
+
if (this.progressRequestedThisRound) {
|
|
1398
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
1399
|
+
event: "auto-continue-suppressed",
|
|
1400
|
+
detail: "a progress report was already requested this round (#95)",
|
|
1401
|
+
});
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1352
1404
|
const isSub = !!this.opts.parent;
|
|
1353
1405
|
const nudge = isSub
|
|
1354
1406
|
? `[harness] Auto-nudge. Re-anchor on YOUR task — and only that:\n\n${this.goal.text.slice(0, 1500)}\n\nThe inherited conversation is reference only; other agents own any older tasks in it. If your task is complete, call finish() now instead of continuing. If blocked, explain why briefly and finish().`
|
|
@@ -1472,6 +1524,11 @@ export class Agent {
|
|
|
1472
1524
|
// skills may have been created last turn — refresh the prompt listing
|
|
1473
1525
|
await this.refreshSkills();
|
|
1474
1526
|
// periodic progress report at turn boundary (no mid-turn interruption)
|
|
1527
|
+
// #95: the flag is per-TURN, not per-round. `maybeRequestProgress` sits
|
|
1528
|
+
// inside the turn loop and auto-continue is checked after the round
|
|
1529
|
+
// ends, so the flag has to mean "this boundary already carried an
|
|
1530
|
+
// instruction" — reset here, set below if the request fires.
|
|
1531
|
+
this.progressRequestedThisRound = false;
|
|
1475
1532
|
await this.maybeRequestProgress();
|
|
1476
1533
|
// keep the context window bounded before spending tokens on a turn
|
|
1477
1534
|
await this.maybeCompact();
|
|
@@ -2284,6 +2341,9 @@ export class Agent {
|
|
|
2284
2341
|
this.turnsSinceProgress >= this.opts.progressMaxQuietTurns;
|
|
2285
2342
|
if (!elapsedOk || !activityOk)
|
|
2286
2343
|
return; // stalling provider → don't waste a turn asking
|
|
2344
|
+
// #95: remember that this boundary already carries an instruction, so the
|
|
2345
|
+
// auto-continue nudge can stand down instead of pairing with it.
|
|
2346
|
+
this.progressRequestedThisRound = true;
|
|
2287
2347
|
this.lastProgressAt = Date.now();
|
|
2288
2348
|
this.activityChars = 0;
|
|
2289
2349
|
this.turnsSinceProgress = 0;
|
package/dist/agent/tools.js
CHANGED
|
@@ -414,7 +414,51 @@ function clipText(s, max) {
|
|
|
414
414
|
const n = Math.max(1000, Math.min(max, 80_000));
|
|
415
415
|
return s.length <= n ? s : `${s.slice(0, n)}\n… [truncated, ${s.length} chars total]`;
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Patterns that backtrack catastrophically (#93).
|
|
419
|
+
*
|
|
420
|
+
* A nested quantifier — a quantified group whose body is itself quantified, or
|
|
421
|
+
* alternation that can match the same text more than one way — makes the engine
|
|
422
|
+
* explore exponentially many splits. Measured on this codebase: `(a+)+b` against
|
|
423
|
+
* 28 characters of "a" took 3.8s, roughly DOUBLING per character added, and a
|
|
424
|
+
* 40-character line would run for days.
|
|
425
|
+
*
|
|
426
|
+
* There is no timeout on this path: `ctx.defaultTimeoutMs` guards `bash` only,
|
|
427
|
+
* so the scan is a synchronous `re.test(line)` with nothing able to interrupt it.
|
|
428
|
+
* The agent process blocks and takes the UI and every other agent with it.
|
|
429
|
+
*
|
|
430
|
+
* A model is poorly placed to reason about backtracking cost and nothing tells it
|
|
431
|
+
* not to try, so the pattern is checked rather than trusted. This rejects the
|
|
432
|
+
* SHAPE rather than trying to bound the runtime, because a synchronous regex
|
|
433
|
+
* cannot be abandoned once `test` starts — the only reliable protection is to
|
|
434
|
+
* never compile one.
|
|
435
|
+
*/
|
|
436
|
+
const BACKTRACKING_PATTERNS = [
|
|
437
|
+
// (a+)+ (a*)* (a+)* (a?)* … — a quantifier wrapping another quantifier
|
|
438
|
+
{ re: /\([^()]*[+*}][^()]*\)\s*[+*{]/, why: "a quantifier nested inside another quantifier" },
|
|
439
|
+
// (a|a)+ — alternation whose branches can match the SAME text. Distinct
|
|
440
|
+
// branches like (foo|bar)+ are fine and common, so this only fires when two
|
|
441
|
+
// branches are literally identical, which is the shape that explodes.
|
|
442
|
+
{ re: /\(([^()|]*)\|\1\)\s*[+*{]/, why: "an alternation with identical branches inside a quantifier" },
|
|
443
|
+
// (.*)* (.+)+ — a wildcard quantified inside a quantifier
|
|
444
|
+
{ re: /\(\s*[.][*+]\s*\)\s*[+*{]/, why: "a wildcard quantified inside a quantifier" },
|
|
445
|
+
];
|
|
446
|
+
/** Above this, the O(lines x pattern) scan itself becomes the cost. */
|
|
447
|
+
const MAX_PATTERN_LENGTH = 1000;
|
|
448
|
+
/**
|
|
449
|
+
* Compile a model-supplied regex, refusing ones that can backtrack
|
|
450
|
+
* catastrophically.
|
|
451
|
+
*
|
|
452
|
+
* Returns an error STRING rather than a RegExp so every existing call site —
|
|
453
|
+
* which already handles that shape for an invalid pattern — needs no change.
|
|
454
|
+
*/
|
|
417
455
|
function compileRegex(pattern, ignoreCase) {
|
|
456
|
+
if (pattern.length > MAX_PATTERN_LENGTH)
|
|
457
|
+
return `pattern too long (${pattern.length} chars, max ${MAX_PATTERN_LENGTH}) — narrow it, or grep for a literal substring instead`;
|
|
458
|
+
for (const { re, why } of BACKTRACKING_PATTERNS) {
|
|
459
|
+
if (re.test(pattern))
|
|
460
|
+
return `pattern rejected: ${why} — that shape backtracks exponentially and would freeze the agent (e.g. "(a+)+b" against 28 characters takes 3.8s). Use a literal search, or simplify the pattern.`;
|
|
461
|
+
}
|
|
418
462
|
try {
|
|
419
463
|
return new RegExp(pattern, ignoreCase ? "i" : "");
|
|
420
464
|
}
|
package/dist/server/api.js
CHANGED
|
@@ -1027,6 +1027,15 @@ export function buildApp(master) {
|
|
|
1027
1027
|
return c.json({ error: "eventId and text required" }, 400);
|
|
1028
1028
|
try {
|
|
1029
1029
|
const r = await a.editPromptAt(body.eventId, body.text, body.tail === "summarize" ? "summarize" : "discard");
|
|
1030
|
+
// #96: the UI button is labelled "fork & RESEND", but nothing resent.
|
|
1031
|
+
// `editPromptAt` rewrites history and returns, leaving the agent idle —
|
|
1032
|
+
// so the edited prompt sat in the timeline looking sent while the model
|
|
1033
|
+
// had never seen it, and the operator had to notice and press start.
|
|
1034
|
+
//
|
|
1035
|
+
// start() here rather than in the client: the route already knows the edit
|
|
1036
|
+
// succeeded, and a client that failed to call it would leave the agent
|
|
1037
|
+
// silently idle with no error anywhere.
|
|
1038
|
+
a.start("edited prompt resent");
|
|
1030
1039
|
return c.json({ ok: true, ...r });
|
|
1031
1040
|
}
|
|
1032
1041
|
catch (err) {
|