teapot-coding-agent 0.14.4 → 0.14.6
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 +24 -4
- package/dist/agent/tools.js +4 -0
- package/dist/master.js +18 -3
- package/dist/server/api.js +51 -0
- package/package.json +1 -1
- package/public/assets/index-BjELe1k2.js +28 -0
- package/public/assets/index-BjELe1k2.js.map +1 -0
- package/public/assets/index-CPT13iCn.css +1 -0
- package/public/assets/{shiki-ofP3ESSG.js → shiki-BLFDwcS0.js} +2 -2
- package/public/assets/{shiki-ofP3ESSG.js.map → shiki-BLFDwcS0.js.map} +1 -1
- package/public/index.html +2 -2
- package/public/assets/index-C6PGj05g.css +0 -1
- package/public/assets/index-DmDgcGhp.js +0 -28
- package/public/assets/index-DmDgcGhp.js.map +0 -1
package/dist/agent/agent.js
CHANGED
|
@@ -51,7 +51,10 @@ Session state is not injected into prompts — fetch it with tools instead:
|
|
|
51
51
|
save_skill(name, description, content, files=[{name, content}]) so future
|
|
52
52
|
sessions can load_skill them — helper scripts go through files and are made
|
|
53
53
|
executable automatically.
|
|
54
|
-
- When you make meaningful progress, call report_progress.
|
|
54
|
+
- When you make meaningful progress, call report_progress. Its output is
|
|
55
|
+
shown to the operator directly (rendered as markdown in the progress
|
|
56
|
+
panel) — do NOT also repeat the same content as a chat message; end the
|
|
57
|
+
turn right after the call (a short "reported." is fine).
|
|
55
58
|
- Be frugal: prefer small precise edits, avoid runaway loops.`;
|
|
56
59
|
export class Agent {
|
|
57
60
|
log;
|
|
@@ -619,11 +622,18 @@ export class Agent {
|
|
|
619
622
|
this.goal.status !== "active" ||
|
|
620
623
|
!this.goal.text.trim())
|
|
621
624
|
break;
|
|
622
|
-
// auto-continue: wait quietly, then nudge with a fresh round
|
|
625
|
+
// auto-continue: wait quietly, then nudge with a fresh round.
|
|
626
|
+
// Sub-agents get a SCOPE-ANCHORED nudge instead of a generic one:
|
|
627
|
+
// "Continue working toward the current goal" made a forked sub-agent
|
|
628
|
+
// re-read its inherited context and drift into one of the parent's
|
|
629
|
+
// older tasks — it would keep going forever without ever finish()ing.
|
|
623
630
|
await this.sleepInterruptible(this.opts.continueDelayMs);
|
|
624
631
|
if (this.stopRequested)
|
|
625
632
|
break;
|
|
626
|
-
const
|
|
633
|
+
const isSub = !!this.opts.parent;
|
|
634
|
+
const nudge = isSub
|
|
635
|
+
? `[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().`
|
|
636
|
+
: "Continue working toward the current goal. If you are blocked, explain why briefly.";
|
|
627
637
|
await this.log.append("prompt", this.currentSession, this.currentBranch, {
|
|
628
638
|
source: "harness",
|
|
629
639
|
text: nudge,
|
|
@@ -819,11 +829,15 @@ export class Agent {
|
|
|
819
829
|
const a = safeParse(call.function.arguments);
|
|
820
830
|
const question = String(a.question ?? "").slice(0, 2000);
|
|
821
831
|
const options = Array.isArray(a.options) ? a.options.map(String).slice(0, 6) : [];
|
|
832
|
+
// the callId lets the UI tell "still open" from "already answered"
|
|
833
|
+
// (a settled tool_result for this id exists) and disable re-answering
|
|
822
834
|
await this.log.append("question", this.currentSession, this.currentBranch, {
|
|
823
835
|
question,
|
|
824
836
|
options,
|
|
837
|
+
callId: call.id,
|
|
825
838
|
});
|
|
826
|
-
await this.answerMeta(call, "question shown to the operator — the loop is parked until they reply"
|
|
839
|
+
await this.answerMeta(call, "question shown to the operator — the loop is parked until they reply. " +
|
|
840
|
+
"Their reply (possibly free text, not one of the options) arrives as your next user message.");
|
|
827
841
|
this.awaitingUser = true;
|
|
828
842
|
this.setStatus("waiting", question.slice(0, 80));
|
|
829
843
|
// control flow: park the loop; the operator's next prompt resumes it
|
|
@@ -993,10 +1007,16 @@ export class Agent {
|
|
|
993
1007
|
this.messages.push({ role: "user", content: request });
|
|
994
1008
|
const res = await this.llmCall(this.buildMessages(), []); // no tools: pure report
|
|
995
1009
|
await this.recordProgress(JSON.stringify({ freeform: res.message.content }));
|
|
1010
|
+
// The report's content is ALREADY shown by the progress embed — logging a
|
|
1011
|
+
// plain assistant message with the same text made the timeline show every
|
|
1012
|
+
// requested report twice. Keep logging it (session restores replay the
|
|
1013
|
+
// exchange faithfully and rebuildMessages needs it) but mark it as an echo;
|
|
1014
|
+
// the web UI filters marked rows out.
|
|
996
1015
|
await this.log.append("message", this.currentSession, this.currentBranch, {
|
|
997
1016
|
role: "assistant",
|
|
998
1017
|
content: res.message.content ?? "",
|
|
999
1018
|
reasoning: res.reasoning,
|
|
1019
|
+
progressEcho: true,
|
|
1000
1020
|
});
|
|
1001
1021
|
this.messages.push(res.message);
|
|
1002
1022
|
}
|
package/dist/agent/tools.js
CHANGED
|
@@ -809,9 +809,13 @@ export const TOOLS = [
|
|
|
809
809
|
// park VISIBLY: the UI shows idle ("waiting on sub-agents") instead of a
|
|
810
810
|
// running spinner, and any user prompt / stop wakes us instantly
|
|
811
811
|
ctx.onIdlePark?.(`waiting on sub-agent${ids?.length === 1 ? ` ${ids[0]}` : "s"}`);
|
|
812
|
+
// the child's report is about to land in our mailbox — a progress report
|
|
813
|
+
// right after it would be pure noise, so re-arm the gate here
|
|
814
|
+
ctx.onProgressGateReset?.();
|
|
812
815
|
try {
|
|
813
816
|
const r = await sa.wait(ids, ms);
|
|
814
817
|
ctx.onIdleUnpark?.();
|
|
818
|
+
ctx.onProgressGateReset?.(); // also after: waiting itself isn't "activity"
|
|
815
819
|
return { ok: true, result: r.note };
|
|
816
820
|
}
|
|
817
821
|
catch (e) {
|
package/dist/master.js
CHANGED
|
@@ -483,10 +483,25 @@ export class Master {
|
|
|
483
483
|
child.importMessages(parent.exportMessages());
|
|
484
484
|
}
|
|
485
485
|
await child.setGoal(`${directive}${o.task}`.slice(0, 2000));
|
|
486
|
+
// SCOPE GUARD: a forked child inherits the parent's whole conversation as
|
|
487
|
+
// reference context — without an explicit boundary, models have been seen
|
|
488
|
+
// "continuing" some EARLIER task that appears in that history instead of
|
|
489
|
+
// the task they were actually spawned for. State the boundary twice
|
|
490
|
+
// (goal + first prompt) so it survives goal-driven auto-continue.
|
|
486
491
|
await child.enqueuePrompt((o.context === "fork"
|
|
487
|
-
? `[harness] You are sub-agent ${id}, spawned by @${parentId}
|
|
488
|
-
|
|
489
|
-
|
|
492
|
+
? `[harness] You are sub-agent ${id}, spawned by @${parentId}.
|
|
493
|
+
|
|
494
|
+
The conversation above is REFERENCE CONTEXT ONLY — background on why this work is needed.
|
|
495
|
+
It contains older tasks and discussions that are NOT yours. Do not resume, continue, or
|
|
496
|
+
finish any of them; other agents own those. If you find yourself working on anything other
|
|
497
|
+
than the task below, STOP and return to it.
|
|
498
|
+
|
|
499
|
+
YOUR ONE AND ONLY TASK — everything else in the inherited context is out of scope:
|
|
500
|
+
|
|
501
|
+
${directive}${o.task}
|
|
502
|
+
|
|
503
|
+
When this task is done (or truly blocked), call finish() with a summary for @${parentId}.`
|
|
504
|
+
: `[harness] You are sub-agent ${id}, spawned by @${parentId}. Work solely on this task:\n\n${directive}${o.task}\n\nWhen it is done (or truly blocked), call finish() with a summary for @${parentId}.`), "harness");
|
|
490
505
|
child.start(`spawned by ${parentId}`);
|
|
491
506
|
console.log(`[teapot] sub-agent ${id} spawned by ${parentId} (context: ${o.context ?? "none"}${persona ? `, persona: ${persona}` : ""})`);
|
|
492
507
|
return { id };
|
package/dist/server/api.js
CHANGED
|
@@ -322,6 +322,57 @@ export function buildApp(master) {
|
|
|
322
322
|
return c.json({ error: err.message }, 400);
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
|
+
// write a file from the web UI's file-tree editor (human action, not an
|
|
326
|
+
// agent tool — allowed even for read-only personas, never notifies the
|
|
327
|
+
// agent). Optional baseContent enables optimistic-concurrency checking.
|
|
328
|
+
app.put("/api/agents/:id/file", async (c) => {
|
|
329
|
+
const a = master.agents.get(c.req.param("id"));
|
|
330
|
+
if (!a)
|
|
331
|
+
return c.json({ error: "not found" }, 404);
|
|
332
|
+
const rel = c.req.query("path") ?? "";
|
|
333
|
+
if (!rel.trim())
|
|
334
|
+
return c.json({ error: "path required" }, 400);
|
|
335
|
+
const body = await c.req.json().catch(() => null);
|
|
336
|
+
if (body === null || typeof body.content !== "string")
|
|
337
|
+
return c.json({ error: "content required" }, 400);
|
|
338
|
+
let abs;
|
|
339
|
+
try {
|
|
340
|
+
abs = safeJoin(a.workspace, rel);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
return c.json({ error: err.message }, 400);
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
let existing = null;
|
|
347
|
+
try {
|
|
348
|
+
existing = await fs.readFile(abs);
|
|
349
|
+
}
|
|
350
|
+
catch (e) {
|
|
351
|
+
if (e.code !== "ENOENT")
|
|
352
|
+
throw e;
|
|
353
|
+
}
|
|
354
|
+
// refuse to clobber binary files (same 8KB NUL probe as GET)
|
|
355
|
+
if (existing && existing.subarray(0, 8192).includes(0))
|
|
356
|
+
return c.json({ error: "binary file — edit refused" }, 400);
|
|
357
|
+
// conflict check: the editor sends what it loaded; if disk moved on,
|
|
358
|
+
// bounce with the fresh content instead of silently overwriting
|
|
359
|
+
if (typeof body.baseContent === "string" &&
|
|
360
|
+
existing &&
|
|
361
|
+
existing.subarray(0, 100_000).toString("utf8") !== body.baseContent) {
|
|
362
|
+
return c.json({
|
|
363
|
+
error: "file changed on disk",
|
|
364
|
+
current: existing.subarray(0, 100_000).toString("utf8"),
|
|
365
|
+
}, 409);
|
|
366
|
+
}
|
|
367
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
368
|
+
const content = Buffer.from(body.content, "utf8");
|
|
369
|
+
await fs.writeFile(abs, content);
|
|
370
|
+
return c.json({ ok: true, path: rel, size: content.length });
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
return c.json({ error: err.message }, 400);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
325
376
|
// ---- config (view/edit from the web UI) ----
|
|
326
377
|
app.get("/api/config", (c) => {
|
|
327
378
|
const mask = (p) => Object.fromEntries(Object.entries(p ?? {}).map(([k, v]) => [
|