ytdb-slate 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/docs/design-principles.md +97 -3
- package/docs/model-failover.md +96 -7
- package/extension/failover.ts +79 -31
- package/extension/handoff.ts +85 -42
- package/extension/index.ts +33 -7
- package/extension/mode.ts +66 -3
- package/extension/model-default.ts +587 -0
- package/extension/notify.ts +20 -0
- package/extension/state.ts +2 -0
- package/extension/threads.ts +12 -0
- package/extension/worker-extensions.ts +392 -0
- package/extension/worker.ts +105 -7
- package/package.json +1 -1
package/extension/index.ts
CHANGED
|
@@ -20,12 +20,14 @@
|
|
|
20
20
|
* Optional config at <config dir>/slate.json (config dir = CONFIG_DIR_NAME,
|
|
21
21
|
* ".pi" by default), honored ONLY when the project is trusted — untrusted
|
|
22
22
|
* projects run on built-in defaults with no project file injection:
|
|
23
|
-
* { "episodeModel": "provider/id", "workerTools": [...],
|
|
23
|
+
* { "episodeModel": "provider/id", "workerTools": [...],
|
|
24
|
+
* "workerExtensions": ["regex", ...], "maxConcurrent": 4,
|
|
24
25
|
* "contextBudget": 256000, "orchestratorModeDefault": true,
|
|
25
26
|
* "orchestratorPromptDocs": ["docs/orchestrator-guidelines.md"],
|
|
26
27
|
* "workerPromptDocs": ["docs/thread-guidelines.md"],
|
|
27
28
|
* "workflow": { "draftPRs": false },
|
|
28
29
|
* "modelFailover": { "provider/id": "provider/id" },
|
|
30
|
+
* "preserveGlobalModelDefault": true,
|
|
29
31
|
* "doctrineExtraPath": "docs/project-doctrine.md",
|
|
30
32
|
* "reviewPerspectivesPath": "docs/review-perspectives.md" }
|
|
31
33
|
* contextBudget also takes { "tokens": N, "overrides": [{ "match": "regex",
|
|
@@ -45,6 +47,12 @@ import { registerSlateMode } from "./mode.ts";
|
|
|
45
47
|
import { SlateStore, type SlateConfig } from "./state.ts";
|
|
46
48
|
import { ThreadManager } from "./threads.ts";
|
|
47
49
|
import { registerSlateTools } from "./tools.ts";
|
|
50
|
+
import {
|
|
51
|
+
createWorkerExtensionResolver,
|
|
52
|
+
EMPTY_WORKER_EXTENSION_SET,
|
|
53
|
+
sanitizeWorkerExtensions,
|
|
54
|
+
type WorkerExtensionSet,
|
|
55
|
+
} from "./worker-extensions.ts";
|
|
48
56
|
|
|
49
57
|
function loadConfig(cwd: string): SlateConfig {
|
|
50
58
|
const file = join(cwd, CONFIG_DIR_NAME, "slate.json");
|
|
@@ -67,7 +75,16 @@ function loadConfig(cwd: string): SlateConfig {
|
|
|
67
75
|
|
|
68
76
|
export default function (pi: ExtensionAPI) {
|
|
69
77
|
const store = new SlateStore(pi);
|
|
70
|
-
|
|
78
|
+
// One worker-extension resolver per session (AD41), reassigned every
|
|
79
|
+
// session_start below. The DOCTRINE consumer (registerSlateMode) reads it
|
|
80
|
+
// through the live `() => resolveWorkerExtensionSet()` indirection because it
|
|
81
|
+
// always belongs to the CURRENT session. ThreadManager instead binds the
|
|
82
|
+
// resolver instance BY VALUE at construction (CN20): a manager orphaned by a
|
|
83
|
+
// session swap must keep resolving against ITS OWN session's frozen set, not a
|
|
84
|
+
// later session's — the live indirection would leak the newer set into the
|
|
85
|
+
// stale manager. Starts as the empty set (feature off) until session_start.
|
|
86
|
+
let resolveWorkerExtensionSet: () => WorkerExtensionSet = () => EMPTY_WORKER_EXTENSION_SET;
|
|
87
|
+
let manager = new ThreadManager(store, {}, resolveWorkerExtensionSet);
|
|
71
88
|
|
|
72
89
|
registerSlateTools(pi, store, () => manager);
|
|
73
90
|
|
|
@@ -77,15 +94,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
77
94
|
// it is honored only for trusted projects; untrusted → built-in defaults.
|
|
78
95
|
const config = ctx.isProjectTrusted() ? loadConfig(ctx.cwd) : {};
|
|
79
96
|
const warn = (msg: string) => (ctx.hasUI ? ctx.ui.notify(msg, "warning") : console.warn(msg));
|
|
80
|
-
// modelFailover and
|
|
81
|
-
// value would otherwise fail silently mid-dispatch / exactly
|
|
82
|
-
// auto-pause was supposed to save the orchestrator's context.
|
|
97
|
+
// modelFailover, contextBudget and workerExtensions are validated eagerly:
|
|
98
|
+
// a malformed value would otherwise fail silently mid-dispatch / exactly
|
|
99
|
+
// when the auto-pause was supposed to save the orchestrator's context.
|
|
83
100
|
config.modelFailover = sanitizeModelFailover(config.modelFailover, warn);
|
|
84
101
|
config.contextBudget = sanitizeContextBudget(config.contextBudget, warn);
|
|
102
|
+
config.workerExtensions = sanitizeWorkerExtensions(config.workerExtensions, warn);
|
|
85
103
|
if (config.contextBudget !== undefined && config.pauseThresholdPercent !== undefined) {
|
|
86
104
|
warn("slate: contextBudget is set — the deprecated pauseThresholdPercent is ignored");
|
|
87
105
|
}
|
|
88
|
-
|
|
106
|
+
// Fresh resolver AFTER sanitization (reads the cleaned patterns) and per
|
|
107
|
+
// session_start (a restart re-resolves). Same warn as the sanitizers so a
|
|
108
|
+
// withheld colliding unit surfaces the same way. First use is later, in a
|
|
109
|
+
// worker open or a doctrine build — after session_start finishes, so tools
|
|
110
|
+
// registered during session_start are captured (AD41).
|
|
111
|
+
resolveWorkerExtensionSet = createWorkerExtensionResolver(pi, () => config.workerExtensions ?? [], warn);
|
|
112
|
+
// Bound BY VALUE (CN20): this manager keeps this session's resolver even if a
|
|
113
|
+
// later session_start replaces the module variable above.
|
|
114
|
+
manager = new ThreadManager(store, config, resolveWorkerExtensionSet);
|
|
89
115
|
store.restore(ctx);
|
|
90
116
|
});
|
|
91
117
|
|
|
@@ -103,5 +129,5 @@ export default function (pi: ExtensionAPI) {
|
|
|
103
129
|
// order-critical relative to the handlers above (different trigger events).
|
|
104
130
|
registerOrchestratorFailover(pi, () => manager.getConfig());
|
|
105
131
|
|
|
106
|
-
registerSlateMode(pi, store, handoff, () => manager.getConfig());
|
|
132
|
+
registerSlateMode(pi, store, handoff, () => manager.getConfig(), () => resolveWorkerExtensionSet());
|
|
107
133
|
}
|
package/extension/mode.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "./paths.ts";
|
|
28
28
|
import { loadPromptDocs } from "./prompt-docs.ts";
|
|
29
29
|
import { orchestratorCostUsd, type SlateConfig, type SlateStore } from "./state.ts";
|
|
30
|
+
import type { WorkerExtensionSet, WorkerExtensionUnit } from "./worker-extensions.ts";
|
|
30
31
|
|
|
31
32
|
const ORCHESTRATOR_TOOLS = ["read", "grep", "find", "ls", "thread", "threads", "episode"];
|
|
32
33
|
|
|
@@ -40,7 +41,68 @@ const ORCHESTRATOR_TOOLS = ["read", "grep", "find", "ls", "thread", "threads", "
|
|
|
40
41
|
* the file exists (cwd-resolved) — missing files are skipped silently like
|
|
41
42
|
* every other project doc.
|
|
42
43
|
*/
|
|
43
|
-
|
|
44
|
+
// A source spec that names WHICH extension (e.g. "npm:foo") is worth showing;
|
|
45
|
+
// the generic scope-ish sources are not, so rule 11 falls back to the unit path.
|
|
46
|
+
const UNINFORMATIVE_SOURCES = new Set(["", "auto", "local", "cli", "project", "user", "temporary", "builtin", "sdk"]);
|
|
47
|
+
|
|
48
|
+
function unitLabel(unit: WorkerExtensionUnit): string {
|
|
49
|
+
return UNINFORMATIVE_SOURCES.has(unit.source) ? unit.path : unit.source;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Per-field caps for the doctrine interpolations (WB21): a tool name is an
|
|
53
|
+
// identifier, a unit label is a package spec or path, a description is a
|
|
54
|
+
// sentence. Caps keep a pathological 2000-char value from bloating every turn.
|
|
55
|
+
const DOCTRINE_NAME_MAX = 64;
|
|
56
|
+
const DOCTRINE_LABEL_MAX = 128;
|
|
57
|
+
const DOCTRINE_DESC_MAX = 140;
|
|
58
|
+
|
|
59
|
+
// ONE sanitizer for every extension-supplied string interpolated into the
|
|
60
|
+
// orchestrator doctrine — tool names, unit labels and descriptions (WB20/WB21/
|
|
61
|
+
// WB22). The doctrine is a numbered, indented plain-text block inside the
|
|
62
|
+
// system prompt, so a raw value must not be able to: (WB20) inject a newline
|
|
63
|
+
// that forges a new numbered directive; (WB21) run on for thousands of
|
|
64
|
+
// characters; or (WB22) carry backticks/markdown that break the block's
|
|
65
|
+
// structure or read as an instruction. Control characters and newlines collapse
|
|
66
|
+
// to spaces, backticks and markdown structural markers are dropped, remaining
|
|
67
|
+
// whitespace is collapsed, and the result is capped with an ellipsis.
|
|
68
|
+
function sanitizeForDoctrine(value: string, max: number): string {
|
|
69
|
+
const cleaned = value
|
|
70
|
+
.replace(/[\u0000-\u001f\u007f\u009b]/g, " ") // control chars + newlines → space (WB20)
|
|
71
|
+
.replace(/[`*_~#>|]/g, " ") // code fences / markdown structure → space (WB22)
|
|
72
|
+
.replace(/\s+/g, " ")
|
|
73
|
+
.trim();
|
|
74
|
+
return cleaned.length > max ? `${cleaned.slice(0, max - 1)}\u2026` : cleaned;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Rule 11 (worker-extension awareness): appended ONLY when workers load extra
|
|
79
|
+
* pi extensions. With no units it returns "" so the doctrine is byte-identical
|
|
80
|
+
* to the pre-feature output (feature-off is unchanged). Phrased to match the
|
|
81
|
+
* imperative register of rules 1–10 (WS20/WS21/WS22): it LEADS with the action
|
|
82
|
+
* (delegate to a thread), states the constraint in the doctrine's own "cannot"
|
|
83
|
+
* voice, and CLOSES with an instruction. The extension/tool listing in the
|
|
84
|
+
* middle is data and renders verbatim.
|
|
85
|
+
*/
|
|
86
|
+
function buildWorkerExtensionsRule(extensions: WorkerExtensionSet): string {
|
|
87
|
+
if (extensions.units.length === 0) return "";
|
|
88
|
+
const lines = [
|
|
89
|
+
"11. Delegate any action that needs one of these worker-loaded pi extensions to a",
|
|
90
|
+
" thread; you cannot call their tools yourself:",
|
|
91
|
+
];
|
|
92
|
+
for (const unit of extensions.units) {
|
|
93
|
+
lines.push(` - ${sanitizeForDoctrine(unitLabel(unit), DOCTRINE_LABEL_MAX)}`);
|
|
94
|
+
for (const tool of unit.tools) {
|
|
95
|
+
lines.push(` ${sanitizeForDoctrine(tool.name, DOCTRINE_NAME_MAX)}: ${sanitizeForDoctrine(tool.description, DOCTRINE_DESC_MAX)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
lines.push(
|
|
99
|
+
" Assume every worker already has them — you need not pass them in a thread's",
|
|
100
|
+
" `tools` allowlist.",
|
|
101
|
+
);
|
|
102
|
+
return `\n${lines.join("\n")}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildDoctrine(cwd: string, config: SlateConfig, trusted: boolean, extensions: WorkerExtensionSet): string {
|
|
44
106
|
// Rule 8 tail: with draft-PR publishing enabled, the umbrella draft PR is
|
|
45
107
|
// one of the gates; otherwise durable records live in the workflow log.
|
|
46
108
|
const rule8Tail =
|
|
@@ -92,7 +154,7 @@ threads execute. Rules:
|
|
|
92
154
|
Read that file only when you must reason about slate itself (explaining
|
|
93
155
|
it, changing the extension, or an unusual routing/compaction decision) —
|
|
94
156
|
never for routine dispatching. Skip the read if it is already in your
|
|
95
|
-
context
|
|
157
|
+
context.${buildWorkerExtensionsRule(extensions)}`;
|
|
96
158
|
}
|
|
97
159
|
|
|
98
160
|
/**
|
|
@@ -130,6 +192,7 @@ export function registerSlateMode(
|
|
|
130
192
|
store: SlateStore,
|
|
131
193
|
hooks: SlateHandoffHooks,
|
|
132
194
|
getConfig: () => SlateConfig,
|
|
195
|
+
getExtensions: () => WorkerExtensionSet,
|
|
133
196
|
): void {
|
|
134
197
|
let savedTools: string[] | undefined;
|
|
135
198
|
let uiCtx: ExtensionContext | undefined;
|
|
@@ -229,7 +292,7 @@ export function registerSlateMode(
|
|
|
229
292
|
// addendum goes LAST so the pause directive is the final word in the
|
|
230
293
|
// prompt, undiluted by the role guidelines.
|
|
231
294
|
const parts = [
|
|
232
|
-
buildDoctrine(ctx.cwd, config, trusted),
|
|
295
|
+
buildDoctrine(ctx.cwd, config, trusted, getExtensions()),
|
|
233
296
|
...loadDoctrineExtra(ctx.cwd, config, trusted).map((d) => `\n\n${d}`),
|
|
234
297
|
...docs.map((d) => `\n\n${d}`),
|
|
235
298
|
];
|