@possumtech/rummy 0.2.8 → 0.3.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.
Files changed (114) hide show
  1. package/.env.example +13 -2
  2. package/EXCEPTIONS.md +46 -0
  3. package/PLUGINS.md +422 -188
  4. package/SPEC.md +440 -106
  5. package/migrations/001_initial_schema.sql +5 -3
  6. package/package.json +17 -5
  7. package/service.js +5 -3
  8. package/src/agent/AgentLoop.js +252 -55
  9. package/src/agent/ContextAssembler.js +20 -4
  10. package/src/agent/KnownStore.js +82 -25
  11. package/src/agent/ProjectAgent.js +4 -1
  12. package/src/agent/ResponseHealer.js +86 -32
  13. package/src/agent/TurnExecutor.js +542 -207
  14. package/src/agent/XmlParser.js +77 -41
  15. package/src/agent/known_store.sql +68 -4
  16. package/src/agent/schemes.sql +3 -0
  17. package/src/agent/tokens.js +7 -21
  18. package/src/agent/turns.sql +15 -1
  19. package/src/hooks/HookRegistry.js +7 -0
  20. package/src/hooks/Hooks.js +15 -0
  21. package/src/hooks/PluginContext.js +14 -1
  22. package/src/hooks/RummyContext.js +16 -4
  23. package/src/hooks/ToolRegistry.js +77 -19
  24. package/src/llm/LlmProvider.js +27 -8
  25. package/src/llm/OpenAiClient.js +20 -0
  26. package/src/llm/OpenRouterClient.js +24 -2
  27. package/src/llm/XaiClient.js +47 -2
  28. package/src/plugins/ask_user/README.md +4 -4
  29. package/src/plugins/ask_user/ask_user.js +5 -5
  30. package/src/plugins/ask_user/ask_userDoc.js +29 -0
  31. package/src/plugins/budget/README.md +31 -0
  32. package/src/plugins/budget/budget.js +55 -0
  33. package/src/plugins/cp/README.md +5 -4
  34. package/src/plugins/cp/cp.js +10 -6
  35. package/src/plugins/cp/cpDoc.js +29 -0
  36. package/src/plugins/engine/engine.sql +1 -8
  37. package/src/plugins/engine/turn_context.sql +4 -9
  38. package/src/plugins/env/README.md +3 -4
  39. package/src/plugins/env/env.js +5 -5
  40. package/src/plugins/env/envDoc.js +29 -0
  41. package/src/plugins/file/README.md +9 -12
  42. package/src/plugins/file/file.js +34 -35
  43. package/src/plugins/get/README.md +2 -2
  44. package/src/plugins/get/get.js +77 -6
  45. package/src/plugins/get/getDoc.js +51 -0
  46. package/src/plugins/hedberg/hedberg.js +2 -1
  47. package/src/plugins/hedberg/matcher.js +10 -29
  48. package/src/plugins/hedberg/normalize.js +28 -0
  49. package/src/plugins/hedberg/patterns.js +25 -27
  50. package/src/plugins/hedberg/sed.js +17 -10
  51. package/src/plugins/index.js +66 -14
  52. package/src/plugins/instructions/README.md +6 -2
  53. package/src/plugins/instructions/instructions.js +20 -4
  54. package/src/plugins/instructions/preamble.md +19 -5
  55. package/src/plugins/known/README.md +10 -7
  56. package/src/plugins/known/known.js +23 -17
  57. package/src/plugins/known/knownDoc.js +34 -0
  58. package/src/plugins/mv/README.md +5 -4
  59. package/src/plugins/mv/mv.js +27 -6
  60. package/src/plugins/mv/mvDoc.js +45 -0
  61. package/src/plugins/performed/README.md +15 -0
  62. package/src/plugins/performed/performed.js +45 -0
  63. package/src/plugins/persona/persona.js +78 -0
  64. package/src/plugins/previous/README.md +3 -2
  65. package/src/plugins/previous/previous.js +33 -24
  66. package/src/plugins/progress/README.md +1 -2
  67. package/src/plugins/progress/progress.js +33 -21
  68. package/src/plugins/prompt/README.md +5 -5
  69. package/src/plugins/prompt/prompt.js +15 -17
  70. package/src/plugins/rm/README.md +4 -4
  71. package/src/plugins/rm/rm.js +32 -20
  72. package/src/plugins/rm/rmDoc.js +30 -0
  73. package/src/plugins/rpc/README.md +15 -28
  74. package/src/plugins/rpc/rpc.js +42 -77
  75. package/src/plugins/set/README.md +13 -12
  76. package/src/plugins/set/set.js +107 -16
  77. package/src/plugins/set/setDoc.js +49 -0
  78. package/src/plugins/sh/README.md +4 -4
  79. package/src/plugins/sh/sh.js +5 -5
  80. package/src/plugins/sh/shDoc.js +29 -0
  81. package/src/plugins/{skills/skills.js → skill/skill.js} +10 -51
  82. package/src/plugins/summarize/README.md +6 -5
  83. package/src/plugins/summarize/summarize.js +7 -6
  84. package/src/plugins/summarize/summarizeDoc.js +33 -0
  85. package/src/plugins/telemetry/telemetry.js +16 -9
  86. package/src/plugins/think/README.md +20 -0
  87. package/src/plugins/think/think.js +5 -0
  88. package/src/plugins/unknown/README.md +6 -5
  89. package/src/plugins/unknown/unknown.js +12 -9
  90. package/src/plugins/unknown/unknownDoc.js +31 -0
  91. package/src/plugins/update/README.md +3 -8
  92. package/src/plugins/update/update.js +7 -6
  93. package/src/plugins/update/updateDoc.js +33 -0
  94. package/src/server/ClientConnection.js +59 -45
  95. package/src/server/RpcRegistry.js +52 -4
  96. package/src/sql/v_model_context.sql +10 -25
  97. package/src/plugins/ask_user/docs.md +0 -2
  98. package/src/plugins/cp/docs.md +0 -2
  99. package/src/plugins/current/README.md +0 -14
  100. package/src/plugins/current/current.js +0 -47
  101. package/src/plugins/env/docs.md +0 -4
  102. package/src/plugins/get/docs.md +0 -10
  103. package/src/plugins/known/docs.md +0 -3
  104. package/src/plugins/mv/docs.md +0 -2
  105. package/src/plugins/rm/docs.md +0 -6
  106. package/src/plugins/set/docs.md +0 -6
  107. package/src/plugins/sh/docs.md +0 -2
  108. package/src/plugins/skills/README.md +0 -25
  109. package/src/plugins/store/README.md +0 -20
  110. package/src/plugins/store/docs.md +0 -6
  111. package/src/plugins/store/store.js +0 -63
  112. package/src/plugins/summarize/docs.md +0 -4
  113. package/src/plugins/unknown/docs.md +0 -5
  114. package/src/plugins/update/docs.md +0 -4
