@possumtech/rummy 0.5.0 → 2.0.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 (157) hide show
  1. package/.env.example +42 -5
  2. package/PLUGINS.md +389 -194
  3. package/README.md +25 -8
  4. package/SPEC.md +934 -373
  5. package/bin/demo.js +166 -0
  6. package/bin/rummy.js +9 -3
  7. package/biome/no-fallbacks.grit +50 -0
  8. package/lang/en.json +2 -2
  9. package/migrations/001_initial_schema.sql +88 -37
  10. package/package.json +13 -11
  11. package/scriptify/ask_run.js +77 -0
  12. package/service.js +50 -9
  13. package/src/agent/AgentLoop.js +476 -335
  14. package/src/agent/ContextAssembler.js +4 -4
  15. package/src/agent/Entries.js +676 -0
  16. package/src/agent/ProjectAgent.js +30 -18
  17. package/src/agent/TurnExecutor.js +232 -421
  18. package/src/agent/XmlParser.js +99 -33
  19. package/src/agent/budget.js +56 -0
  20. package/src/agent/errors.js +22 -0
  21. package/src/agent/httpStatus.js +39 -0
  22. package/src/agent/known_checks.sql +8 -4
  23. package/src/agent/known_queries.sql +9 -13
  24. package/src/agent/known_store.sql +280 -125
  25. package/src/agent/materializeContext.js +104 -0
  26. package/src/agent/runs.sql +29 -7
  27. package/src/agent/schemes.sql +14 -3
  28. package/src/agent/tokens.js +6 -0
  29. package/src/agent/turns.sql +9 -9
  30. package/src/hooks/HookRegistry.js +6 -5
  31. package/src/hooks/Hooks.js +44 -3
  32. package/src/hooks/PluginContext.js +29 -21
  33. package/src/{server → hooks}/RpcRegistry.js +2 -1
  34. package/src/hooks/RummyContext.js +139 -35
  35. package/src/hooks/ToolRegistry.js +21 -16
  36. package/src/llm/LlmProvider.js +66 -89
  37. package/src/llm/errors.js +21 -0
  38. package/src/llm/retry.js +63 -0
  39. package/src/plugins/ask_user/README.md +1 -1
  40. package/src/plugins/ask_user/ask_user.js +37 -12
  41. package/src/plugins/ask_user/ask_userDoc.js +2 -25
  42. package/src/plugins/ask_user/ask_userDoc.md +10 -0
  43. package/src/plugins/budget/README.md +27 -25
  44. package/src/plugins/budget/budget.js +306 -88
  45. package/src/plugins/cp/README.md +2 -2
  46. package/src/plugins/cp/cp.js +29 -11
  47. package/src/plugins/cp/cpDoc.js +2 -15
  48. package/src/plugins/cp/cpDoc.md +7 -0
  49. package/src/plugins/engine/README.md +2 -2
  50. package/src/plugins/engine/engine.sql +4 -4
  51. package/src/plugins/engine/turn_context.sql +10 -10
  52. package/src/plugins/env/README.md +20 -5
  53. package/src/plugins/env/env.js +45 -6
  54. package/src/plugins/env/envDoc.js +2 -23
  55. package/src/plugins/env/envDoc.md +13 -0
  56. package/src/plugins/error/README.md +16 -0
  57. package/src/plugins/error/error.js +151 -0
  58. package/src/plugins/file/README.md +6 -6
  59. package/src/plugins/file/file.js +15 -2
  60. package/src/plugins/get/README.md +1 -1
  61. package/src/plugins/get/get.js +103 -48
  62. package/src/plugins/get/getDoc.js +2 -32
  63. package/src/plugins/get/getDoc.md +36 -0
  64. package/src/plugins/hedberg/README.md +1 -2
  65. package/src/plugins/hedberg/hedberg.js +8 -4
  66. package/src/plugins/hedberg/matcher.js +16 -17
  67. package/src/plugins/hedberg/normalize.js +0 -48
  68. package/src/plugins/helpers.js +42 -2
  69. package/src/plugins/index.js +146 -123
  70. package/src/plugins/instructions/README.md +35 -9
  71. package/src/plugins/instructions/instructions.js +244 -9
  72. package/src/plugins/instructions/instructions.md +33 -0
  73. package/src/plugins/instructions/instructions_104.md +7 -0
  74. package/src/plugins/instructions/instructions_105.md +38 -0
  75. package/src/plugins/instructions/instructions_106.md +21 -0
  76. package/src/plugins/instructions/instructions_107.md +10 -0
  77. package/src/plugins/instructions/instructions_108.md +0 -0
  78. package/src/plugins/instructions/protocol.js +12 -0
  79. package/src/plugins/known/README.md +2 -2
  80. package/src/plugins/known/known.js +68 -36
  81. package/src/plugins/known/knownDoc.js +2 -17
  82. package/src/plugins/known/knownDoc.md +8 -0
  83. package/src/plugins/log/README.md +48 -0
  84. package/src/plugins/log/log.js +129 -0
  85. package/src/plugins/mv/README.md +2 -2
  86. package/src/plugins/mv/mv.js +55 -22
  87. package/src/plugins/mv/mvDoc.js +2 -18
  88. package/src/plugins/mv/mvDoc.md +10 -0
  89. package/src/plugins/ollama/README.md +15 -0
  90. package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
  91. package/src/plugins/openai/README.md +17 -0
  92. package/src/plugins/openai/openai.js +120 -0
  93. package/src/plugins/openrouter/README.md +27 -0
  94. package/src/plugins/openrouter/openrouter.js +121 -0
  95. package/src/plugins/persona/README.md +20 -0
  96. package/src/plugins/persona/persona.js +9 -16
  97. package/src/plugins/policy/README.md +21 -0
  98. package/src/plugins/policy/policy.js +29 -14
  99. package/src/plugins/prompt/README.md +1 -1
  100. package/src/plugins/prompt/prompt.js +64 -16
  101. package/src/plugins/rm/README.md +1 -1
  102. package/src/plugins/rm/rm.js +56 -12
  103. package/src/plugins/rm/rmDoc.js +2 -20
  104. package/src/plugins/rm/rmDoc.md +13 -0
  105. package/src/plugins/rpc/README.md +2 -2
  106. package/src/plugins/rpc/rpc.js +525 -296
  107. package/src/plugins/set/README.md +1 -1
  108. package/src/plugins/set/set.js +318 -75
  109. package/src/plugins/set/setDoc.js +2 -35
  110. package/src/plugins/set/setDoc.md +22 -0
  111. package/src/plugins/sh/README.md +28 -5
  112. package/src/plugins/sh/sh.js +50 -6
  113. package/src/plugins/sh/shDoc.js +2 -23
  114. package/src/plugins/sh/shDoc.md +13 -0
  115. package/src/plugins/skill/README.md +23 -0
  116. package/src/plugins/skill/skill.js +14 -18
  117. package/src/plugins/stream/README.md +101 -0
  118. package/src/plugins/stream/stream.js +290 -0
  119. package/src/plugins/telemetry/README.md +1 -1
  120. package/src/plugins/telemetry/telemetry.js +129 -80
  121. package/src/plugins/think/README.md +1 -1
  122. package/src/plugins/think/think.js +12 -0
  123. package/src/plugins/think/thinkDoc.js +2 -15
  124. package/src/plugins/think/thinkDoc.md +7 -0
  125. package/src/plugins/unknown/README.md +3 -3
  126. package/src/plugins/unknown/unknown.js +47 -19
  127. package/src/plugins/unknown/unknownDoc.js +2 -21
  128. package/src/plugins/unknown/unknownDoc.md +11 -0
  129. package/src/plugins/update/README.md +1 -1
  130. package/src/plugins/update/update.js +83 -5
  131. package/src/plugins/update/updateDoc.js +2 -30
  132. package/src/plugins/update/updateDoc.md +8 -0
  133. package/src/plugins/xai/README.md +23 -0
  134. package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
  135. package/src/plugins/yolo/yolo.js +192 -0
  136. package/src/server/ClientConnection.js +64 -37
  137. package/src/server/SocketServer.js +23 -10
  138. package/src/server/protocol.js +11 -0
  139. package/src/sql/v_model_context.sql +27 -31
  140. package/src/sql/v_run_log.sql +9 -14
  141. package/EXCEPTIONS.md +0 -46
  142. package/FIDELITY_CONTRACT.md +0 -172
  143. package/src/agent/KnownStore.js +0 -337
  144. package/src/agent/ResponseHealer.js +0 -241
  145. package/src/llm/OpenAiClient.js +0 -100
  146. package/src/llm/OpenRouterClient.js +0 -100
  147. package/src/plugins/budget/recovery.js +0 -47
  148. package/src/plugins/instructions/preamble.md +0 -45
  149. package/src/plugins/performed/README.md +0 -15
  150. package/src/plugins/performed/performed.js +0 -45
  151. package/src/plugins/previous/README.md +0 -16
  152. package/src/plugins/previous/previous.js +0 -56
  153. package/src/plugins/progress/README.md +0 -16
  154. package/src/plugins/progress/progress.js +0 -43
  155. package/src/plugins/summarize/README.md +0 -19
  156. package/src/plugins/summarize/summarize.js +0 -32
  157. package/src/plugins/summarize/summarizeDoc.js +0 -27
