ytdb-slate 0.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/LICENSE +201 -0
- package/README.md +72 -0
- package/docs/design-principles.md +183 -0
- package/docs/pr-publishing.md +123 -0
- package/docs/review-rules.md +172 -0
- package/docs/track-workflow.md +310 -0
- package/extension/episodes.ts +200 -0
- package/extension/handoff.ts +271 -0
- package/extension/index.ts +84 -0
- package/extension/mode.ts +283 -0
- package/extension/paths.ts +19 -0
- package/extension/prompt-docs.ts +49 -0
- package/extension/render.ts +138 -0
- package/extension/state.ts +191 -0
- package/extension/threads.ts +311 -0
- package/extension/tools.ts +134 -0
- package/extension/worker.ts +119 -0
- package/package.json +26 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slate orchestrator tools (ExecPlan D3): thread / threads / episode.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
import { renderThreadCall, renderThreadResult } from "./render.ts";
|
|
9
|
+
import type { SlateStore } from "./state.ts";
|
|
10
|
+
import type { DispatchProgress, ThreadManager } from "./threads.ts";
|
|
11
|
+
|
|
12
|
+
export function registerSlateTools(pi: ExtensionAPI, store: SlateStore, getManager: () => ThreadManager): void {
|
|
13
|
+
pi.registerTool({
|
|
14
|
+
name: "thread",
|
|
15
|
+
label: "Thread",
|
|
16
|
+
description: [
|
|
17
|
+
"Dispatch ONE bounded action to a persistent worker thread and receive back an episode",
|
|
18
|
+
"(a compressed, structured record of what the thread did).",
|
|
19
|
+
"Omit `thread` to create a new thread (give it a short `name`); pass an existing thread id",
|
|
20
|
+
"to continue that thread — it retains the context of all its previous actions.",
|
|
21
|
+
"Use `context` to inject prior episodes (by id, from any thread) into the action.",
|
|
22
|
+
"Threads are serial; to parallelize, dispatch to DIFFERENT threads in one message.",
|
|
23
|
+
"`model` (\"provider/id\") and `tools` apply only when creating a new thread.",
|
|
24
|
+
"An episode header of STATUS: FAILED means the action failed — read it and adapt.",
|
|
25
|
+
"Tasks that modify repository files require the track workflow's pre-implementation gates to have run first.",
|
|
26
|
+
].join(" "),
|
|
27
|
+
promptSnippet: "Dispatch one bounded action to a persistent worker thread; returns a compressed episode",
|
|
28
|
+
promptGuidelines: [
|
|
29
|
+
"Use the thread tool to delegate bounded tactical actions to worker threads; dispatch independent actions to different threads in the same message to run them in parallel.",
|
|
30
|
+
"Pass prior episode ids in the thread tool's context parameter instead of restating their content.",
|
|
31
|
+
],
|
|
32
|
+
parameters: Type.Object({
|
|
33
|
+
thread: Type.Optional(Type.String({ description: "Existing thread id to continue (e.g. \"t1\")" })),
|
|
34
|
+
name: Type.Optional(Type.String({ description: "Short name for a NEW thread (e.g. \"recon\")" })),
|
|
35
|
+
task: Type.String({ description: "The single bounded action to execute" }),
|
|
36
|
+
context: Type.Optional(
|
|
37
|
+
Type.Array(Type.String(), { description: "Episode ids to inject as context (e.g. [\"t1.e2\"])" }),
|
|
38
|
+
),
|
|
39
|
+
model: Type.Optional(Type.String({ description: "Worker model \"provider/id\" (new threads only)" })),
|
|
40
|
+
tools: Type.Optional(Type.Array(Type.String(), { description: "Worker tool allowlist (new threads only)" })),
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
44
|
+
const onProgress = (p: DispatchProgress) => {
|
|
45
|
+
onUpdate?.({
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `[${p.threadName}] ${p.done ? (p.status ?? "done") : "running"}\n${p.lines.slice(-8).join("\n")}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
details: { threadId: p.threadId, threadName: p.threadName, lines: p.lines, usage: p.usage, done: p.done },
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = await getManager().dispatch(
|
|
57
|
+
{
|
|
58
|
+
threadId: params.thread,
|
|
59
|
+
name: params.name,
|
|
60
|
+
task: params.task,
|
|
61
|
+
contextEpisodeIds: params.context,
|
|
62
|
+
model: params.model,
|
|
63
|
+
tools: params.tools,
|
|
64
|
+
},
|
|
65
|
+
ctx,
|
|
66
|
+
signal,
|
|
67
|
+
onProgress,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const headline = `[episode ${result.episode.id} | thread ${result.thread.id} "${result.thread.name}" | STATUS: ${result.episode.status === "ok" ? "OK" : "FAILED"}]`;
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: `${headline}\n\n${result.episodeText}` }],
|
|
73
|
+
details: {
|
|
74
|
+
threadId: result.thread.id,
|
|
75
|
+
threadName: result.thread.name,
|
|
76
|
+
episodeId: result.episode.id,
|
|
77
|
+
status: result.episode.status,
|
|
78
|
+
episodeFile: result.episode.file,
|
|
79
|
+
usage: result.usage,
|
|
80
|
+
done: true,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
renderCall(args, theme) {
|
|
86
|
+
return renderThreadCall(args as never, theme as never);
|
|
87
|
+
},
|
|
88
|
+
renderResult(result, options, theme) {
|
|
89
|
+
return renderThreadResult(result as never, options as never, theme as never);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
pi.registerTool({
|
|
94
|
+
name: "threads",
|
|
95
|
+
label: "Threads",
|
|
96
|
+
description:
|
|
97
|
+
"List all worker threads: id, name, status, episodes so far, and last activity. " +
|
|
98
|
+
"Use this to decide whether to continue an existing thread or create a new one.",
|
|
99
|
+
promptSnippet: "List worker threads and their episodes",
|
|
100
|
+
parameters: Type.Object({}),
|
|
101
|
+
async execute() {
|
|
102
|
+
const threads = [...store.threads.values()];
|
|
103
|
+
if (threads.length === 0) {
|
|
104
|
+
return { content: [{ type: "text", text: "No threads yet. Use the thread tool to create one." }], details: { count: 0 } };
|
|
105
|
+
}
|
|
106
|
+
const lines = threads.map((t) => {
|
|
107
|
+
const episodes = t.episodeIds.length > 0 ? t.episodeIds.join(", ") : "(none)";
|
|
108
|
+
return `${t.id} "${t.name}" [${t.status}]${t.model ? ` model=${t.model}` : ""} — episodes: ${episodes} — updated ${new Date(t.updatedAt).toISOString()}`;
|
|
109
|
+
});
|
|
110
|
+
return { content: [{ type: "text", text: lines.join("\n") }], details: { count: threads.length } };
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
pi.registerTool({
|
|
115
|
+
name: "episode",
|
|
116
|
+
label: "Episode",
|
|
117
|
+
description: "Fetch the full text of a stored episode by id (e.g. \"t1.e2\").",
|
|
118
|
+
promptSnippet: "Fetch a stored episode's full text by id",
|
|
119
|
+
parameters: Type.Object({
|
|
120
|
+
id: Type.String({ description: "Episode id, e.g. \"t1.e2\"" }),
|
|
121
|
+
}),
|
|
122
|
+
async execute(_toolCallId, params) {
|
|
123
|
+
const episode = store.episodes.get(params.id);
|
|
124
|
+
if (!episode) {
|
|
125
|
+
const known = [...store.episodes.keys()].join(", ") || "none";
|
|
126
|
+
throw new Error(`Unknown episode "${params.id}". Known episodes: ${known}`);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: readFileSync(episode.file, "utf8") }],
|
|
130
|
+
details: { id: episode.id, threadId: episode.threadId, status: episode.status },
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker sessions: in-process pi SDK AgentSessions with the recursion guard.
|
|
3
|
+
*
|
|
4
|
+
* A worker loads NO extensions, skills, prompt templates, or themes
|
|
5
|
+
* (DefaultResourceLoader no* options) — so a worker can never see slate tools
|
|
6
|
+
* (ExecPlan D7, depth-1 guard) — and inherits the HOST session's project-trust
|
|
7
|
+
* state via an explicit SettingsManager, so untrusted projects get neither
|
|
8
|
+
* project-local settings nor the project SYSTEM.md override in workers.
|
|
9
|
+
* Worker conversations persist under
|
|
10
|
+
* <config dir>/slate/threads/*.jsonl (CONFIG_DIR_NAME, ".pi" by default)
|
|
11
|
+
* and are reopened via SessionManager.open.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdirSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
AuthStorage,
|
|
18
|
+
CONFIG_DIR_NAME,
|
|
19
|
+
createAgentSession,
|
|
20
|
+
DefaultResourceLoader,
|
|
21
|
+
getAgentDir,
|
|
22
|
+
ModelRegistry,
|
|
23
|
+
SessionManager,
|
|
24
|
+
SettingsManager,
|
|
25
|
+
type ExtensionContext,
|
|
26
|
+
} from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import { loadPromptDocs } from "./prompt-docs.ts";
|
|
28
|
+
|
|
29
|
+
export type WorkerSession = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_WORKER_TOOLS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
32
|
+
|
|
33
|
+
const WORKER_PREAMBLE = [
|
|
34
|
+
"You are a worker thread executing ONE bounded action for an orchestrator.",
|
|
35
|
+
"Do the action fully, then stop.",
|
|
36
|
+
"Your final message must state: what you did, what you found, files you touched,",
|
|
37
|
+
"and anything the orchestrator must know.",
|
|
38
|
+
].join(" ");
|
|
39
|
+
|
|
40
|
+
export function threadsDir(cwd: string): string {
|
|
41
|
+
return resolve(cwd, CONFIG_DIR_NAME, "slate", "threads");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function episodesDir(cwd: string): string {
|
|
45
|
+
return resolve(cwd, CONFIG_DIR_NAME, "slate", "episodes");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve a "provider/id" model string against the registry; throws a clear error if unknown. */
|
|
49
|
+
export function resolveModel(ctx: ExtensionContext, spec: string) {
|
|
50
|
+
const slash = spec.indexOf("/");
|
|
51
|
+
if (slash <= 0) throw new Error(`Invalid model spec "${spec}" — expected "provider/id"`);
|
|
52
|
+
const model = ctx.modelRegistry.find(spec.slice(0, slash), spec.slice(slash + 1));
|
|
53
|
+
if (!model) throw new Error(`Unknown model "${spec}" — not found in the model registry`);
|
|
54
|
+
return model;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function openWorkerSession(opts: {
|
|
58
|
+
ctx: ExtensionContext;
|
|
59
|
+
sessionFile?: string; // resume when provided, else create new under <config dir>/slate/threads/
|
|
60
|
+
model?: string; // "provider/id"
|
|
61
|
+
tools?: string[];
|
|
62
|
+
promptDocs?: string[]; // role-guideline doc paths, cwd-relative (default none)
|
|
63
|
+
}): Promise<WorkerSession> {
|
|
64
|
+
const { ctx } = opts;
|
|
65
|
+
const dir = threadsDir(ctx.cwd);
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
|
|
68
|
+
const agentDir = getAgentDir();
|
|
69
|
+
const authStorage = AuthStorage.create();
|
|
70
|
+
const modelRegistry = ModelRegistry.create(authStorage);
|
|
71
|
+
|
|
72
|
+
// Trust propagation (mirrors vanilla pi's runtime SettingsManager): when no
|
|
73
|
+
// settingsManager is passed, the SDK default-constructs one with
|
|
74
|
+
// projectTrusted=true, which would make workers honor project-local
|
|
75
|
+
// settings and the <config dir>/SYSTEM.md override even in projects the
|
|
76
|
+
// user has NOT trusted. Carry the host session's actual trust decision into
|
|
77
|
+
// both the resource loader and the session.
|
|
78
|
+
const settingsManager = SettingsManager.create(ctx.cwd, agentDir, {
|
|
79
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Role-guideline doc content is captured when this session object is
|
|
83
|
+
// created: a live session keeps its system prompt until disposed; a
|
|
84
|
+
// thread reopened later (e.g. after a pi restart) re-reads the docs at
|
|
85
|
+
// their then-current content. Blocks go in separator-free — pi core
|
|
86
|
+
// joins appendSystemPrompt entries with "\n\n". Trust gate: project files
|
|
87
|
+
// are never injected into worker prompts for untrusted projects.
|
|
88
|
+
const promptDocs = ctx.isProjectTrusted() ? loadPromptDocs(ctx.cwd, opts.promptDocs ?? []) : [];
|
|
89
|
+
const loader = new DefaultResourceLoader({
|
|
90
|
+
cwd: ctx.cwd,
|
|
91
|
+
agentDir,
|
|
92
|
+
settingsManager,
|
|
93
|
+
noExtensions: true, // recursion guard (D7)
|
|
94
|
+
noSkills: true,
|
|
95
|
+
noPromptTemplates: true,
|
|
96
|
+
noThemes: true,
|
|
97
|
+
appendSystemPrompt: [WORKER_PREAMBLE, ...promptDocs],
|
|
98
|
+
});
|
|
99
|
+
await loader.reload();
|
|
100
|
+
|
|
101
|
+
const model = opts.model ? resolveModel(ctx, opts.model) : ctx.model;
|
|
102
|
+
|
|
103
|
+
const sessionManager = opts.sessionFile
|
|
104
|
+
? SessionManager.open(opts.sessionFile)
|
|
105
|
+
: SessionManager.create(ctx.cwd, dir);
|
|
106
|
+
|
|
107
|
+
const { session } = await createAgentSession({
|
|
108
|
+
cwd: ctx.cwd,
|
|
109
|
+
agentDir,
|
|
110
|
+
authStorage,
|
|
111
|
+
modelRegistry,
|
|
112
|
+
model: model ?? undefined,
|
|
113
|
+
tools: opts.tools && opts.tools.length > 0 ? opts.tools : DEFAULT_WORKER_TOOLS,
|
|
114
|
+
resourceLoader: loader,
|
|
115
|
+
sessionManager,
|
|
116
|
+
settingsManager,
|
|
117
|
+
});
|
|
118
|
+
return session;
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ytdb-slate",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Slate: a thread-weaving orchestration extension for the pi coding agent — bounded worker actions, compressed episodes, mandatory research/design/adversarial/track review workflow, optional draft-PR publishing.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package"
|
|
8
|
+
],
|
|
9
|
+
"files": [
|
|
10
|
+
"extension",
|
|
11
|
+
"docs",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"pi": {
|
|
16
|
+
"extensions": [
|
|
17
|
+
"./extension/index.ts"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@earendil-works/pi-ai": "*",
|
|
22
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
23
|
+
"@earendil-works/pi-tui": "*",
|
|
24
|
+
"typebox": "*"
|
|
25
|
+
}
|
|
26
|
+
}
|