@@ -5,9 +5,8 @@ Proposes shell command execution for client approval.
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `sh`
8
- - **Modes**: act only
9
- - **Category**: act
10
- - **Handler**: Upserts the entry as `proposed` state. The client must approve execution.
8
+ - **Category**: `logging`
9
+ - **Handler**: Upserts the entry at status 202 (proposed). The client must approve execution.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,5 @@ Shows `sh {command}` followed by the entry body.
15
14
 
16
15
  ## Behavior
17
16
 
18
- All shell commands require client-side approval — nothing executes server-side. Act mode only; blocked in ask mode.
17
+ All shell commands require client-side approval — nothing executes
18
+ server-side. Act mode only; excluded in ask mode by `resolveForLoop`.
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./shDoc.js";
2
2
 
3
3
  export default class Sh {
4
4
  #core;
@@ -9,10 +9,10 @@ export default class Sh {
9
9
  core.on("handler", this.handler.bind(this));
10
10
  core.on("full", this.full.bind(this));
11
11
  core.on("summary", this.summary.bind(this));
12
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
13
- core.filter("instructions.toolDocs", async (content) =>
14
- content ? `${content}\n\n${docs}` : docs,
15
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.sh = docs;
14
+ return docsMap;
15
+ });
16
16
  }
17
17
 
