teapot-coding-agent 0.20.3 → 0.21.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/dist/agent/agent.js +144 -3
- package/dist/agent/tools.js +23 -5
- package/package.json +1 -1
- package/public/assets/{index-C2TbSfLS.js → index-DAwoZAYb.js} +5 -5
- package/public/assets/index-DAwoZAYb.js.map +1 -0
- package/public/assets/{index-CdlCF52N.css → index-DuSLfBE1.css} +1 -1
- package/public/assets/{shiki-kwez3hxP.js → shiki-Vlf9Oct9.js} +2 -2
- package/public/assets/{shiki-kwez3hxP.js.map → shiki-Vlf9Oct9.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-C2TbSfLS.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* periodic behaviour (progress reports, scheduled tasks) is driven by the
|
|
7
7
|
* master's single low-frequency scheduler tick or by turn boundaries.
|
|
8
8
|
*/
|
|
9
|
-
import { promises as fs } from "node:fs";
|
|
9
|
+
import { promises as fs, existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { EventLog, readEvents } from "../log/events.js";
|
|
12
12
|
import { chat, chatStream } from "./llm.js";
|
|
@@ -170,6 +170,7 @@ export class Agent {
|
|
|
170
170
|
readOnly: this.opts.readOnlyTools,
|
|
171
171
|
onIdlePark: (reason) => this.parkForTool(reason),
|
|
172
172
|
onIdleUnpark: () => this.unparkFromTool(),
|
|
173
|
+
onBackgroundExit: (info) => void this.onBackgroundJobExit(info),
|
|
173
174
|
};
|
|
174
175
|
// the session id IS the directory name — one directory per incarnation
|
|
175
176
|
this.mainSession = path.basename(opts.sessionDir);
|
|
@@ -256,6 +257,28 @@ export class Agent {
|
|
|
256
257
|
* operator isn't staring at a running spinner for minutes. The run chain is
|
|
257
258
|
* still parked inside the tool — but any prompt/stop wakes it immediately.
|
|
258
259
|
*/
|
|
260
|
+
/**
|
|
261
|
+
* A background shell (bash background=true) finished. Two channels:
|
|
262
|
+
* 1. a system_note row in the timeline — the operator sees the outcome
|
|
263
|
+
* without opening anything;
|
|
264
|
+
* 2. a queued harness prompt, delivered at the next turn boundary — the
|
|
265
|
+
* agent learns exit code + output tail WITHOUT having to poll
|
|
266
|
+
* bash_output blindly. Failed jobs say so loudly so the model reacts.
|
|
267
|
+
*/
|
|
268
|
+
onBackgroundJobExit(info) {
|
|
269
|
+
const ok = info.code === 0;
|
|
270
|
+
const one = info.cmd.replace(/\s+/g, " ").slice(0, 80);
|
|
271
|
+
void this.log.append("system_note", this.currentSession, this.currentBranch, {
|
|
272
|
+
event: "background-exit",
|
|
273
|
+
jobId: info.id,
|
|
274
|
+
code: info.code,
|
|
275
|
+
durationMs: info.durationMs,
|
|
276
|
+
cmd: one,
|
|
277
|
+
failed: !ok,
|
|
278
|
+
});
|
|
279
|
+
// queue the report for the next turn boundary (agent-visible channel)
|
|
280
|
+
this.onBackgroundJobExitQueued(info);
|
|
281
|
+
}
|
|
259
282
|
parkForTool(reason) {
|
|
260
283
|
if (this.parkedByTool || this.status !== "running")
|
|
261
284
|
return;
|
|
@@ -808,6 +831,7 @@ export class Agent {
|
|
|
808
831
|
* call payload — not merely when it was logged.
|
|
809
832
|
*/
|
|
810
833
|
drainPendingPrompts() {
|
|
834
|
+
this.drainBackgroundExits();
|
|
811
835
|
for (const p of this.pendingPrompts.splice(0)) {
|
|
812
836
|
// multimodal: images ride as OpenAI content parts (text first, then
|
|
813
837
|
// images); plain prompts keep the string form for cache friendliness
|
|
@@ -843,6 +867,34 @@ export class Agent {
|
|
|
843
867
|
}
|
|
844
868
|
/** ids of user prompts that have entered an LLM call payload (recent first-capped) */
|
|
845
869
|
deliveredPrompts = [];
|
|
870
|
+
/**
|
|
871
|
+
* Background jobs that exited since the last LLM call. Their outcomes are
|
|
872
|
+
* folded into ONE harness message at the turn boundary — the agent learns
|
|
873
|
+
* exit codes and output tails without polling bash_output.
|
|
874
|
+
*/
|
|
875
|
+
bgExits = [];
|
|
876
|
+
onBackgroundJobExitQueued(info) {
|
|
877
|
+
this.bgExits.push(info);
|
|
878
|
+
if (this.bgExits.length > 32)
|
|
879
|
+
this.bgExits.shift(); // cap runaway spam
|
|
880
|
+
bus.emit("update", { kind: "agent-update", agentId: this.opts.id });
|
|
881
|
+
}
|
|
882
|
+
/** fold queued background-exit reports into the next LLM call (once each) */
|
|
883
|
+
drainBackgroundExits() {
|
|
884
|
+
if (!this.bgExits.length)
|
|
885
|
+
return;
|
|
886
|
+
const exits = this.bgExits.splice(0);
|
|
887
|
+
const lines = exits.map((e) => {
|
|
888
|
+
const status = e.code === 0 ? "finished OK" : `FAILED with exit code ${e.code ?? "?"}`;
|
|
889
|
+
const tail = e.outputTail.trim();
|
|
890
|
+
return (`- job ${e.id}: ${status} after ${(e.durationMs / 1000).toFixed(1)}s — ${e.cmd.slice(0, 100)}` +
|
|
891
|
+
(tail ? `\n output tail:\n${tail.split("\n").slice(-8).map((l) => " | " + l).join("\n")}` : ""));
|
|
892
|
+
});
|
|
893
|
+
const text = `[harness] Background job report:\n${lines.join("\n")}\n\n` +
|
|
894
|
+
"React to failures now if needed. Full output stays available via bash_output(job_id).";
|
|
895
|
+
this.messages.push({ role: "user", content: text });
|
|
896
|
+
void this.log.append("prompt", this.currentSession, this.currentBranch, { source: "harness", text }).catch(() => { });
|
|
897
|
+
}
|
|
846
898
|
/** Resolves when all queued work (including a running loop) has settled. */
|
|
847
899
|
settled() {
|
|
848
900
|
return this.runChain.catch(() => { });
|
|
@@ -1418,11 +1470,100 @@ export class Agent {
|
|
|
1418
1470
|
return finished;
|
|
1419
1471
|
}
|
|
1420
1472
|
buildMessages() {
|
|
1421
|
-
// deliberately static: [system] + append-only history keeps prefix caches hot
|
|
1473
|
+
// deliberately static: [system] + append-only history keeps prefix caches hot.
|
|
1474
|
+
// The workspace context block (AGENTS.md + ls snapshot) is appended to the
|
|
1475
|
+
// system message ONCE per session — changing it mid-session would re-price
|
|
1476
|
+
// the whole prefix cache, so it is captured at first use and frozen.
|
|
1477
|
+
const sys = this.systemPrompt();
|
|
1422
1478
|
const hasSystem = this.messages[0]?.role === "system";
|
|
1423
|
-
const head = [{ role: "system", content:
|
|
1479
|
+
const head = [{ role: "system", content: sys }];
|
|
1424
1480
|
return hasSystem ? [...head, ...this.messages.slice(1)] : [...head, ...this.messages];
|
|
1425
1481
|
}
|
|
1482
|
+
/** AGENTS.md content cached for the session (empty string = none/read-failed) */
|
|
1483
|
+
agentsMd = "";
|
|
1484
|
+
agentsMdLoaded = false;
|
|
1485
|
+
/** one-time workspace listing injected with the system prompt */
|
|
1486
|
+
wsSnapshot = "";
|
|
1487
|
+
systemPrompt() {
|
|
1488
|
+
if (!this.agentsMdLoaded) {
|
|
1489
|
+
this.agentsMdLoaded = true;
|
|
1490
|
+
// AGENTS.md + workspace listing are captured ONCE and PERSISTED to the
|
|
1491
|
+
// session dir, so a restart re-issues the identical system prompt (the
|
|
1492
|
+
// prefix cache survives; the snapshot is a session artifact, not live
|
|
1493
|
+
// truth — list_dir shows current state).
|
|
1494
|
+
const snapPath = path.join(this.opts.sessionDir, "workspace-snapshot.txt");
|
|
1495
|
+
try {
|
|
1496
|
+
// a previous incarnation already froze a snapshot → reuse it verbatim.
|
|
1497
|
+
// The sentinel covers EMPTY workspaces too ("" would look like "no
|
|
1498
|
+
// file" and make the restart re-list, breaking prefix-cache identity).
|
|
1499
|
+
this.wsSnapshot = readFileSync(snapPath, "utf8");
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
this.wsSnapshot = "";
|
|
1503
|
+
}
|
|
1504
|
+
if (this.wsSnapshot === "" && existsSync(snapPath)) {
|
|
1505
|
+
this.wsSnapshot = "(empty workspace)"; // the file exists but was empty
|
|
1506
|
+
}
|
|
1507
|
+
if (!this.wsSnapshot) {
|
|
1508
|
+
try {
|
|
1509
|
+
this.agentsMd = existsSync(path.join(this.opts.workspace, "AGENTS.md"))
|
|
1510
|
+
? readFileSync(path.join(this.opts.workspace, "AGENTS.md"), "utf8").slice(0, 12_000)
|
|
1511
|
+
: "";
|
|
1512
|
+
}
|
|
1513
|
+
catch {
|
|
1514
|
+
this.agentsMd = "";
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
const entries = readdirSync(this.opts.workspace, { withFileTypes: true });
|
|
1518
|
+
const lines = [];
|
|
1519
|
+
for (const e of entries.slice(0, 60)) {
|
|
1520
|
+
if (e.name.startsWith("."))
|
|
1521
|
+
continue;
|
|
1522
|
+
let size = "";
|
|
1523
|
+
if (!e.isDirectory()) {
|
|
1524
|
+
try {
|
|
1525
|
+
size = `${(statSync(path.join(this.opts.workspace, e.name)).size / 1024).toFixed(1)}K`;
|
|
1526
|
+
}
|
|
1527
|
+
catch {
|
|
1528
|
+
/* vanished */
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
lines.push(e.isDirectory() ? `${e.name}/` : `${e.name} (${size})`);
|
|
1532
|
+
}
|
|
1533
|
+
this.wsSnapshot = lines.join("\n");
|
|
1534
|
+
if (!this.wsSnapshot)
|
|
1535
|
+
this.wsSnapshot = "(empty workspace)";
|
|
1536
|
+
try {
|
|
1537
|
+
void fs.writeFile(snapPath, this.wsSnapshot, "utf8");
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
/* best-effort persistence */
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
catch {
|
|
1544
|
+
this.wsSnapshot = "(workspace not readable)";
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
try {
|
|
1549
|
+
this.agentsMd = existsSync(path.join(this.opts.workspace, "AGENTS.md"))
|
|
1550
|
+
? readFileSync(path.join(this.opts.workspace, "AGENTS.md"), "utf8").slice(0, 12_000)
|
|
1551
|
+
: "";
|
|
1552
|
+
}
|
|
1553
|
+
catch {
|
|
1554
|
+
this.agentsMd = "";
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
let sys = SYSTEM_TEMPLATE;
|
|
1559
|
+
if (this.agentsMd.trim()) {
|
|
1560
|
+
sys += `\n\n## Project instructions (AGENTS.md)\n\n${this.agentsMd}`;
|
|
1561
|
+
}
|
|
1562
|
+
if (this.wsSnapshot) {
|
|
1563
|
+
sys += `\n\n## Workspace snapshot (top level)\n${this.wsSnapshot}\nUse list_dir/read_file for anything deeper — do NOT list again what is already here.`;
|
|
1564
|
+
}
|
|
1565
|
+
return sys;
|
|
1566
|
+
}
|
|
1426
1567
|
async handleFinish(argsJson) {
|
|
1427
1568
|
const args = safeParse(argsJson);
|
|
1428
1569
|
if (args.goalComplete === true && this.goal.status !== "done") {
|
package/dist/agent/tools.js
CHANGED
|
@@ -83,7 +83,23 @@ function startBackgroundShell(cmd, ctx) {
|
|
|
83
83
|
};
|
|
84
84
|
child.stdout?.on("data", collect);
|
|
85
85
|
child.stderr?.on("data", collect);
|
|
86
|
-
child.on("close", (code) => {
|
|
86
|
+
child.on("close", (code) => {
|
|
87
|
+
sh.exited = true;
|
|
88
|
+
sh.code = code;
|
|
89
|
+
// tell the harness so the agent learns the outcome WITHOUT polling
|
|
90
|
+
try {
|
|
91
|
+
ctx.onBackgroundExit?.({
|
|
92
|
+
id,
|
|
93
|
+
code,
|
|
94
|
+
cmd,
|
|
95
|
+
durationMs: Date.now() - sh.startedAt,
|
|
96
|
+
outputTail: sh.out.slice(-2000),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
/* notification is best-effort */
|
|
101
|
+
}
|
|
102
|
+
});
|
|
87
103
|
map.set(id, sh);
|
|
88
104
|
return id;
|
|
89
105
|
}
|
|
@@ -709,8 +725,9 @@ export const TOOLS = [
|
|
|
709
725
|
timeout_ms: { type: "number", description: `default ${DEFAULT_TIMEOUT_MS}` },
|
|
710
726
|
background: {
|
|
711
727
|
type: "boolean",
|
|
712
|
-
description: "true = start the command and return IMMEDIATELY with a job id.
|
|
713
|
-
"
|
|
728
|
+
description: "true = start the command and return IMMEDIATELY with a job id. You are NOTIFIED at your " +
|
|
729
|
+
"next turn boundary when it exits (exit code + output tail) — no polling needed. " +
|
|
730
|
+
"Use bash_output only to fetch FULL output mid-run or after. Use for dev servers, watchers, long builds.",
|
|
714
731
|
},
|
|
715
732
|
},
|
|
716
733
|
required: ["command"],
|
|
@@ -720,8 +737,9 @@ export const TOOLS = [
|
|
|
720
737
|
const id = startBackgroundShell(str(args.command), ctx);
|
|
721
738
|
return {
|
|
722
739
|
ok: true,
|
|
723
|
-
result: `started in background (job ${id}).
|
|
724
|
-
`
|
|
740
|
+
result: `started in background (job ${id}). It runs while you work — you'll be notified here ` +
|
|
741
|
+
`when it exits (exit code + output tail). Full output: bash_output(job_id="${id}"). ` +
|
|
742
|
+
`Kill: bash_output(job_id="${id}", action="kill").`,
|
|
725
743
|
};
|
|
726
744
|
}
|
|
727
745
|
return runShell(str(args.command), ctx, Math.min(num(args.timeout_ms, ctx.defaultTimeoutMs), 600_000));
|