@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
package/lib/config.ts ADDED
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Persistent tool registry config helpers
3
+ * Zones: registry config, persistence, migration boundary
4
+ * Owns tools.json loading, normalization, unsupported-shape rejection, and serialization
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs";
8
+
9
+ import { writeJsonAtomic } from "./file-state.ts";
10
+ import { normalizeToolName } from "./identity.ts";
11
+ import * as CommandTemplates from "./command-templates.ts";
12
+ import * as RecipeReferences from "./recipe-references.ts";
13
+ import * as Schema from "./schema.ts";
14
+ import type { CommandTemplateValue } from "./command-templates.ts";
15
+
16
+ export interface RegisteredTool {
17
+ name: string;
18
+ description: string;
19
+ args: string[];
20
+ defaults: Record<string, string>;
21
+ argTypes?: Record<string, Schema.ToolArgType>;
22
+ recipe?: RecipeReferences.TemplateRecipeConfig;
23
+ template?: CommandTemplateValue;
24
+ storedArgs?: string[];
25
+ storedDefaults?: Record<string, string>;
26
+ }
27
+
28
+ export interface LoadConfigResult {
29
+ tools: Map<string, RegisteredTool>;
30
+ warnings: string[];
31
+ changed: boolean;
32
+ }
33
+
34
+ export function serializeTools(
35
+ source: Map<string, RegisteredTool>,
36
+ ): Record<string, unknown> {
37
+ const entries = [...source.entries()].sort(([a], [b]) => a.localeCompare(b));
38
+ const result: Record<string, unknown> = {};
39
+ for (const [name, cfg] of entries) {
40
+ const entry: Record<string, unknown> = {
41
+ description: cfg.description,
42
+ };
43
+ if (cfg.storedArgs && cfg.storedArgs.length > 0)
44
+ entry.args = cfg.storedArgs;
45
+ if (cfg.storedDefaults && Object.keys(cfg.storedDefaults).length > 0)
46
+ entry.defaults = cfg.storedDefaults;
47
+ if (cfg.recipe?.name) entry.name = cfg.recipe.name;
48
+ if (cfg.recipe?.async !== undefined) entry.async = cfg.recipe.async;
49
+ if (cfg.recipe?.state_dir) entry.state_dir = cfg.recipe.state_dir;
50
+ if (cfg.recipe?.values) entry.values = cfg.recipe.values;
51
+ if (cfg.template) entry.template = cfg.template;
52
+ result[name] = entry;
53
+ }
54
+ return result;
55
+ }
56
+
57
+ export function saveTools(
58
+ path: string,
59
+ source: Map<string, RegisteredTool>,
60
+ ): string | undefined {
61
+ try {
62
+ writeJsonAtomic(path, serializeTools(source));
63
+ return undefined;
64
+ } catch (error) {
65
+ return getErrorMessage(error);
66
+ }
67
+ }
68
+
69
+ export function getStoredEntries(
70
+ raw: unknown,
71
+ ): Array<[string | undefined, unknown]> {
72
+ if (Array.isArray(raw)) return raw.map((value) => [undefined, value]);
73
+ if (raw && typeof raw === "object") {
74
+ return Object.entries(raw as Record<string, unknown>);
75
+ }
76
+ return [];
77
+ }
78
+
79
+ function getStoredTemplate(value: unknown): CommandTemplateValue | undefined {
80
+ if (typeof value === "string") return value.trim() || undefined;
81
+ if (!Array.isArray(value)) return undefined;
82
+ const template = value as CommandTemplates.CommandTemplateConfig[];
83
+ return CommandTemplates.expandCommandTemplateConfigs({ template }).length > 0
84
+ ? template
85
+ : undefined;
86
+ }
87
+
88
+ function formatTemplateForDescription(template: CommandTemplateValue): string {
89
+ return typeof template === "string" ? template : JSON.stringify(template);
90
+ }
91
+
92
+ export function normalizeStoredTool(
93
+ key: string | undefined,
94
+ value: unknown,
95
+ reservedToolNames: Set<string>,
96
+ ): { cfg?: RegisteredTool; changed: boolean; warning?: string } {
97
+ if (!value || typeof value !== "object") {
98
+ return {
99
+ changed: true,
100
+ warning: `Invalid tool entry: ${key ?? "<array item>"}`,
101
+ };
102
+ }
103
+ const record = value as Record<string, unknown>;
104
+ const rawName = key ?? (typeof record.name === "string" ? record.name : "");
105
+ const name = normalizeToolName(rawName);
106
+ if (!name) {
107
+ return {
108
+ changed: true,
109
+ warning: `Invalid tool name: ${rawName || key || "<empty>"}`,
110
+ };
111
+ }
112
+ if (reservedToolNames.has(name)) {
113
+ return { changed: true, warning: `Reserved tool name skipped: ${name}` };
114
+ }
115
+ const template = getStoredTemplate(record.template);
116
+ if (!template && typeof record.script === "string") {
117
+ return {
118
+ changed: false,
119
+ warning: `Tool "${name}" uses unsupported script config. Use template because pi-actors cannot load script entries.`,
120
+ };
121
+ }
122
+ if (Object.hasOwn(record, "tool")) {
123
+ return {
124
+ changed: false,
125
+ warning: `Tool "${name}" cannot define tool; use template directly.`,
126
+ };
127
+ }
128
+ if (record.job !== undefined || record.recipe !== undefined) {
129
+ return {
130
+ changed: false,
131
+ warning: `Tool "${name}" uses unsupported job/recipe config. Use template with optional name and async fields.`,
132
+ };
133
+ }
134
+ const keyedRecipeName =
135
+ key !== undefined && typeof record.name === "string" && record.name.trim()
136
+ ? record.name.trim()
137
+ : undefined;
138
+ if ((keyedRecipeName || typeof record.async === "boolean") && !template) {
139
+ return {
140
+ changed: false,
141
+ warning: `Tool "${name}" uses recipe config without template. Add template to make it a co-located template recipe.`,
142
+ };
143
+ }
144
+ if (!template) {
145
+ return { changed: true, warning: `Tool "${name}" has no template` };
146
+ }
147
+ const recipeName =
148
+ keyedRecipeName ?? (typeof record.async === "boolean" ? name : undefined);
149
+ const recipe: RecipeReferences.TemplateRecipeConfig | undefined = recipeName
150
+ ? {
151
+ name: recipeName,
152
+ ...(typeof record.async === "boolean" ? { async: record.async } : {}),
153
+ ...(typeof record.state_dir === "string" && record.state_dir.trim()
154
+ ? { state_dir: record.state_dir.trim() }
155
+ : {}),
156
+ template,
157
+ ...(record.values &&
158
+ typeof record.values === "object" &&
159
+ !Array.isArray(record.values)
160
+ ? { values: record.values as Record<string, unknown> }
161
+ : {}),
162
+ }
163
+ : undefined;
164
+ const isRecipe = RecipeReferences.isRecipeTool(template, recipe);
165
+ const recipeTemplate = RecipeReferences.getRecipeTemplate(template);
166
+ const argTemplate = recipeTemplate ?? template;
167
+ const description =
168
+ typeof record.description === "string" && record.description.trim()
169
+ ? record.description.trim()
170
+ : isRecipe
171
+ ? `${recipe?.async === true || RecipeReferences.isAsyncRecipeReference(template) ? "Start async" : "Execute"} template recipe: ${recipe?.name ?? formatTemplateForDescription(template)}`
172
+ : `Execute command template: ${formatTemplateForDescription(template)}`;
173
+ const declarations = Schema.normalizeStoredToolArgDeclarations(
174
+ record.args,
175
+ record.defaults,
176
+ );
177
+ const storedArgs = declarations.provided
178
+ ? declarations.declarations
179
+ : undefined;
180
+ const storedDefaults =
181
+ declarations.provided && Object.keys(declarations.defaults).length > 0
182
+ ? declarations.defaults
183
+ : undefined;
184
+ const argTemplateConfig: CommandTemplates.CommandTemplateConfig =
185
+ typeof argTemplate === "object" && !Array.isArray(argTemplate)
186
+ ? {
187
+ ...argTemplate,
188
+ ...(storedArgs !== undefined ? { args: storedArgs } : {}),
189
+ defaults: {
190
+ ...(argTemplate.defaults ?? {}),
191
+ ...declarations.defaults,
192
+ },
193
+ }
194
+ : {
195
+ args: storedArgs,
196
+ defaults: declarations.defaults,
197
+ template: argTemplate,
198
+ };
199
+ const inferredArgTypes = Schema.getTemplateArgTypes(argTemplateConfig);
200
+ const argTypes = { ...inferredArgTypes, ...declarations.argTypes };
201
+ const cfg = {
202
+ name,
203
+ description,
204
+ args:
205
+ isRecipe && storedArgs !== undefined
206
+ ? Schema.getExplicitToolArgNames(storedArgs)
207
+ : RecipeReferences.isRecipeReference(template) && !recipeTemplate
208
+ ? Schema.getExplicitToolArgNames(storedArgs)
209
+ : Schema.getToolArgNames(argTemplateConfig),
210
+ defaults: declarations.defaults,
211
+ ...(Object.keys(argTypes).length > 0 ? { argTypes } : {}),
212
+ ...(recipe ? { recipe } : {}),
213
+ template,
214
+ ...(storedArgs !== undefined ? { storedArgs } : {}),
215
+ ...(storedDefaults !== undefined ? { storedDefaults } : {}),
216
+ };
217
+ const changed =
218
+ (key === undefined && record.name !== undefined) ||
219
+ record.label !== undefined ||
220
+ JSON.stringify(record.template) !== JSON.stringify(template) ||
221
+ description !== record.description ||
222
+ declarations.changed;
223
+ return { cfg, changed };
224
+ }
225
+
226
+ export function loadToolConfig(
227
+ path: string,
228
+ reservedToolNames: Set<string>,
229
+ ): LoadConfigResult {
230
+ const warnings: string[] = [];
231
+ const tools = new Map<string, RegisteredTool>();
232
+ let changed = false;
233
+ if (!existsSync(path)) return { tools, warnings, changed };
234
+ try {
235
+ const raw = JSON.parse(readFileSync(path, "utf8"));
236
+ const entries = getStoredEntries(raw);
237
+ for (const [key, value] of entries) {
238
+ const result = normalizeStoredTool(key, value, reservedToolNames);
239
+ changed = changed || result.changed;
240
+ if (result.warning) warnings.push(result.warning);
241
+ if (!result.cfg) continue;
242
+ if (tools.has(result.cfg.name)) {
243
+ warnings.push(
244
+ `Duplicate tool kept from last entry: ${result.cfg.name}`,
245
+ );
246
+ }
247
+ if (tools.has(result.cfg.name)) changed = true;
248
+ tools.set(result.cfg.name, result.cfg);
249
+ }
250
+ if (entries.length === 0 && raw && typeof raw !== "object") {
251
+ warnings.push(`Invalid ${path} format`);
252
+ }
253
+ if (entries.length === 0 && raw && typeof raw !== "object") changed = true;
254
+ return { tools, warnings, changed };
255
+ } catch (error) {
256
+ return {
257
+ tools,
258
+ warnings: [`Failed to load ${path}: ${getErrorMessage(error)}`],
259
+ changed: false,
260
+ };
261
+ }
262
+ }
263
+
264
+ function getErrorMessage(error: unknown): string {
265
+ return error instanceof Error ? error.message : String(error);
266
+ }