18
18
  async handler(entry, rummy) {
@@ -0,0 +1,29 @@
1
+ // Tool doc for <sh>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax
6
+ ["## <sh>[command]</sh> - Run a shell command with side effects"],
7
+
8
+ // --- Examples: install and test — real mutations
9
+ [
10
+ "Example: <sh>npm install express</sh>",
11
+ "Package install. Shows a real side-effect command.",
12
+ ],
13
+ [
14
+ "Example: <sh>npm test</sh>",
15
+ "Test execution. Another common side-effect action.",
16
+ ],
17
+
18
+ // --- Constraints
19
+ [
20
+ "* YOU MUST NOT use <sh/> to read, create, or edit files — use <get/> and <set/>",
21
+ "Forces file operations through the entry system. Prevents untracked mutations.",
22
+ ],
23
+ [
24
+ "* YOU MUST use <env/> for commands without side effects",
25
+ "Reinforces the env/sh split. Read = env, mutate = sh.",
26
+ ],
27
+ ];
28
+
29
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,15 +1,17 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
- export default class Skills {
4
+ export default class Skill {
5
5
  #core;
6
6
 
7
7
  constructor(core) {
8
8
  this.#core = core;
9
9
  core.registerScheme({
10
10
  name: "skill",
11
- category: "knowledge",
11
+ category: "data",
12
12
  });
13
+ core.hooks.tools.onView("skill", (entry) => entry.body);
14
+
13
15
  const r = core.hooks.rpc.registry;
14
16
 
15
17
  r.register("skill/add", {
@@ -22,19 +24,12 @@ export default class Skills {
22
24
 
23
25
  const body = await loadFile("skills", params.name);
24
26
  const store = ctx.projectAgent.entries;
25
- await store.upsert(
26
- runRow.id,
27
- runRow.next_turn,
28
- `skill://${params.name}`,
29
- body,
30
- 200,
31
- {
32
- attributes: {
33
- name: params.name,
34
- source: filePath("skills", params.name),
35
- },
27
+ await store.upsert(runRow.id, 0, `skill://${params.name}`, body, 200, {
28
+ attributes: {
29
+ name: params.name,
30
+ source: filePath("skills", params.name),
36
31
  },
37
- );
32
+ });
38
33
 
39
34
  return { status: "ok", skill: params.name };
40
35
  },
@@ -97,43 +92,7 @@ export default class Skills {
97
92
  requiresInit: true,
98
93
  });
99
94
 
100
- r.register("persona/set", {
101
- handler: async (params, ctx) => {
102
- if (!params.run) throw new Error("run is required");
103
-
104
- const runRow = await ctx.db.get_run_by_alias.get({ alias: params.run });
105
- if (!runRow) throw new Error(`Run not found: ${params.run}`);
106
-
107
- let text = params.text;
108
- if (params.name && !text) {
109
- text = await loadFile("personas", params.name);
110
- }
111
-
112
- await ctx.db.update_run_config.run({
113
- id: runRow.id,
114
- temperature: null,
115
- persona: text || null,
116
- context_limit: null,
117
- model: null,
118
- });
119
-
120
- return { status: "ok" };
121
- },
122
- description:
123
- "Set persona on a run. Pass name or text. Pass neither to clear.",
124
- params: {
125
- run: "string — run alias",
126
- name: "string? — persona filename (without .md)",
127
- text: "string? — raw persona text (overrides name)",
128
- },
129
- requiresInit: true,
130
- });
131
-
132
- r.register("listPersonas", {
133
- handler: async () => listAvailable("personas"),
134
- description: "List available persona files. Returns [{ name, path }].",
135
- requiresInit: true,
136
- });
95
+ // Persona methods extracted to persona plugin.
137
96
  }
138
97
  }
139
98
 
@@ -1,13 +1,12 @@
1
1
  # summarize
2
2
 
3
- Structural tool for model-generated summaries.
3
+ Lifecycle signal the model declares it has completed the task.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `summarize`
8
- - **Modes**: ask, act
9
- - **Category**: structural
10
- - **Handler**: None — projection only.
8
+ - **Category**: `logging`
9
+ - **Handler**: None — recorded by TurnExecutor as a lifecycle signal.
11
10
 
12
11
  ## Projection
13
12
 
@@ -15,4 +14,6 @@ Shows `summarize` followed by the entry body.
15
14
 
16
15
  ## Behavior
17
16
 
18
- No handler logic. The tool registration exists so the model can emit summary entries that appear in context via projection.
17
+ If the model sends `<summarize>` but actions in the same turn failed,
18
+ TurnExecutor overrides it to `<update>` — the model's assertion that
19
+ it's done is false.
@@ -1,17 +1,18 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./summarizeDoc.js";
2
2
 
3
3
  export default class Summarize {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({ category: "structural" });
8
+ core.ensureTool();
9
+ core.registerScheme({ category: "logging" });
9
10
  core.on("full", this.full.bind(this));
10
11
  core.on("summary", this.summary.bind(this));
11
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
12
- core.filter("instructions.toolDocs", async (content) =>
13
- content ? `${content}\n\n${docs}` : docs,
14
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.summarize = docs;
14
+ return docsMap;
15
+ });
15
16
  }
16
17
 
17
18
  full(entry) {
@@ -0,0 +1,33 @@
1
+ // Tool doc for <summarize>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax
6
+ ["## <summarize>[answer or summary]</summarize> - Signal completion"],
7
+
8
+ // --- Examples: answer and task completion
9
+ [
10
+ "Example: <summarize>The port is 8080</summarize>",
11
+ "Direct answer. Shows summarize as the vehicle for delivering answers.",
12
+ ],
13
+ [
14
+ "Example: <summarize>Installed express, updated config</summarize>",
15
+ "Task summary. Shows summarize for action completion.",
16
+ ],
17
+
18
+ // --- Constraints: RFC-style MUST/MUST NOT
19
+ [
20
+ "* YOU MUST use <summarize> when done — describes the final state",
21
+ "Completion signal. Without this, the loop continues indefinitely.",
22
+ ],
23
+ [
24
+ "* YOU MUST NOT use <summarize> if still working — use <update/> instead",
25
+ "Mutual exclusion with update. Prevents premature completion.",
26
+ ],
27
+ [
28
+ "* YOU MUST keep <summarize> to <= 80 characters",
29
+ "Length cap. Matches the summary attribute constraint. Prevents verbose output.",
30
+ ],
31
+ ];
32
+
33
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,4 +1,4 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
4
  export default class Telemetry {
@@ -75,8 +75,8 @@ export default class Telemetry {
75
75
  result,
76
76
  responseMessage,
77
77
  content,
78
- commands,
79
78
  unparsed,
79
+ assembledTokens,
80
80
  systemMsg,
81
81
  userMsg,
82
82
  }) {
@@ -85,17 +85,20 @@ export default class Telemetry {
85
85
  // assistant://N — the model's raw response
86
86
  await store.upsert(runId, turn, `assistant://${turn}`, content, 200, {
87
87
  loopId,
88
+ fidelity: "archive",
88
89
  });
89
90
 
90
91
  // system://N, user://N — assembled messages as audit
91
92
  if (systemMsg) {
92
93
  await store.upsert(runId, turn, `system://${turn}`, systemMsg, 200, {
93
94
  loopId,
95
+ fidelity: "archive",
94
96
  });
95
97
  }
96
98
  if (userMsg) {
97
99
  await store.upsert(runId, turn, `user://${turn}`, userMsg, 200, {
98
100
  loopId,
101
+ fidelity: "archive",
99
102
  });
100
103
  }
101
104
 
@@ -112,7 +115,7 @@ export default class Telemetry {
112
115
  model: result.model || null,
113
116
  }),
114
117
  200,
115
- { loopId },
118
+ { loopId, fidelity: "archive" },
116
119
  );
117
120
 
118
121
  // reasoning://N
@@ -123,7 +126,7 @@ export default class Telemetry {
123
126
  `reasoning://${turn}`,
124
127
  responseMessage.reasoning_content,
125
128
  200,
126
- { loopId },
129
+ { loopId, fidelity: "archive" },
127
130
  );
128
131
  }
129
132
 
@@ -131,6 +134,7 @@ export default class Telemetry {
131
134
  if (unparsed) {
132
135
  await store.upsert(runId, turn, `content://${turn}`, unparsed, 200, {
133
136
  loopId,
137
+ fidelity: "archive",
134
138
  });
135
139
  }
136
140
 
@@ -147,8 +151,13 @@ export default class Telemetry {
147
151
  usage.completion_tokens_details?.reasoning_tokens ||
148
152
  usage.output_tokens_details?.reasoning_tokens ||
149
153
  0;
154
+ // Use LLM's actual prompt_tokens as the ground-truth context size when available.
155
+ // This back-fills context_tokens so get_last_context_tokens reflects reality for the next turn.
156
+ const actualContextTokens = usage.prompt_tokens || assembledTokens || 0;
150
157
  await rummy.db.update_turn_stats.run({
151
158
  id: rummy.turnId,
159
+ context_tokens: actualContextTokens,
160
+ reasoning_content: responseMessage?.reasoning_content || null,
152
161
  prompt_tokens: usage.prompt_tokens ?? 0,
153
162
  cached_tokens: cachedTokens ?? 0,
154
163
  completion_tokens: usage.completion_tokens ?? 0,
@@ -187,10 +196,8 @@ export default class Telemetry {
187
196
 
188
197
  #flush() {
189
198
  if (!this.#lastRunPath || this.#turnLog.length === 0) return;
190
- try {
191
- writeFileSync(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`);
192
- } catch {
193
- // RUMMY_HOME may not exist yet
194
- }
199
+ writeFile(this.#lastRunPath, `${this.#turnLog.join("\n")}\n`).catch(
200
+ () => {},
201
+ );
195
202
  }
196
203
  }
@@ -0,0 +1,20 @@
1
+ # think
2
+
3
+ Provides a `<think>` tag for model reasoning. Not a tool — does not
4
+ appear in the tool list.
5
+
6
+ ## Registration
7
+
8
+ - **Scheme**: `think` — `category: "logging"`, `model_visible: 0`
9
+ - **No handler, no view, no tool registration**
10
+
11
+ ## Behavior
12
+
13
+ The model writes `<think>reasoning</think>` before tool commands.
14
+ XmlParser captures it, TurnExecutor records it as a `think://` entry.
15
+ Invisible to the model on subsequent turns (`model_visible: 0`).
16
+ Available for debugging and audit.
17
+
18
+ Models with server-side reasoning (extended thinking) use that
19
+ capability independently. The `<think>` tag is a floor — every model
20
+ gets at least this.
@@ -0,0 +1,5 @@
1
+ export default class Think {
2
+ constructor(core) {
3
+ core.registerScheme({ modelVisible: 0, category: "logging" });
4
+ }
5
+ }
@@ -7,9 +7,9 @@ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
7
7
  ## Registration
8
8
 
9
9
  - **Tool**: `unknown`
10
- - **Modes**: ask, act
11
- - **Category**: structural
10
+ - **Category**: `unknown`
12
11
  - **Handler**: None — recorded by TurnExecutor, deduplicated against existing unknowns.
12
+ - **Filter**: `assembly.system` at priority 300 — renders `<unknowns>` section.
13
13
 
14
14
  ## Projection
15
15
 
@@ -18,6 +18,7 @@ The Rumsfeld mechanism. The model registers what it doesn't know before acting.
18
18
  ## Behavior
19
19
 
20
20
  Unknowns are sticky — they persist across turns until the model explicitly
21
- stores or removes them. The model investigates unknowns using `<get>`,
22
- `<env>`, or `<ask_user>`, then removes resolved ones with `<rm>`.
23
- Server deduplicates on insert.
21
+ removes them with `<rm>`. The model investigates unknowns using `<get>`,
22
+ `<env>`, or `<ask_user>`, then removes resolved ones. Server deduplicates
23
+ on insert. Each unknown renders with turn, fidelity, and tokens for
24
+ temporal reasoning and context management.
@@ -1,19 +1,20 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./unknownDoc.js";
2
2
 
3
3
  export default class Unknown {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
+ core.ensureTool();
8
9
  core.registerScheme({
9
- category: "knowledge",
10
+ category: "unknown",
10
11
  });
11
12
  core.on("full", this.full.bind(this));
12
13
  core.filter("assembly.system", this.assembleUnknowns.bind(this), 300);
13
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
14
- core.filter("instructions.toolDocs", async (content) =>
15
- content ? `${content}\n\n${docs}` : docs,
16
- );
14
+ core.filter("instructions.toolDocs", async (docsMap) => {
15
+ docsMap.unknown = docs;
16
+ return docsMap;
17
+ });
17
18
  }
18
19
 
19
20
  full(entry) {
@@ -24,9 +25,11 @@ export default class Unknown {
24
25
  const entries = ctx.rows.filter((r) => r.category === "unknown");
25
26
  if (entries.length === 0) return content;
26
27
 
27
- const lines = entries.map(
28
- (u) => `<unknown path="${u.path}">${u.body}</unknown>`,
29
- );
28
+ const lines = entries.map((u) => {
29
+ const fidelity = u.fidelity ? ` fidelity="${u.fidelity}"` : "";
30
+ const tokens = u.tokens ? ` tokens="${u.tokens}"` : "";
31
+ return `<unknown path="${u.path}" turn="${u.source_turn || u.turn}"${fidelity}${tokens}>${u.body}</unknown>`;
32
+ });
30
33
  return `${content}\n\n<unknowns>\n${lines.join("\n")}\n</unknowns>`;
31
34
  }
32
35
  }
@@ -0,0 +1,31 @@
1
+ // Tool doc for <unknown>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax: body = what you need to learn
6
+ [
7
+ `## <unknown>[specific thing I need to learn]</unknown> - Track open questions`,
8
+ ],
9
+
10
+ // --- Examples: concrete unknowns, not abstract
11
+ [
12
+ `Example: <unknown path="unknown://answer">contents of answer.txt</unknown>`,
13
+ `Specific and actionable. Shows that unknowns are concrete investigation targets.`,
14
+ ],
15
+ [
16
+ `Example: <unknown>which database adapter is configured</unknown>`,
17
+ `Domain question. Shows unknowns for configuration/architecture questions.`,
18
+ ],
19
+
20
+ // --- Lifecycle: register → investigate → resolve
21
+ [
22
+ `* Investigate with Tool Commands`,
23
+ `Cross-tool lifecycle: unknowns drive get/env/ask_user actions.`,
24
+ ],
25
+ [
26
+ `* When resolved or irrelevant, remove with <rm path="unknown://..."/>`,
27
+ `Cross-tool lifecycle: rm cleans resolved unknowns from context.`,
28
+ ],
29
+ ];
30
+
31
+ export default LINES.map(([text]) => text).join("\n");
@@ -1,18 +1,13 @@
1
1
  # update
2
2
 
3
- Structural tool for model-generated progress updates.
3
+ Lifecycle signal the model declares it has more work to do.
4
4
 
5
5
  ## Registration
6
6
 
7
7
  - **Tool**: `update`
8
- - **Modes**: ask, act
9
- - **Category**: structural
10
- - **Handler**: None — projection only.
8
+ - **Category**: `logging`
9
+ - **Handler**: None — recorded by TurnExecutor as a lifecycle signal.
11
10
 
12
11
  ## Projection
13
12
 
14
13
  Shows `update` followed by the entry body.
15
-
16
- ## Behavior
17
-
18
- No handler logic. Allows the model to emit progress/status entries that appear in context via projection.
@@ -1,17 +1,18 @@
1
- import { readFileSync } from "node:fs";
1
+ import docs from "./updateDoc.js";
2
2
 
3
3
  export default class Update {
4
4
  #core;
5
5
 
6
6
  constructor(core) {
7
7
  this.#core = core;
8
- core.registerScheme({ category: "structural" });
8
+ core.ensureTool();
9
+ core.registerScheme({ category: "logging" });
9
10
  core.on("full", this.full.bind(this));
10
11
  core.on("summary", this.summary.bind(this));
11
- const docs = readFileSync(new URL("./docs.md", import.meta.url), "utf8");
12
- core.filter("instructions.toolDocs", async (content) =>
13
- content ? `${content}\n\n${docs}` : docs,
14
- );
12
+ core.filter("instructions.toolDocs", async (docsMap) => {
13
+ docsMap.update = docs;
14
+ return docsMap;
15
+ });
15
16
  }
16
17
 
17
18
  full(entry) {
@@ -0,0 +1,33 @@
1
+ // Tool doc for <update>. Each entry: [text, rationale].
2
+ // Text goes to the model. Rationale stays in source.
3
+ // Changing ANY line requires reading ALL rationales first.
4
+ const LINES = [
5
+ // --- Syntax
6
+ ["## <update>[brief status]</update> - Signal continuation"],
7
+
8
+ // --- Examples: research progress and multi-step work
9
+ [
10
+ "Example: <update>Reading config files</update>",
11
+ "Progress checkpoint. Shows update as a status signal, not a log entry.",
12
+ ],
13
+ [
14
+ "Example: <update>Found 3 issues, fixing first</update>",
15
+ "Multi-step progress. Shows update for ongoing work.",
16
+ ],
17
+
18
+ // --- Constraints: RFC-style MUST/MUST NOT
19
+ [
20
+ "* YOU MUST use <update> if still working — describes the current state",
21
+ "Continuation signal. Triggers the next turn in the loop.",
22
+ ],
23
+ [
24
+ "* YOU MUST NOT use <update> if done — use <summarize/> instead",
25
+ "Mutual exclusion with summarize. Prevents infinite loops.",
26
+ ],
27
+ [
28
+ "* YOU MUST keep <update> to <= 80 characters",
29
+ "Length cap. Prevents models from writing essays in status updates.",
30
+ ],
31
+ ];
32
+
33
+ export default LINES.map(([text]) => text).join("\n");