teapot-coding-agent 0.18.2 → 0.19.1
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 +138 -16
- package/dist/config-schema.js +3 -0
- package/dist/master.js +5 -0
- package/package.json +1 -1
- package/public/assets/index-B7e29JKk.js +30 -0
- package/public/assets/index-B7e29JKk.js.map +1 -0
- package/public/assets/index-BE_Iu8Yy.css +1 -0
- package/public/assets/{shiki-CjZVqXtP.js → shiki-g5IhKfqD.js} +2 -2
- package/public/assets/{shiki-CjZVqXtP.js.map → shiki-g5IhKfqD.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-CdjELTD1.js +0 -30
- package/public/assets/index-CdjELTD1.js.map +0 -1
- package/public/assets/index-F3NQPOfy.css +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -107,6 +107,8 @@ export class Agent {
|
|
|
107
107
|
lastUsage;
|
|
108
108
|
/** messages.length right after the last successful compaction */
|
|
109
109
|
compactedAtLen = 0;
|
|
110
|
+
/** live compaction phase for UI progress ("summarizing"/"harvesting") */
|
|
111
|
+
compactPhase = "";
|
|
110
112
|
/** set by ask_user: the loop is parked until the operator replies */
|
|
111
113
|
awaitingUser = false;
|
|
112
114
|
/** real assistant output since the last progress report (chars / turns) */
|
|
@@ -122,6 +124,7 @@ export class Agent {
|
|
|
122
124
|
continueDelayMs: 15_000,
|
|
123
125
|
autoCompact: true,
|
|
124
126
|
maxConsecutiveToolErrors: 5,
|
|
127
|
+
maxTurnsPerRound: 200,
|
|
125
128
|
contextTokenBudget: 96_000,
|
|
126
129
|
contextWindowTokens: 0,
|
|
127
130
|
restoreSession: true,
|
|
@@ -675,6 +678,9 @@ export class Agent {
|
|
|
675
678
|
// mislabeling every inferred-window model "manual override"
|
|
676
679
|
compactAtIsManual: this.compactBudgetIsManual,
|
|
677
680
|
window: this.opts.contextWindowTokens || 0,
|
|
681
|
+
// live compaction phase ("summarizing"/"harvesting"/"done") — lets any
|
|
682
|
+
// client show progress even if it missed the bus event
|
|
683
|
+
...(this.compactPhase ? { compacting: this.compactPhase } : {}),
|
|
678
684
|
},
|
|
679
685
|
pendingPrompts: this.pendingPrompts.length,
|
|
680
686
|
todo: this.todo.slice(0, 32_000), // match set_todo's cap — no silent truncation
|
|
@@ -833,7 +839,37 @@ export class Agent {
|
|
|
833
839
|
async loop() {
|
|
834
840
|
while (!this.stopRequested) {
|
|
835
841
|
try {
|
|
836
|
-
const
|
|
842
|
+
const turnsAtStart = this.stats.turns;
|
|
843
|
+
let finished = await this.runTurnsUntilIdle();
|
|
844
|
+
// the round ended on the per-round turn cap (not finish/stop): tell
|
|
845
|
+
// the model to report and wrap up, then fall through to auto-continue
|
|
846
|
+
// — a soft checkpoint instead of the old hard runaway error
|
|
847
|
+
const cap = Math.max(0, this.opts.maxTurnsPerRound ?? 200);
|
|
848
|
+
if (!finished &&
|
|
849
|
+
cap > 0 &&
|
|
850
|
+
this.stats.turns - turnsAtStart >= cap &&
|
|
851
|
+
!this.stopRequested &&
|
|
852
|
+
!this.awaitingUser &&
|
|
853
|
+
!this.pendingPrompts.length // real user input outranks the wrap-up nudge
|
|
854
|
+
) {
|
|
855
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
856
|
+
event: "round-turn-cap",
|
|
857
|
+
turns: cap,
|
|
858
|
+
});
|
|
859
|
+
await this.maybeRequestProgress();
|
|
860
|
+
const nudge = `[harness] This round ran ${cap} turns, so it ends here. ` +
|
|
861
|
+
"Report progress via report_progress now; then either call finish() if the goal is met, " +
|
|
862
|
+
"or state exactly where you will resume. Work CONTINUES in the next round automatically.";
|
|
863
|
+
this.messages.push({ role: "user", content: nudge });
|
|
864
|
+
await this.log.append("prompt", this.currentSession, this.currentBranch, {
|
|
865
|
+
source: "harness",
|
|
866
|
+
text: nudge,
|
|
867
|
+
});
|
|
868
|
+
// the wrap-up nudge IS this round's continuation prompt — starting
|
|
869
|
+
// another round on top of it would stack a generic auto-continue
|
|
870
|
+
// nudge too, so go straight to the next round instead
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
837
873
|
if (this.stopRequested)
|
|
838
874
|
break;
|
|
839
875
|
// fresh user input arrived while we were finishing up — another round now
|
|
@@ -935,7 +971,12 @@ export class Agent {
|
|
|
935
971
|
*/
|
|
936
972
|
async runTurnsUntilIdle() {
|
|
937
973
|
let finished = false;
|
|
938
|
-
|
|
974
|
+
// maxTurnsPerRound is a SOFT cap (0 disables): reaching it nudges the
|
|
975
|
+
// model to wrap up instead of hard-failing the round — long autonomous
|
|
976
|
+
// stretches are normal here, and a throw used to leave status=error with
|
|
977
|
+
// no auto-recovery even though autoContinue would happily keep going.
|
|
978
|
+
const cap = Math.max(0, this.opts.maxTurnsPerRound ?? 200);
|
|
979
|
+
for (let guard = 0; cap === 0 || guard < cap; guard++) {
|
|
939
980
|
if (this.stopRequested)
|
|
940
981
|
return finished;
|
|
941
982
|
// deliver prompts queued while the previous turn was running
|
|
@@ -1244,7 +1285,9 @@ export class Agent {
|
|
|
1244
1285
|
if (finished)
|
|
1245
1286
|
return true;
|
|
1246
1287
|
}
|
|
1247
|
-
|
|
1288
|
+
// cap reached — loop() logs the note, asks for a progress report and
|
|
1289
|
+
// nudges the model; this round simply ends (no hard error)
|
|
1290
|
+
return finished;
|
|
1248
1291
|
}
|
|
1249
1292
|
buildMessages() {
|
|
1250
1293
|
// deliberately static: [system] + append-only history keeps prefix caches hot
|
|
@@ -1330,20 +1373,48 @@ export class Agent {
|
|
|
1330
1373
|
text: request,
|
|
1331
1374
|
});
|
|
1332
1375
|
this.messages.push({ role: "user", content: request });
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
//
|
|
1336
|
-
//
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
// the
|
|
1376
|
+
// Give the model the report_progress TOOL here. The old no-tools call
|
|
1377
|
+
// made the model answer in plain text while its own mental "did I report?"
|
|
1378
|
+
// ledger stayed unsolved — it then re-sent a voluntary report_progress
|
|
1379
|
+
// right after, and the timeline showed every requested report TWICE.
|
|
1380
|
+
const res = await this.llmCall(this.buildMessages(), allToolSpecs());
|
|
1381
|
+
const calls = res.message.tool_calls ?? [];
|
|
1382
|
+
// the assistant turn MUST enter history before its tool answers, or the
|
|
1383
|
+
// sequence [user, tool] is invalid and every later request 400s
|
|
1384
|
+
this.messages.push(res.message);
|
|
1340
1385
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
1341
1386
|
role: "assistant",
|
|
1342
1387
|
content: res.message.content ?? "",
|
|
1343
1388
|
reasoning: res.reasoning,
|
|
1389
|
+
...(calls.length ? { toolCalls: calls.map((c) => ({ id: c.id, name: c.function.name })) } : {}),
|
|
1344
1390
|
progressEcho: true,
|
|
1345
1391
|
});
|
|
1346
|
-
|
|
1392
|
+
let reported = calls.some((c) => c.function.name === "report_progress");
|
|
1393
|
+
for (const call of calls) {
|
|
1394
|
+
if (call.function.name === "report_progress") {
|
|
1395
|
+
await this.recordProgress(call.function.arguments);
|
|
1396
|
+
}
|
|
1397
|
+
this.messages.push({ role: "tool", tool_call_id: call.id, content: "progress recorded" });
|
|
1398
|
+
// log both sides so restores replay the exact exchange (meta-style)
|
|
1399
|
+
await this.log.append("tool_call", this.currentSession, this.currentBranch, {
|
|
1400
|
+
callId: call.id,
|
|
1401
|
+
name: call.function.name,
|
|
1402
|
+
args: safeParse(call.function.arguments),
|
|
1403
|
+
argsRaw: call.function.arguments,
|
|
1404
|
+
});
|
|
1405
|
+
await this.log.append("tool_result", this.currentSession, this.currentBranch, {
|
|
1406
|
+
callId: call.id,
|
|
1407
|
+
name: call.function.name,
|
|
1408
|
+
ok: true,
|
|
1409
|
+
durationMs: 0,
|
|
1410
|
+
result: "progress recorded",
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
if (!reported && !(res.message.content ?? "").trim()) {
|
|
1414
|
+
// degenerate: neither a tool call nor prose — record a stub so the
|
|
1415
|
+
// operator sees SOMETHING at the requested moment
|
|
1416
|
+
await this.recordProgress(JSON.stringify({ freeform: "(no report produced)" }));
|
|
1417
|
+
}
|
|
1347
1418
|
}
|
|
1348
1419
|
/* ---------- context compaction ---------- */
|
|
1349
1420
|
/**
|
|
@@ -1480,15 +1551,34 @@ export class Agent {
|
|
|
1480
1551
|
}
|
|
1481
1552
|
cut = this.safeCut(cut);
|
|
1482
1553
|
if (cut <= 0)
|
|
1483
|
-
return; // nothing safely compactable (
|
|
1554
|
+
return; // nothing safely compactable (phase never armed yet)
|
|
1484
1555
|
const old = this.messages.slice(0, cut);
|
|
1485
1556
|
const oldCount = old.length;
|
|
1486
1557
|
let summary = "";
|
|
1487
1558
|
let mode = "summarize";
|
|
1488
1559
|
let summarizedCount = oldCount;
|
|
1489
1560
|
let droppedCount = 0;
|
|
1561
|
+
// progress is visible: announce the phase up front, then stream the
|
|
1562
|
+
// summarizer's output as it generates (a long summarize used to look like
|
|
1563
|
+
// the agent silently hung — hundreds of thousands of tokens take a while)
|
|
1564
|
+
bus.emit("update", {
|
|
1565
|
+
kind: "compaction-progress",
|
|
1566
|
+
agentId: this.opts.id,
|
|
1567
|
+
phase: "summarizing",
|
|
1568
|
+
summarized: oldCount,
|
|
1569
|
+
});
|
|
1570
|
+
this.compactPhase = "summarizing";
|
|
1490
1571
|
try {
|
|
1491
|
-
summary = await this.summarize(old)
|
|
1572
|
+
summary = await this.summarize(old, (text) => {
|
|
1573
|
+
bus.emit("update", {
|
|
1574
|
+
kind: "llm-delta",
|
|
1575
|
+
agentId: this.opts.id,
|
|
1576
|
+
text: `[compact] ${text}`,
|
|
1577
|
+
reasoning: "",
|
|
1578
|
+
});
|
|
1579
|
+
});
|
|
1580
|
+
this.compactPhase = "harvesting";
|
|
1581
|
+
bus.emit("update", { kind: "compaction-progress", agentId: this.opts.id, phase: "harvesting" });
|
|
1492
1582
|
if (summary)
|
|
1493
1583
|
await this.harvestLessons(summary); // durable knowledge → memory.md
|
|
1494
1584
|
}
|
|
@@ -1502,8 +1592,11 @@ export class Agent {
|
|
|
1502
1592
|
mode = "truncate";
|
|
1503
1593
|
summarizedCount = 0;
|
|
1504
1594
|
const halfCut = this.safeCut(Math.floor(oldCount / 2));
|
|
1505
|
-
if (halfCut <= 0)
|
|
1595
|
+
if (halfCut <= 0) {
|
|
1596
|
+
this.compactPhase = "";
|
|
1597
|
+
bus.emit("update", { kind: "compaction-progress", agentId: this.opts.id, phase: "done" });
|
|
1506
1598
|
return;
|
|
1599
|
+
}
|
|
1507
1600
|
droppedCount = halfCut;
|
|
1508
1601
|
this.messages = this.messages.slice(halfCut);
|
|
1509
1602
|
}
|
|
@@ -1539,9 +1632,11 @@ export class Agent {
|
|
|
1539
1632
|
dropped: droppedCount,
|
|
1540
1633
|
mode,
|
|
1541
1634
|
});
|
|
1635
|
+
this.compactPhase = "";
|
|
1636
|
+
bus.emit("update", { kind: "compaction-progress", agentId: this.opts.id, phase: "done" });
|
|
1542
1637
|
}
|
|
1543
1638
|
/** Ask the model for dense continuation notes over the compacted range. */
|
|
1544
|
-
async summarize(old) {
|
|
1639
|
+
async summarize(old, onDelta) {
|
|
1545
1640
|
const transcript = old
|
|
1546
1641
|
.map((m) => {
|
|
1547
1642
|
const who = m.role === "tool" ? "tool" : m.role;
|
|
@@ -1561,7 +1656,10 @@ export class Agent {
|
|
|
1561
1656
|
"(gotchas, user preferences, what worked) — or omit the section if there are none.",
|
|
1562
1657
|
},
|
|
1563
1658
|
{ role: "user", content: `Conversation:\n\n${transcript}\n\nWrite the continuation notes now.` },
|
|
1564
|
-
], [], undefined)
|
|
1659
|
+
], [], undefined, // never abortable by stop(): losing the summary would lose history
|
|
1660
|
+
onDelta
|
|
1661
|
+
? (s) => onDelta(s.text)
|
|
1662
|
+
: undefined);
|
|
1565
1663
|
return res.message.content ?? "";
|
|
1566
1664
|
}
|
|
1567
1665
|
/**
|
|
@@ -1961,6 +2059,30 @@ function rebuildMessagesFrom(list) {
|
|
|
1961
2059
|
openCalls.delete(String(d.callId ?? ""));
|
|
1962
2060
|
flushUsers();
|
|
1963
2061
|
}
|
|
2062
|
+
else if (e.type === "compaction") {
|
|
2063
|
+
// A compaction REPLACED everything before this point with a summary.
|
|
2064
|
+
// Replaying the raw prefix as well resurrected the full pre-compact
|
|
2065
|
+
// history after every restart/fork (one live session came back at
|
|
2066
|
+
// ~580k tokens after a 580k→207k compaction): the model re-read weeks
|
|
2067
|
+
// of settled work it was told had been summarized, and its behavior
|
|
2068
|
+
// fell apart. Mirror the runtime transform instead — the summary note
|
|
2069
|
+
// becomes the history's head. Compactions only run at turn boundaries
|
|
2070
|
+
// (serialized on the run chain), so no tool batch can be split here.
|
|
2071
|
+
if (d.mode === "truncate") {
|
|
2072
|
+
// `dropped` IS the number of leading messages the live pass removed
|
|
2073
|
+
msgs.splice(0, Math.min(msgs.length, Number(d.dropped ?? 0)));
|
|
2074
|
+
}
|
|
2075
|
+
else {
|
|
2076
|
+
const summarized = Math.min(msgs.length, Number(d.summarized ?? 0) || msgs.length);
|
|
2077
|
+
const sum = typeof d.summary === "string" ? d.summary : "";
|
|
2078
|
+
msgs.splice(0, summarized, {
|
|
2079
|
+
role: "user",
|
|
2080
|
+
content: `[harness] Context was compacted: ${summarized} earlier messages were summarized. ` +
|
|
2081
|
+
"Goal and notes are managed by the harness (AGENTS.md / memory.md are injected into your prompt when present).\n\n" +
|
|
2082
|
+
`## Summary of earlier conversation\n${sum || "(summary unavailable)"}`,
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
1964
2086
|
else if (e.type === "progress") {
|
|
1965
2087
|
// progress events may follow an assistant report_progress call that
|
|
1966
2088
|
// has no logged tool result — patch it in when present
|
package/dist/config-schema.js
CHANGED
|
@@ -41,6 +41,9 @@ export const ConfigPatchSchema = z.object({
|
|
|
41
41
|
contextTokenBudget: z.number().int().min(1_000).nullable().optional(),
|
|
42
42
|
contextWindowTokens: z.number().int().min(1_000).optional(),
|
|
43
43
|
maxSpawnDepth: z.number().int().min(0).max(8).optional(),
|
|
44
|
+
// soft cap on LLM turns in one round; reaching it nudges the model to wrap
|
|
45
|
+
// up (not an error) — 0 disables the cap
|
|
46
|
+
maxTurnsPerRound: z.number().int().min(0).optional(),
|
|
44
47
|
tasks: z.array(TaskSchema).optional(),
|
|
45
48
|
});
|
|
46
49
|
/** Format a ZodError into a compact operator-readable message. */
|
package/dist/master.js
CHANGED
|
@@ -166,6 +166,8 @@ function printAgentEvent(e) {
|
|
|
166
166
|
line = `${c("33", "⟲ restore")} branch ${d.branch}, ${d.messages} messages`;
|
|
167
167
|
else if (d.event === "model-changed")
|
|
168
168
|
line = `${c("33", "🧦 model")} → ${d.model} (${d.provider})`;
|
|
169
|
+
else if (d.event === "round-turn-cap")
|
|
170
|
+
line = `${c("33", "⏸ round cap")} ${d.turns} turns — nudging report + wrap-up, next round continues`;
|
|
169
171
|
break;
|
|
170
172
|
}
|
|
171
173
|
default:
|
|
@@ -226,6 +228,8 @@ export class Master {
|
|
|
226
228
|
}
|
|
227
229
|
if (patch.maxSpawnDepth !== undefined)
|
|
228
230
|
this.config.maxSpawnDepth = patch.maxSpawnDepth;
|
|
231
|
+
if (patch.maxTurnsPerRound !== undefined)
|
|
232
|
+
this.config.maxTurnsPerRound = patch.maxTurnsPerRound;
|
|
229
233
|
if (patch.tasks) {
|
|
230
234
|
this.config.tasks = patch.tasks;
|
|
231
235
|
// rebuild schedule table live
|
|
@@ -375,6 +379,7 @@ export class Master {
|
|
|
375
379
|
sessionDir,
|
|
376
380
|
progressIntervalMs: this.config.progressIntervalMs,
|
|
377
381
|
...(this.config.progressMinChars ? { progressMinChars: this.config.progressMinChars } : {}),
|
|
382
|
+
...(this.config.maxTurnsPerRound !== undefined ? { maxTurnsPerRound: this.config.maxTurnsPerRound } : {}),
|
|
378
383
|
autoContinue: ac.autoContinue ?? true,
|
|
379
384
|
...(this.config.contextTokenBudget ? { contextTokenBudget: this.config.contextTokenBudget } : {}),
|
|
380
385
|
...(ac.contextWindowTokens || this.config.contextWindowTokens || inferredWindow
|