teapot-coding-agent 0.21.0 → 0.22.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 +121 -9
- package/dist/agent/tools.js +7 -0
- package/dist/master.js +12 -2
- package/package.json +1 -1
- package/public/assets/index-BYPOxhDK.js +30 -0
- package/public/assets/index-BYPOxhDK.js.map +1 -0
- package/public/assets/index-DTpRHulN.css +1 -0
- package/public/assets/{shiki-Vlf9Oct9.js → shiki-CNn7MpJ1.js} +2 -2
- package/public/assets/{shiki-Vlf9Oct9.js.map → shiki-CNn7MpJ1.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-DAwoZAYb.js +0 -30
- package/public/assets/index-DAwoZAYb.js.map +0 -1
- package/public/assets/index-DuSLfBE1.css +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -10,7 +10,7 @@ import { promises as fs, existsSync, readFileSync, readdirSync, statSync } from
|
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { EventLog, readEvents } from "../log/events.js";
|
|
12
12
|
import { chat, chatStream } from "./llm.js";
|
|
13
|
-
import { executeTool, toolSpecs, currentSkills } from "./tools.js";
|
|
13
|
+
import { executeTool, toolSpecs, currentSkills, safeJoin, isContextOverflow } from "./tools.js";
|
|
14
14
|
import { bus } from "../bus.js";
|
|
15
15
|
/**
|
|
16
16
|
* SYSTEM_TEMPLATE must stay byte-identical across every request of a session:
|
|
@@ -171,6 +171,7 @@ export class Agent {
|
|
|
171
171
|
onIdlePark: (reason) => this.parkForTool(reason),
|
|
172
172
|
onIdleUnpark: () => this.unparkFromTool(),
|
|
173
173
|
onBackgroundExit: (info) => void this.onBackgroundJobExit(info),
|
|
174
|
+
onFileRead: (p) => this.trackFileRead(p),
|
|
174
175
|
};
|
|
175
176
|
// the session id IS the directory name — one directory per incarnation
|
|
176
177
|
this.mainSession = path.basename(opts.sessionDir);
|
|
@@ -265,6 +266,11 @@ export class Agent {
|
|
|
265
266
|
* agent learns exit code + output tail WITHOUT having to poll
|
|
266
267
|
* bash_output blindly. Failed jobs say so loudly so the model reacts.
|
|
267
268
|
*/
|
|
269
|
+
/** recently read workspace files (most recent first) — post-compact restore */
|
|
270
|
+
recentReads = [];
|
|
271
|
+
trackFileRead(p) {
|
|
272
|
+
this.recentReads = [p, ...this.recentReads.filter((x) => x !== p)].slice(0, 5);
|
|
273
|
+
}
|
|
268
274
|
onBackgroundJobExit(info) {
|
|
269
275
|
const ok = info.code === 0;
|
|
270
276
|
const one = info.cmd.replace(/\s+/g, " ").slice(0, 80);
|
|
@@ -1182,6 +1188,25 @@ export class Agent {
|
|
|
1182
1188
|
detail: "stopped before any output arrived",
|
|
1183
1189
|
});
|
|
1184
1190
|
}
|
|
1191
|
+
// Provider context overflow → compact ONCE and retry the turn
|
|
1192
|
+
// (OpenCode-style overflow recovery). even with autoCompact off, an
|
|
1193
|
+
// actual overflow is proof the estimate missed; a second overflow is
|
|
1194
|
+
// returned as a real error instead of looping.
|
|
1195
|
+
if (isContextOverflow(err) && !this.stopRequested) {
|
|
1196
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
1197
|
+
event: "overflow-recovery",
|
|
1198
|
+
detail: String(err.message).slice(0, 200),
|
|
1199
|
+
});
|
|
1200
|
+
const before = this.stats.compactions;
|
|
1201
|
+
await this.maybeCompact(true);
|
|
1202
|
+
if (this.stats.compactions > before) {
|
|
1203
|
+
await this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
1204
|
+
event: "overflow-recovered",
|
|
1205
|
+
});
|
|
1206
|
+
guard--; // the retry does not count against this round's cap
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1185
1210
|
throw err;
|
|
1186
1211
|
}
|
|
1187
1212
|
if (res.usage) {
|
|
@@ -1619,10 +1644,30 @@ export class Agent {
|
|
|
1619
1644
|
await this.setGoalStatus("done");
|
|
1620
1645
|
}
|
|
1621
1646
|
}
|
|
1647
|
+
// Capture the last few conversation turns BEFORE the final marker so the
|
|
1648
|
+
// parent gets real substance (findings, file paths, numbers), not just
|
|
1649
|
+
// whatever the model typed into the finish() summary argument.
|
|
1650
|
+
const recent = [];
|
|
1651
|
+
for (let i = this.messages.length - 1; i >= 0 && recent.length < 6; i--) {
|
|
1652
|
+
const m = this.messages[i];
|
|
1653
|
+
if (m.role === "user")
|
|
1654
|
+
continue; // prompts/noise — keep agent output only
|
|
1655
|
+
if (m.role === "assistant" && !(m.content ?? "").trim())
|
|
1656
|
+
continue;
|
|
1657
|
+
const who = m.role === "tool" ? "tool" : m.role;
|
|
1658
|
+
let line = `[${who}] ${(m.content ?? "").trim()}`;
|
|
1659
|
+
if (m.role === "tool") {
|
|
1660
|
+
// tool results are prefixed "(failed)" by the logger when they fail —
|
|
1661
|
+
// the raw content is what matters here, and cap hard: these can be huge
|
|
1662
|
+
line = `[${who}] ${(m.content ?? "").trim().slice(0, 1200)}`;
|
|
1663
|
+
}
|
|
1664
|
+
recent.unshift(line.slice(0, 1500));
|
|
1665
|
+
}
|
|
1622
1666
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
1623
1667
|
role: "assistant",
|
|
1624
1668
|
final: true,
|
|
1625
1669
|
content: String(args.summary ?? ""),
|
|
1670
|
+
...(recent.length ? { recentContext: recent } : {}),
|
|
1626
1671
|
});
|
|
1627
1672
|
}
|
|
1628
1673
|
async maybeRequestProgress() {
|
|
@@ -1772,14 +1817,33 @@ export class Agent {
|
|
|
1772
1817
|
break;
|
|
1773
1818
|
}
|
|
1774
1819
|
const PRUNE_MIN = 3_000;
|
|
1820
|
+
// Tool-kind aware pruning (Claude Code MicroCompact style): high-volume,
|
|
1821
|
+
// REPRODUCIBLE results (file reads, shell, grep) are safe to clip because
|
|
1822
|
+
// the model can re-issue them; one-shot results (spawned agent reports,
|
|
1823
|
+
// web fetches) are kept — re-fetching is impossible or expensive.
|
|
1824
|
+
const PRUNABLE_TOOLS = new Set([
|
|
1825
|
+
"bash", "read_file", "list_dir", "grep", "glob", "write_file", "edit_file", "apply_patch",
|
|
1826
|
+
"bash_output",
|
|
1827
|
+
]);
|
|
1775
1828
|
let saved = 0;
|
|
1776
1829
|
let count = 0;
|
|
1830
|
+
// callId → tool name, from the assistant turn that issued each call
|
|
1831
|
+
// (a tool result's PREVIOUS message is not reliably its caller when
|
|
1832
|
+
// parallel calls interleave)
|
|
1833
|
+
const callerTool = new Map();
|
|
1834
|
+
for (const m of this.messages) {
|
|
1835
|
+
for (const t of m.tool_calls ?? [])
|
|
1836
|
+
callerTool.set(t.id, t.function.name);
|
|
1837
|
+
}
|
|
1777
1838
|
for (let i = 0; i < boundary; i++) {
|
|
1778
1839
|
const m = this.messages[i];
|
|
1779
|
-
|
|
1840
|
+
const toolName = m.tool_call_id ? callerTool.get(m.tool_call_id) ?? "" : "";
|
|
1841
|
+
if (m.role === "tool" &&
|
|
1842
|
+
PRUNABLE_TOOLS.has(toolName) &&
|
|
1843
|
+
(m.content?.length ?? 0) > PRUNE_MIN) {
|
|
1780
1844
|
const len = m.content.length;
|
|
1781
1845
|
saved += len - 400;
|
|
1782
|
-
m.content = m.content.slice(0, 400) + `\n…[pruned ${len} bytes of tool output]`;
|
|
1846
|
+
m.content = m.content.slice(0, 400) + `\n…[pruned ${len} bytes of ${toolName || "tool"} output]`;
|
|
1783
1847
|
count++;
|
|
1784
1848
|
}
|
|
1785
1849
|
}
|
|
@@ -1883,6 +1947,31 @@ export class Agent {
|
|
|
1883
1947
|
const after = this.estimateTokens();
|
|
1884
1948
|
this.stats.compactions++;
|
|
1885
1949
|
this.compactedAtLen = this.messages.length;
|
|
1950
|
+
// Post-compact file restore (Claude Code style): re-attach the most
|
|
1951
|
+
// recently read files so the model doesn't burn its first turns re-reading
|
|
1952
|
+
// exactly what it had in context a moment ago. Budget: 3 files × 400 lines.
|
|
1953
|
+
if (this.recentReads.length && mode === "summarize") {
|
|
1954
|
+
const restored = [];
|
|
1955
|
+
for (const rel of this.recentReads.slice(0, 3)) {
|
|
1956
|
+
try {
|
|
1957
|
+
const abs = safeJoin(this.opts.workspace, rel);
|
|
1958
|
+
const text = await fs.readFile(abs, "utf8");
|
|
1959
|
+
const head = text.split("\n").slice(0, 400).join("\n");
|
|
1960
|
+
restored.push(`--- ${rel}${text.length > head.length ? " (first 400 lines)" : ""} ---\n${head}`);
|
|
1961
|
+
}
|
|
1962
|
+
catch {
|
|
1963
|
+
/* file vanished since the read — skip */
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
if (restored.length) {
|
|
1967
|
+
this.messages.splice(1, 0, {
|
|
1968
|
+
role: "user",
|
|
1969
|
+
content: `[harness] These files were recently read and are re-attached for continuity ` +
|
|
1970
|
+
`(they may have changed — re-read if precision matters):\n\n${restored.join("\n\n")}`,
|
|
1971
|
+
});
|
|
1972
|
+
this.recentReads = this.recentReads.slice(0, restored.length); // keep order, drop misses
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1886
1975
|
// the operator can inspect WHAT was remembered — a dedicated typed event
|
|
1887
1976
|
// (not an agent message) keeps it out of the model's own voice
|
|
1888
1977
|
await this.log.append("compaction", this.currentSession, this.currentBranch, {
|
|
@@ -1915,21 +2004,44 @@ export class Agent {
|
|
|
1915
2004
|
.join("\n\n")
|
|
1916
2005
|
.slice(-120_000);
|
|
1917
2006
|
const fn = this.opts.chatFn ?? chat;
|
|
2007
|
+
// Structured sections (Claude Code style): fixed headings survive MULTIPLE
|
|
2008
|
+
// successive compactions far better than free-form prose — each pass knows
|
|
2009
|
+
// where "pending tasks" vs "user messages" live, so nothing silently
|
|
2010
|
+
// merges away. The <analysis> scratchpad gives the model a thinking space;
|
|
2011
|
+
// it is stripped from the stored summary (formatCompactSummary).
|
|
1918
2012
|
const res = await fn(this.opts.llm, [
|
|
1919
2013
|
{
|
|
1920
2014
|
role: "system",
|
|
1921
|
-
content: "You compress a coding agent's conversation into dense notes for it to continue working
|
|
1922
|
-
"
|
|
1923
|
-
"
|
|
1924
|
-
|
|
1925
|
-
"
|
|
2015
|
+
content: "You compress a coding agent's conversation into dense notes for it to continue working.\n\n" +
|
|
2016
|
+
"First think inside an <analysis> block: what mattered, what changed, what is unresolved.\n" +
|
|
2017
|
+
"Then output ONLY the following sections, each starting with its exact heading:\n\n" +
|
|
2018
|
+
"## Primary request and intent\n" +
|
|
2019
|
+
"## Key technical concepts\n" +
|
|
2020
|
+
"## Files and code sections\n" +
|
|
2021
|
+
"## Errors and fixes\n" +
|
|
2022
|
+
"## Problem solving\n" +
|
|
2023
|
+
"## All user messages\n" + // verbatim-ish: intent survives repeated compaction
|
|
2024
|
+
"## Pending tasks\n" +
|
|
2025
|
+
"## Current work\n" +
|
|
2026
|
+
"## Next step\n" +
|
|
2027
|
+
"## Durable lessons\n\n" +
|
|
2028
|
+
"Rules: be terse bullet points, no prose flourishes. Quote file paths and key numbers exactly. " +
|
|
2029
|
+
"All user messages are listed nearly verbatim — they carry intent that must survive every future compaction. " +
|
|
2030
|
+
'Durable lessons lists reusable insights worth keeping forever (gotchas, preferences, what worked); omit the section if empty. ' +
|
|
2031
|
+
"The <analysis> block is discarded before your notes are stored, so think freely there.",
|
|
1926
2032
|
},
|
|
1927
2033
|
{ role: "user", content: `Conversation:\n\n${transcript}\n\nWrite the continuation notes now.` },
|
|
1928
2034
|
], [], undefined, // never abortable by stop(): losing the summary would lose history
|
|
1929
2035
|
onDelta
|
|
1930
2036
|
? (s) => onDelta(s.text)
|
|
1931
2037
|
: undefined);
|
|
1932
|
-
return res.message.content ?? "";
|
|
2038
|
+
return this.formatCompactSummary(res.message.content ?? "");
|
|
2039
|
+
}
|
|
2040
|
+
/** strip the <analysis> scratchpad — it improves thinking but must not
|
|
2041
|
+
consume post-compact context tokens */
|
|
2042
|
+
formatCompactSummary(raw) {
|
|
2043
|
+
const m = raw.match(/<analysis>[\s\S]*?<\/analysis>\s*/);
|
|
2044
|
+
return m ? raw.slice(m[0].length).trim() : raw.trim();
|
|
1933
2045
|
}
|
|
1934
2046
|
/**
|
|
1935
2047
|
* Extract the "## Durable lessons" block from a compaction summary and
|
package/dist/agent/tools.js
CHANGED
|
@@ -103,6 +103,12 @@ function startBackgroundShell(cmd, ctx) {
|
|
|
103
103
|
map.set(id, sh);
|
|
104
104
|
return id;
|
|
105
105
|
}
|
|
106
|
+
/** Heuristic: did this provider error mean "prompt exceeded the context window"?
|
|
107
|
+
* Wording varies by provider (OpenRouter/OpenAI/Anthropic/local servers). */
|
|
108
|
+
export function isContextOverflow(err) {
|
|
109
|
+
const msg = String(err?.message ?? "").toLowerCase();
|
|
110
|
+
return (/context (length|window|limit)|too many tokens|prompt is too long|max.{0,12}tokens.{0,20}(exceed|surpass)|request too large|payload too large|exceeds.{0,20}limit/.test(msg) || /\b(400|413)\b.*token/.test(msg));
|
|
111
|
+
}
|
|
106
112
|
/** Run a command in its own process group; kill the whole group on timeout. */
|
|
107
113
|
function runShell(cmd, ctx, timeoutMs) {
|
|
108
114
|
return new Promise((resolve) => {
|
|
@@ -515,6 +521,7 @@ export const TOOLS = [
|
|
|
515
521
|
async run(args, ctx) {
|
|
516
522
|
const p = safeJoin(ctx.cwd, str(args.path));
|
|
517
523
|
const text = await readText(p);
|
|
524
|
+
ctx.onFileRead?.(str(args.path));
|
|
518
525
|
const lines = text.split("\n");
|
|
519
526
|
// grep mode
|
|
520
527
|
if (typeof args.pattern === "string" && args.pattern !== "") {
|
package/dist/master.js
CHANGED
|
@@ -666,8 +666,18 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
666
666
|
return;
|
|
667
667
|
// forward outcomes so the parent hears the result without polling
|
|
668
668
|
if (e.type === "message" && e.data.final) {
|
|
669
|
-
const
|
|
670
|
-
|
|
669
|
+
const d = e.data;
|
|
670
|
+
const summary = String(d.content ?? "").trim();
|
|
671
|
+
// the finish() summary alone is often thin — the agent's last few real
|
|
672
|
+
// conversation turns (findings, paths, numbers) carry the substance.
|
|
673
|
+
// Include them so the parent can act without re-reading the child log.
|
|
674
|
+
let report = summary;
|
|
675
|
+
if (Array.isArray(d.recentContext) && d.recentContext.length) {
|
|
676
|
+
report +=
|
|
677
|
+
"\n\n[Last conversation turns before finishing]\n" +
|
|
678
|
+
d.recentContext.map((r) => r).join("\n\n");
|
|
679
|
+
}
|
|
680
|
+
parent.enqueuePrompt(`[harness] Sub-agent ${ac.id} finished. Final report:\n${report.slice(0, 6000) || "(no summary)"}`, "harness");
|
|
671
681
|
}
|
|
672
682
|
else if (e.type === "error") {
|
|
673
683
|
parent.enqueuePrompt(`[harness] Sub-agent ${ac.id} hit an error: ${String(e.data.message ?? "").slice(0, 500)}`, "harness");
|