teapot-coding-agent 0.6.0 → 0.7.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/README.md CHANGED
@@ -110,18 +110,24 @@ master (Hono server, src/master.ts + src/server/api.ts)
110
110
  never hard-coded.
111
111
  - **Tools** (`src/agent/tools.ts`): provider-agnostic JSON-schema function
112
112
  specs — `read_file`, `write_file`, `edit_file`, `list_dir`, `bash` (git goes
113
- through bash), plus meta tools `finish` / `report_progress` / `set_goal` /
114
- `set_memory`. Paths are confined to the workspace; bash runs detached in its
115
- own process group and the whole group is SIGKILLed on timeout.
113
+ through bash), plus meta tools `finish` / `report_progress` / `get_goal` /
114
+ `set_goal` / `read_memory` / `set_memory` / `list_skills`. Paths are
115
+ confined to the workspace; bash runs detached in its own process group and
116
+ the whole group is SIGKILLed on timeout.
117
+ - **Cache-friendly prompt design** — the system prompt is byte-identical on
118
+ every turn; session state (goal, memory, skills) is fetched via tools, never
119
+ injected. Combined with the append-only message history this keeps provider
120
+ prefix caches hot, so long sessions pay incremental input prices instead of
121
+ re-sending full context every turn.
116
122
  - **Session storage** — everything teapot manages lives under
117
123
  `<dataDir>/sessions/<sid>/` (`chat.jsonl`, `goal.md`, `memory.md`), so agent
118
124
  workspaces stay clean. Each incarnation gets a fresh `<agentId>-<uuid>`
119
125
  directory (no history leaks across projects); restarts reuse the latest one.
120
126
  Legacy layouts are migrated automatically.
121
- - **Goal / knowledge** — goal + memory are harness-managed and injected into
122
- the prompt every turn (agents update them via `set_goal` / `set_memory`);
123
- `AGENTS.md` is optional project knowledge in the workspace root that is
124
- injected when present. Nothing is seeded into your project anymore.
127
+ - **Goal / knowledge** — goal + memory are harness-managed and read/written
128
+ through tools (`get_goal` / `set_goal` / `read_memory` / `set_memory`);
129
+ `AGENTS.md` is optional project knowledge in the workspace root that agents
130
+ are told to read at session start. Nothing is seeded into your project.
125
131
 
126
132
  ### Web UI
127
133
 
@@ -14,24 +14,32 @@ import { executeTool, toolSpecs, currentSkills } from "./tools.js";
14
14
  import { bus } from "../bus.js";
15
15
  const SYSTEM_TEMPLATE = `You are a coding agent working autonomously inside a workspace.
16
16
 
17
- ## Goal (managed by the harness)
18
- The current goal is injected into this prompt by the harness. Change it with
19
- set_goal(text) when the objective itself changes; call finish(goalComplete=true)
20
- only when the goal is fully achieved.
17
+ This system prompt is intentionally STATIC — session state is never injected
18
+ into it, so API prompt caches stay hot across turns. Fetch state with tools
19
+ instead; they are cheap and always current.
21
20
 
22
- ## Persistent context (all optional — create only if useful)
23
- - AGENTS.md (workspace root): project knowledge/conventions. When present, the
24
- harness injects it into this prompt; keep it current with edit_file/write_file.
25
- - memory.md: your durable notes, harness-managed update them with
26
- set_memory(content); they are injected into future prompts.
27
- - skills/: reusable playbooks via load_skill / save_skill.
21
+ ## Goal (harness-managed)
22
+ - get_goal() current objective + status. Call it at session start, after a
23
+ compaction notice, or whenever you lose the thread.
24
+ - set_goal(text) change the objective itself (not routine updates).
25
+ - finish(goalComplete=true, summary) goal fully achieved.
26
+
27
+ ## Your notes (memory.md, harness-managed)
28
+ - read_memory() / set_memory(content) → durable notes injected nowhere else.
29
+ Keep them terse: decisions, gotchas, where you left off.
30
+
31
+ ## Skills (reusable playbooks)
32
+ - list_skills() → what exists. load_skill(name) → full playbook.
33
+ - save_skill(name, description, content) → distill a reusable procedure.
34
+
35
+ ## Project knowledge
36
+ - AGENTS.md in the workspace root (optional): conventions & commands. Read it
37
+ with read_file at session start when present, keep it current.
28
38
 
29
39
  ## Rules
30
40
  - Work step by step with tools. Verify results (run tests/builds) before claiming progress.
31
- - When a task matches an available skill's description, load_skill it first and follow the playbook.
32
- - When you develop a reusable procedure, save_skill it — skills persist and are offered to future sessions.
41
+ - When a loaded skill matches your task, follow its playbook.
33
42
  - When you make meaningful progress, call report_progress.
34
- - When the goal is fully achieved, call finish(goalComplete=true) with a short summary.
35
43
  - Be frugal: prefer small precise edits, avoid runaway loops.`;
