teapot-coding-agent 0.27.2 → 0.28.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 +30 -7
- package/dist/agent/tools.js +2 -1
- package/dist/index.js +50 -14
- package/dist/master.js +23 -1
- package/dist/server/api.js +24 -0
- package/package.json +1 -1
- package/public/assets/index-DWsd_EOl.js +30 -0
- package/public/assets/index-DWsd_EOl.js.map +1 -0
- package/public/assets/{shiki-DYylGhua.js → shiki-D0LgHyHH.js} +2 -2
- package/public/assets/{shiki-DYylGhua.js.map → shiki-D0LgHyHH.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-NorOxnzt.js +0 -30
- package/public/assets/index-NorOxnzt.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -27,11 +27,35 @@ import { bus } from "../bus.js";
|
|
|
27
27
|
* satisfied the request — sent a voluntary report_progress right after, and
|
|
28
28
|
* every requested report appeared TWICE on the timeline.
|
|
29
29
|
*/
|
|
30
|
-
export const AUTO_CONTINUE_NUDGE =
|
|
30
|
+
export const AUTO_CONTINUE_NUDGE =
|
|
31
|
+
// #136: the prefix marks this as harness text, not something the operator typed.
|
|
32
|
+
// Unprefixed, the model read it as a request to reply — and prose ends the round
|
|
33
|
+
// without finishing, which starts the next one.
|
|
34
|
+
"[harness] " +
|
|
35
|
+
"Continue working toward the current goal. If you are blocked, explain why briefly. " +
|
|
31
36
|
// name the tool explicitly, the way the sub-agent nudge already does
|
|
32
37
|
"When the goal is genuinely met — or truly blocked — call finish() " +
|
|
33
38
|
"(finish(goalComplete=true) when met, finish(goalComplete=false) when blocked) " +
|
|
34
39
|
"instead of replying in prose; prose here does not stop the loop.";
|
|
40
|
+
/**
|
|
41
|
+
* The auto-continue nudge with the CURRENT GOAL restated.
|
|
42
|
+
*
|
|
43
|
+
* #136: the nudge said "the current goal" and never said what it was. The sub-agent
|
|
44
|
+
* variant has always included its task text for exactly this reason — a forked
|
|
45
|
+
* child otherwise drifts back into the inherited conversation and keeps going
|
|
46
|
+
* forever (#106) — but the ROOT nudge had no such reminder, so a root agent that
|
|
47
|
+
* had lost the thread had nothing to re-anchor on and simply continued until the
|
|
48
|
+
* `maxConsecutiveIdleRounds` valve tripped.
|
|
49
|
+
*
|
|
50
|
+
* The goal is clipped like the sub-agent's: a 20k-character goal would otherwise
|
|
51
|
+
* re-enter the context on EVERY round.
|
|
52
|
+
*/
|
|
53
|
+
export function autoContinueNudge(goalText) {
|
|
54
|
+
const goal = goalText.trim();
|
|
55
|
+
if (!goal)
|
|
56
|
+
return AUTO_CONTINUE_NUDGE;
|
|
57
|
+
return `${AUTO_CONTINUE_NUDGE}\nCurrent goal:\n=====\n${goal.slice(0, 1500)}\n=====`;
|
|
58
|
+
}
|
|
35
59
|
export const PROGRESS_REQUEST =
|
|
36
60
|
// The leading sentence is kept VERBATIM: it is what existing harness prompts,
|
|
37
61
|
// tests and any operator muscle-memory key off. #45 changed the INSTRUCTION
|
|
@@ -1586,9 +1610,13 @@ export class Agent {
|
|
|
1586
1610
|
break;
|
|
1587
1611
|
}
|
|
1588
1612
|
const isSub = !!this.opts.parent;
|
|
1613
|
+
// #136: the ROOT nudge now restates the goal too, exactly as the sub-agent
|
|
1614
|
+
// one always has. Both branches need it — a child drifts back into the
|
|
1615
|
+
// inherited conversation, a root agent loses the thread, and in both cases
|
|
1616
|
+
// "continue toward the current goal" is unactionable without the goal text.
|
|
1589
1617
|
const nudge = isSub
|
|
1590
1618
|
? `[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().`
|
|
1591
|
-
:
|
|
1619
|
+
: autoContinueNudge(this.goal.text);
|
|
1592
1620
|
await this.log.append("prompt", this.currentSession, this.currentBranch, {
|
|
1593
1621
|
source: "harness",
|
|
1594
1622
|
text: nudge,
|
|
@@ -1632,11 +1660,6 @@ export class Agent {
|
|
|
1632
1660
|
if (!this.stopRequested && !this.awaitingUser)
|
|
1633
1661
|
this.setStatus("idle", "round complete");
|
|
1634
1662
|
}
|
|
1635
|
-
/**
|
|
1636
|
-
* One LLM call with a fresh abort controller so stop() can interrupt it
|
|
1637
|
-
* immediately, plus loop-level retries for provider flakiness (the SDK
|
|
1638
|
-
* already backsoff 429/5xx; this covers exhausted rate limits and 400s).
|
|
1639
|
-
*/
|
|
1640
1663
|
/**
|
|
1641
1664
|
* One LLM call with a fresh abort controller so stop() can interrupt it
|
|
1642
1665
|
* immediately, plus loop-level retries for provider flakiness (the SDK
|
package/dist/agent/tools.js
CHANGED
|
@@ -1195,7 +1195,8 @@ export const TOOLS = [
|
|
|
1195
1195
|
// #126: the single most common edit_file failure in the real logs (22
|
|
1196
1196
|
// occurrences) said only "old_text is required", while the tool's own
|
|
1197
1197
|
// schema already documents the better form. Name it and say what to do —
|
|
1198
|
-
// the pattern master.ts uses for "not your sub-agent" (
|
|
1198
|
+
// the pattern master.ts uses for "not your sub-agent" (its
|
|
1199
|
+
// `messageChild`),
|
|
1199
1200
|
// which turned a confusing refusal into a re-addressable one.
|
|
1200
1201
|
if (!oldText)
|
|
1201
1202
|
return {
|
package/dist/index.js
CHANGED
|
@@ -90,24 +90,49 @@ function supervise(pid) {
|
|
|
90
90
|
process.on(sig, () => forward(sig));
|
|
91
91
|
}
|
|
92
92
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Parse `teapot [--port N] [-p N] [--host addr] [--config file] [config.json]`.
|
|
95
|
+
*
|
|
96
|
+
* Extracted from main() and exported so it can be tested. It was inline before,
|
|
97
|
+
* which left `index.ts` at 48% branch coverage with NO test importing it at all —
|
|
98
|
+
* the parser decides which config file is read and which port is bound, so a
|
|
99
|
+
* regression here starts the wrong server on the wrong port with no error anyone
|
|
100
|
+
* would read.
|
|
101
|
+
*
|
|
102
|
+
* The bare-word rule keeps the FIRST positional as the config path: a later bare
|
|
103
|
+
* word is a stray argument, not an override, and silently replacing the config
|
|
104
|
+
* with it would be worse than ignoring it.
|
|
105
|
+
*/
|
|
106
|
+
export function parseArgs(argv) {
|
|
95
107
|
let cfgArg;
|
|
96
108
|
let portOverride;
|
|
97
109
|
let hostOverride;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
for (let i = 0; i < argv.length; i++) {
|
|
111
|
+
const a = argv[i];
|
|
112
|
+
if (a === "--port" || a === "-p") {
|
|
113
|
+
const raw = argv[++i];
|
|
114
|
+
const n = Number(raw);
|
|
115
|
+
// NaN is dropped rather than applied: `config.port = NaN` would fall through
|
|
116
|
+
// to the listen call and produce a far more confusing failure than ignoring
|
|
117
|
+
// a mistyped flag
|
|
118
|
+
if (!Number.isNaN(n))
|
|
119
|
+
portOverride = n;
|
|
120
|
+
}
|
|
121
|
+
else if (a === "--host") {
|
|
122
|
+
hostOverride = argv[++i];
|
|
123
|
+
}
|
|
124
|
+
else if (a === "--config" || a === "-c") {
|
|
125
|
+
cfgArg = argv[++i];
|
|
126
|
+
}
|
|
127
|
+
else if (!a.startsWith("-") && cfgArg === undefined) {
|
|
108
128
|
cfgArg = a;
|
|
129
|
+
}
|
|
109
130
|
}
|
|
110
|
-
|
|
131
|
+
return { cfgArg, portOverride, hostOverride, help: argv.includes("--help") || argv.includes("-h") };
|
|
132
|
+
}
|
|
133
|
+
async function main() {
|
|
134
|
+
const { cfgArg, portOverride, hostOverride, help } = parseArgs(process.argv.slice(2));
|
|
135
|
+
if (help) {
|
|
111
136
|
console.log("teapot [--port N] [--host addr] [--config file.json] [config.json]");
|
|
112
137
|
console.log(" --host 127.0.0.1 (default) · 0.0.0.0 exposes the UI to your network");
|
|
113
138
|
console.log(" --acp speak the Agent Client Protocol on stdio instead of serving HTTP (#15)");
|
|
@@ -229,4 +254,15 @@ async function main() {
|
|
|
229
254
|
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
230
255
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
231
256
|
}
|
|
232
|
-
void main()
|
|
257
|
+
// Only when RUN, not when imported. `void main()` at module scope meant importing
|
|
258
|
+
// this file for `parseArgs` started a whole server — which is why index.ts had no
|
|
259
|
+
// test importing it and sat at 48% branch coverage. The check is the standard
|
|
260
|
+
// ESM "am I the entry point" test, and `node --experimental-strip-types src/index.ts`
|
|
261
|
+
// and `node dist/index.js` both still take this branch.
|
|
262
|
+
const isEntry = process.argv[1] !== undefined &&
|
|
263
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
264
|
+
(process.argv[1] === new URL(import.meta.url).pathname ||
|
|
265
|
+
process.argv[1].endsWith("/index.js") ||
|
|
266
|
+
process.argv[1].endsWith("/index.ts"));
|
|
267
|
+
if (isEntry)
|
|
268
|
+
void main();
|
package/dist/master.js
CHANGED
|
@@ -568,8 +568,30 @@ export class Master {
|
|
|
568
568
|
// #130: a spawned child is `idle` until start() runs, and that lazy-load
|
|
569
569
|
// flip is its FIRST transition — so requiring a prior `running` is what stops
|
|
570
570
|
// the parent being told the child settled before it began.
|
|
571
|
+
// #135: reworded, because the old line was wrong in two ways.
|
|
572
|
+
//
|
|
573
|
+
// "Continue your task, or finish() if the work is done" was addressed to the
|
|
574
|
+
// PARENT — but the parent did not spawn this child for its own sake, and
|
|
575
|
+
// when the child DID call finish() the parent already receives a proper
|
|
576
|
+
// "Sub-agent X finished. Final report: …" (the `finished. Final report:`
|
|
577
|
+
// prompt below). So the nudge
|
|
578
|
+
// restated as an instruction what the report already stated as fact, and
|
|
579
|
+
// invited the parent to `finish()` ITS OWN goal over a child's status.
|
|
580
|
+
//
|
|
581
|
+
// The new wording says what happened and what to do about it, and points at
|
|
582
|
+
// the report when there is one:
|
|
583
|
+
//
|
|
584
|
+
// child gave a summary -> "…returned without calling finish(); its final
|
|
585
|
+
// message follows." (the report arrives separately)
|
|
586
|
+
// child said nothing -> "…went idle without reporting. If that is
|
|
587
|
+
// unexpected, message it or re-spawn it."
|
|
571
588
|
if (this.reportedRunning.has(agentId) && SETTLED.has(status))
|
|
572
|
-
parent.enqueuePrompt(`[harness] @${agentId} is now ${status}
|
|
589
|
+
parent.enqueuePrompt(`[harness] Sub-agent @${agentId} is now ${status} without having called finish(), so it has not reported a result.` +
|
|
590
|
+
(status === "error"
|
|
591
|
+
? ` Check its last output before deciding whether to re-spawn it.`
|
|
592
|
+
: status === "stopped"
|
|
593
|
+
? ` It may have been stopped mid-task; message it to resume, or re-spawn it.`
|
|
594
|
+
: ` If you expected a result, message it or re-spawn it.`), "harness");
|
|
573
595
|
if (status === "running")
|
|
574
596
|
this.reportedRunning.add(agentId);
|
|
575
597
|
else if (SETTLED.has(status))
|
package/dist/server/api.js
CHANGED
|
@@ -1134,6 +1134,30 @@ export function buildApp(master) {
|
|
|
1134
1134
|
// only used for the "load older" affordance, where approximate is fine.
|
|
1135
1135
|
const branch = c.req.query("branch");
|
|
1136
1136
|
const before = c.req.query("before");
|
|
1137
|
+
// #54: `?after=<id>` returns only what landed AFTER that event.
|
|
1138
|
+
//
|
|
1139
|
+
// This is the cursor the reconnect path was missing. Without it a dropped
|
|
1140
|
+
// socket is recovered by re-fetching the whole tail — 2,000 events by default
|
|
1141
|
+
// — and throwing away all but the few that are new. That is what makes the
|
|
1142
|
+
// architecture REST-authoritative: the frontend can only learn "something
|
|
1143
|
+
// changed" and must re-read to find out what.
|
|
1144
|
+
//
|
|
1145
|
+
// It is also the only cursor that can be cheap here: the tail path scans
|
|
1146
|
+
// backwards from EOF, so an `after` lookup that lands near the end reads a
|
|
1147
|
+
// small window instead of parsing the whole log.
|
|
1148
|
+
const after = c.req.query("after");
|
|
1149
|
+
if (after && !before && !branch) {
|
|
1150
|
+
const all = await readEventsCached(filePath);
|
|
1151
|
+
const idx = all.findIndex((e) => e.id === after);
|
|
1152
|
+
// an unknown cursor means the client's view is not a prefix of ours (a
|
|
1153
|
+
// compacted or rotated log), so a full tail is the only safe answer
|
|
1154
|
+
if (idx === -1) {
|
|
1155
|
+
const tail = await readEventsTail(filePath, limit);
|
|
1156
|
+
return c.json({ events: tail.events, total: tail.total, partial: tail.truncated, gap: true });
|
|
1157
|
+
}
|
|
1158
|
+
const events = all.slice(idx + 1);
|
|
1159
|
+
return c.json({ events, total: all.length, appended: events.length > 0 });
|
|
1160
|
+
}
|
|
1137
1161
|
if (!before && !branch) {
|
|
1138
1162
|
const tail = await readEventsTail(filePath, limit);
|
|
1139
1163
|
return c.json({ events: tail.events, total: tail.total, partial: tail.truncated });
|