@llblab/pi-actors 0.12.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.
Files changed (86) hide show
  1. package/AGENTS.md +72 -0
  2. package/BACKLOG.md +38 -0
  3. package/CHANGELOG.md +179 -0
  4. package/README.md +338 -0
  5. package/docs/README.md +21 -0
  6. package/docs/actor-messages.md +149 -0
  7. package/docs/async-runs.md +335 -0
  8. package/docs/command-templates.md +424 -0
  9. package/docs/component-recipes.md +148 -0
  10. package/docs/recipe-library.md +176 -0
  11. package/docs/task-first-recipes.md +233 -0
  12. package/docs/template-recipes.md +285 -0
  13. package/docs/tool-registry.md +142 -0
  14. package/index.ts +198 -0
  15. package/lib/actor-messages.ts +120 -0
  16. package/lib/async-runs.ts +688 -0
  17. package/lib/command-templates.ts +795 -0
  18. package/lib/config.ts +266 -0
  19. package/lib/execution.ts +720 -0
  20. package/lib/file-state.ts +24 -0
  21. package/lib/identity.ts +29 -0
  22. package/lib/observability.ts +525 -0
  23. package/lib/output.ts +123 -0
  24. package/lib/paths.ts +35 -0
  25. package/lib/prompts.ts +75 -0
  26. package/lib/recipe-references.ts +586 -0
  27. package/lib/registry.ts +302 -0
  28. package/lib/runtime.ts +101 -0
  29. package/lib/schema.ts +402 -0
  30. package/lib/temp.ts +44 -0
  31. package/lib/tools.ts +651 -0
  32. package/package.json +52 -0
  33. package/recipes/music-player.json +25 -0
  34. package/recipes/pipeline-architect-coordinator.json +88 -0
  35. package/recipes/pipeline-artifact-report.json +52 -0
  36. package/recipes/pipeline-artifact-write.json +66 -0
  37. package/recipes/pipeline-async-run-ops.json +67 -0
  38. package/recipes/pipeline-checkpoint-continuation.json +57 -0
  39. package/recipes/pipeline-development-tasking.json +73 -0
  40. package/recipes/pipeline-docs-maintenance.json +72 -0
  41. package/recipes/pipeline-media-library.json +51 -0
  42. package/recipes/pipeline-quorum-review.json +72 -0
  43. package/recipes/pipeline-release-readiness.json +83 -0
  44. package/recipes/pipeline-repo-health.json +81 -0
  45. package/recipes/pipeline-research-synthesis.json +87 -0
  46. package/recipes/pipeline-review-readiness.json +49 -0
  47. package/recipes/subagent-artifact.json +26 -0
  48. package/recipes/subagent-checkpoint.json +27 -0
  49. package/recipes/subagent-conflict-report.json +25 -0
  50. package/recipes/subagent-contradiction-map.json +26 -0
  51. package/recipes/subagent-critic.json +28 -0
  52. package/recipes/subagent-evidence-map.json +26 -0
  53. package/recipes/subagent-followup.json +27 -0
  54. package/recipes/subagent-judge.json +26 -0
  55. package/recipes/subagent-merge.json +26 -0
  56. package/recipes/subagent-message.json +29 -0
  57. package/recipes/subagent-normalize.json +24 -0
  58. package/recipes/subagent-plan.json +26 -0
  59. package/recipes/subagent-prompt.json +22 -0
  60. package/recipes/subagent-quorum.json +41 -0
  61. package/recipes/subagent-review-coordinator.json +107 -0
  62. package/recipes/subagent-review.json +30 -0
  63. package/recipes/subagent-task-card.json +28 -0
  64. package/recipes/subagent-tools.json +17 -0
  65. package/recipes/subagent-verify.json +27 -0
  66. package/recipes/subagents-prompts.json +32 -0
  67. package/recipes/utility-actor-message.json +24 -0
  68. package/recipes/utility-artifact-manifest.json +17 -0
  69. package/recipes/utility-artifact-write.json +17 -0
  70. package/recipes/utility-changelog-head.json +12 -0
  71. package/recipes/utility-changelog-section.json +14 -0
  72. package/recipes/utility-git-log.json +12 -0
  73. package/recipes/utility-git-status.json +10 -0
  74. package/recipes/utility-jsonl-tail.json +11 -0
  75. package/recipes/utility-markdown-index.json +15 -0
  76. package/recipes/utility-package-summary.json +12 -0
  77. package/recipes/utility-playlist-build.json +18 -0
  78. package/recipes/utility-playlist-scan.json +12 -0
  79. package/recipes/utility-run-state-files.json +14 -0
  80. package/recipes/utility-run-summary.json +12 -0
  81. package/recipes/utility-validate-recipe.json +14 -0
  82. package/recipes/utility-validation-wrapper.json +14 -0
  83. package/scripts/async-runner.mjs +170 -0
  84. package/scripts/music-player.mjs +637 -0
  85. package/scripts/recipe-utils.mjs +273 -0
  86. package/scripts/validate-recipe.mjs +89 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Registry mutation use-cases