36
44
  export class Agent {
37
45
  log;
@@ -97,17 +105,6 @@ export class Agent {
97
105
  /* keep previous cache */
98
106
  }
99
107
  }
100
- skillsListing() {
101
- if (this.skillsCache.length === 0) {
102
- return ("## Skills\n" +
103
- "No skills exist yet. When you develop a reusable procedure worth keeping " +
104
- "(build steps, checklists, project conventions), distill it into a durable playbook " +
105
- "with save_skill so future sessions can load it via load_skill.");
106
- }
107
- return ("## Skills (reusable playbooks)\n" +
108
- "When the current task matches a description below, call load_skill(name) first and follow it.\n" +
109
- this.skillsCache.map((s) => `- ${s.name}: ${s.description || "(no description)"}`).join("\n"));
110
- }
111
108
  callLlm(messages, tools, onDelta) {
112
109
  const fn = this.opts.chatFn ?? chatStream;
113
110
  return fn(this.opts.llm, messages, tools, this.abort?.signal, onDelta);
@@ -479,7 +476,7 @@ export class Agent {
479
476
  turn: ++this.stats.turns,
480
477
  });
481
478
  // stream the assistant reply live to connected clients
482
- const res = await this.llmCall(await this.buildMessages(), allToolSpecs(), (s) => {
479
+ const res = await this.llmCall(this.buildMessages(), allToolSpecs(), (s) => {
483
480
  bus.emit("update", {
484
481
  kind: "llm-delta",
485
482
  agentId: this.opts.id,
@@ -537,6 +534,31 @@ export class Agent {
537
534
  });
538
535
  continue;
539
536
  }
537
+ if (call.function.name === "get_goal") {
538
+ this.messages.push({
539
+ role: "tool",
540
+ tool_call_id: call.id,
541
+ content: JSON.stringify({ goal: this.goal.text || "(none set)", status: this.goal.status }, null, 1),
542
+ });
543
+ continue;
544
+ }
545
+ if (call.function.name === "read_memory") {
546
+ const mem = await fs.readFile(this.memoryFile, "utf8").catch(() => "");
547
+ this.messages.push({
548
+ role: "tool",
549
+ tool_call_id: call.id,
550
+ content: mem.trim() || "(memory.md is empty — nothing noted yet)",
551
+ });
552
+ continue;
553
+ }
554
+ if (call.function.name === "list_skills") {
555
+ await this.refreshSkills();
556
+ const list = this.skillsCache.length
557
+ ? this.skillsCache.map((s) => `- ${s.name}: ${s.description || "(no description)"}`).join("\n")
558
+ : "(no skills yet — create one with save_skill)";
559
+ this.messages.push({ role: "tool", tool_call_id: call.id, content: list });
560
+ continue;
561
+ }
540
562
  if (call.function.name === "set_memory") {
541
563
  const a = safeParse(call.function.arguments);
542
564
  const content = String(a.content ?? "").slice(0, 32_000);
@@ -574,37 +596,10 @@ export class Agent {
574
596
  }
575
597
  throw new Error("runaway detection: too many turns in one round (>200)");
576
598
  }
577
- /** AGENTS.md is optional project knowledge — injected when present. */
578
- agentsMdCache = null;
579
- async readAgentsMd() {
580
- try {
581
- const p = path.join(this.workspace, "AGENTS.md");
582
- const st = await fs.stat(p);
583
- if (this.agentsMdCache?.mtimeMs === st.mtimeMs)
584
- return this.agentsMdCache.text;
585
- const text = await fs.readFile(p, "utf8");
586
- this.agentsMdCache = { mtimeMs: st.mtimeMs, text };
587
- return text;
588
- }
589
- catch {
590
- this.agentsMdCache = null;
591
- return "";
592
- }
593
- }
594
- async buildMessages() {
595
- const sys = [SYSTEM_TEMPLATE];
596
- if (this.goal.text) {
597
- sys.push(`## Current goal (${this.goal.status})\n${this.goal.text}`);
598
- }
599
- const agentsMd = await this.readAgentsMd();
600
- if (agentsMd.trim())
601
- sys.push(`## Project knowledge (AGENTS.md)\n${clipText(agentsMd, 8000)}`);
602
- const memory = await fs.readFile(this.memoryFile, "utf8").catch(() => "");
603
- if (memory.trim())
604
- sys.push(`## Your notes (memory.md)\n${clipText(memory, 4000)}`);
605
- sys.push(this.skillsListing());
599
+ buildMessages() {
600
+ // deliberately static: [system] + append-only history keeps prefix caches hot
606
601
  const hasSystem = this.messages[0]?.role === "system";
607
- const head = [{ role: "system", content: sys.join("\n\n") }];
602
+ const head = [{ role: "system", content: SYSTEM_TEMPLATE }];
608
603
  return hasSystem ? [...head, ...this.messages.slice(1)] : [...head, ...this.messages];
609
604
  }
610
605
  async handleFinish(argsJson) {
@@ -629,7 +624,7 @@ export class Agent {
629
624
  text: request,
630
625
  });
631
626
  this.messages.push({ role: "user", content: request });
632
- const res = await this.llmCall(await this.buildMessages(), []); // no tools: pure report
627
+ const res = await this.llmCall(this.buildMessages(), []); // no tools: pure report
633
628
  await this.recordProgress(JSON.stringify({ freeform: res.message.content }));
634
629
  await this.log.append("message", this.currentSession, this.currentBranch, {
635
630
  role: "assistant",
@@ -843,7 +838,7 @@ function allToolSpecs() {
843
838
  type: "function",
844
839
  function: {
845
840
  name: "set_goal",
846
- description: "Replace the harness-managed goal text (it is injected into your prompt). Use when the objective itself changes — not for routine updates.",
841
+ description: "Replace the harness-managed goal text. Use when the objective itself changes — not for routine updates.",
847
842
  parameters: {
848
843
  type: "object",
849
844
  properties: { text: { type: "string" } },
@@ -851,11 +846,27 @@ function allToolSpecs() {
851
846
  },
852
847
  },
853
848
  },
849
+ {
850
+ type: "function",
851
+ function: {
852
+ name: "get_goal",
853
+ description: "Fetch the current goal and its status. Cheap — call at session start, after a compaction notice, or when unsure.",
854
+ parameters: { type: "object", properties: {} },
855
+ },
856
+ },
857
+ {
858
+ type: "function",
859
+ function: {
860
+ name: "read_memory",
861
+ description: "Read your durable notes (memory.md).",
862
+ parameters: { type: "object", properties: {} },
863
+ },
864
+ },
854
865
  {
855
866
  type: "function",
856
867
  function: {
857
868
  name: "set_memory",
858
- description: "Overwrite your durable session notes (memory.md, injected into future prompts). Keep them terse: decisions, gotchas, where you left off.",
869
+ description: "Overwrite your durable notes (memory.md). Keep them terse: decisions, gotchas, where you left off.",
859
870
  parameters: {
860
871
  type: "object",
861
872
  properties: { content: { type: "string" } },
@@ -863,11 +874,16 @@ function allToolSpecs() {
863
874
  },
864
875
  },
865
876
  },
877
+ {
878
+ type: "function",
879
+ function: {
880
+ name: "list_skills",
881
+ description: "List available skills (name + description). Call before load_skill or to avoid duplicating an existing skill.",
882
+ parameters: { type: "object", properties: {} },
883
+ },
884
+ },
866
885
  ];
867
886
  }
868
- function clipText(s, max) {
869
- return s.length <= max ? s : s.slice(0, max) + `\n… [truncated, ${s.length} chars total]`;
870
- }
871
887
  function str(v) {
872
888
  return typeof v === "string" ? v : "";
873
889
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",