@possumtech/rummy 0.4.0 → 2.0.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/.env.example +21 -4
- package/PLUGINS.md +389 -194
- package/README.md +25 -8
- package/SPEC.md +850 -373
- package/bin/demo.js +166 -0
- package/bin/rummy.js +9 -3
- package/biome/no-fallbacks.grit +50 -0
- package/lang/en.json +2 -2
- package/migrations/001_initial_schema.sql +88 -37
- package/package.json +6 -4
- package/service.js +50 -9
- package/src/agent/AgentLoop.js +460 -331
- package/src/agent/ContextAssembler.js +4 -2
- package/src/agent/Entries.js +655 -0
- package/src/agent/ProjectAgent.js +30 -18
- package/src/agent/TurnExecutor.js +232 -379
- package/src/agent/XmlParser.js +242 -67
- package/src/agent/budget.js +56 -0
- package/src/agent/errors.js +22 -0
- package/src/agent/httpStatus.js +39 -0
- package/src/agent/known_checks.sql +8 -4
- package/src/agent/known_queries.sql +9 -13
- package/src/agent/known_store.sql +275 -118
- package/src/agent/materializeContext.js +102 -0
- package/src/agent/runs.sql +10 -7
- package/src/agent/schemes.sql +14 -3
- package/src/agent/turns.sql +9 -9
- package/src/hooks/HookRegistry.js +6 -5
- package/src/hooks/Hooks.js +44 -3
- package/src/hooks/PluginContext.js +35 -21
- package/src/{server → hooks}/RpcRegistry.js +2 -1
- package/src/hooks/RummyContext.js +140 -37
- package/src/hooks/ToolRegistry.js +36 -35
- package/src/llm/LlmProvider.js +64 -90
- package/src/llm/errors.js +21 -0
- package/src/plugins/ask_user/README.md +1 -1
- package/src/plugins/ask_user/ask_user.js +37 -12
- package/src/plugins/ask_user/ask_userDoc.js +2 -23
- package/src/plugins/ask_user/ask_userDoc.md +10 -0
- package/src/plugins/budget/README.md +27 -23
- package/src/plugins/budget/budget.js +261 -69
- package/src/plugins/cp/README.md +2 -2
- package/src/plugins/cp/cp.js +31 -13
- package/src/plugins/cp/cpDoc.js +2 -23
- package/src/plugins/cp/cpDoc.md +7 -0
- package/src/plugins/engine/README.md +2 -2
- package/src/plugins/engine/engine.sql +4 -4
- package/src/plugins/engine/turn_context.sql +10 -10
- package/src/plugins/env/README.md +20 -5
- package/src/plugins/env/env.js +47 -8
- package/src/plugins/env/envDoc.js +2 -23
- package/src/plugins/env/envDoc.md +13 -0
- package/src/plugins/error/README.md +16 -0
- package/src/plugins/error/error.js +151 -0
- package/src/plugins/file/README.md +6 -6
- package/src/plugins/file/file.js +15 -7
- package/src/plugins/get/README.md +1 -1
- package/src/plugins/get/get.js +125 -49
- package/src/plugins/get/getDoc.js +2 -43
- package/src/plugins/get/getDoc.md +36 -0
- package/src/plugins/hedberg/README.md +1 -2
- package/src/plugins/hedberg/hedberg.js +8 -4
- package/src/plugins/hedberg/matcher.js +16 -17
- package/src/plugins/hedberg/normalize.js +0 -48
- package/src/plugins/helpers.js +43 -3
- package/src/plugins/index.js +146 -123
- package/src/plugins/instructions/README.md +35 -9
- package/src/plugins/instructions/instructions.js +126 -12
- package/src/plugins/instructions/instructions.md +25 -0
- package/src/plugins/instructions/instructions_104.md +7 -0
- package/src/plugins/instructions/instructions_105.md +46 -0
- package/src/plugins/instructions/instructions_106.md +0 -0
- package/src/plugins/instructions/instructions_107.md +0 -0
- package/src/plugins/instructions/instructions_108.md +8 -0
- package/src/plugins/instructions/protocol.js +12 -0
- package/src/plugins/known/README.md +2 -2
- package/src/plugins/known/known.js +77 -45
- package/src/plugins/known/knownDoc.js +2 -29
- package/src/plugins/known/knownDoc.md +8 -0
- package/src/plugins/log/README.md +48 -0
- package/src/plugins/log/log.js +109 -0
- package/src/plugins/mv/README.md +2 -2
- package/src/plugins/mv/mv.js +57 -24
- package/src/plugins/mv/mvDoc.js +2 -29
- package/src/plugins/mv/mvDoc.md +10 -0
- package/src/plugins/ollama/README.md +15 -0
- package/src/{llm/OllamaClient.js → plugins/ollama/ollama.js} +40 -18
- package/src/plugins/openai/README.md +17 -0
- package/src/plugins/openai/openai.js +120 -0
- package/src/plugins/openrouter/README.md +27 -0
- package/src/plugins/openrouter/openrouter.js +121 -0
- package/src/plugins/persona/README.md +20 -0
- package/src/plugins/persona/persona.js +9 -16
- package/src/plugins/policy/README.md +21 -0
- package/src/plugins/policy/policy.js +29 -14
- package/src/plugins/prompt/README.md +1 -1
- package/src/plugins/prompt/prompt.js +63 -18
- package/src/plugins/rm/README.md +1 -1
- package/src/plugins/rm/rm.js +58 -14
- package/src/plugins/rm/rmDoc.js +2 -24
- package/src/plugins/rm/rmDoc.md +13 -0
- package/src/plugins/rpc/README.md +2 -2
- package/src/plugins/rpc/rpc.js +515 -296
- package/src/plugins/set/README.md +1 -1
- package/src/plugins/set/set.js +318 -77
- package/src/plugins/set/setDoc.js +2 -35
- package/src/plugins/set/setDoc.md +22 -0
- package/src/plugins/sh/README.md +28 -5
- package/src/plugins/sh/sh.js +52 -8
- package/src/plugins/sh/shDoc.js +2 -23
- package/src/plugins/sh/shDoc.md +13 -0
- package/src/plugins/skill/README.md +23 -0
- package/src/plugins/skill/skill.js +14 -17
- package/src/plugins/stream/README.md +101 -0
- package/src/plugins/stream/stream.js +290 -0
- package/src/plugins/telemetry/README.md +1 -1
- package/src/plugins/telemetry/telemetry.js +148 -74
- package/src/plugins/think/README.md +1 -1
- package/src/plugins/think/think.js +14 -1
- package/src/plugins/think/thinkDoc.js +2 -17
- package/src/plugins/think/thinkDoc.md +7 -0
- package/src/plugins/unknown/README.md +3 -3
- package/src/plugins/unknown/unknown.js +56 -21
- package/src/plugins/unknown/unknownDoc.js +2 -25
- package/src/plugins/unknown/unknownDoc.md +11 -0
- package/src/plugins/update/README.md +1 -1
- package/src/plugins/update/update.js +67 -5
- package/src/plugins/update/updateDoc.js +2 -27
- package/src/plugins/update/updateDoc.md +8 -0
- package/src/plugins/xai/README.md +23 -0
- package/src/{llm/XaiClient.js → plugins/xai/xai.js} +58 -37
- package/src/server/ClientConnection.js +64 -37
- package/src/server/SocketServer.js +23 -10
- package/src/server/protocol.js +11 -0
- package/src/sql/functions/slugify.js +13 -1
- package/src/sql/v_model_context.sql +27 -31
- package/src/sql/v_run_log.sql +9 -14
- package/EXCEPTIONS.md +0 -46
- package/src/agent/KnownStore.js +0 -338
- package/src/agent/ResponseHealer.js +0 -188
- package/src/llm/OpenAiClient.js +0 -100
- package/src/llm/OpenRouterClient.js +0 -100
- package/src/plugins/budget/recovery.js +0 -47
- package/src/plugins/instructions/preamble.md +0 -37
- package/src/plugins/performed/README.md +0 -15
- package/src/plugins/performed/performed.js +0 -45
- package/src/plugins/previous/README.md +0 -16
- package/src/plugins/previous/previous.js +0 -60
- package/src/plugins/progress/README.md +0 -16
- package/src/plugins/progress/progress.js +0 -26
- package/src/plugins/summarize/README.md +0 -19
- package/src/plugins/summarize/summarize.js +0 -32
- package/src/plugins/summarize/summarizeDoc.js +0 -28
|
@@ -1,28 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// Text goes to the model. Rationale stays in source.
|
|
3
|
-
// Changing ANY line requires reading ALL rationales first.
|
|
4
|
-
const LINES = [
|
|
5
|
-
["## <update>[brief status]</update> - Signal continuation"],
|
|
6
|
-
[
|
|
7
|
-
"Example: <update>Reading config files</update>",
|
|
8
|
-
"Progress checkpoint. Status signal, not a log entry.",
|
|
9
|
-
],
|
|
10
|
-
[
|
|
11
|
-
"Example: <update>Found 3 issues, fixing first</update>",
|
|
12
|
-
"Multi-step progress. Ongoing work.",
|
|
13
|
-
],
|
|
14
|
-
[
|
|
15
|
-
"* YOU MUST use <update></update> if still working — describes the current state",
|
|
16
|
-
"Continuation signal. Triggers the next turn.",
|
|
17
|
-
],
|
|
18
|
-
[
|
|
19
|
-
"* YOU MUST NOT use <update> if done — use <summarize/> instead",
|
|
20
|
-
"Mutual exclusion with summarize.",
|
|
21
|
-
],
|
|
22
|
-
[
|
|
23
|
-
"* YOU MUST keep <update> to <= 80 characters",
|
|
24
|
-
"Length cap.",
|
|
25
|
-
],
|
|
26
|
-
];
|
|
1
|
+
import { loadDoc } from "../helpers.js";
|
|
27
2
|
|
|
28
|
-
export default
|
|
3
|
+
export default loadDoc(import.meta.url, "updateDoc.md");
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
## <update status="N">{brief status}</update> - Status report (exactly one per turn, at the end)
|
|
2
|
+
<!-- Header defines position, frequency, and status code requirement. -->
|
|
3
|
+
|
|
4
|
+
REQUIRED: the valid values of N are defined by your current stage instructions.
|
|
5
|
+
<!-- Single source of truth for codes is the current phase instructions block, not this doc. Listing codes here leaks termination knowledge (e.g. 200) that strong models use to short-circuit the protocol. -->
|
|
6
|
+
|
|
7
|
+
REQUIRED: YOU MUST keep <update></update> body to <= 80 characters.
|
|
8
|
+
<!-- Length cap. -->
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# xai
|
|
2
|
+
|
|
3
|
+
xAI (Grok) LLM provider. Handles model aliases prefixed with `xai/`
|
|
4
|
+
(e.g. `xai/grok-2`).
|
|
5
|
+
|
|
6
|
+
## Env
|
|
7
|
+
|
|
8
|
+
- `XAI_BASE_URL` — full responses endpoint (e.g.
|
|
9
|
+
`https://api.x.ai/v1/responses`). Plugin is inert if unset.
|
|
10
|
+
- `XAI_API_KEY` — bearer token.
|
|
11
|
+
|
|
12
|
+
## Response Normalization
|
|
13
|
+
|
|
14
|
+
xAI's response shape differs from OpenAI's. The plugin walks
|
|
15
|
+
`data.output[]`, collecting text from items of type `message` as
|
|
16
|
+
`content` and items of type `reasoning` as `reasoning_content`, then
|
|
17
|
+
emits the common OpenAI-shaped envelope.
|
|
18
|
+
|
|
19
|
+
## Context Size
|
|
20
|
+
|
|
21
|
+
Tries `/models` first for a `context_length` field, then the
|
|
22
|
+
`/language-models/<id>` endpoint as a fallback. Results are cached
|
|
23
|
+
per model for the plugin lifetime.
|
|
@@ -1,24 +1,46 @@
|
|
|
1
|
-
import msg from "
|
|
1
|
+
import msg from "../../agent/messages.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const FETCH_TIMEOUT = Number(process.env.RUMMY_FETCH_TIMEOUT);
|
|
4
|
+
if (!FETCH_TIMEOUT) throw new Error("RUMMY_FETCH_TIMEOUT must be set");
|
|
5
|
+
|
|
6
|
+
const PROVIDER = "xai";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* xAI (Grok) LLM provider plugin. Registers with hooks.llm.providers if
|
|
10
|
+
* XAI_BASE_URL is set; inert otherwise. Handles model aliases of the
|
|
11
|
+
* form `xai/{modelName}`. Normalizes xAI's distinct response shape
|
|
12
|
+
* into the common OpenAI-shaped envelope.
|
|
13
|
+
*/
|
|
14
|
+
export default class Xai {
|
|
4
15
|
#baseUrl;
|
|
5
16
|
#apiKey;
|
|
6
17
|
#contextCache = new Map();
|
|
7
18
|
|
|
8
|
-
constructor(
|
|
19
|
+
constructor(core) {
|
|
20
|
+
const baseUrl = process.env.XAI_BASE_URL;
|
|
21
|
+
if (!baseUrl) return;
|
|
9
22
|
this.#baseUrl = baseUrl;
|
|
10
|
-
this.#apiKey =
|
|
23
|
+
this.#apiKey = process.env.XAI_API_KEY;
|
|
24
|
+
|
|
25
|
+
const wireModel = (alias) => alias.split("/").slice(1).join("/");
|
|
26
|
+
|
|
27
|
+
core.hooks.llm.providers.push({
|
|
28
|
+
name: PROVIDER,
|
|
29
|
+
matches: (model) => model.split("/")[0] === PROVIDER,
|
|
30
|
+
completion: (messages, model, options) =>
|
|
31
|
+
this.#completion(messages, wireModel(model), options),
|
|
32
|
+
getContextSize: (model) => this.#getContextSize(wireModel(model)),
|
|
33
|
+
});
|
|
11
34
|
}
|
|
12
35
|
|
|
13
|
-
async completion(messages, model, options = {}) {
|
|
36
|
+
async #completion(messages, model, options = {}) {
|
|
14
37
|
if (!this.#apiKey) throw new Error(msg("error.xai_api_key_missing"));
|
|
15
38
|
|
|
16
39
|
const body = { model, input: messages };
|
|
17
40
|
if (options.temperature !== undefined)
|
|
18
41
|
body.temperature = options.temperature;
|
|
19
42
|
|
|
20
|
-
const
|
|
21
|
-
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
43
|
+
const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT);
|
|
22
44
|
const signal = options.signal
|
|
23
45
|
? AbortSignal.any([options.signal, timeoutSignal])
|
|
24
46
|
: timeoutSignal;
|
|
@@ -37,29 +59,22 @@ export default class XaiClient {
|
|
|
37
59
|
const error = await response.text();
|
|
38
60
|
if (response.status === 401 || response.status === 403) {
|
|
39
61
|
throw new Error(
|
|
40
|
-
msg("error.xai_auth", {
|
|
41
|
-
status: `${response.status} - ${error}`,
|
|
42
|
-
}),
|
|
62
|
+
msg("error.xai_auth", { status: `${response.status} - ${error}` }),
|
|
43
63
|
);
|
|
44
64
|
}
|
|
45
65
|
throw new Error(
|
|
46
|
-
msg("error.xai_api", {
|
|
47
|
-
status: `${response.status} - ${error}`,
|
|
48
|
-
}),
|
|
66
|
+
msg("error.xai_api", { status: `${response.status} - ${error}` }),
|
|
49
67
|
);
|
|
50
68
|
}
|
|
51
69
|
|
|
52
|
-
|
|
53
|
-
return this.#normalize(data);
|
|
70
|
+
return this.#normalize(await response.json());
|
|
54
71
|
}
|
|
55
72
|
|
|
56
73
|
#normalize(data) {
|
|
57
|
-
const output = data.output || [];
|
|
58
|
-
|
|
59
74
|
let content = "";
|
|
60
75
|
let reasoningContent = null;
|
|
61
76
|
|
|
62
|
-
for (const item of output) {
|
|
77
|
+
for (const item of data.output) {
|
|
63
78
|
if (item.type === "reasoning") {
|
|
64
79
|
const text = this.#extractText(item.content);
|
|
65
80
|
if (text)
|
|
@@ -73,9 +88,13 @@ export default class XaiClient {
|
|
|
73
88
|
}
|
|
74
89
|
}
|
|
75
90
|
|
|
76
|
-
const usage = data
|
|
77
|
-
const inputTokens = usage.input_tokens
|
|
78
|
-
const outputTokens = usage.output_tokens
|
|
91
|
+
const { usage } = data;
|
|
92
|
+
const inputTokens = usage.input_tokens;
|
|
93
|
+
const outputTokens = usage.output_tokens;
|
|
94
|
+
// Optional per xAI API; absent on providers that don't surface them.
|
|
95
|
+
const cached = usage.input_tokens_details?.cached_tokens;
|
|
96
|
+
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens;
|
|
97
|
+
const costTicks = usage.cost_in_usd_ticks;
|
|
79
98
|
return {
|
|
80
99
|
choices: [
|
|
81
100
|
{
|
|
@@ -88,11 +107,11 @@ export default class XaiClient {
|
|
|
88
107
|
],
|
|
89
108
|
usage: {
|
|
90
109
|
prompt_tokens: inputTokens,
|
|
91
|
-
cached_tokens:
|
|
110
|
+
cached_tokens: cached === undefined ? 0 : cached,
|
|
92
111
|
completion_tokens: outputTokens,
|
|
93
|
-
reasoning_tokens:
|
|
112
|
+
reasoning_tokens: reasoningTokens === undefined ? 0 : reasoningTokens,
|
|
94
113
|
total_tokens: inputTokens + outputTokens,
|
|
95
|
-
cost:
|
|
114
|
+
cost: costTicks === undefined ? 0 : costTicks / 10_000_000_000,
|
|
96
115
|
},
|
|
97
116
|
};
|
|
98
117
|
}
|
|
@@ -100,29 +119,30 @@ export default class XaiClient {
|
|
|
100
119
|
#extractText(content) {
|
|
101
120
|
if (typeof content === "string") return content;
|
|
102
121
|
if (!Array.isArray(content)) return null;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
);
|
|
122
|
+
const joined = content
|
|
123
|
+
.filter((c) => c.type === "text" || c.type === "output_text")
|
|
124
|
+
.map((c) => c.text)
|
|
125
|
+
.join("\n");
|
|
126
|
+
return joined ? joined : null;
|
|
109
127
|
}
|
|
110
128
|
|
|
111
|
-
async getContextSize(model) {
|
|
129
|
+
async #getContextSize(model) {
|
|
112
130
|
if (this.#contextCache.has(model)) return this.#contextCache.get(model);
|
|
113
|
-
|
|
114
131
|
if (!this.#apiKey) throw new Error(msg("error.xai_api_key_missing"));
|
|
115
132
|
|
|
116
|
-
// Query xAI models endpoint
|
|
117
133
|
const modelsUrl = this.#baseUrl.replace(/\/responses$/, "/models");
|
|
118
134
|
const res = await fetch(modelsUrl, {
|
|
119
135
|
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
120
136
|
signal: AbortSignal.timeout(5000),
|
|
121
137
|
});
|
|
122
|
-
|
|
123
138
|
if (res.ok) {
|
|
124
139
|
const data = await res.json();
|
|
125
|
-
|
|
140
|
+
// xAI's /models returns either { data: [...] } or { models: [...] }
|
|
141
|
+
// depending on the API version; accept either and crash otherwise.
|
|
142
|
+
let models;
|
|
143
|
+
if (data.data) models = data.data;
|
|
144
|
+
else if (data.models) models = data.models;
|
|
145
|
+
else throw new Error("xAI /models response has neither data nor models");
|
|
126
146
|
const entry = models.find(
|
|
127
147
|
(m) => m.id === model || `${m.id}-latest` === model,
|
|
128
148
|
);
|
|
@@ -132,16 +152,17 @@ export default class XaiClient {
|
|
|
132
152
|
}
|
|
133
153
|
}
|
|
134
154
|
|
|
135
|
-
// Try /v1/language-models for richer metadata
|
|
136
155
|
const langUrl = this.#baseUrl.replace(
|
|
137
156
|
/\/responses$/,
|
|
138
157
|
`/language-models/${model}`,
|
|
139
158
|
);
|
|
159
|
+
// Optional endpoint probe. If the network call fails (404 on older
|
|
160
|
+
// API versions, timeout, etc.) we fall through to the next strategy
|
|
161
|
+
// below; a terminal throw fires if no strategy resolves.
|
|
140
162
|
const langRes = await fetch(langUrl, {
|
|
141
163
|
headers: { Authorization: `Bearer ${this.#apiKey}` },
|
|
142
164
|
signal: AbortSignal.timeout(5000),
|
|
143
165
|
}).catch(() => null);
|
|
144
|
-
|
|
145
166
|
if (langRes?.ok) {
|
|
146
167
|
const langData = await langRes.json();
|
|
147
168
|
if (langData?.context_length) {
|
|
@@ -8,6 +8,7 @@ export default class ClientConnection {
|
|
|
8
8
|
#hooks;
|
|
9
9
|
#rpcRegistry;
|
|
10
10
|
#rpcLogPending = new Map();
|
|
11
|
+
#shutdownPromise = null;
|
|
11
12
|
#context = {
|
|
12
13
|
projectId: null,
|
|
13
14
|
projectRoot: null,
|
|
@@ -21,7 +22,13 @@ export default class ClientConnection {
|
|
|
21
22
|
this.#projectAgent = new ProjectAgent(db, hooks);
|
|
22
23
|
|
|
23
24
|
this.#ws.on("message", (data) => this.#handleMessage(data));
|
|
24
|
-
this.#ws.on("close", () =>
|
|
25
|
+
this.#ws.on("close", () => {
|
|
26
|
+
// Fire-and-forget: the Promise is cached by `shutdown()` so
|
|
27
|
+
// server-initiated close can await the same work.
|
|
28
|
+
this.shutdown().catch((err) => {
|
|
29
|
+
console.warn(`[RUMMY] shutdown on ws close failed: ${err.message}`);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
25
32
|
|
|
26
33
|
this.#setupNotifications();
|
|
27
34
|
}
|
|
@@ -63,6 +70,16 @@ export default class ClientConnection {
|
|
|
63
70
|
}
|
|
64
71
|
};
|
|
65
72
|
|
|
73
|
+
#onStreamCancelled = (payload) => {
|
|
74
|
+
if (payload.projectId === this.#context.projectId) {
|
|
75
|
+
this.#sendNotification("stream/cancelled", {
|
|
76
|
+
run: payload.run,
|
|
77
|
+
path: payload.path,
|
|
78
|
+
reason: payload.reason,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
66
83
|
#onState = (payload) => {
|
|
67
84
|
if (payload.projectId === this.#context.projectId) {
|
|
68
85
|
this.#sendNotification("run/state", {
|
|
@@ -79,18 +96,37 @@ export default class ClientConnection {
|
|
|
79
96
|
|
|
80
97
|
#setupNotifications() {
|
|
81
98
|
this.#hooks.run.progress.on(this.#onProgress);
|
|
82
|
-
this.#hooks.
|
|
99
|
+
this.#hooks.proposal.pending.on(this.#onProposal);
|
|
83
100
|
this.#hooks.ui.render.on(this.#onRender);
|
|
84
101
|
this.#hooks.ui.notify.on(this.#onNotify);
|
|
85
102
|
this.#hooks.run.state.on(this.#onState);
|
|
103
|
+
this.#hooks.stream.cancelled.on(this.#onStreamCancelled);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
#teardown() {
|
|
89
107
|
this.#hooks.run.progress.off(this.#onProgress);
|
|
90
|
-
this.#hooks.
|
|
108
|
+
this.#hooks.proposal.pending.off(this.#onProposal);
|
|
91
109
|
this.#hooks.ui.render.off(this.#onRender);
|
|
92
110
|
this.#hooks.ui.notify.off(this.#onNotify);
|
|
93
111
|
this.#hooks.run.state.off(this.#onState);
|
|
112
|
+
this.#hooks.stream.cancelled.off(this.#onStreamCancelled);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Abort in-flight runs on this connection and wait for them to
|
|
117
|
+
* settle. Idempotent: `ws.on("close")` and server-initiated close
|
|
118
|
+
* both call this; the cached Promise guarantees the work happens
|
|
119
|
+
* exactly once and both callers observe the same completion.
|
|
120
|
+
*/
|
|
121
|
+
shutdown() {
|
|
122
|
+
if (!this.#shutdownPromise) {
|
|
123
|
+
this.#shutdownPromise = (async () => {
|
|
124
|
+
await this.#projectAgent.shutdown();
|
|
125
|
+
this.#teardown();
|
|
126
|
+
if (this.#ws.readyState === 1) this.#ws.terminate();
|
|
127
|
+
})();
|
|
128
|
+
}
|
|
129
|
+
return this.#shutdownPromise;
|
|
94
130
|
}
|
|
95
131
|
|
|
96
132
|
#buildHandlerContext() {
|
|
@@ -113,11 +149,8 @@ export default class ClientConnection {
|
|
|
113
149
|
|
|
114
150
|
async #handleMessage(data) {
|
|
115
151
|
let id = null;
|
|
116
|
-
const debug = process.env.RUMMY_DEBUG === "true";
|
|
117
152
|
try {
|
|
118
153
|
const rawMessage = await this.#hooks.socket.message.raw.filter(data);
|
|
119
|
-
if (debug) console.log(`[SOCKET] IN: ${rawMessage.toString()}`);
|
|
120
|
-
|
|
121
154
|
const message = JSON.parse(rawMessage.toString());
|
|
122
155
|
|
|
123
156
|
const filteredRequest = await this.#hooks.rpc.request.filter(message);
|
|
@@ -131,15 +164,13 @@ export default class ClientConnection {
|
|
|
131
164
|
projectId: this.#context.projectId,
|
|
132
165
|
});
|
|
133
166
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (logRow) this.#rpcLogPending.set(id, logRow.id);
|
|
142
|
-
} catch {}
|
|
167
|
+
const logRow = await this.#db.log_rpc_call.get({
|
|
168
|
+
project_id: this.#context.projectId,
|
|
169
|
+
method,
|
|
170
|
+
rpc_id: id,
|
|
171
|
+
params: params ? JSON.stringify(params) : null,
|
|
172
|
+
});
|
|
173
|
+
if (logRow) this.#rpcLogPending.set(id, logRow.id);
|
|
143
174
|
|
|
144
175
|
const resolvedMethod = method === "rpc/discover" ? "discover" : method;
|
|
145
176
|
const registration = this.#rpcRegistry.get(resolvedMethod);
|
|
@@ -150,17 +181,19 @@ export default class ClientConnection {
|
|
|
150
181
|
throw new Error(msg("error.not_initialized"));
|
|
151
182
|
}
|
|
152
183
|
|
|
184
|
+
// JSON-RPC requests may omit `params` entirely.
|
|
185
|
+
const handlerParams = params === undefined ? {} : params;
|
|
153
186
|
let result;
|
|
154
187
|
if (registration.longRunning) {
|
|
155
188
|
result = await registration.handler(
|
|
156
|
-
|
|
189
|
+
handlerParams,
|
|
157
190
|
this.#buildHandlerContext(),
|
|
158
191
|
);
|
|
159
192
|
} else {
|
|
160
|
-
const timeout = Number(process.env.RUMMY_RPC_TIMEOUT)
|
|
193
|
+
const timeout = Number(process.env.RUMMY_RPC_TIMEOUT);
|
|
161
194
|
let timer;
|
|
162
195
|
result = await Promise.race([
|
|
163
|
-
registration.handler(
|
|
196
|
+
registration.handler(handlerParams, this.#buildHandlerContext()),
|
|
164
197
|
new Promise((_, reject) => {
|
|
165
198
|
timer = setTimeout(
|
|
166
199
|
() =>
|
|
@@ -198,43 +231,37 @@ export default class ClientConnection {
|
|
|
198
231
|
const logId = this.#rpcLogPending.get(id);
|
|
199
232
|
if (logId) {
|
|
200
233
|
this.#rpcLogPending.delete(id);
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
} catch {}
|
|
234
|
+
await this.#db.log_rpc_result.run({
|
|
235
|
+
id: logId,
|
|
236
|
+
result: finalResult
|
|
237
|
+
? JSON.stringify(finalResult).slice(0, 4096)
|
|
238
|
+
: null,
|
|
239
|
+
});
|
|
209
240
|
}
|
|
210
241
|
} catch (error) {
|
|
211
242
|
console.error(`[RUMMY] RPC Error: ${error.message}`);
|
|
212
243
|
console.error(`[RUMMY] Stack: ${error.stack}`);
|
|
244
|
+
// JSON-RPC: error responses for malformed requests with no id
|
|
245
|
+
// MUST carry null per the spec.
|
|
213
246
|
this.#send({
|
|
214
247
|
jsonrpc: "2.0",
|
|
215
248
|
error: { code: -32603, message: error.message },
|
|
216
|
-
id: id
|
|
249
|
+
id: id === undefined ? null : id,
|
|
217
250
|
});
|
|
218
251
|
await this.#hooks.rpc.error.emit({ id, error });
|
|
219
252
|
|
|
220
253
|
const errLogId = this.#rpcLogPending.get(id);
|
|
221
254
|
if (errLogId) {
|
|
222
255
|
this.#rpcLogPending.delete(id);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
228
|
-
} catch {}
|
|
256
|
+
await this.#db.log_rpc_error.run({
|
|
257
|
+
id: errLogId,
|
|
258
|
+
error: error.message,
|
|
259
|
+
});
|
|
229
260
|
}
|
|
230
261
|
}
|
|
231
262
|
}
|
|
232
263
|
|
|
233
264
|
#send(payload) {
|
|
234
|
-
const debug = process.env.RUMMY_DEBUG === "true";
|
|
235
|
-
if (debug) {
|
|
236
|
-
console.log(`[SOCKET] OUT: ${JSON.stringify(payload, null, 2)}`);
|
|
237
|
-
}
|
|
238
265
|
if (this.#ws.readyState === 1) {
|
|
239
266
|
this.#ws.send(JSON.stringify(payload));
|
|
240
267
|
}
|
|
@@ -5,17 +5,23 @@ export default class SocketServer {
|
|
|
5
5
|
#db;
|
|
6
6
|
#wss;
|
|
7
7
|
#hooks;
|
|
8
|
+
#connections = new Set();
|
|
8
9
|
|
|
9
10
|
constructor(db, options) {
|
|
10
11
|
this.#db = db;
|
|
11
12
|
this.#hooks = options.hooks;
|
|
12
13
|
this.#wss = new WebSocketServer(options);
|
|
13
14
|
|
|
14
|
-
this.#wss.on("connection", (ws,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
this.#wss.on("connection", (ws, _req) => {
|
|
16
|
+
const conn = new ClientConnection(ws, this.#db, this.#hooks);
|
|
17
|
+
this.#connections.add(conn);
|
|
18
|
+
// Remove from the tracking set only after the connection's
|
|
19
|
+
// shutdown drain has fully settled — not on raw ws-close —
|
|
20
|
+
// so server close() can still find and await an in-progress
|
|
21
|
+
// shutdown kicked off by a client-initiated disconnect.
|
|
22
|
+
ws.on("close", () => {
|
|
23
|
+
conn.shutdown().finally(() => this.#connections.delete(conn));
|
|
24
|
+
});
|
|
19
25
|
});
|
|
20
26
|
|
|
21
27
|
this.#wss.on("error", (_err) => {
|
|
@@ -31,12 +37,19 @@ export default class SocketServer {
|
|
|
31
37
|
this.#wss.on(event, handler);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
close() {
|
|
35
|
-
|
|
40
|
+
async close() {
|
|
41
|
+
// Drain in-flight runs on each connection before closing the
|
|
42
|
+
// socket — otherwise detached kickoff Promises keep the Node
|
|
43
|
+
// event loop alive past server shutdown.
|
|
44
|
+
const shutdowns = [];
|
|
45
|
+
for (const conn of this.#connections) {
|
|
46
|
+
shutdowns.push(conn.shutdown().catch(() => {}));
|
|
47
|
+
}
|
|
48
|
+
await Promise.all(shutdowns);
|
|
49
|
+
this.#connections.clear();
|
|
50
|
+
|
|
51
|
+
await new Promise((resolve) => {
|
|
36
52
|
if (!this.#wss) return resolve();
|
|
37
|
-
for (const client of this.#wss.clients) {
|
|
38
|
-
client.terminate();
|
|
39
|
-
}
|
|
40
53
|
this.#wss.close(resolve);
|
|
41
54
|
});
|
|
42
55
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server↔client wire-protocol version. Bumped whenever the RPC shape
|
|
3
|
+
* or notification payload shape changes in a way that breaks existing
|
|
4
|
+
* clients. Clients pass their own version in `rummy/hello`; server
|
|
5
|
+
* rejects MAJOR mismatch. Git commit log is the changelog.
|
|
6
|
+
*
|
|
7
|
+
* MAJOR — breaking change (removed/renamed method, shape change)
|
|
8
|
+
* MINOR — additive change (new method, new optional field)
|
|
9
|
+
* PATCH — internal fix visible to the wire shape
|
|
10
|
+
*/
|
|
11
|
+
export const RUMMY_PROTOCOL_VERSION = "2.0.0";
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
export const deterministic = true;
|
|
2
2
|
|
|
3
|
+
// Build URI paths the model can round-trip:
|
|
4
|
+
// "history,mongol,khan" → "history/mongol/khan" (commas become path separators)
|
|
5
|
+
// "contents of Document 1" → "contents_of_Document_1" (spaces become underscores)
|
|
6
|
+
// Slice on decoded text, then split-encode-join per segment so / survives as
|
|
7
|
+
// a separator while anything URL-unsafe inside a segment gets escaped.
|
|
3
8
|
export default function slugify(text) {
|
|
4
9
|
if (!text) return "";
|
|
5
|
-
return
|
|
10
|
+
return text
|
|
11
|
+
.slice(0, 80)
|
|
12
|
+
.replace(/,/g, "/")
|
|
13
|
+
.replace(/ /g, "_")
|
|
14
|
+
.split("/")
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.map(encodeURIComponent)
|
|
17
|
+
.join("/");
|
|
6
18
|
}
|
|
@@ -3,30 +3,26 @@ CREATE VIEW IF NOT EXISTS v_model_context AS
|
|
|
3
3
|
WITH
|
|
4
4
|
visible AS (
|
|
5
5
|
SELECT
|
|
6
|
-
|
|
7
|
-
,
|
|
8
|
-
,
|
|
9
|
-
,
|
|
10
|
-
,
|
|
11
|
-
,
|
|
12
|
-
,
|
|
13
|
-
,
|
|
14
|
-
,
|
|
15
|
-
,
|
|
16
|
-
,
|
|
6
|
+
rv.run_id
|
|
7
|
+
, rv.id
|
|
8
|
+
, e.path
|
|
9
|
+
, e.body
|
|
10
|
+
, e.scheme
|
|
11
|
+
, rv.state
|
|
12
|
+
, rv.outcome
|
|
13
|
+
, rv.visibility
|
|
14
|
+
, rv.turn
|
|
15
|
+
, rv.updated_at
|
|
16
|
+
, e.attributes
|
|
17
17
|
, COALESCE(s.category, 'logging') AS category
|
|
18
18
|
, CASE
|
|
19
|
-
|
|
20
|
-
WHEN ke.fidelity = 'archive' THEN NULL
|
|
21
|
-
-- 202 Accepted (proposed) hidden until resolved
|
|
22
|
-
WHEN ke.status = 202 THEN NULL
|
|
23
|
-
-- Audit schemes (model_visible = 0) hidden
|
|
19
|
+
WHEN rv.visibility = 'archived' THEN NULL
|
|
24
20
|
WHEN s.model_visible = 0 THEN NULL
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
JOIN schemes AS s ON s.name = COALESCE(
|
|
21
|
+
ELSE rv.visibility
|
|
22
|
+
END AS effective_visibility
|
|
23
|
+
FROM run_views AS rv
|
|
24
|
+
JOIN entries AS e ON e.id = rv.entry_id
|
|
25
|
+
JOIN schemes AS s ON s.name = COALESCE(e.scheme, 'file')
|
|
30
26
|
),
|
|
31
27
|
projected AS (
|
|
32
28
|
SELECT
|
|
@@ -34,27 +30,28 @@ projected AS (
|
|
|
34
30
|
, id
|
|
35
31
|
, path
|
|
36
32
|
, scheme
|
|
37
|
-
,
|
|
38
|
-
,
|
|
33
|
+
, state
|
|
34
|
+
, outcome
|
|
35
|
+
, effective_visibility AS visibility
|
|
39
36
|
, turn
|
|
40
37
|
, updated_at
|
|
41
38
|
, attributes
|
|
42
39
|
-- Category comes from schemes table — plugins declare it via registerScheme().
|
|
43
40
|
, category
|
|
44
|
-
, tokens
|
|
45
41
|
, CASE
|
|
46
|
-
WHEN
|
|
42
|
+
WHEN effective_visibility IN ('visible', 'summarized') THEN body
|
|
47
43
|
ELSE ''
|
|
48
44
|
END AS body
|
|
49
45
|
FROM visible
|
|
50
|
-
WHERE
|
|
46
|
+
WHERE effective_visibility IS NOT NULL
|
|
51
47
|
)
|
|
52
48
|
SELECT
|
|
53
49
|
run_id
|
|
54
50
|
, path
|
|
55
51
|
, scheme
|
|
56
|
-
,
|
|
57
|
-
,
|
|
52
|
+
, visibility
|
|
53
|
+
, state
|
|
54
|
+
, outcome
|
|
58
55
|
, body
|
|
59
56
|
, attributes
|
|
60
57
|
, category
|
|
@@ -71,13 +68,12 @@ SELECT
|
|
|
71
68
|
ELSE 5
|
|
72
69
|
END
|
|
73
70
|
, CASE scheme WHEN 'skill' THEN 0 ELSE 1 END
|
|
74
|
-
, CASE
|
|
75
|
-
WHEN '
|
|
71
|
+
, CASE visibility
|
|
72
|
+
WHEN 'summarized' THEN 0
|
|
76
73
|
ELSE 1
|
|
77
74
|
END
|
|
78
75
|
, turn
|
|
79
76
|
, updated_at
|
|
80
77
|
, id
|
|
81
78
|
) AS ordinal
|
|
82
|
-
, countTokens(body) AS tokens
|
|
83
79
|
FROM projected;
|
package/src/sql/v_run_log.sql
CHANGED
|
@@ -4,20 +4,15 @@ SELECT
|
|
|
4
4
|
ke.run_id
|
|
5
5
|
, ke.path
|
|
6
6
|
, ke.body
|
|
7
|
-
, ke.
|
|
8
|
-
,
|
|
9
|
-
,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
, json_extract(ke.attributes, '$.path')
|
|
13
|
-
, json_extract(ke.attributes, '$.question')
|
|
14
|
-
, ''
|
|
15
|
-
) AS target
|
|
7
|
+
, ke.state
|
|
8
|
+
, ke.outcome
|
|
9
|
+
, ke.turn
|
|
10
|
+
, ke.scheme AS tool
|
|
11
|
+
, ke.attributes
|
|
16
12
|
FROM known_entries AS ke
|
|
17
|
-
JOIN schemes AS s ON s.name =
|
|
13
|
+
JOIN schemes AS s ON s.name = ke.scheme
|
|
18
14
|
WHERE
|
|
19
|
-
|
|
20
|
-
AND ke.
|
|
21
|
-
AND
|
|
22
|
-
AND ke.scheme NOT IN ('system', 'reasoning', 'model', 'content')
|
|
15
|
+
s.category IN ('logging', 'prompt', 'unknown')
|
|
16
|
+
AND ke.state != 'proposed'
|
|
17
|
+
AND ke.scheme != 'run'
|
|
23
18
|
ORDER BY ke.id;
|