3
+ * Zones: registry mutations, persistence, runtime activation
4
+ * Owns register/update/delete validation, persistence, runtime side effects, and result payloads
5
+ */
6
+
7
+ import * as Config from "./config.ts";
8
+ import * as Identity from "./identity.ts";
9
+ import * as Output from "./output.ts";
10
+ import * as CommandTemplates from "./command-templates.ts";
11
+ import * as RecipeReferences from "./recipe-references.ts";
12
+ import * as Schema from "./schema.ts";
13
+
14
+ export interface RegisterToolInput {
15
+ name?: string;
16
+ description?: string;
17
+ async?: boolean;
18
+ state_dir?: string;
19
+ template?: CommandTemplates.CommandTemplateValue | null;
20
+ args?: string;
21
+ update?: boolean;
22
+ values?: Record<string, unknown>;
23
+ }
24
+
25
+ export interface RegisterToolResultDetails {
26
+ args?: string[];
27
+ async?: boolean;
28
+ config?: string;
29
+ defaults?: Record<string, string>;
30
+ recipeName?: string;
31
+ state_dir?: string;
32
+ template?: CommandTemplates.CommandTemplateValue;
33
+ templateWarnings?: string[];
34
+ tool: string;
35
+ }
36
+
37
+ export interface RegisterToolResult {
38
+ content: Array<{ type: "text"; text: string }>;
39
+ details: RegisterToolResultDetails;
40
+ }
41
+
42
+ export interface RegisterToolRuntimeDeps<TContext> {
43
+ configPath: string;
44
+ getExternalToolConflict: (name: string) => string | undefined;
45
+ getTools: () => Map<string, Config.RegisteredTool>;
46
+ getActiveTools: () => string[];
47
+ notify: (
48
+ ctx: TContext,
49
+ message: string,
50
+ type: "info" | "warning" | "error",
51
+ ) => void;
52
+ registerRuntimeTool: (cfg: Config.RegisteredTool) => void;
53
+ reservedToolNames: Set<string>;
54
+ setActiveTools: (toolNames: string[]) => void;
55
+ }
56
+
57
+ function textContent(text: string) {
58
+ return { type: "text" as const, text };
59
+ }
60
+
61
+ function listTools<TContext>(
62
+ deps: RegisterToolRuntimeDeps<TContext>,
63
+ ): RegisterToolResult {
64
+ const names = [...deps.getTools().keys()].sort();
65
+ return {
66
+ content: [
67
+ textContent(
68
+ Output.formatToolText(
69
+ names.length > 0
70
+ ? `Registered tools:\n${names.map((name) => `- ${name}`).join("\n")}`
71
+ : "No registered tools.",
72
+ ),
73
+ ),
74
+ ],
75
+ details: { tool: "register_tool" },
76
+ };
77
+ }
78
+
79
+ function deleteTool<TContext>(
80
+ name: string,
81
+ ctx: TContext,
82
+ deps: RegisterToolRuntimeDeps<TContext>,
83
+ ): RegisterToolResult {
84
+ const tools = deps.getTools();
85
+ if (!tools.has(name)) {
86
+ return {
87
+ content: [
88
+ textContent(Output.formatToolText(`Tool "${name}" not found.`)),
89
+ ],
90
+ details: { tool: name },
91
+ };
92
+ }
93
+ const nextTools = new Map(tools);
94
+ nextTools.delete(name);
95
+ const saveError = Config.saveTools(deps.configPath, nextTools);
96
+ if (saveError) {
97
+ throw new Error(
98
+ Output.formatToolText(`Failed to persist tool deletion: ${saveError}`),
99
+ );
100
+ }
101
+ tools.delete(name);
102
+ deps.setActiveTools(
103
+ deps.getActiveTools().filter((toolName) => toolName !== name),
104
+ );
105
+ deps.notify(ctx, `Deleted tool: ${name}`, "info");
106
+ return {
107
+ content: [
108
+ textContent(
109
+ Output.formatToolText(
110
+ `Deleted tool "${name}". Reload to remove it from the complete registry.`,
111
+ ),
112
+ ),
113
+ ],
114
+ details: { config: deps.configPath, tool: name },
115
+ };
116
+ }
117
+
118
+ function getInputTemplate(
119
+ value: CommandTemplates.CommandTemplateValue | null | undefined,
120
+ ): CommandTemplates.CommandTemplateValue | null | undefined {
121
+ if (typeof value === "string") return value.trim();
122
+ if (value === null || value === undefined) return value;
123
+ if (Array.isArray(value)) {
124
+ const steps = CommandTemplates.expandCommandTemplateConfigs({
125
+ template: value,
126
+ });
127
+ if (steps.length === 0)
128
+ throw new Error(
129
+ Output.formatToolText("Tool template sequence is empty."),
130
+ );
131
+ return value;
132
+ }
133
+ throw new Error(
134
+ Output.formatToolText("Tool template must be a string or sequence."),
135
+ );
136
+ }
137
+
138
+ function buildConfig(
139
+ name: string,
140
+ input: RegisterToolInput,
141
+ existing: Config.RegisteredTool | undefined,
142
+ ): Config.RegisteredTool {
143
+ const explicitArgs =
144
+ input.args === undefined
145
+ ? undefined
146
+ : Schema.parseToolArgDeclarations(input.args);
147
+ if (explicitArgs?.error)
148
+ throw new Error(Output.formatToolText(explicitArgs.error));
149
+ const description = (input.description ?? existing?.description ?? "").trim();
150
+ if (!description) {
151
+ throw new Error(
152
+ Output.formatToolText("Tool description is required unless deleting."),
153
+ );
154
+ }
155
+ const template = getInputTemplate(input.template);
156
+ if (template === null) {
157
+ throw new Error(
158
+ Output.formatToolText("Tool template cannot be null here."),
159
+ );
160
+ }
161
+ const finalTemplate =
162
+ template === undefined || template === "" ? existing?.template : template;
163
+ if (!finalTemplate) {
164
+ throw new Error(Output.formatToolText("Tool template is required."));
165
+ }
166
+ const inputRecipe = typeof input.async === "boolean" ? name : undefined;
167
+ const recipe: RecipeReferences.TemplateRecipeConfig | undefined = inputRecipe
168
+ ? {
169
+ name: inputRecipe,
170
+ ...(typeof input.async === "boolean" ? { async: input.async } : {}),
171
+ ...(typeof input.state_dir === "string" && input.state_dir.trim()
172
+ ? { state_dir: input.state_dir.trim() }
173
+ : {}),
174
+ template: finalTemplate,
175
+ ...(input.values && typeof input.values === "object"
176
+ ? { values: input.values }
177
+ : {}),
178
+ }
179
+ : template === undefined
180
+ ? existing?.recipe
181
+ : undefined;
182
+ const defaults = explicitArgs?.defaults ?? existing?.storedDefaults ?? {};
183
+ const storedArgs = explicitArgs
184
+ ? explicitArgs.declarations
185
+ : existing?.storedArgs;
186
+ const storedDefaults =
187
+ Object.keys(defaults).length > 0 ? defaults : undefined;
188
+ const recipeTemplate = RecipeReferences.getRecipeTemplate(finalTemplate);
189
+ const argTemplate = recipeTemplate ?? finalTemplate;
190
+ const argTemplateConfig: CommandTemplates.CommandTemplateConfig =
191
+ typeof argTemplate === "object" && !Array.isArray(argTemplate)
192
+ ? {
193
+ ...argTemplate,
194
+ ...(storedArgs !== undefined ? { args: storedArgs } : {}),
195
+ defaults: { ...(argTemplate.defaults ?? {}), ...defaults },
196
+ }
197
+ : {
198
+ args: storedArgs,
199
+ defaults,
200
+ template: argTemplate,
201
+ };
202
+ const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
203
+ const argTypes = {
204
+ ...inferredArgTypes,
205
+ ...(existing?.argTypes ?? {}),
206
+ ...(explicitArgs?.argTypes ?? {}),
207
+ };
208
+ return {
209
+ name,
210
+ description,
211
+ template: finalTemplate,
212
+ ...(recipe ? { recipe } : {}),
213
+ args:
214
+ RecipeReferences.isRecipeTool(finalTemplate, recipe) &&
215
+ storedArgs !== undefined
216
+ ? Schema.getExplicitToolArgNames(storedArgs)
217
+ : RecipeReferences.isRecipeReference(finalTemplate) && !recipeTemplate
218
+ ? Schema.getExplicitToolArgNames(storedArgs)
219
+ : Schema.getToolArgNames(argTemplateConfig),
220
+ defaults,
221
+ ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
222
+ ...(storedArgs !== undefined ? { storedArgs } : {}),
223
+ ...(storedDefaults !== undefined ? { storedDefaults } : {}),
224
+ };
225
+ }
226
+
227
+ export async function executeRegisterTool<TContext>(
228
+ params: unknown,
229
+ ctx: TContext,
230
+ deps: RegisterToolRuntimeDeps<TContext>,
231
+ ): Promise<RegisterToolResult> {
232
+ const input = params as RegisterToolInput;
233
+ if (!input.name) return listTools(deps);
234
+ const name = Identity.normalizeToolName(input.name);
235
+ if (!name) throw new Error(Output.formatToolText("Invalid tool name."));
236
+ if (deps.reservedToolNames.has(name)) {
237
+ throw new Error(Output.formatToolText(`Reserved tool name: ${name}`));
238
+ }
239
+ const templateProvided = Object.hasOwn(input, "template");
240
+ const template = getInputTemplate(input.template);
241
+ if (templateProvided && (template === null || template === ""))
242
+ return deleteTool(name, ctx, deps);
243
+ const tools = deps.getTools();
244
+ const existing = tools.get(name);
245
+ const conflict = deps.getExternalToolConflict(name);
246
+ if (conflict) throw new Error(Output.formatToolText(conflict));
247
+ if (existing && !input.update) {
248
+ throw new Error(
249
+ Output.formatToolText(
250
+ `Tool "${name}" already registered. Use update=true to overwrite.`,
251
+ ),
252
+ );
253
+ }
254
+ if (template === undefined && !existing) {
255
+ throw new Error(
256
+ Output.formatToolText("Tool template is required for new registrations."),
257
+ );
258
+ }
259
+ const cfg = buildConfig(name, input, existing);
260
+ const nextTools = new Map(tools);
261
+ nextTools.set(name, cfg);
262
+ const saveError = Config.saveTools(deps.configPath, nextTools);
263
+ if (saveError) {
264
+ throw new Error(
265
+ Output.formatToolText(
266
+ `Failed to persist tool registration: ${saveError}`,
267
+ ),
268
+ );
269
+ }
270
+ tools.set(name, cfg);
271
+ deps.registerRuntimeTool(cfg);
272
+ deps.notify(ctx, `Tool persisted: ${name}`, "info");
273
+ const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
274
+ typeof cfg.template === "object" && !Array.isArray(cfg.template)
275
+ ? cfg.template
276
+ : { template: cfg.template! },
277
+ );
278
+ const warningText =
279
+ templateWarnings.length > 0
280
+ ? `\nWarnings:\n${templateWarnings.map((warning) => `- ${warning}`).join("\n")}`
281
+ : "";
282
+ return {
283
+ content: [
284
+ textContent(
285
+ Output.formatToolText(
286
+ `${existing ? "Updated" : "Registered"} tool "${name}" (args: ${Schema.formatToolArgs(cfg.args)}).${warningText}`,
287
+ ),
288
+ ),
289
+ ],
290
+ details: {
291
+ args: cfg.args,
292
+ config: deps.configPath,
293
+ defaults: cfg.defaults,
294
+ ...(cfg.recipe?.async !== undefined ? { async: cfg.recipe.async } : {}),
295
+ ...(cfg.recipe?.name ? { recipeName: cfg.recipe.name } : {}),
296
+ ...(cfg.recipe?.state_dir ? { state_dir: cfg.recipe.state_dir } : {}),
297
+ ...(cfg.template ? { template: cfg.template } : {}),
298
+ ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
299
+ tool: name,
300
+ },
301
+ };
302
+ }
package/lib/runtime.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Tool registry runtime coordinator
3
+ * Zones: runtime coordination, registry loading, pi tools
4
+ * Owns persisted tool loading, conflict detection, runtime registration, and warning notification
5
+ */
6
+
7
+ import * as Config from "./config.ts";
8
+ import type { RegisteredToolExec } from "./execution.ts";
9
+ import * as Tools from "./tools.ts";
10
+
11
+ export interface RuntimeContext {
12
+ hasUI: boolean;
13
+ ui: {
14
+ notify(message: string, type?: "info" | "warning" | "error"): void;
15
+ };
16
+ }
17
+
18
+ export interface ToolInfoLike {
19
+ name: string;
20
+ }
21
+
22
+ export interface ToolRegistryRuntimeDeps {
23
+ configPath: string;
24
+ exec: RegisteredToolExec;
25
+ getAllTools: () => ToolInfoLike[];
26
+ registerTool: (
27
+ definition: ReturnType<typeof Tools.createRuntimeToolDefinition>,
28
+ ) => void;
29
+ reservedToolNames: Set<string>;
30
+ }
31
+
32
+ export interface ToolRegistryRuntime {
33
+ getExternalToolConflict(name: string): string | undefined;
34
+ getTools(): Map<string, Config.RegisteredTool>;
35
+ loadTools(ctx: RuntimeContext): void;
36
+ notify(
37
+ ctx: RuntimeContext,
38
+ message: string,
39
+ type: "info" | "warning" | "error",
40
+ ): void;
41
+ registerRuntimeTool(cfg: Config.RegisteredTool): void;
42
+ }
43
+
44
+ export function createAutoToolsRuntime(
45
+ deps: ToolRegistryRuntimeDeps,
46
+ ): ToolRegistryRuntime {
47
+ const tools = new Map<string, Config.RegisteredTool>();
48
+ const runtimeTools = new Set<string>();
49
+ function notify(
50
+ ctx: RuntimeContext,
51
+ message: string,
52
+ type: "info" | "warning" | "error",
53
+ ) {
54
+ if (ctx.hasUI) ctx.ui.notify(message, type);
55
+ }
56
+ function getExternalToolConflict(name: string): string | undefined {
57
+ if (runtimeTools.has(name)) return undefined;
58
+ const existing = deps.getAllTools().find((tool) => tool.name === name);
59
+ return existing
60
+ ? `Tool "${name}" is already registered outside pi-actors.`
61
+ : undefined;
62
+ }
63
+ function registerRuntimeTool(cfg: Config.RegisteredTool) {
64
+ deps.registerTool(Tools.createRuntimeToolDefinition(cfg, deps.exec));
65
+ runtimeTools.add(cfg.name);
66
+ }
67
+ function loadTools(ctx: RuntimeContext) {
68
+ const loaded = Config.loadToolConfig(
69
+ deps.configPath,
70
+ deps.reservedToolNames,
71
+ );
72
+ tools.clear();
73
+ for (const [name, cfg] of loaded.tools) tools.set(name, cfg);
74
+ if (loaded.changed) {
75
+ const saveError = Config.saveTools(deps.configPath, tools);
76
+ if (saveError) {
77
+ loaded.warnings.push(
78
+ `Failed to normalize ${deps.configPath}: ${saveError}`,
79
+ );
80
+ }
81
+ }
82
+ for (const cfg of tools.values()) {
83
+ const conflict = getExternalToolConflict(cfg.name);
84
+ if (conflict) {
85
+ loaded.warnings.push(conflict);
86
+ continue;
87
+ }
88
+ registerRuntimeTool(cfg);
89
+ }
90
+ if (loaded.warnings.length > 0) {
91
+ notify(ctx, `Auto-tools: ${loaded.warnings.join("; ")}`, "warning");
92
+ }
93
+ }
94
+ return {
95
+ getExternalToolConflict,
96
+ getTools: () => tools,
97
+ loadTools,
98
+ notify,
99
+ registerRuntimeTool,
100
+ };
101
+ }