@@ -1,30 +1,74 @@
1
+ import { logPathToDataBase } from "../helpers.js";
1
2
  import docs from "./shDoc.js";
2
3
 
4
+ const LOG_ACTION_RE = /^log:\/\/turn_\d+\/(\w+)\//;
5
+
3
6
  export default class Sh {
4
7
  #core;
5
8
 
6
9
  constructor(core) {
7
10
  this.#core = core;
8
- core.registerScheme();
11
+ // `sh` scheme holds the streamed stdout/stderr payload — that's
12
+ // data the model reads, not an audit record. The log entry at
13
+ // log://turn_N/sh/{slug} (scheme=log, category=logging) is the
14
+ // audit record; it lives in a separate namespace by design.
15
+ // See SPEC §streaming_entries and the scheme/category invariant.
16
+ core.registerScheme({ category: "data" });
9
17
  core.on("handler", this.handler.bind(this));
10
- core.on("promoted", this.full.bind(this));
11
- core.on("demoted", this.summary.bind(this));
18
+ core.on("visible", this.full.bind(this));
19
+ core.on("summarized", this.summary.bind(this));
12
20
  core.filter("instructions.toolDocs", async (docsMap) => {
13
21
  docsMap.sh = docs;
14
22
  return docsMap;
15
23
  });
24
+ core.on("proposal.accepted", this.#onAccepted.bind(this));
25
+ }
26
+
27
+ async #onAccepted(ctx) {
28
+ const m = LOG_ACTION_RE.exec(ctx.path);
29
+ if (m?.[1] !== "sh") return;
30
+ let command = "";
31
+ if (ctx.attrs?.command) command = ctx.attrs.command;
32
+ else if (ctx.attrs?.summary) command = ctx.attrs.summary;
33
+ const turn = (await ctx.db.get_run_by_id.get({ id: ctx.runId })).next_turn;
34
+ const dataBase = logPathToDataBase(ctx.path);
35
+ for (const ch of [1, 2]) {
36
+ await ctx.entries.set({
37
+ runId: ctx.runId,
38
+ turn,
39
+ path: `${dataBase}_${ch}`,
40
+ body: "",
41
+ state: "streaming",
42
+ visibility: "summarized",
43
+ attributes: { command, summary: command, channel: ch },
44
+ });
45
+ }
46
+ await ctx.entries.set({
47
+ runId: ctx.runId,
48
+ path: ctx.path,
49
+ state: "resolved",
50
+ body: `ran '${command}' (in progress). Output: ${dataBase}_1, ${dataBase}_2`,
51
+ });
16
52
  }
17
53
 
18
54
  async handler(entry, rummy) {
19
55
  const { entries: store, sequence: turn, runId, loopId } = rummy;
20
- await store.upsert(runId, turn, entry.resultPath, entry.body, 202, {
21
- attributes: entry.attributes,
56
+ // Proposal at 202 with the command as summary and empty body the
57
+ // body fills in on accept (log message about the action). Data
58
+ // entries with stdout/stderr are created on accept in resolve().
59
+ await store.set({
60
+ runId,
61
+ turn,
62
+ path: entry.resultPath,
63
+ body: "",
64
+ state: "proposed",
65
+ attributes: { ...entry.attributes, summary: entry.attributes.command },
22
66
  loopId,
23
67
  });
24
68
  }
25
69
 
26
70
  full(entry) {
27
- return `# sh ${entry.attributes.command || ""}\n${entry.body}`;
71
+ return `# sh ${entry.attributes.command}\n${entry.body}`;
28
72
  }
29
73
 
30
74
  summary() {
@@ -1,24 +1,3 @@
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
- ["## <sh>[command]</sh> - Run a shell command with side effects"],
6
- [
7
- "Example: <sh>npm install express</sh>",
8
- "Package install. Real side-effect command.",
9
- ],
10
- [
11
- "Example: <sh>npm test</sh>",
12
- "Test execution. Another common side-effect action.",
13
- ],
14
- [
15
- "* YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>",
16
- "Forces file operations through the entry system.",
17
- ],
18
- [
19
- "* YOU MUST use <env></env> for commands without side effects",
20
- "Reinforces the env/sh split. Read = env, mutate = sh.",
21
- ],
22
- ];
1
+ import { loadDoc } from "../helpers.js";
23
2
 
24
- export default LINES.map(([text]) => text).join("\n");
3
+ export default loadDoc(import.meta.url, "shDoc.md");
@@ -0,0 +1,13 @@
1
+ ## <sh>[command]</sh> - Run a shell command with side effects
2
+
3
+ Example: <sh>npm install express</sh>
4
+ <!-- Package install. Real side-effect command. -->
5
+
6
+ Example: <sh>npm test</sh>
7
+ <!-- Test execution. Another common side-effect action. -->
8
+
9
+ * YOU MUST NOT use <sh></sh> to read, create, or edit files — use <get></get> and <set></set>
10
+ <!-- Forces file operations through the entry system. -->
11
+
12
+ * YOU MUST use <env></env> for commands without side effects
13
+ <!-- Reinforces the env/sh split. Read = env, mutate = sh. -->
@@ -0,0 +1,23 @@
1
+ # skill {#skill_plugin}
2
+
3
+ Runtime skill management. A skill is a markdown file that gets
4
+ attached to a run as a `skill://<name>` entry. Models see skills
5
+ like any other entry.
6
+
7
+ ## Files
8
+
9
+ - **skill.js** — RPC registration and skill file loading.
10
+
11
+ ## Registration
12
+
13
+ - **Scheme**: `skill` (category: `data`)
14
+ - **Projections**: visible → body; summarized → empty.
15
+
16
+ ## RPC Methods
17
+
18
+ | Method | Params | Notes |
19
+ |--------|--------|-------|
20
+ | `skill/add` | `{ run, name }` | Load `${RUMMY_HOME}/skills/<name>.md` and attach it to the run as `skill://<name>`. |
21
+ | `skill/remove` | `{ run, name }` | Remove the skill entry from the run. |
22
+ | `getSkills` | `{ run }` | Skills active on a run. |
23
+ | `listSkills` | — | Available skill files on disk. |
@@ -10,8 +10,8 @@ export default class Skill {
10
10
  name: "skill",
11
11
  category: "data",
12
12
  });
13
- core.hooks.tools.onView("skill", (entry) => entry.body, "promoted");
14
- core.hooks.tools.onView("skill", () => "", "demoted");
13
+ core.hooks.tools.onView("skill", (entry) => entry.body, "visible");
14
+ core.hooks.tools.onView("skill", () => "", "summarized");
15
15
 
16
16
  const r = core.hooks.rpc.registry;
17
17
 
@@ -25,7 +25,12 @@ export default class Skill {
25
25
 
26
26
  const body = await loadFile("skills", params.name);
27
27
  const store = ctx.projectAgent.entries;
28
- await store.upsert(runRow.id, 0, `skill://${params.name}`, body, 200, {
28
+ await store.set({
29
+ runId: runRow.id,
30
+ turn: 0,
31
+ path: `skill://${params.name}`,
32
+ body,
33
+ state: "resolved",
29
34
  attributes: {
30
35
  name: params.name,
31
36
  source: filePath("skills", params.name),
@@ -52,7 +57,7 @@ export default class Skill {
52
57
  if (!runRow) throw new Error(`Run not found: ${params.run}`);
53
58
 
54
59
  const store = ctx.projectAgent.entries;
55
- await store.remove(runRow.id, `skill://${params.name}`);
60
+ await store.rm({ runId: runRow.id, path: `skill://${params.name}` });
56
61
 
57
62
  return { status: "ok" };
58
63
  },
@@ -112,23 +117,14 @@ function filePath(subfolder, name) {
112
117
  async function loadFile(subfolder, name) {
113
118
  const path = filePath(subfolder, name);
114
119
  if (!path) throw new Error("RUMMY_HOME not configured");
115
- try {
116
- return await fs.readFile(path, "utf8");
117
- } catch (err) {
118
- if (err.code === "ENOENT") throw new Error(`Not found: ${path}`);
119
- throw err;
120
- }
120
+ return fs.readFile(path, "utf8");
121
121
  }
122
122
 
123
123
  async function listAvailable(subfolder) {
124
124
  const dir = configDir(subfolder);
125
125
  if (!dir) return [];
126
- try {
127
- const files = await fs.readdir(dir);
128
- return files
129
- .filter((f) => f.endsWith(".md"))
130
- .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
131
- } catch {
132
- return [];
133
- }
126
+ const files = await fs.readdir(dir);
127
+ return files
128
+ .filter((f) => f.endsWith(".md"))
129
+ .map((f) => ({ name: f.replace(".md", ""), path: join(dir, f) }));
134
130
  }
@@ -0,0 +1,101 @@
1
+ # stream {#stream_plugin}
2
+
3
+ Generic streaming entry infrastructure. Provides RPC methods that any
4
+ producer plugin (sh, env, future: search, fetch, watch) can use to
5
+ populate data entries over time.
6
+
7
+ ## Namespace split
8
+
9
+ A streaming action lives in **two namespaces** by design:
10
+
11
+ - **Log entry** (audit record): `log://turn_N/{action}/{slug}` —
12
+ scheme=`log`, category=`logging`. Created by the producer's dispatch
13
+ handler (via `TurnExecutor` → `logPath`). This is the proposal the
14
+ client resolves. Renders inside `<log>`.
15
+ - **Data channels** (payload): `{action}://turn_N/{slug}_1`,
16
+ `{action}://turn_N/{slug}_2`, ... — scheme=`{action}` (sh, env, ...),
17
+ category=`data`. Created at status=102 on proposal acceptance. Grow
18
+ via `stream`; terminal via `stream/completed` / `stream/aborted` /
19
+ `stream/cancel`. Render inside `<context>`.
20
+
21
+ The stream RPC `path` param is always the **log-entry path** (that's
22
+ what clients receive on `run/proposal`). The server derives the data
23
+ base path internally via `logPathToDataBase`. See
24
+ [scheme_category_split](#scheme_category_split).
25
+
26
+ ## RPC Methods
27
+
28
+ ### `stream { run, path, channel, chunk }`
29
+
30
+ Append `chunk` to the data channel entry at `{dataBase}_{channel}`,
31
+ where `dataBase` is derived from the log path. Entry must exist
32
+ (created by the producer plugin on proposal acceptance, at status=102).
33
+
34
+ Unix FD convention for the channel number: 1=stdout, 2=stderr, higher
35
+ numbers for additional producer channels.
36
+
37
+ ### `stream/completed { run, path, exit_code?, duration? }`
38
+
39
+ Transition all `{dataBase}_*` data channels to terminal status:
40
+ - `exit_code=0` (or omitted) → status=200
41
+ - `exit_code≠0` → status=500
42
+
43
+ Rewrite the log entry at `path` with a summary: command, exit code,
44
+ duration, and channel sizes.
45
+
46
+ ### `stream/aborted { run, path, reason?, duration? }`
47
+
48
+ Client-initiated cancellation. Transition all `{dataBase}_*` data
49
+ channels to status **499 (Client Closed Request)** — the de-facto HTTP
50
+ status for a request terminated by the client. Rewrite the log entry
51
+ body to note the abort (with optional `reason` and `duration`).
52
+
53
+ Client contract: kill the underlying process first, then call
54
+ `stream/aborted`. Body of each data channel is preserved at whatever
55
+ content was streamed before the kill.
56
+
57
+ ### `stream/cancel { run, path, reason? }`
58
+
59
+ Server-initiated cancellation. Any client (or internal server code) can
60
+ cancel a streaming producer — the server transitions channels to **499**
61
+ immediately and pushes a `stream/cancelled` notification to all connected
62
+ clients so they can kill their local processes.
63
+
64
+ Also serves as **stale 102 cleanup**: if the originating client died
65
+ mid-stream (`stream/completed` never arrived), any client can call
66
+ `stream/cancel` to mark orphaned entries terminal.
67
+
68
+ ## Producer Plugin Contract
69
+
70
+ A streaming producer plugin:
71
+
72
+ 1. On model dispatch, writes the **proposal/log entry** at
73
+ `log://turn_N/{action}/{slug}` at status=202 (this is automatic —
74
+ `TurnExecutor` builds the path via `logPath`; the producer's
75
+ `handler` just persists it).
76
+ 2. On `proposal.accepted`, derives the data base
77
+ (`logPathToDataBase(ctx.path)`) and creates **data entries** at
78
+ `{dataBase}_1`, `{dataBase}_2`, etc. at status=102, category=data,
79
+ visibility=summarized, empty body. Then rewrites the log entry body
80
+ to reference the channel paths.
81
+ 3. Client or external producer calls the `stream` RPC with chunks as
82
+ they arrive.
83
+ 4. When the producer is done, the client/producer calls
84
+ `stream/completed`.
85
+
86
+ Current producers:
87
+ - **sh** — shell commands with side effects (stdout ch1, stderr ch2)
88
+ - **env** — safe shell (stdout ch1, stderr ch2)
89
+
90
+ Future producers that could adopt this pattern:
91
+ - **search** — web search results streaming in (primary ch1, warnings ch2)
92
+ - **fetch** — large page fetch (body ch1, redirects/headers ch2)
93
+ - **tail** — log file following (lines ch1)
94
+ - **watch** — file system events (events ch1)
95
+
96
+ ## Not a Model-Facing Tool
97
+
98
+ No scheme registration, no tooldoc, no dispatch handler. The model
99
+ interacts with streamed output via `<get>` on the data entries; the
100
+ stream plugin is purely RPC infrastructure that clients and producer
101
+ plugins use.
@@ -0,0 +1,290 @@
1
+ import { logPathToDataBase } from "../helpers.js";
2
+
3
+ /**
4
+ * Stream plugin — generic streaming entry infrastructure.
5
+ *
6
+ * Receives chunks from the client (or any producer) and appends them to
7
+ * existing data entries. Producers (sh/env handlers) create the data
8
+ * entries at status=102 on proposal acceptance; this plugin handles the
9
+ * subsequent append + terminal-status transition via two RPC methods.
10
+ *
11
+ * RPC `path` param is the **log-entry path** (log://turn_N/{action}/{slug}
12
+ * — that's what the client sees on `run/proposal`). Channels live under
13
+ * the producer scheme ({action}://turn_N/{slug}_N) for a clean
14
+ * data-vs-logging namespace split; this plugin derives the data base from
15
+ * the log path on every RPC call.
16
+ *
17
+ * Not a model-facing tool. No scheme, no tooldoc, no dispatch handler.
18
+ * Pure RPC plumbing that any streaming-producer plugin can leverage.
19
+ */
20
+ export default class Stream {
21
+ #core;
22
+
23
+ constructor(core) {
24
+ this.#core = core;
25
+ const hooks = core.hooks;
26
+ const r = hooks.rpc.registry;
27
+
28
+ // stream: append a chunk to a streaming entry.
29
+ // Entry path is constructed as `${path}_${channel}` per the Unix FD
30
+ // convention (1=stdout, 2=stderr, higher=other producer channels).
31
+ r.register("stream", {
32
+ handler: async (params, ctx) => {
33
+ if (!params.run) throw new Error("run is required");
34
+ if (!params.path) throw new Error("path is required");
35
+ if (params.channel == null)
36
+ throw new Error("channel is required (numeric)");
37
+ if (params.chunk == null) throw new Error("chunk is required");
38
+
39
+ const runRow = await ctx.db.get_run_by_alias.get({
40
+ alias: params.run,
41
+ });
42
+ if (!runRow) throw new Error(`run not found: ${params.run}`);
43
+
44
+ const dataBase = logPathToDataBase(params.path);
45
+ if (!dataBase) {
46
+ throw new Error(
47
+ `path must be a log entry (log://turn_N/...); got: ${params.path}`,
48
+ );
49
+ }
50
+ const entryPath = `${dataBase}_${params.channel}`;
51
+ await ctx.projectAgent.entries.set({
52
+ runId: runRow.id,
53
+ path: entryPath,
54
+ body: params.chunk,
55
+ append: true,
56
+ });
57
+ return { status: "ok" };
58
+ },
59
+ description:
60
+ "Append a chunk to a streaming entry channel. Used by clients and producers to grow a 102 entry's body.",
61
+ params: {
62
+ run: "string — run alias",
63
+ path: "string — log-entry path (log://turn_N/{action}/{slug}); server derives the data channel path",
64
+ channel: "number — channel index (Unix FD: 1=stdout, 2=stderr)",
65
+ chunk: "string — content to append to the entry body",
66
+ },
67
+ requiresInit: true,
68
+ });
69
+
70
+ // stream/completed: transition all data channels for this producer
71
+ // to their terminal status and finalize the log entry body.
72
+ r.register("stream/completed", {
73
+ handler: async (params, ctx) => {
74
+ if (!params.run) throw new Error("run is required");
75
+ if (!params.path) throw new Error("path is required");
76
+
77
+ const runRow = await ctx.db.get_run_by_alias.get({
78
+ alias: params.run,
79
+ });
80
+ if (!runRow) throw new Error(`run not found: ${params.run}`);
81
+ const runId = runRow.id;
82
+
83
+ const { exit_code: exitCode = 0, duration = null } = params;
84
+ const terminalState = exitCode === 0 ? "resolved" : "failed";
85
+ const terminalOutcome = exitCode === 0 ? null : `exit:${exitCode}`;
86
+
87
+ const dataBase = logPathToDataBase(params.path);
88
+ if (!dataBase) {
89
+ throw new Error(
90
+ `path must be a log entry (log://turn_N/...); got: ${params.path}`,
91
+ );
92
+ }
93
+ // Find all `{dataBase}_*` data entries (channels 1, 2, ...).
94
+ const store = ctx.projectAgent.entries;
95
+ const channels = await store.getEntriesByPattern(
96
+ runId,
97
+ `${dataBase}_*`,
98
+ null,
99
+ );
100
+ for (const ch of channels) {
101
+ await store.set({
102
+ runId,
103
+ path: ch.path,
104
+ state: terminalState,
105
+ body: ch.body,
106
+ outcome: terminalOutcome,
107
+ });
108
+ }
109
+
110
+ // Update the log entry body with final stats. Keep it terse —
111
+ // one line summarizing exit code, duration, and channel sizes.
112
+ const logEntry = await store.getAttributes(runId, params.path);
113
+ let command = "";
114
+ if (logEntry?.command) command = logEntry.command;
115
+ else if (logEntry?.summary) command = logEntry.summary;
116
+ const channelSummary = channels
117
+ .map((c) => {
118
+ const size = c.body ? `${c.tokens} tokens` : "empty";
119
+ return `${c.path} (${size})`;
120
+ })
121
+ .join(", ");
122
+ const dur = duration ? ` (${duration})` : "";
123
+ const exitLabel = exitCode === 0 ? "exit=0" : `exit=${exitCode}`;
124
+ const body = `ran '${command}', ${exitLabel}${dur}. Output: ${channelSummary}`;
125
+ await store.set({ runId, path: params.path, state: "resolved", body });
126
+
127
+ return { ok: true, channels: channels.length };
128
+ },
129
+ description:
130
+ "Finalize a streaming producer. Transitions all `{path}_*` data channels to terminal status (200 on exit_code=0, 500 otherwise) and rewrites the log entry body with exit code, duration, and channel sizes.",
131
+ params: {
132
+ run: "string — run alias",
133
+ path: "string — log-entry path (log://turn_N/{action}/{slug}); server derives the data channel path",
134
+ exit_code:
135
+ "number? — exit code (0=success→200, non-zero=failure→500). Defaults to 0 for non-process producers.",
136
+ duration: "string? — human-readable duration for the log entry",
137
+ },
138
+ requiresInit: true,
139
+ });
140
+
141
+ // stream/aborted: client-initiated cancellation. Transitions all data
142
+ // channels to status 499 (Client Closed Request — the de-facto HTTP
143
+ // status for client-terminated requests) and rewrites the log entry
144
+ // body to note the abort. Shape mirrors stream/completed for client
145
+ // symmetry: same run/path addressing, same channel sweep.
146
+ r.register("stream/aborted", {
147
+ handler: async (params, ctx) => {
148
+ if (!params.run) throw new Error("run is required");
149
+ if (!params.path) throw new Error("path is required");
150
+
151
+ const runRow = await ctx.db.get_run_by_alias.get({
152
+ alias: params.run,
153
+ });
154
+ if (!runRow) throw new Error(`run not found: ${params.run}`);
155
+ const runId = runRow.id;
156
+
157
+ const { duration = null, reason = null } = params;
158
+
159
+ const dataBase = logPathToDataBase(params.path);
160
+ if (!dataBase) {
161
+ throw new Error(
162
+ `path must be a log entry (log://turn_N/...); got: ${params.path}`,
163
+ );
164
+ }
165
+ const store = ctx.projectAgent.entries;
166
+ const channels = await store.getEntriesByPattern(
167
+ runId,
168
+ `${dataBase}_*`,
169
+ null,
170
+ );
171
+ for (const ch of channels) {
172
+ await store.set({
173
+ runId,
174
+ path: ch.path,
175
+ state: "cancelled",
176
+ body: ch.body,
177
+ outcome: reason ? reason : "aborted",
178
+ });
179
+ }
180
+
181
+ const logEntry = await store.getAttributes(runId, params.path);
182
+ let command = "";
183
+ if (logEntry?.command) command = logEntry.command;
184
+ else if (logEntry?.summary) command = logEntry.summary;
185
+ const channelSummary = channels
186
+ .map((c) => {
187
+ const size = c.body ? `${c.tokens} tokens` : "empty";
188
+ return `${c.path} (${size})`;
189
+ })
190
+ .join(", ");
191
+ const qualifiers = [];
192
+ if (reason) qualifiers.push(reason);
193
+ if (duration) qualifiers.push(duration);
194
+ const qualifier = qualifiers.length
195
+ ? ` (${qualifiers.join(", ")})`
196
+ : "";
197
+ const body = `aborted '${command}'${qualifier}. Output: ${channelSummary}`;
198
+ await store.set({ runId, path: params.path, state: "resolved", body });
199
+
200
+ return { status: "ok", channels: channels.length };
201
+ },
202
+ description:
203
+ "Abort a streaming producer. Transitions all `{path}_*` data channels to status 499 (Client Closed Request) and rewrites the log entry body to note the abort.",
204
+ params: {
205
+ run: "string — run alias",
206
+ path: "string — log-entry path (log://turn_N/{action}/{slug}); server derives the data channel path",
207
+ reason:
208
+ "string? — human-readable abort reason (e.g. 'user cancelled', 'timeout')",
209
+ duration: "string? — human-readable duration at abort time",
210
+ },
211
+ requiresInit: true,
212
+ });
213
+
214
+ // stream/cancel: server-initiated cancellation. Any client (or
215
+ // internal server code) can cancel a streaming producer — the server
216
+ // transitions channels to 499 immediately and pushes a
217
+ // stream/cancelled notification so connected clients can kill their
218
+ // local processes. Also serves as stale 102 cleanup: if the client
219
+ // died mid-stream, call stream/cancel to mark orphaned entries terminal.
220
+ r.register("stream/cancel", {
221
+ handler: async (params, ctx) => {
222
+ if (!params.run) throw new Error("run is required");
223
+ if (!params.path) throw new Error("path is required");
224
+
225
+ const runRow = await ctx.db.get_run_by_alias.get({
226
+ alias: params.run,
227
+ });
228
+ if (!runRow) throw new Error(`run not found: ${params.run}`);
229
+ const runId = runRow.id;
230
+
231
+ const { reason = null } = params;
232
+
233
+ const dataBase = logPathToDataBase(params.path);
234
+ if (!dataBase) {
235
+ throw new Error(
236
+ `path must be a log entry (log://turn_N/...); got: ${params.path}`,
237
+ );
238
+ }
239
+ const store = ctx.projectAgent.entries;
240
+ const channels = await store.getEntriesByPattern(
241
+ runId,
242
+ `${dataBase}_*`,
243
+ null,
244
+ );
245
+ for (const ch of channels) {
246
+ await store.set({
247
+ runId,
248
+ path: ch.path,
249
+ state: "cancelled",
250
+ body: ch.body,
251
+ outcome: reason ? reason : "cancelled",
252
+ });
253
+ }
254
+
255
+ const logEntry = await store.getAttributes(runId, params.path);
256
+ let command = "";
257
+ if (logEntry?.command) command = logEntry.command;
258
+ else if (logEntry?.summary) command = logEntry.summary;
259
+ const channelSummary = channels
260
+ .map((c) => {
261
+ const size = c.body ? `${c.tokens} tokens` : "empty";
262
+ return `${c.path} (${size})`;
263
+ })
264
+ .join(", ");
265
+ const qualifier = reason ? ` (${reason})` : "";
266
+ const body = `cancelled '${command}'${qualifier}. Output: ${channelSummary}`;
267
+ await store.set({ runId, path: params.path, state: "resolved", body });
268
+
269
+ // Notify connected clients so they can kill local processes.
270
+ hooks.stream.cancelled.emit({
271
+ projectId: ctx.projectId,
272
+ run: params.run,
273
+ path: params.path,
274
+ reason,
275
+ });
276
+
277
+ return { ok: true, channels: channels.length };
278
+ },
279
+ description:
280
+ "Server-initiated cancellation. Transitions all `{path}_*` data channels to status 499 and pushes a stream/cancelled notification to connected clients. Also used for stale 102 cleanup when the originating client is gone.",
281
+ params: {
282
+ run: "string — run alias",
283
+ path: "string — log-entry path (log://turn_N/{action}/{slug}); server derives the data channel path",
284
+ reason:
285
+ "string? — cancellation reason (e.g. 'budget exceeded', 'stale cleanup', 'user cancelled from another client')",
286
+ },
287
+ requiresInit: true,
288
+ });
289
+ }
290
+ }
@@ -1,4 +1,4 @@
1
- # telemetry
1
+ # telemetry {#telemetry_plugin}
2
2
 
3
3
  Console logging for RPC lifecycle and turn events.
4
4