@llblab/pi-actors 0.34.1 → 0.35.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/BACKLOG.md +2 -20
- package/CHANGELOG.md +4 -0
- package/README.md +10 -1
- package/dist/lib/prompts.d.ts +1 -0
- package/dist/lib/prompts.js +1 -0
- package/dist/lib/recipes-discovery.js +25 -0
- package/dist/lib/registry.d.ts +3 -0
- package/dist/lib/registry.js +60 -1
- package/dist/lib/tools-register.js +1 -0
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/tool-registry.md +2 -2
- package/lib/prompts.ts +2 -0
- package/lib/recipes-discovery.ts +29 -0
- package/lib/registry.ts +94 -1
- package/lib/tools-register.ts +1 -0
- package/package.json +1 -1
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
package/BACKLOG.md
CHANGED
|
@@ -53,24 +53,6 @@ No open hotfix items.
|
|
|
53
53
|
|
|
54
54
|
The backlog is intentionally pruned to the 20% of work most likely to deliver 80% of value for `pi-actors` as a local actor kernel. Bias toward consolidation, smaller public surface area, and reliability over new feature breadth.
|
|
55
55
|
|
|
56
|
-
### M-18 Draft Recipe Promotion UX
|
|
57
|
-
|
|
58
|
-
- Priority: High.
|
|
59
|
-
- Status: Planned.
|
|
60
|
-
- Goal: Make successful ad hoc actor patterns easy to promote manually from draft memory into active user recipe memory.
|
|
61
|
-
- Why now: Draft recipes under `~/.pi/agent/recipes/drafts` are replayable but intentionally not active tools, and the two-stage memory model needs an explicit operator-gated promotion path.
|
|
62
|
-
- Direction:
|
|
63
|
-
- List draft recipes with source run, timestamp, fingerprint, description/template preview, and validation status.
|
|
64
|
-
- Promote a selected draft to `~/.pi/agent/recipes/<name>.json` only through an explicit action or explicit tool argument.
|
|
65
|
-
- Run recipe validation/doctor before writing and expose collision/shadowing diagnostics.
|
|
66
|
-
- Preserve draft files unless deletion is explicitly requested.
|
|
67
|
-
- Prefer extending existing registry/tool surfaces over adding a new public noun.
|
|
68
|
-
- Acceptance:
|
|
69
|
-
- Draft recipes remain non-tools until promotion.
|
|
70
|
-
- Promotion writes atomically and never auto-promotes.
|
|
71
|
-
- Tests cover valid promotion, invalid draft, name collision, and packaged-recipe shadowing.
|
|
72
|
-
- Docs explain draft memory vs active tool memory in one compact section.
|
|
73
|
-
|
|
74
56
|
### M-19 Recipe Doctor Risk Labels v2
|
|
75
57
|
|
|
76
58
|
- Priority: Medium.
|
|
@@ -154,7 +136,7 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
154
136
|
## Suggested Milestone Order
|
|
155
137
|
|
|
156
138
|
```text
|
|
157
|
-
Next milestone: M-
|
|
158
|
-
Then: M-
|
|
139
|
+
Next milestone: M-19 Recipe Doctor Risk Labels v2.
|
|
140
|
+
Then: M-20 Runtime Recipe Triage.
|
|
159
141
|
Small cleanup lane: continue opportunistic domain polish only when a real ownership boundary appears.
|
|
160
142
|
```
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.35.0: Draft Recipe Promotion UX
|
|
6
|
+
|
|
7
|
+
- `[Recipes]` Completed M-18 Draft Recipe Promotion UX: `inspect target=recipes view=summary verbose=true` now exposes draft timestamps, fingerprints, validation state, source run when known, and template previews, while `register_tool name=<tool> draft=<path>` promotes a validated draft into active recipe memory without deleting the draft and rejects collisions unless `update=true` is explicit.
|
|
8
|
+
|
|
5
9
|
## 0.34.1: Message Delivery Outcome Hotfix
|
|
6
10
|
|
|
7
11
|
- `[Dogfood]` Added a deterministic actor-worker stale-claim smoke covering an intentionally claimed branch inbox record; the worker now reports stale claim counts in both `worker-status.json` and its awaiting-assignment room event without adding auto-recovery or scheduler policy.
|
package/README.md
CHANGED
|
@@ -207,7 +207,8 @@ Rules:
|
|
|
207
207
|
- User recipes override same-name lower-priority recipes;
|
|
208
208
|
- Same-id JSON recipes shadow Markdown recipes in the same priority layer;
|
|
209
209
|
- Packaged recipes are standard-library components, not automatically installed operator policy;
|
|
210
|
-
-
|
|
210
|
+
- Draft recipes in `~/.pi/agent/recipes/drafts/` are replayable memory, not active tools;
|
|
211
|
+
- `register_tool` creates, updates, lists, deletes, or explicitly promotes draft recipe files through the normal agent interface.
|
|
211
212
|
|
|
212
213
|
Example foreground tool:
|
|
213
214
|
|
|
@@ -226,6 +227,14 @@ register_tool name=docs_review \
|
|
|
226
227
|
args="scope:path,model:string"
|
|
227
228
|
```
|
|
228
229
|
|
|
230
|
+
Promote a successful captured draft only after an explicit operator decision:
|
|
231
|
+
|
|
232
|
+
```text
|
|
233
|
+
register_tool name=docs_review draft=~/.pi/agent/recipes/drafts/spawned-run.json
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Promotion validates the draft, writes `~/.pi/agent/recipes/<name>.json`, preserves the draft, and rejects collisions unless `update=true` is supplied.
|
|
237
|
+
|
|
229
238
|
Inspect the discovered registry:
|
|
230
239
|
|
|
231
240
|
```text
|
package/dist/lib/prompts.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local
|
|
|
10
10
|
export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
|
|
11
11
|
readonly name: "Tool name in snake_case (e.g., 'transcribe')";
|
|
12
12
|
readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
|
|
13
|
+
readonly draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.";
|
|
13
14
|
readonly async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.";
|
|
14
15
|
readonly state_dir: "Optional async run state directory for a co-located template recipe.";
|
|
15
16
|
readonly template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.";
|
package/dist/lib/prompts.js
CHANGED
|
@@ -30,6 +30,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
30
30
|
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
31
31
|
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
32
32
|
description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
33
|
+
draft: "Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
|
|
33
34
|
async: "Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
34
35
|
state_dir: "Optional async run state directory for a co-located template recipe.",
|
|
35
36
|
template: "Command template with {arg} or {arg=default} placeholders, or a template recipe JSON path/name. With async, this is the co-located recipe body. Bare recipe names resolve under ~/.pi/agent/recipes. Omitted updates keep the old template. Empty string deletes the tool.",
|
|
@@ -425,14 +425,39 @@ export function getShadowedLaunchDiagnostic(result, id) {
|
|
|
425
425
|
reason: active.invalid ? "shadowed_invalid" : "shadowed_disabled",
|
|
426
426
|
};
|
|
427
427
|
}
|
|
428
|
+
function templatePreview(value) {
|
|
429
|
+
const rendered = typeof value === "string"
|
|
430
|
+
? value
|
|
431
|
+
: value === undefined
|
|
432
|
+
? undefined
|
|
433
|
+
: JSON.stringify(value);
|
|
434
|
+
return rendered && rendered.length > 120
|
|
435
|
+
? `${rendered.slice(0, 117)}...`
|
|
436
|
+
: rendered;
|
|
437
|
+
}
|
|
428
438
|
export function listDraftRecipes(root) {
|
|
429
439
|
return listRecipeFiles(root).map((path) => {
|
|
430
440
|
const id = RecipesReferences.getRecipeIdFromPath(path);
|
|
441
|
+
const bytes = readFileSync(path);
|
|
442
|
+
const stat = statSync(path);
|
|
431
443
|
const config = RecipesReferences.readRawRecipeConfig(path);
|
|
444
|
+
const resolved = RecipesReferences.readResolvedRecipeConfig(path);
|
|
445
|
+
const diagnostics = getRecipeConfigDiagnostics(path, resolved);
|
|
446
|
+
const sourceRun = String(config?.description ?? "").match(/spawn run ([^\s]+)/)?.[1];
|
|
447
|
+
const preview = templatePreview(config?.template);
|
|
432
448
|
return {
|
|
433
449
|
id,
|
|
434
450
|
path,
|
|
451
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
452
|
+
size: bytes.byteLength,
|
|
453
|
+
created_at: stat.birthtime.toISOString(),
|
|
454
|
+
modified_at: stat.mtime.toISOString(),
|
|
455
|
+
valid: Boolean(resolved),
|
|
456
|
+
diagnostics,
|
|
435
457
|
...(config?.description ? { description: config.description } : {}),
|
|
458
|
+
...(sourceRun ? { source_run: sourceRun } : {}),
|
|
459
|
+
...(config?.async !== undefined ? { async: config.async } : {}),
|
|
460
|
+
...(preview ? { template_preview: preview } : {}),
|
|
436
461
|
};
|
|
437
462
|
});
|
|
438
463
|
}
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface RegisterToolInput {
|
|
|
11
11
|
async?: boolean;
|
|
12
12
|
state_dir?: string;
|
|
13
13
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
14
|
+
draft?: string;
|
|
14
15
|
args?: string;
|
|
15
16
|
update?: boolean;
|
|
16
17
|
values?: Record<string, unknown>;
|
|
@@ -20,6 +21,8 @@ export interface RegisterToolResultDetails {
|
|
|
20
21
|
async?: boolean;
|
|
21
22
|
config?: string;
|
|
22
23
|
defaults?: Record<string, string>;
|
|
24
|
+
draft?: string;
|
|
25
|
+
promoted?: boolean;
|
|
23
26
|
recipeName?: string;
|
|
24
27
|
state_dir?: string;
|
|
25
28
|
template?: CommandTemplates.CommandTemplateValue;
|
package/dist/lib/registry.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Owns register/update/delete validation, persistence, runtime side effects, and result payloads
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
7
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
8
8
|
import * as CommandTemplates from "./command-templates.js";
|
|
9
9
|
import * as ExecutionOutput from "./execution-output.js";
|
|
10
10
|
import { writeJsonAtomic } from "./file-state.js";
|
|
@@ -32,6 +32,62 @@ function getRecipeRoot(deps) {
|
|
|
32
32
|
function getToolRecipePath(deps, name) {
|
|
33
33
|
return join(getRecipeRoot(deps), `${name}.json`);
|
|
34
34
|
}
|
|
35
|
+
function assertDraftPath(deps, draft) {
|
|
36
|
+
const draftRoot = resolve(getRecipeRoot(deps), "drafts");
|
|
37
|
+
const path = resolve(draft);
|
|
38
|
+
const relation = relative(draftRoot, path);
|
|
39
|
+
if (relation === "" ||
|
|
40
|
+
relation.startsWith("..") ||
|
|
41
|
+
resolve(relation) === relation) {
|
|
42
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft must be under ${draftRoot}. Use inspect target=recipes view=summary to list drafts.`));
|
|
43
|
+
}
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft not found: ${path}`));
|
|
46
|
+
}
|
|
47
|
+
return path;
|
|
48
|
+
}
|
|
49
|
+
function promoteDraftRecipe(name, input, ctx, deps) {
|
|
50
|
+
const draftPath = assertDraftPath(deps, String(input.draft ?? ""));
|
|
51
|
+
const targetPath = getToolRecipePath(deps, name);
|
|
52
|
+
const tools = deps.getTools();
|
|
53
|
+
const existing = tools.get(name);
|
|
54
|
+
const conflict = deps.getExternalToolConflict(name);
|
|
55
|
+
if (conflict)
|
|
56
|
+
throw new Error(ExecutionOutput.formatToolText(conflict));
|
|
57
|
+
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
58
|
+
throw new Error(ExecutionOutput.formatToolText(`Tool "${name}" already registered. Use update=true to overwrite.`));
|
|
59
|
+
}
|
|
60
|
+
const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
|
|
61
|
+
if (!config) {
|
|
62
|
+
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
|
|
63
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft recipe is invalid${reason ? `: ${reason}` : "."}`));
|
|
64
|
+
}
|
|
65
|
+
const promoted = buildConfig(name, {
|
|
66
|
+
...input,
|
|
67
|
+
description: input.description ?? config.description,
|
|
68
|
+
template: draftPath,
|
|
69
|
+
}, existing);
|
|
70
|
+
const raw = RecipesReferences.readRawRecipeConfig(draftPath);
|
|
71
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
72
|
+
writeJsonAtomic(targetPath, raw);
|
|
73
|
+
promoted.template = targetPath;
|
|
74
|
+
promoted.sourcePath = targetPath;
|
|
75
|
+
tools.set(name, promoted);
|
|
76
|
+
deps.registerRuntimeTool(promoted);
|
|
77
|
+
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
textContent(ExecutionOutput.formatToolText(`${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`)),
|
|
81
|
+
],
|
|
82
|
+
details: {
|
|
83
|
+
args: promoted.args,
|
|
84
|
+
config: targetPath,
|
|
85
|
+
draft: draftPath,
|
|
86
|
+
promoted: true,
|
|
87
|
+
tool: name,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
35
91
|
function persistToolRecipe(deps, cfg) {
|
|
36
92
|
const path = getToolRecipePath(deps, cfg.name);
|
|
37
93
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -173,6 +229,9 @@ export async function executeRegisterTool(params, ctx, deps) {
|
|
|
173
229
|
if (deps.reservedToolNames.has(name)) {
|
|
174
230
|
throw new Error(ExecutionOutput.formatToolText(`Reserved tool name: ${name}`));
|
|
175
231
|
}
|
|
232
|
+
if (typeof input.draft === "string" && input.draft.trim()) {
|
|
233
|
+
return promoteDraftRecipe(name, input, ctx, deps);
|
|
234
|
+
}
|
|
176
235
|
const templateProvided = Object.hasOwn(input, "template");
|
|
177
236
|
const template = getInputTemplate(input.template);
|
|
178
237
|
if (templateProvided && (template === null || template === ""))
|
|
@@ -24,6 +24,7 @@ export function createRegisterToolDefinition(deps) {
|
|
|
24
24
|
args: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.args),
|
|
25
25
|
async: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.async),
|
|
26
26
|
description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
|
|
27
|
+
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
27
28
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
28
29
|
state_dir: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir),
|
|
29
30
|
template: unionSchema([
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.35.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
package/docs/tool-registry.md
CHANGED
|
@@ -10,7 +10,7 @@ The registry source is location-discovered recipes, not a live tool-only JSON fi
|
|
|
10
10
|
|
|
11
11
|
- `~/.pi/agent/recipes/*.json` and `*.md` are the highest-priority user recipe root and the operator-managed tool set.
|
|
12
12
|
- Recipes in that root are tools by location.
|
|
13
|
-
- `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one
|
|
13
|
+
- `~/.pi/agent/recipes/drafts/*.json` stores captured inline-spawn draft recipes, not registered tools. Promote one with `register_tool name=<tool_name> draft=<draft_path>` or by manually moving/copying it up one level into `~/.pi/agent/recipes`. `inspect target=recipes view=summary` reports their count, and verbose output lists their paths, timestamps, fingerprints, validation state, source run when known, descriptions, and template previews for explicit replay or promotion.
|
|
14
14
|
- Packaged pi-actors recipes are the lower-priority standard library of declarative actor config components, not automatically registered tools.
|
|
15
15
|
- Ad hoc recipe files outside the user recipe root are components unless explicitly registered/copied into `~/.pi/agent/recipes`.
|
|
16
16
|
- Recipe identity is the filename basename; `~/.pi/agent/recipes/docs_review.json` and `docs_review.md` both have id/tool name `docs_review`.
|
|
@@ -51,7 +51,7 @@ register_tool name=call_subagent \
|
|
|
51
51
|
template="pi -p --model {model} --no-tools {prompt}" args="prompt:string,model:string"
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding.
|
|
54
|
+
Use `update=true` to overwrite an existing tool. Omit `template` and co-located recipe fields during update to keep the previous execution binding. To promote a captured draft, pass `name` plus `draft` with a path under `~/.pi/agent/recipes/drafts`; promotion validates the draft before writing `~/.pi/agent/recipes/<name>.json`, preserves the draft file, rejects name collisions unless `update=true`, and leaves shadowing evidence visible through `inspect target=recipes view=summary` or `view=doctor`.
|
|
55
55
|
|
|
56
56
|
`template` may also be a standard command-template sequence for multi-step tools. Timeout is disabled by default; add explicit positive `timeout` values when individual steps should fail closed:
|
|
57
57
|
|
package/lib/prompts.ts
CHANGED
|
@@ -38,6 +38,8 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
|
38
38
|
name: "Tool name in snake_case (e.g., 'transcribe')",
|
|
39
39
|
description:
|
|
40
40
|
"Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.",
|
|
41
|
+
draft:
|
|
42
|
+
"Promote a draft recipe path from ~/.pi/agent/recipes/drafts into an active named recipe under ~/.pi/agent/recipes. Requires name; use update=true to overwrite.",
|
|
41
43
|
async:
|
|
42
44
|
"Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
43
45
|
state_dir:
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -574,14 +574,43 @@ export function getShadowedLaunchDiagnostic(
|
|
|
574
574
|
};
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
+
function templatePreview(value: unknown): string | undefined {
|
|
578
|
+
const rendered =
|
|
579
|
+
typeof value === "string"
|
|
580
|
+
? value
|
|
581
|
+
: value === undefined
|
|
582
|
+
? undefined
|
|
583
|
+
: JSON.stringify(value);
|
|
584
|
+
return rendered && rendered.length > 120
|
|
585
|
+
? `${rendered.slice(0, 117)}...`
|
|
586
|
+
: rendered;
|
|
587
|
+
}
|
|
588
|
+
|
|
577
589
|
export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
|
|
578
590
|
return listRecipeFiles(root).map((path) => {
|
|
579
591
|
const id = RecipesReferences.getRecipeIdFromPath(path);
|
|
592
|
+
const bytes = readFileSync(path);
|
|
593
|
+
const stat = statSync(path);
|
|
580
594
|
const config = RecipesReferences.readRawRecipeConfig(path);
|
|
595
|
+
const resolved = RecipesReferences.readResolvedRecipeConfig(path);
|
|
596
|
+
const diagnostics = getRecipeConfigDiagnostics(path, resolved);
|
|
597
|
+
const sourceRun = String(config?.description ?? "").match(
|
|
598
|
+
/spawn run ([^\s]+)/,
|
|
599
|
+
)?.[1];
|
|
600
|
+
const preview = templatePreview(config?.template);
|
|
581
601
|
return {
|
|
582
602
|
id,
|
|
583
603
|
path,
|
|
604
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
605
|
+
size: bytes.byteLength,
|
|
606
|
+
created_at: stat.birthtime.toISOString(),
|
|
607
|
+
modified_at: stat.mtime.toISOString(),
|
|
608
|
+
valid: Boolean(resolved),
|
|
609
|
+
diagnostics,
|
|
584
610
|
...(config?.description ? { description: config.description } : {}),
|
|
611
|
+
...(sourceRun ? { source_run: sourceRun } : {}),
|
|
612
|
+
...(config?.async !== undefined ? { async: config.async } : {}),
|
|
613
|
+
...(preview ? { template_preview: preview } : {}),
|
|
585
614
|
};
|
|
586
615
|
});
|
|
587
616
|
}
|
package/lib/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
8
|
-
import { dirname, join } from "node:path";
|
|
8
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as CommandTemplates from "./command-templates.ts";
|
|
11
11
|
import * as Config from "./config.ts";
|
|
@@ -22,6 +22,7 @@ export interface RegisterToolInput {
|
|
|
22
22
|
async?: boolean;
|
|
23
23
|
state_dir?: string;
|
|
24
24
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
25
|
+
draft?: string;
|
|
25
26
|
args?: string;
|
|
26
27
|
update?: boolean;
|
|
27
28
|
values?: Record<string, unknown>;
|
|
@@ -32,6 +33,8 @@ export interface RegisterToolResultDetails {
|
|
|
32
33
|
async?: boolean;
|
|
33
34
|
config?: string;
|
|
34
35
|
defaults?: Record<string, string>;
|
|
36
|
+
draft?: string;
|
|
37
|
+
promoted?: boolean;
|
|
35
38
|
recipeName?: string;
|
|
36
39
|
state_dir?: string;
|
|
37
40
|
template?: CommandTemplates.CommandTemplateValue;
|
|
@@ -95,6 +98,93 @@ function getToolRecipePath<TContext>(
|
|
|
95
98
|
return join(getRecipeRoot(deps), `${name}.json`);
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
function assertDraftPath<TContext>(
|
|
102
|
+
deps: RegisterToolRuntimeDeps<TContext>,
|
|
103
|
+
draft: string,
|
|
104
|
+
): string {
|
|
105
|
+
const draftRoot = resolve(getRecipeRoot(deps), "drafts");
|
|
106
|
+
const path = resolve(draft);
|
|
107
|
+
const relation = relative(draftRoot, path);
|
|
108
|
+
if (
|
|
109
|
+
relation === "" ||
|
|
110
|
+
relation.startsWith("..") ||
|
|
111
|
+
resolve(relation) === relation
|
|
112
|
+
) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
ExecutionOutput.formatToolText(
|
|
115
|
+
`Draft must be under ${draftRoot}. Use inspect target=recipes view=summary to list drafts.`,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!existsSync(path)) {
|
|
120
|
+
throw new Error(ExecutionOutput.formatToolText(`Draft not found: ${path}`));
|
|
121
|
+
}
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function promoteDraftRecipe<TContext>(
|
|
126
|
+
name: string,
|
|
127
|
+
input: RegisterToolInput,
|
|
128
|
+
ctx: TContext,
|
|
129
|
+
deps: RegisterToolRuntimeDeps<TContext>,
|
|
130
|
+
): RegisterToolResult {
|
|
131
|
+
const draftPath = assertDraftPath(deps, String(input.draft ?? ""));
|
|
132
|
+
const targetPath = getToolRecipePath(deps, name);
|
|
133
|
+
const tools = deps.getTools();
|
|
134
|
+
const existing = tools.get(name);
|
|
135
|
+
const conflict = deps.getExternalToolConflict(name);
|
|
136
|
+
if (conflict) throw new Error(ExecutionOutput.formatToolText(conflict));
|
|
137
|
+
if ((existing || existsSync(targetPath)) && !input.update) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
ExecutionOutput.formatToolText(
|
|
140
|
+
`Tool "${name}" already registered. Use update=true to overwrite.`,
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const config = RecipesReferences.readResolvedRecipeConfig(draftPath);
|
|
145
|
+
if (!config) {
|
|
146
|
+
const reason = RecipesReferences.diagnoseRawRecipeConfigFailure(draftPath);
|
|
147
|
+
throw new Error(
|
|
148
|
+
ExecutionOutput.formatToolText(
|
|
149
|
+
`Draft recipe is invalid${reason ? `: ${reason}` : "."}`,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const promoted = buildConfig(
|
|
154
|
+
name,
|
|
155
|
+
{
|
|
156
|
+
...input,
|
|
157
|
+
description: input.description ?? config.description,
|
|
158
|
+
template: draftPath,
|
|
159
|
+
},
|
|
160
|
+
existing,
|
|
161
|
+
);
|
|
162
|
+
const raw = RecipesReferences.readRawRecipeConfig(draftPath)!;
|
|
163
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
164
|
+
writeJsonAtomic(targetPath, raw);
|
|
165
|
+
promoted.template = targetPath;
|
|
166
|
+
promoted.sourcePath = targetPath;
|
|
167
|
+
tools.set(name, promoted);
|
|
168
|
+
deps.registerRuntimeTool(promoted);
|
|
169
|
+
deps.notify(ctx, `Promoted draft recipe: ${name}`, "info");
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
textContent(
|
|
173
|
+
ExecutionOutput.formatToolText(
|
|
174
|
+
`${existing ? "Updated" : "Registered"} tool "${name}" from draft recipe.`,
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
],
|
|
178
|
+
details: {
|
|
179
|
+
args: promoted.args,
|
|
180
|
+
config: targetPath,
|
|
181
|
+
draft: draftPath,
|
|
182
|
+
promoted: true,
|
|
183
|
+
tool: name,
|
|
184
|
+
} as RegisterToolResultDetails & Record<string, unknown>,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
98
188
|
function persistToolRecipe<TContext>(
|
|
99
189
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
100
190
|
cfg: Config.RegisteredTool,
|
|
@@ -284,6 +374,9 @@ export async function executeRegisterTool<TContext>(
|
|
|
284
374
|
ExecutionOutput.formatToolText(`Reserved tool name: ${name}`),
|
|
285
375
|
);
|
|
286
376
|
}
|
|
377
|
+
if (typeof input.draft === "string" && input.draft.trim()) {
|
|
378
|
+
return promoteDraftRecipe(name, input, ctx, deps);
|
|
379
|
+
}
|
|
287
380
|
const templateProvided = Object.hasOwn(input, "template");
|
|
288
381
|
const template = getInputTemplate(input.template);
|
|
289
382
|
if (templateProvided && (template === null || template === ""))
|
package/lib/tools-register.ts
CHANGED
|
@@ -36,6 +36,7 @@ export function createRegisterToolDefinition<TContext>(
|
|
|
36
36
|
description: stringSchema(
|
|
37
37
|
Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description,
|
|
38
38
|
),
|
|
39
|
+
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
39
40
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
40
41
|
state_dir: stringSchema(
|
|
41
42
|
Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.state_dir,
|
package/package.json
CHANGED
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.
|
|
5
|
+
version: 0.35.0
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
package/skills/swarm/SKILL.md
CHANGED