@possumtech/rummy 2.0.0 → 2.1.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 +31 -5
- package/BENCH_ENVIRONMENT.md +230 -0
- package/CLIENT_INTERFACE.md +396 -0
- package/PLUGINS.md +93 -1
- package/SPEC.md +389 -28
- package/bin/postinstall.js +2 -2
- package/bin/rummy.js +2 -2
- package/last_run.txt +5617 -0
- package/migrations/001_initial_schema.sql +2 -1
- package/package.json +13 -9
- package/scriptify/ask_run.js +77 -0
- package/scriptify/cache_probe.js +66 -0
- package/scriptify/cache_probe_grok.js +74 -0
- package/service.js +22 -11
- package/src/agent/AgentLoop.js +62 -157
- package/src/agent/ContextAssembler.js +2 -9
- package/src/agent/Entries.js +54 -98
- package/src/agent/ProjectAgent.js +4 -11
- package/src/agent/TurnExecutor.js +48 -83
- package/src/agent/XmlParser.js +247 -273
- package/src/agent/budget.js +5 -28
- package/src/agent/config.js +38 -0
- package/src/agent/errors.js +7 -13
- package/src/agent/httpStatus.js +1 -19
- package/src/agent/known_queries.sql +1 -1
- package/src/agent/known_store.sql +12 -2
- package/src/agent/materializeContext.js +15 -18
- package/src/agent/pathEncode.js +5 -0
- package/src/agent/rummyHome.js +9 -0
- package/src/agent/runs.sql +37 -0
- package/src/agent/tokens.js +7 -7
- package/src/hooks/HookRegistry.js +1 -16
- package/src/hooks/Hooks.js +8 -33
- package/src/hooks/PluginContext.js +3 -21
- package/src/hooks/RpcRegistry.js +1 -4
- package/src/hooks/RummyContext.js +6 -16
- package/src/hooks/ToolRegistry.js +5 -15
- package/src/llm/LlmProvider.js +41 -33
- package/src/llm/errors.js +41 -4
- package/src/llm/openaiStream.js +125 -0
- package/src/llm/retry.js +109 -0
- package/src/plugins/budget/budget.js +55 -76
- package/src/plugins/cli/README.md +87 -0
- package/src/plugins/cli/bin.js +61 -0
- package/src/plugins/cli/cli.js +120 -0
- package/src/plugins/env/README.md +2 -1
- package/src/plugins/env/env.js +4 -6
- package/src/plugins/env/envDoc.md +2 -2
- package/src/plugins/error/error.js +23 -23
- package/src/plugins/file/file.js +2 -22
- package/src/plugins/get/get.js +12 -34
- package/src/plugins/get/getDoc.md +8 -6
- package/src/plugins/hedberg/edits.js +1 -11
- package/src/plugins/hedberg/hedberg.js +3 -26
- package/src/plugins/hedberg/normalize.js +1 -5
- package/src/plugins/hedberg/patterns.js +4 -15
- package/src/plugins/hedberg/sed.js +1 -7
- package/src/plugins/helpers.js +28 -20
- package/src/plugins/index.js +25 -41
- package/src/plugins/instructions/README.md +18 -0
- package/src/plugins/instructions/instructions.js +97 -38
- package/src/plugins/instructions/instructions.md +24 -15
- package/src/plugins/instructions/instructions_104.md +5 -4
- package/src/plugins/instructions/instructions_105.md +29 -36
- package/src/plugins/instructions/instructions_106.md +22 -0
- package/src/plugins/instructions/instructions_107.md +17 -0
- package/src/plugins/instructions/instructions_108.md +0 -8
- package/src/plugins/known/README.md +26 -6
- package/src/plugins/known/known.js +37 -34
- package/src/plugins/log/README.md +2 -2
- package/src/plugins/log/log.js +27 -34
- package/src/plugins/ollama/ollama.js +50 -66
- package/src/plugins/openai/openai.js +26 -44
- package/src/plugins/openrouter/openrouter.js +28 -52
- package/src/plugins/policy/README.md +8 -2
- package/src/plugins/policy/policy.js +8 -21
- package/src/plugins/prompt/README.md +22 -0
- package/src/plugins/prompt/prompt.js +14 -16
- package/src/plugins/rm/rm.js +5 -2
- package/src/plugins/rm/rmDoc.md +4 -4
- package/src/plugins/rpc/README.md +2 -1
- package/src/plugins/rpc/rpc.js +62 -48
- package/src/plugins/set/README.md +5 -1
- package/src/plugins/set/set.js +23 -33
- package/src/plugins/set/setDoc.md +1 -1
- package/src/plugins/sh/README.md +2 -1
- package/src/plugins/sh/sh.js +5 -11
- package/src/plugins/sh/shDoc.md +2 -2
- package/src/plugins/stream/README.md +6 -5
- package/src/plugins/stream/stream.js +6 -35
- package/src/plugins/telemetry/telemetry.js +26 -19
- package/src/plugins/think/think.js +4 -7
- package/src/plugins/unknown/unknown.js +8 -13
- package/src/plugins/update/update.js +42 -25
- package/src/plugins/update/updateDoc.md +3 -3
- package/src/plugins/xai/xai.js +30 -20
- package/src/plugins/yolo/yolo.js +159 -0
- package/src/server/ClientConnection.js +17 -47
- package/src/server/SocketServer.js +14 -14
- package/src/server/protocol.js +1 -10
- package/src/sql/functions/slugify.js +5 -7
- package/src/sql/v_model_context.sql +4 -11
- package/turns/cli_1777462658211/turn_001.txt +772 -0
- package/turns/cli_1777462658211/turn_002.txt +606 -0
- package/turns/cli_1777462658211/turn_003.txt +667 -0
- package/turns/cli_1777462658211/turn_004.txt +297 -0
- package/turns/cli_1777462658211/turn_005.txt +301 -0
- package/turns/cli_1777462658211/turn_006.txt +262 -0
- package/turns/cli_1777465095132/turn_001.txt +715 -0
- package/turns/cli_1777465095132/turn_002.txt +236 -0
- package/turns/cli_1777465095132/turn_003.txt +287 -0
- package/turns/cli_1777465095132/turn_004.txt +694 -0
- package/turns/cli_1777465095132/turn_005.txt +422 -0
- package/turns/cli_1777465095132/turn_006.txt +365 -0
- package/turns/cli_1777465095132/turn_007.txt +885 -0
- package/turns/cli_1777465095132/turn_008.txt +1277 -0
- package/turns/cli_1777465095132/turn_009.txt +736 -0
|
@@ -313,10 +313,7 @@ function collectDescendants(node, out) {
|
|
|
313
313
|
|
|
314
314
|
// --- Public API ---
|
|
315
315
|
|
|
316
|
-
|
|
317
|
-
* hedmatch — does the pattern match the ENTIRE string?
|
|
318
|
-
* For path matching, WHERE clauses, full-string comparison.
|
|
319
|
-
*/
|
|
316
|
+
// hedmatch — full-string match (path, WHERE clause).
|
|
320
317
|
export function hedmatch(pattern, string) {
|
|
321
318
|
if (string === null) return false;
|
|
322
319
|
|
|
@@ -345,11 +342,7 @@ export function hedmatch(pattern, string) {
|
|
|
345
342
|
return false;
|
|
346
343
|
}
|
|
347
344
|
|
|
348
|
-
|
|
349
|
-
* hedsearch — find the pattern anywhere IN the string.
|
|
350
|
-
* For substring search, content filtering, "does this text contain...".
|
|
351
|
-
* Returns { found, match, index } or { found: false }.
|
|
352
|
-
*/
|
|
345
|
+
// hedsearch — substring match → { found, match, index }.
|
|
353
346
|
export function hedsearch(pattern, string) {
|
|
354
347
|
if (string === null) return { found: false };
|
|
355
348
|
|
|
@@ -400,10 +393,7 @@ export function hedsearch(pattern, string) {
|
|
|
400
393
|
return { found: false };
|
|
401
394
|
}
|
|
402
395
|
|
|
403
|
-
|
|
404
|
-
* hedreplace — find pattern in string, replace with replacement.
|
|
405
|
-
* Returns the new string, or null if pattern not found.
|
|
406
|
-
*/
|
|
396
|
+
// hedreplace — substitute; null when pattern not found.
|
|
407
397
|
export function hedreplace(pattern, replacement, string) {
|
|
408
398
|
if (string === null) return null;
|
|
409
399
|
|
|
@@ -446,5 +436,4 @@ export function hedreplace(pattern, replacement, string) {
|
|
|
446
436
|
return null;
|
|
447
437
|
}
|
|
448
438
|
|
|
449
|
-
// SQL functions
|
|
450
|
-
// that import from this library. Filename = SQL function name.
|
|
439
|
+
// SQL functions live in sibling files; filename = SQL function name.
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Sed syntax parsing. Handles s/search/replace/flags with:
|
|
3
|
-
* - Escaped delimiters (\\/)
|
|
4
|
-
* - Chained commands (s/a/b/ s/c/d/)
|
|
5
|
-
* - Flag extraction (g, i, m, s, v)
|
|
6
|
-
*/
|
|
7
|
-
|
|
1
|
+
// Parses s/search/replace/flags with escaped delimiters, chains, and g/i/m/s/v flags.
|
|
8
2
|
function splitSed(str, delim) {
|
|
9
3
|
const parts = [];
|
|
10
4
|
let current = "";
|
package/src/plugins/helpers.js
CHANGED
|
@@ -2,13 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
* Read a sibling tooldoc markdown file and return its model-facing text.
|
|
7
|
-
* Strips HTML comments (rationale stays in source, never reaches the model)
|
|
8
|
-
* and collapses any blank-line runs left behind. Each plugin's Doc.js is a
|
|
9
|
-
* one-liner that defers to this so authors edit normal markdown instead of
|
|
10
|
-
* a JS array of [text, rationale] pairs.
|
|
11
|
-
*/
|
|
5
|
+
// Read sibling tooldoc .md; strips HTML comments (rationale stays out of the model packet).
|
|
12
6
|
export function loadDoc(metaUrl, name) {
|
|
13
7
|
const dir = dirname(fileURLToPath(metaUrl));
|
|
14
8
|
return readFileSync(join(dir, name), "utf8")
|
|
@@ -17,23 +11,37 @@ export function loadDoc(metaUrl, name) {
|
|
|
17
11
|
.trim();
|
|
18
12
|
}
|
|
19
13
|
|
|
20
|
-
|
|
21
|
-
* Translate a log entry path into its companion data-scheme base path.
|
|
22
|
-
* `log://turn_N/{action}/{rest}` → `{action}://turn_N/{rest}`.
|
|
23
|
-
* Streaming producers (sh, env) create data channel entries under the
|
|
24
|
-
* producer scheme while the audit record lives in the log scheme; this
|
|
25
|
-
* helper bridges the two namespaces. Returns null for non-log paths.
|
|
26
|
-
*/
|
|
14
|
+
// log://turn_N/{action}/{rest} → {action}://turn_N/{rest}; null if not a log path.
|
|
27
15
|
export function logPathToDataBase(logPath) {
|
|
28
16
|
const m = logPath?.match(/^log:\/\/turn_(\d+)\/([^/]+)\/(.+)$/);
|
|
29
17
|
if (!m) return null;
|
|
30
18
|
return `${m[2]}://turn_${m[1]}/${m[3]}`;
|
|
31
19
|
}
|
|
32
20
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
21
|
+
// env/sh stdout/stderr summary projection: header with line range + last
|
|
22
|
+
// TAIL_LINES of body. The header tells the model exactly which slice is
|
|
23
|
+
// shown so it can issue <get line="N" limit="M"/> for the rest without
|
|
24
|
+
// re-running the command.
|
|
25
|
+
export function streamSummary(label, entry, TAIL_LINES = 12) {
|
|
26
|
+
if (!entry.body) return "";
|
|
27
|
+
const { body, attributes } = entry;
|
|
28
|
+
const command = attributes.command;
|
|
29
|
+
const channel = attributes.channel === 2 ? "stderr" : "stdout";
|
|
30
|
+
const trailingNewline = body.endsWith("\n");
|
|
31
|
+
const lines = trailingNewline
|
|
32
|
+
? body.slice(0, -1).split("\n")
|
|
33
|
+
: body.split("\n");
|
|
34
|
+
const total = lines.length;
|
|
35
|
+
if (total <= TAIL_LINES) {
|
|
36
|
+
return `# ${label} ${command} (${channel}, ${total}L)\n${body}`;
|
|
37
|
+
}
|
|
38
|
+
const startLine = total - TAIL_LINES + 1;
|
|
39
|
+
const tail =
|
|
40
|
+
lines.slice(-TAIL_LINES).join("\n") + (trailingNewline ? "\n" : "");
|
|
41
|
+
return `# ${label} ${command} (${channel}, tail L${startLine}-${total}/${total}; <get line="1" limit="N"/> for head)\n${tail}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Pattern-result log entry shared by get/set/store/rm.
|
|
37
45
|
export async function storePatternResult(
|
|
38
46
|
store,
|
|
39
47
|
runId,
|
|
@@ -42,13 +50,13 @@ export async function storePatternResult(
|
|
|
42
50
|
path,
|
|
43
51
|
bodyFilter,
|
|
44
52
|
matches,
|
|
45
|
-
{
|
|
53
|
+
{ manifest = false, loopId = null, attributes = null } = {},
|
|
46
54
|
) {
|
|
47
55
|
const logSlug = await store.logPath(runId, turn, scheme, path);
|
|
48
56
|
const filter = bodyFilter ? ` body="${bodyFilter}"` : "";
|
|
49
57
|
const total = matches.reduce((s, m) => s + m.tokens, 0);
|
|
50
58
|
const listing = matches.map((m) => `${m.path} (${m.tokens})`).join("\n");
|
|
51
|
-
const prefix =
|
|
59
|
+
const prefix = manifest ? "MANIFEST " : "";
|
|
52
60
|
const body = `${prefix}${scheme} path="${path}"${filter}: ${matches.length} matched (${total} tokens)\n${listing}`;
|
|
53
61
|
await store.set({
|
|
54
62
|
runId,
|
package/src/plugins/index.js
CHANGED
|
@@ -3,28 +3,18 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { readdir, stat } from "node:fs/promises";
|
|
4
4
|
import { basename, isAbsolute, join } from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import config from "../agent/config.js";
|
|
6
7
|
import PluginContext from "../hooks/PluginContext.js";
|
|
7
8
|
|
|
9
|
+
const { PLUGINS_LOAD_TIMEOUT } = config;
|
|
10
|
+
|
|
8
11
|
let globalPrefix;
|
|
9
12
|
function getGlobalPrefix() {
|
|
10
13
|
globalPrefix ??= execSync("npm prefix -g", { encoding: "utf8" }).trim();
|
|
11
14
|
return globalPrefix;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
* Plugin loader:
|
|
16
|
-
* 1. Walk filesystem + env vars to collect plugin descriptors.
|
|
17
|
-
* 2. Import each and instantiate with a fresh PluginContext.
|
|
18
|
-
*
|
|
19
|
-
* Returns a Map of name → PluginContext for the caller to pass to
|
|
20
|
-
* initPlugins. No module-global state — each caller owns its set.
|
|
21
|
-
*
|
|
22
|
-
* Plugin constructors must be declarative (SPEC surfaces): they
|
|
23
|
-
* register schemes, hooks, filters, RPC methods — but don't dereference
|
|
24
|
-
* infrastructure that might not be ready yet. Because the plugin
|
|
25
|
-
* contract makes constructors side-effect-free on each other, load
|
|
26
|
-
* order doesn't matter and there is no dependency system.
|
|
27
|
-
*/
|
|
17
|
+
// Walk filesystem + env vars, import, instantiate; constructors must stay declarative.
|
|
28
18
|
export async function registerPlugins(dirs = [], hooks) {
|
|
29
19
|
const uniqueDirs = [...new Set(dirs.map((d) => join(d)))];
|
|
30
20
|
|
|
@@ -39,12 +29,22 @@ export async function registerPlugins(dirs = [], hooks) {
|
|
|
39
29
|
try {
|
|
40
30
|
const module = await withTimeout(
|
|
41
31
|
import(d.url),
|
|
42
|
-
|
|
32
|
+
PLUGINS_LOAD_TIMEOUT,
|
|
43
33
|
`Plugin import timed out: ${d.source}`,
|
|
44
34
|
);
|
|
45
35
|
resolved.push({ ...d, Plugin: module.default });
|
|
46
36
|
} catch (err) {
|
|
47
|
-
|
|
37
|
+
// Core plugins live on disk and are part of rummy's contract;
|
|
38
|
+
// their failure is structural and must crash. Third-party
|
|
39
|
+
// plugins (RUMMY_PLUGIN_<x>) are user-installed and may be
|
|
40
|
+
// busted; we log loudly and continue without them.
|
|
41
|
+
if (d.source.startsWith("env:")) {
|
|
42
|
+
console.error(
|
|
43
|
+
`[RUMMY] Plugin import failed: ${d.name} — ${err.message}`,
|
|
44
|
+
);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Core plugin '${d.name}' import failed`, { cause: err });
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -53,7 +53,11 @@ export async function registerPlugins(dirs = [], hooks) {
|
|
|
53
53
|
try {
|
|
54
54
|
await instantiatePlugin(r, hooks, instances);
|
|
55
55
|
} catch (err) {
|
|
56
|
-
|
|
56
|
+
if (r.source.startsWith("env:")) {
|
|
57
|
+
console.error(`[RUMMY] Plugin load failed: ${r.name} — ${err.message}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Core plugin '${r.name}' load failed`, { cause: err });
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
return instances;
|
|
@@ -63,7 +67,7 @@ async function instantiatePlugin({ name, Plugin, source }, hooks, instances) {
|
|
|
63
67
|
if (typeof Plugin?.register === "function") {
|
|
64
68
|
await withTimeout(
|
|
65
69
|
Plugin.register(hooks),
|
|
66
|
-
|
|
70
|
+
PLUGINS_LOAD_TIMEOUT,
|
|
67
71
|
`Plugin register timed out: ${source}`,
|
|
68
72
|
);
|
|
69
73
|
return;
|
|
@@ -89,26 +93,14 @@ const AUDIT_SCHEMES = [
|
|
|
89
93
|
|
|
90
94
|
const PROMPT_SCHEMES = ["prompt"];
|
|
91
95
|
|
|
92
|
-
// Lifecycle
|
|
93
|
-
// state. Writable by system (internal bookkeeping), plugin (extensions),
|
|
94
|
-
// and client (RPC in Phase 4).
|
|
96
|
+
// Lifecycle entries mirror server state; writable by system/plugin/client.
|
|
95
97
|
const LIFECYCLE_SCHEMES = ["run"];
|
|
96
98
|
|
|
97
|
-
// Unified log namespace for action history entries under
|
|
98
|
-
// log://turn_N/scheme/slug.
|
|
99
99
|
const LOG_SCHEMES = ["log"];
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
* After DB is ready, upsert declared schemes and bootstrap audit/prompt
|
|
103
|
-
* schemes. Takes the plugin collection returned by registerPlugins.
|
|
104
|
-
* Per-plugin store/db access is provided per-turn via RummyContext;
|
|
105
|
-
* PluginContext itself holds only name + hooks.
|
|
106
|
-
*/
|
|
101
|
+
// Bootstraps audit/prompt/log/lifecycle schemes; called after DB is ready.
|
|
107
102
|
export async function initPlugins(db, hooks, instances) {
|
|
108
103
|
for (const name of AUDIT_SCHEMES) {
|
|
109
|
-
// Audit schemes are written only by system-level code (reasoning,
|
|
110
|
-
// user/assistant/model messages, etc.). Closing the door on model
|
|
111
|
-
// writes and plugin writes here.
|
|
112
104
|
await db.upsert_scheme.run({
|
|
113
105
|
name,
|
|
114
106
|
model_visible: 0,
|
|
@@ -118,8 +110,6 @@ export async function initPlugins(db, hooks, instances) {
|
|
|
118
110
|
});
|
|
119
111
|
}
|
|
120
112
|
for (const name of PROMPT_SCHEMES) {
|
|
121
|
-
// Prompt entries are created by the prompt plugin on user input;
|
|
122
|
-
// model doesn't emit <set path="prompt://...">.
|
|
123
113
|
await db.upsert_scheme.run({
|
|
124
114
|
name,
|
|
125
115
|
model_visible: 1,
|
|
@@ -138,9 +128,6 @@ export async function initPlugins(db, hooks, instances) {
|
|
|
138
128
|
});
|
|
139
129
|
}
|
|
140
130
|
for (const name of LIFECYCLE_SCHEMES) {
|
|
141
|
-
// Lifecycle entries are client-addressable mirrors of server state.
|
|
142
|
-
// Not model-visible. System writes internally; plugins and clients
|
|
143
|
-
// write via the 6 primitives.
|
|
144
131
|
await db.upsert_scheme.run({
|
|
145
132
|
name,
|
|
146
133
|
model_visible: 0,
|
|
@@ -156,7 +143,7 @@ export async function initPlugins(db, hooks, instances) {
|
|
|
156
143
|
}
|
|
157
144
|
}
|
|
158
145
|
|
|
159
|
-
//
|
|
146
|
+
// Default scheme for tools that ensureTool'd but didn't registerScheme.
|
|
160
147
|
const registered = new Set();
|
|
161
148
|
for (const ctx of instances.values()) {
|
|
162
149
|
for (const s of ctx.schemes) registered.add(s.name);
|
|
@@ -177,7 +164,6 @@ export async function initPlugins(db, hooks, instances) {
|
|
|
177
164
|
}
|
|
178
165
|
|
|
179
166
|
function resolvePlugin(packageName) {
|
|
180
|
-
// Check local node_modules first, then global
|
|
181
167
|
const localDir = join(process.cwd(), "node_modules", packageName);
|
|
182
168
|
if (existsSync(join(localDir, "package.json"))) return localDir;
|
|
183
169
|
const globalDir = join(getGlobalPrefix(), "lib", "node_modules", packageName);
|
|
@@ -256,8 +242,6 @@ async function collectFromDir(dir, isRoot, descriptors) {
|
|
|
256
242
|
}
|
|
257
243
|
}
|
|
258
244
|
|
|
259
|
-
const PLUGIN_LOAD_TIMEOUT = 10000;
|
|
260
|
-
|
|
261
245
|
function withTimeout(promise, ms, message) {
|
|
262
246
|
return Promise.race([
|
|
263
247
|
promise,
|
|
@@ -29,6 +29,24 @@ phase directive so prompt caching holds across turns within a run.
|
|
|
29
29
|
- `protocol.js` — placeholder module reserved for deterministic
|
|
30
30
|
protocol rule enforcement. Currently pass-through.
|
|
31
31
|
|
|
32
|
+
## Navigation validation
|
|
33
|
+
|
|
34
|
+
`validateNavigation(status, rummy)` rejects illegal stage transitions
|
|
35
|
+
emitted via `<update status="N">`:
|
|
36
|
+
|
|
37
|
+
- **Forward skip** — `nextPhase > currentPhase + 1`. Models advancing
|
|
38
|
+
more than one stage at once are jumping past required work. Returns
|
|
39
|
+
and continuations (`nextPhase ≤ currentPhase`) always pass.
|
|
40
|
+
- **Status 200 outside Deployment** — 200 is Deployment Completion.
|
|
41
|
+
Emitting it from earlier phases skips the actual Deployment work.
|
|
42
|
+
- **Deployment with prior prompts** — entering or remaining in
|
|
43
|
+
Deployment (phase 7) requires zero visible PRIOR prompts. Covers
|
|
44
|
+
167 (entry), 177 / 200 (continuation, completion).
|
|
45
|
+
|
|
46
|
+
On rejection the update entry is marked `rejected` (the phase router
|
|
47
|
+
skips it) and an error log is emitted; rejections count as normal
|
|
48
|
+
strikes.
|
|
49
|
+
|
|
32
50
|
## Cache shape
|
|
33
51
|
|
|
34
52
|
- System message includes the base template + tool docs + persona.
|
|
@@ -6,12 +6,7 @@ const baseInstructions = readFileSync(
|
|
|
6
6
|
"utf8",
|
|
7
7
|
);
|
|
8
8
|
|
|
9
|
-
// 1XY
|
|
10
|
-
// phaseForStatus to select next turn's <instructions>. Phases 4–9 are
|
|
11
|
-
// reserved (status codes 1X4..1X9); add new phases by dropping in
|
|
12
|
-
// `instructions_10N.md`. Absent files render no <instructions> block —
|
|
13
|
-
// the model runs on base instructions only. This lets you route ahead
|
|
14
|
-
// of writing the prose (e.g. an upcoming "ask lite" phase 9).
|
|
9
|
+
// 1XY phase routing; see plugin README.
|
|
15
10
|
const PHASES = [4, 5, 6, 7, 8, 9];
|
|
16
11
|
const phaseInstructions = Object.fromEntries(
|
|
17
12
|
PHASES.flatMap((p) => {
|
|
@@ -23,19 +18,12 @@ const TURN_FROM_PATH = /^log:\/\/turn_(\d+)\/update\//;
|
|
|
23
18
|
|
|
24
19
|
function phaseForStatus(status) {
|
|
25
20
|
if (status == null) return 4;
|
|
26
|
-
if (status === 200) return
|
|
21
|
+
if (status === 200) return 7;
|
|
27
22
|
const last = status % 10;
|
|
28
23
|
return PHASES.includes(last) ? last : 4;
|
|
29
24
|
}
|
|
30
25
|
|
|
31
|
-
//
|
|
32
|
-
// emission's status. Used by the assembly.user filter so the phase
|
|
33
|
-
// instructions ride with the user message (dynamic, expected to
|
|
34
|
-
// change every turn) instead of the system prompt (stable, cached).
|
|
35
|
-
// Validation is upstream (update.js isValidStatus + 422 error log) so
|
|
36
|
-
// we trust the status and route on it directly — a whitelist here
|
|
37
|
-
// silently drops advertised completion codes whose contracts drift,
|
|
38
|
-
// which is worse than a noisy fallback.
|
|
26
|
+
// Latest non-rejected update status from materialized rows.
|
|
39
27
|
function latestUpdateStatusFromRows(rows) {
|
|
40
28
|
let bestTurn = -1;
|
|
41
29
|
let bestStatus = null;
|
|
@@ -49,6 +37,7 @@ function latestUpdateStatusFromRows(rows) {
|
|
|
49
37
|
: r.attributes;
|
|
50
38
|
const status = attrs?.status;
|
|
51
39
|
if (status == null) continue;
|
|
40
|
+
if (attrs?.rejected) continue;
|
|
52
41
|
if (turn > bestTurn || (turn === bestTurn && status > bestStatus)) {
|
|
53
42
|
bestTurn = turn;
|
|
54
43
|
bestStatus = status;
|
|
@@ -66,25 +55,22 @@ export default class Instructions {
|
|
|
66
55
|
core.on("turn.started", this.onTurnStarted.bind(this));
|
|
67
56
|
core.hooks.instructions.resolveSystemPrompt =
|
|
68
57
|
this.resolveSystemPrompt.bind(this);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
core.filter("assembly.user", this.assembleInstructions.bind(this),
|
|
58
|
+
core.hooks.instructions.validateNavigation =
|
|
59
|
+
this.validateNavigation.bind(this);
|
|
60
|
+
core.hooks.instructions.findLatestSummary =
|
|
61
|
+
this.findLatestSummary.bind(this);
|
|
62
|
+
core.filter("assembly.user", this.assembleInstructions.bind(this), 200);
|
|
74
63
|
new Protocol(core);
|
|
75
64
|
}
|
|
76
65
|
|
|
77
|
-
|
|
78
|
-
* Materialize the system prompt for a run: look up the
|
|
79
|
-
* instructions://system entry, project it through the promoted view.
|
|
80
|
-
* TurnExecutor calls this once per turn before context assembly.
|
|
81
|
-
*/
|
|
66
|
+
// Project instructions://system through the visible view; called once per turn.
|
|
82
67
|
async resolveSystemPrompt(rummy) {
|
|
83
68
|
const { entries: store, runId, hooks } = rummy;
|
|
84
69
|
const entries = await store.getEntriesByPattern(
|
|
85
70
|
runId,
|
|
86
71
|
"instructions://system",
|
|
87
72
|
null,
|
|
73
|
+
{ includeAuditSchemes: true },
|
|
88
74
|
);
|
|
89
75
|
// The entry is always written by onTurnStarted before this runs.
|
|
90
76
|
const entry = entries[0];
|
|
@@ -102,16 +88,97 @@ export default class Instructions {
|
|
|
102
88
|
});
|
|
103
89
|
}
|
|
104
90
|
|
|
91
|
+
// Reject illegal stage navigation; see plugin README.
|
|
92
|
+
async validateNavigation(status, rummy) {
|
|
93
|
+
const currentPhase = await this.#getCurrentPhase(rummy);
|
|
94
|
+
const nextPhase = phaseForStatus(status);
|
|
95
|
+
if (nextPhase > currentPhase + 1) {
|
|
96
|
+
return { ok: false, reason: "Illegal navigation attempt" };
|
|
97
|
+
}
|
|
98
|
+
if (status === 200 && currentPhase !== 7) {
|
|
99
|
+
return { ok: false, reason: "Illegal navigation attempt" };
|
|
100
|
+
}
|
|
101
|
+
if (nextPhase === 7) {
|
|
102
|
+
const visible = await this.#countVisiblePriorPrompts(rummy);
|
|
103
|
+
if (visible > 0) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: `Illegal navigation attempt: ${visible} visible prior prompts`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { ok: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #getCurrentPhase(rummy) {
|
|
114
|
+
// `**` not `*`: update slugs may contain URL-encoded `/`.
|
|
115
|
+
const updates = await rummy.entries.getEntriesByPattern(
|
|
116
|
+
rummy.runId,
|
|
117
|
+
"log://*/update/**",
|
|
118
|
+
null,
|
|
119
|
+
);
|
|
120
|
+
let bestTurn = -1;
|
|
121
|
+
let bestStatus = null;
|
|
122
|
+
for (const e of updates) {
|
|
123
|
+
const m = TURN_FROM_PATH.exec(e.path);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
const turn = Number(m[1]);
|
|
126
|
+
if (turn >= rummy.sequence) continue;
|
|
127
|
+
const attrs =
|
|
128
|
+
typeof e.attributes === "string"
|
|
129
|
+
? JSON.parse(e.attributes)
|
|
130
|
+
: e.attributes;
|
|
131
|
+
if (attrs?.rejected) continue;
|
|
132
|
+
if (attrs?.status == null) continue;
|
|
133
|
+
if (turn > bestTurn) {
|
|
134
|
+
bestTurn = turn;
|
|
135
|
+
bestStatus = attrs.status;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return phaseForStatus(bestStatus);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Latest phase-7 success (status=200); state-machine knowledge lives here, not AgentLoop.
|
|
142
|
+
findLatestSummary(logEntries) {
|
|
143
|
+
return logEntries
|
|
144
|
+
.filter((e) => {
|
|
145
|
+
if (!TURN_FROM_PATH.test(e.path)) return false;
|
|
146
|
+
const attrs =
|
|
147
|
+
typeof e.attributes === "string"
|
|
148
|
+
? JSON.parse(e.attributes)
|
|
149
|
+
: e.attributes;
|
|
150
|
+
return attrs?.status === 200;
|
|
151
|
+
})
|
|
152
|
+
.at(-1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async #countVisiblePriorPrompts(rummy) {
|
|
156
|
+
const prompts = await rummy.entries.getEntriesByPattern(
|
|
157
|
+
rummy.runId,
|
|
158
|
+
"prompt://*",
|
|
159
|
+
null,
|
|
160
|
+
);
|
|
161
|
+
const visible = prompts.filter((p) => p.visibility === "visible");
|
|
162
|
+
if (visible.length === 0) return 0;
|
|
163
|
+
// Exclude the latest prompt; only PRIOR prompts trigger demote-before-Deployment.
|
|
164
|
+
let maxNum = -1;
|
|
165
|
+
for (const p of visible) {
|
|
166
|
+
const m = /^prompt:\/\/(\d+)$/.exec(p.path);
|
|
167
|
+
if (m && Number(m[1]) > maxNum) maxNum = Number(m[1]);
|
|
168
|
+
}
|
|
169
|
+
return visible.filter((p) => {
|
|
170
|
+
const m = /^prompt:\/\/(\d+)$/.exec(p.path);
|
|
171
|
+
return !m || Number(m[1]) !== maxNum;
|
|
172
|
+
}).length;
|
|
173
|
+
}
|
|
174
|
+
|
|
105
175
|
async onTurnStarted({ rummy }) {
|
|
106
176
|
const { entries: store, sequence: turn, runId } = rummy;
|
|
107
177
|
const runRow = await store.getRun(runId);
|
|
108
178
|
const toolSet = rummy.toolSet
|
|
109
179
|
? [...rummy.toolSet]
|
|
110
180
|
: this.#core.hooks.tools.names;
|
|
111
|
-
// instructions://
|
|
112
|
-
// No per-turn phase state on this entry — keeps the system
|
|
113
|
-
// prompt cache-stable across turns. Phase selection happens at
|
|
114
|
-
// assembly.user time from the current row set.
|
|
181
|
+
// instructions://system stays cache-stable; phase selection at assembly.user.
|
|
115
182
|
await store.set({
|
|
116
183
|
runId,
|
|
117
184
|
turn,
|
|
@@ -120,8 +187,6 @@ export default class Instructions {
|
|
|
120
187
|
state: "resolved",
|
|
121
188
|
writer: "system",
|
|
122
189
|
attributes: {
|
|
123
|
-
// runRow.persona is a nullable TEXT column; absent row is
|
|
124
|
-
// a system bug — let the null propagate if runRow exists.
|
|
125
190
|
persona: runRow.persona,
|
|
126
191
|
toolSet,
|
|
127
192
|
},
|
|
@@ -137,7 +202,6 @@ export default class Instructions {
|
|
|
137
202
|
{},
|
|
138
203
|
{ toolSet: activeTools },
|
|
139
204
|
);
|
|
140
|
-
// Hidden tools are excluded at the registry level (see ToolRegistry).
|
|
141
205
|
const sorted = this.#core.hooks.tools.advertisedNames.filter((n) =>
|
|
142
206
|
activeTools.has(n),
|
|
143
207
|
);
|
|
@@ -153,12 +217,7 @@ export default class Instructions {
|
|
|
153
217
|
return prompt;
|
|
154
218
|
}
|
|
155
219
|
|
|
156
|
-
//
|
|
157
|
-
// block in the user message. Runs at priority 250 — after <log>
|
|
158
|
-
// and <unknowns>, immediately before <prompt>. System prompt stays
|
|
159
|
-
// static so prompt caching keeps its prefix intact across turns.
|
|
160
|
-
// A routed phase without an instructions_10N.md file emits nothing —
|
|
161
|
-
// the model proceeds on base instructions alone.
|
|
220
|
+
// Render <instructions> for current phase; absent phase file → no block.
|
|
162
221
|
assembleInstructions(content, ctx) {
|
|
163
222
|
const status = latestUpdateStatusFromRows(ctx.rows);
|
|
164
223
|
const step = phaseInstructions[phaseForStatus(status)];
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
XML Commands Available: [%TOOLS%]
|
|
2
2
|
|
|
3
|
-
# FCRM
|
|
3
|
+
# FCRM State Machine
|
|
4
4
|
|
|
5
|
-
You are a Folksonomic Context Relevance Maximization (FCRM)
|
|
6
|
-
* Definition Stage: Register everything unknown about the prompt request.
|
|
7
|
-
* Discovery Stage: Discover, Distill, and Demote source entries to resolve unknowns into knowns.
|
|
8
|
-
* Deployment Stage: Act on the prompt.
|
|
5
|
+
You are a Folksonomic Context Relevance Maximization (FCRM) State Machine.
|
|
9
6
|
|
|
10
|
-
|
|
7
|
+
YOU MUST ONLY perform the actions corresponding with your current stage:
|
|
8
|
+
* Decomposition Stage: Determine, define, and decompose key unknown and unresolved into unknown:// entries
|
|
9
|
+
* Distillation Stage: discovering relevant source entries, then distilling into known:// entries to resolve unknowns
|
|
10
|
+
* Demotion Stage: Demote the unknown entries, source entries, prompts, and log events after distillation is completed
|
|
11
|
+
* Deployment Stage: Act on the current prompt after relevant context is distilled and irrelevant context is demoted
|
|
12
|
+
* Resolution Stage: Evaluation of context relevance maximization, state machine compliance, and prompt resolution.
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
## Visibility States: Promote and Demote Visibility State to Control Context Relevance
|
|
15
|
+
* visible: Full entry body in context, uses `tokens="N"` context budget
|
|
16
|
+
* summarized: Short summary in context, very small context budget penalty
|
|
17
|
+
* archived: Hidden from context, recallable later by path reference or pattern search
|
|
18
|
+
|
|
19
|
+
* Leverage the FCRM's Visibility States with folksonomic taxonomies and tags to store and recall unlimited information.
|
|
20
|
+
* When an entry is "visible", it will appear in both the summarized and visible sections.
|
|
21
|
+
* The `tokens="N"` shows how much context is consumed if "visible". Entries consume very few tokens when summarized.
|
|
22
|
+
|
|
23
|
+
YOU MUST NOT allow the `tokens="N"` sum of source entries, prompts, or log events to exceed `tokensFree` budget.
|
|
18
24
|
|
|
19
25
|
# Commands
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
Example: <set path="
|
|
23
|
-
Example: <get path="src/*.txt"
|
|
27
|
+
YOU MUST NOT use shell commands for file operations. Files are also entries that require XML Commands.
|
|
28
|
+
Example: <set path="projectFile.txt">new file content</set>
|
|
29
|
+
Example: <get path="src/*.txt" manifest/>
|
|
30
|
+
|
|
31
|
+
* Files, entries, prompts, and log events are all accessible with the XML Commands.
|
|
32
|
+
* Entries without a `{scheme}://` are files. They can be read and modified through the unified XML Commands interface.
|
|
24
33
|
|
|
25
34
|
[%TOOLDOCS%]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# Decomposition Stage: YOU MUST ONLY create topical, taxonomized, and tagged unknown:// entries
|
|
2
|
+
|
|
3
|
+
YOU MUST decompose the prompt into the key information, issues, and items that are unknown and/or unresolved.
|
|
2
4
|
|
|
3
|
-
YOU MUST create topical, taxonomized, and tagged unknown:// entries for missing information you need to discover.
|
|
4
5
|
Example: <set path="unknown://countries/france/capital" summary="countries,france,capital,geography,trivia">What is the capital of France?</set>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
## Turn Termination (CHOOSE ONLY ONE):
|
|
8
|
+
* Decomposition Stage Completion: <update status="145">prompt decomposed</update>
|