@llblab/pi-actors 0.57.1 → 0.58.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/AGENTS.md +8 -0
- package/BACKLOG.md +13 -0
- package/CHANGELOG.md +7 -0
- package/README.md +7 -1
- package/dist/lib/file-state.d.ts +1 -0
- package/dist/lib/file-state.js +8 -2
- package/dist/lib/prompts.d.ts +2 -1
- package/dist/lib/prompts.js +6 -2
- package/dist/lib/recipes-discovery.d.ts +4 -0
- package/dist/lib/recipes-discovery.js +22 -0
- package/dist/lib/recipes-references.d.ts +1 -0
- package/dist/lib/recipes-references.js +1 -1
- package/dist/lib/recipes-usage.js +12 -9
- package/dist/lib/registry.d.ts +5 -1
- package/dist/lib/registry.js +199 -7
- package/dist/lib/tools-inspect.js +6 -3
- package/dist/lib/tools-register.js +1 -0
- package/dist/lib/tools-response.js +6 -1
- package/dist/skills/actors/SKILL.md +10 -1
- package/dist/skills/actors/references/persistent-tools.md +20 -2
- package/dist/skills/recipe-memory/SKILL.md +6 -0
- package/docs/README.md +1 -0
- package/docs/capability-memory.md +166 -0
- package/docs/inspection.md +3 -1
- package/docs/releasing.md +4 -0
- package/docs/tool-registry.md +20 -1
- package/lib/file-state.ts +7 -3
- package/lib/prompts.ts +7 -2
- package/lib/recipes-discovery.ts +26 -0
- package/lib/recipes-references.ts +1 -1
- package/lib/recipes-usage.ts +11 -8
- package/lib/registry.ts +195 -10
- package/lib/tools-inspect.ts +6 -3
- package/lib/tools-register.ts +1 -0
- package/lib/tools-response.ts +8 -1
- package/package.json +1 -1
- package/skills/actors/SKILL.md +10 -1
- package/skills/actors/references/persistent-tools.md +20 -2
- package/skills/recipe-memory/SKILL.md +6 -0
package/lib/registry.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* Owns register/update/delete validation, persistence, runtime side effects, and result payloads
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
8
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
8
|
+
import { basename, dirname, isAbsolute, 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";
|
|
@@ -28,6 +28,7 @@ export interface RegisterToolInput {
|
|
|
28
28
|
name?: string;
|
|
29
29
|
description?: string;
|
|
30
30
|
async?: boolean;
|
|
31
|
+
expose?: boolean;
|
|
31
32
|
template?: CommandTemplates.CommandTemplateValue | null;
|
|
32
33
|
from?: string;
|
|
33
34
|
defaults?: Record<string, unknown>;
|
|
@@ -38,13 +39,16 @@ export interface RegisterToolInput {
|
|
|
38
39
|
|
|
39
40
|
export interface RegisterToolResultDetails {
|
|
40
41
|
active_tool?: boolean;
|
|
41
|
-
activation?: "current_session" | "unverified";
|
|
42
|
+
activation?: "current_session" | "unverified" | "not_requested";
|
|
42
43
|
activation_boundary?: string;
|
|
43
44
|
args?: string[];
|
|
44
45
|
async?: boolean;
|
|
45
46
|
callable_now?: boolean;
|
|
46
47
|
defaults?: Record<string, string>;
|
|
47
48
|
host_registered?: boolean;
|
|
49
|
+
kind?: "retained_recipe";
|
|
50
|
+
recipe?: string;
|
|
51
|
+
result?: "retained";
|
|
48
52
|
next_actions?: string[];
|
|
49
53
|
optional_args?: string[];
|
|
50
54
|
persisted?: boolean;
|
|
@@ -811,12 +815,18 @@ function executeRegisterToolUnlocked<TContext>(
|
|
|
811
815
|
params: unknown,
|
|
812
816
|
ctx: TContext,
|
|
813
817
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
814
|
-
): RegisterToolResult {
|
|
818
|
+
): RegisterToolResult | (() => RegisterToolResult) {
|
|
815
819
|
const rawInput =
|
|
816
820
|
params && typeof params === "object"
|
|
817
821
|
? (params as RegisterToolInput)
|
|
818
822
|
: ({} as RegisterToolInput);
|
|
819
823
|
assertRegisterToolInputModes(rawInput);
|
|
824
|
+
if (Object.hasOwn(rawInput, "expose")) {
|
|
825
|
+
const exposure = rawInput.expose;
|
|
826
|
+
if (typeof exposure !== "boolean") throw new RetainedRecipeRefusal("expose", "expose must be Boolean.");
|
|
827
|
+
// Select under the existing name reservation; acquire retention locks only after it releases.
|
|
828
|
+
if (exposure === false) return () => retainRecipe(rawInput, ctx, deps);
|
|
829
|
+
}
|
|
820
830
|
let input = rawInput;
|
|
821
831
|
if (!input.name) return listTools(deps);
|
|
822
832
|
const name = Identity.normalizeToolName(input.name);
|
|
@@ -995,16 +1005,191 @@ function executeRegisterToolUnlocked<TContext>(
|
|
|
995
1005
|
};
|
|
996
1006
|
}
|
|
997
1007
|
|
|
1008
|
+
class RetainedRecipeRefusal extends Error {
|
|
1009
|
+
readonly reason: string;
|
|
1010
|
+
constructor(field: string, reason: string, cause?: unknown) {
|
|
1011
|
+
super(ExecutionOutput.formatToolText(registryMetadataText({ field, result: reason })), { cause });
|
|
1012
|
+
this.reason = reason;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function retainedDirectory(path: string) {
|
|
1017
|
+
const stat = lstatSync(path, { bigint: true });
|
|
1018
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
1019
|
+
throw new RetainedRecipeRefusal("storage", "Retained storage must be a regular non-symlink directory.");
|
|
1020
|
+
}
|
|
1021
|
+
return { path, realpath: realpathSync.native(path), dev: stat.dev, ino: stat.ino };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function recheckRetainedDirectory(frame: ReturnType<typeof retainedDirectory>): void {
|
|
1025
|
+
const current = retainedDirectory(frame.path);
|
|
1026
|
+
if (current.realpath !== frame.realpath || current.dev !== frame.dev || current.ino !== frame.ino) {
|
|
1027
|
+
throw new RetainedRecipeRefusal("storage", "Retained storage identity changed.");
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function retainedSlotAbsent(path: string, ownTargetPublished = false): void {
|
|
1032
|
+
for (const candidate of ownTargetPublished ? [path.replace(/\.json$/u, ".md")] : [path, path.replace(/\.json$/u, ".md")]) {
|
|
1033
|
+
try { lstatSync(candidate); }
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
let code: unknown;
|
|
1036
|
+
try { code = (error as NodeJS.ErrnoException | undefined)?.code; }
|
|
1037
|
+
catch (observationError) { throw new AggregateError([error, observationError], "Retained slot observation failed.", { cause: error }); }
|
|
1038
|
+
if (code === "ENOENT") continue;
|
|
1039
|
+
throw error;
|
|
1040
|
+
}
|
|
1041
|
+
throw new RetainedRecipeRefusal("name", "Retained Recipe already exists; this path is create-only.");
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function retainedFile(path: string) {
|
|
1046
|
+
const stat = lstatSync(path, { bigint: true });
|
|
1047
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
1048
|
+
throw new RetainedRecipeRefusal("storage", "Retained target must be a regular non-symlink file.");
|
|
1049
|
+
}
|
|
1050
|
+
if (stat.size > BigInt(RecipesReferences.MAX_RECIPE_FILE_BYTES)) throw new RetainedRecipeRefusal("storage", "Retained target exceeds the source size limit.");
|
|
1051
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size, mtime: stat.mtimeNs, ctime: stat.ctimeNs };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function withinRetainedSource(root: string, path: string): boolean {
|
|
1055
|
+
const relation = relative(resolve(root), resolve(path));
|
|
1056
|
+
return relation === "" || (!isAbsolute(relation) && !/^\.\.(?:[\\/]|$)/u.test(relation));
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function assertRetainedSourceLifetime(source: string, root: string, field: string): void {
|
|
1060
|
+
const drafts = join(root, "drafts");
|
|
1061
|
+
if (withinRetainedSource(drafts, source) ||
|
|
1062
|
+
(existsSync(drafts) && existsSync(source) && withinRetainedSource(realpathSync.native(drafts), realpathSync.native(source)))) {
|
|
1063
|
+
throw new RetainedRecipeRefusal(field, "Retained creation cannot reference a managed disposable draft.");
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function assertRetainedTemplateLifetimes(value: unknown, path: string, root: string, context: RecipesContext.RecipeResolutionContext): void {
|
|
1068
|
+
if (typeof value === "string") {
|
|
1069
|
+
const source = RecipesReferences.resolveRecipeReferencePath(value, dirname(path), context.activeSkills);
|
|
1070
|
+
if (source) assertRetainedSourceLifetime(source, root, "template");
|
|
1071
|
+
} else if (Array.isArray(value)) {
|
|
1072
|
+
for (const child of value) assertRetainedTemplateLifetimes(child, path, root, context);
|
|
1073
|
+
} else if (value && typeof value === "object") {
|
|
1074
|
+
const node = value as Record<string, unknown>;
|
|
1075
|
+
assertRetainedTemplateLifetimes(node.template, path, root, context);
|
|
1076
|
+
assertRetainedTemplateLifetimes(node.recover, path, root, context);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function retainRecipe<TContext>(
|
|
1081
|
+
input: RegisterToolInput,
|
|
1082
|
+
ctx: TContext,
|
|
1083
|
+
deps: RegisterToolRuntimeDeps<TContext>,
|
|
1084
|
+
): RegisterToolResult {
|
|
1085
|
+
assertRegisterToolInputModes(input);
|
|
1086
|
+
if (typeof input.name !== "string" || !input.name.trim()) {
|
|
1087
|
+
throw new RetainedRecipeRefusal("name", "Retained creation requires a name.");
|
|
1088
|
+
}
|
|
1089
|
+
const name = Identity.normalizeToolName(input.name);
|
|
1090
|
+
if (!name || deps.reservedToolNames.has(name)) {
|
|
1091
|
+
throw new RetainedRecipeRefusal("name", "Invalid or reserved retained Recipe name.");
|
|
1092
|
+
}
|
|
1093
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
1094
|
+
if (blocker) throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
1095
|
+
if (input.update || Object.hasOwn(input, "draft")) {
|
|
1096
|
+
throw new RetainedRecipeRefusal("expose", "Retained creation does not support update or draft promotion.");
|
|
1097
|
+
}
|
|
1098
|
+
const template = getInputTemplate(input.template);
|
|
1099
|
+
if (template === null || template === "") {
|
|
1100
|
+
throw new RetainedRecipeRefusal("template", "Retained deletion is not supported.");
|
|
1101
|
+
}
|
|
1102
|
+
if (template === undefined && !input.from) {
|
|
1103
|
+
throw new RetainedRecipeRefusal("source", "Retained creation requires template or from.");
|
|
1104
|
+
}
|
|
1105
|
+
const root = resolve(getRecipeRoot(deps)), library = join(root, "library"), path = join(library, `${name}.json`);
|
|
1106
|
+
const context = getRegistrationResolutionContext(ctx, deps);
|
|
1107
|
+
const from = input.from === undefined ? undefined : normalizeFromReference(input.from, context);
|
|
1108
|
+
if (from) {
|
|
1109
|
+
const source = from.endsWith(".json") || from.endsWith(".md") ? from
|
|
1110
|
+
: RecipesReferences.resolveRecipeReferencePath(from, context.cwd, context.activeSkills);
|
|
1111
|
+
if (source) assertRetainedSourceLifetime(source, root, "from");
|
|
1112
|
+
}
|
|
1113
|
+
const authored = buildAuthoredRecipe({ ...input, ...(from ? { from } : {}) }, undefined, undefined);
|
|
1114
|
+
if (!from) assertRetainedTemplateLifetimes(authored.template, path, root, context);
|
|
1115
|
+
const bytes = `${JSON.stringify(authored, null, 2)}\n`;
|
|
1116
|
+
if (Buffer.byteLength(bytes) > RecipesReferences.MAX_RECIPE_FILE_BYTES) {
|
|
1117
|
+
throw new RetainedRecipeRefusal("source", "Retained Recipe exceeds the 1 MiB source limit.");
|
|
1118
|
+
}
|
|
1119
|
+
const candidate = RecipesDiscovery.admitUserRecipe(path, context, authored);
|
|
1120
|
+
if (!candidate.validated || !candidate.tool) {
|
|
1121
|
+
throw new RetainedRecipeRefusal("source", "Retained Recipe admission failed; check the source and caller argument contract.", candidate.diagnostics);
|
|
1122
|
+
}
|
|
1123
|
+
let stage = "storage";
|
|
1124
|
+
try {
|
|
1125
|
+
return withFileMutationLock(root, () => {
|
|
1126
|
+
mkdirSync(root, { recursive: true });
|
|
1127
|
+
const rootFrame = retainedDirectory(root);
|
|
1128
|
+
return withFileMutationLock(library, () => {
|
|
1129
|
+
recheckRetainedDirectory(rootFrame);
|
|
1130
|
+
mkdirSync(library, { recursive: true });
|
|
1131
|
+
const libraryFrame = retainedDirectory(library);
|
|
1132
|
+
const relation = relative(rootFrame.realpath, libraryFrame.realpath);
|
|
1133
|
+
if ((process.platform === "win32" ? relation.toLowerCase() : relation) !== "library") {
|
|
1134
|
+
throw new RetainedRecipeRefusal("storage", "Retained library escapes its Recipe root.");
|
|
1135
|
+
}
|
|
1136
|
+
const recheck = () => { recheckRetainedDirectory(rootFrame); recheckRetainedDirectory(libraryFrame); };
|
|
1137
|
+
return withFileMutationLock(path, () => {
|
|
1138
|
+
recheck(); retainedSlotAbsent(path);
|
|
1139
|
+
stage = "persistence";
|
|
1140
|
+
writeTextAtomic(path, bytes, { exclusive: true, onBeforeReplace: () => { recheck(); retainedSlotAbsent(path); } });
|
|
1141
|
+
stage = "persisted_admission";
|
|
1142
|
+
recheck(); retainedSlotAbsent(path, true);
|
|
1143
|
+
const before = retainedFile(path);
|
|
1144
|
+
if (readFileSync(path, "utf8") !== bytes) throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
|
|
1145
|
+
const admitted = RecipesDiscovery.admitUserRecipe(path, context);
|
|
1146
|
+
if (!admitted.validated || !admitted.tool) throw new RetainedRecipeRefusal("source", "Persisted retained Recipe admission failed.", admitted.diagnostics);
|
|
1147
|
+
const after = retainedFile(path);
|
|
1148
|
+
if (Object.keys(before).some(key => before[key as keyof typeof before] !== after[key as keyof typeof after]) ||
|
|
1149
|
+
readFileSync(path, "utf8") !== bytes) throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
|
|
1150
|
+
recheck(); retainedSlotAbsent(path, true);
|
|
1151
|
+
stage = "receipt";
|
|
1152
|
+
const args = RecipesDiscovery.summarizeRegisteredToolArgs(admitted.tool);
|
|
1153
|
+
const source = from ? from.endsWith(".json") || from.endsWith(".md") ? `<explicit-file:${basename(from)}>` : from : "template";
|
|
1154
|
+
const recipe = registrySelected("recipe", path), selectedName = registrySelected("tool", name);
|
|
1155
|
+
const selectedSource = registrySelected("source", source);
|
|
1156
|
+
const required = ToolsResponse.selectYamlField("required_args", args.required, REGISTRY_METADATA_BUDGET);
|
|
1157
|
+
const optional = ToolsResponse.selectYamlField("optional_args", args.optional, REGISTRY_METADATA_BUDGET);
|
|
1158
|
+
const facts = { kind: "retained_recipe" as const, result: "retained" as const, persisted: true, resolved: true,
|
|
1159
|
+
validated: true, activation: "not_requested" as const, async: admitted.async };
|
|
1160
|
+
const text = growRegistryDocument(() => ({ ...selectedName.selected, ...facts, ...recipe.selected, ...selectedSource.selected,
|
|
1161
|
+
...required.selected, ...optional.selected, next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } }),
|
|
1162
|
+
[recipe, selectedName, selectedSource, required, optional]);
|
|
1163
|
+
const result = {
|
|
1164
|
+
content: [textContent(ExecutionOutput.formatToolText(text))],
|
|
1165
|
+
details: { ...facts, tool: name, recipe: path, source, required_args: args.required, optional_args: args.optional,
|
|
1166
|
+
next_actions: ["inspect target=recipes view=summary verbose=true"] },
|
|
1167
|
+
};
|
|
1168
|
+
stage = "release";
|
|
1169
|
+
return result;
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
const reason = error instanceof RetainedRecipeRefusal ? { reason: error.reason } : {};
|
|
1175
|
+
throw new Error(ExecutionOutput.formatToolText(registryMetadataText({ operation: "retain", stage, result: "unconfirmed",
|
|
1176
|
+
effects: "unknown; inspect retained inventory before retry", ...reason,
|
|
1177
|
+
next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } })), { cause: error });
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
|
|
998
1181
|
export async function executeRegisterTool<TContext>(
|
|
999
1182
|
params: unknown,
|
|
1000
1183
|
ctx: TContext,
|
|
1001
1184
|
deps: RegisterToolRuntimeDeps<TContext>,
|
|
1002
1185
|
): Promise<RegisterToolResult> {
|
|
1003
1186
|
const input = params as RegisterToolInput;
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
executeRegisterToolUnlocked(params, ctx, deps)
|
|
1009
|
-
|
|
1187
|
+
let result: RegisterToolResult | (() => RegisterToolResult);
|
|
1188
|
+
if (!input.name) result = executeRegisterToolUnlocked(params, ctx, deps);
|
|
1189
|
+
else {
|
|
1190
|
+
const name = Identity.normalizeToolName(input.name);
|
|
1191
|
+
result = !name ? executeRegisterToolUnlocked(params, ctx, deps)
|
|
1192
|
+
: withFileMutationLock(getToolRecipePath(deps, name), () => executeRegisterToolUnlocked(params, ctx, deps));
|
|
1193
|
+
}
|
|
1194
|
+
return typeof result === "function" ? result() : result;
|
|
1010
1195
|
}
|
package/lib/tools-inspect.ts
CHANGED
|
@@ -448,9 +448,6 @@ function inspectRecipes(
|
|
|
448
448
|
if (view === "reviews") {
|
|
449
449
|
return ReviewDiagnostics.readAutomaticReviewDiagnostics({ recipeRoot });
|
|
450
450
|
}
|
|
451
|
-
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
452
|
-
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
453
|
-
]);
|
|
454
451
|
const resolutionContext =
|
|
455
452
|
context && typeof context === "object" && "recipeResolutionContext" in context
|
|
456
453
|
? (context as {
|
|
@@ -506,7 +503,11 @@ function inspectRecipes(
|
|
|
506
503
|
...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
|
|
507
504
|
}),
|
|
508
505
|
);
|
|
506
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
507
|
+
{ root: recipeRoot, defaultTool: true, mutableUsage: true, resolutionContext },
|
|
508
|
+
]);
|
|
509
509
|
const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
|
|
510
|
+
const retained = RecipesDiscovery.inventoryRetainedRecipes(recipeRoot, resolutionContext);
|
|
510
511
|
const summary = {
|
|
511
512
|
...discoverySummary,
|
|
512
513
|
...(registryStatus
|
|
@@ -532,6 +533,8 @@ function inspectRecipes(
|
|
|
532
533
|
error: `Ambiguous active Skill Recipe namespace ${name}`,
|
|
533
534
|
})),
|
|
534
535
|
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
536
|
+
retained: retained.recipes,
|
|
537
|
+
retained_diagnostics: retained.diagnostics,
|
|
535
538
|
};
|
|
536
539
|
return {
|
|
537
540
|
...summary,
|
package/lib/tools-register.ts
CHANGED
|
@@ -40,6 +40,7 @@ export function createRegisterToolDefinition<TContext>(
|
|
|
40
40
|
Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.defaults,
|
|
41
41
|
),
|
|
42
42
|
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
43
|
+
expose: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.expose),
|
|
43
44
|
from: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.from),
|
|
44
45
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
45
46
|
template: unionSchema([
|
package/lib/tools-response.ts
CHANGED
|
@@ -517,6 +517,12 @@ export function recipeRegistryNextActions(
|
|
|
517
517
|
if (summary.skill_recipe_catalog_partial === true) {
|
|
518
518
|
actions.push("inspect target=recipes view=imports verbose=true");
|
|
519
519
|
}
|
|
520
|
+
if (
|
|
521
|
+
(Array.isArray(summary.retained) && summary.retained.length > 0) ||
|
|
522
|
+
(Array.isArray(summary.retained_diagnostics) && summary.retained_diagnostics.length > 0)
|
|
523
|
+
) {
|
|
524
|
+
actions.push("inspect target=recipes view=summary verbose=true");
|
|
525
|
+
}
|
|
520
526
|
if (drafts.length > 0) {
|
|
521
527
|
actions.push("inspect target=recipes view=summary verbose=true");
|
|
522
528
|
const firstPath =
|
|
@@ -563,6 +569,7 @@ export function compactRecipeRegistry(
|
|
|
563
569
|
const active = count("active", summary.active), shadowed = count("shadowed", summary.shadowed);
|
|
564
570
|
const invalid = count("invalid", summary.invalid), disabled = count("disabled", summary.disabled);
|
|
565
571
|
const diagnostics = count("diagnostics", summary.diagnostics), drafts = count("drafts", summary.drafts);
|
|
572
|
+
const retained = count("retained", summary.retained);
|
|
566
573
|
const recommendations = count("recommendations", summary.recommendations);
|
|
567
574
|
const skillCatalogPartial = summary.skill_recipe_catalog_partial === true;
|
|
568
575
|
const currentPolicy = Array.isArray(summary.active)
|
|
@@ -574,7 +581,7 @@ export function compactRecipeRegistry(
|
|
|
574
581
|
? (summary.next_actions as string[])
|
|
575
582
|
: [];
|
|
576
583
|
const hint = nextActions.length ? nextActionMetadata(nextActions[0]!, Limits.RUN_DELIVERY_MODEL_MAX_BYTES) : undefined;
|
|
577
|
-
const render = (): Record<string, unknown> => ({ recipes: "registry", active, drafts, shadowed, invalid, disabled,
|
|
584
|
+
const render = (): Record<string, unknown> => ({ recipes: "registry", active, drafts, retained, shadowed, invalid, disabled,
|
|
578
585
|
current_policy: currentPolicy, skill_catalog_partial: skillCatalogPartial, recommendations, diagnostics,
|
|
579
586
|
...(hint ? hint.selected : {}) });
|
|
580
587
|
return hint
|
package/package.json
CHANGED
package/skills/actors/SKILL.md
CHANGED
|
@@ -19,6 +19,9 @@ Make a maintained capability a persistent agent-callable tool
|
|
|
19
19
|
Keep the same capability but narrow caller defaults
|
|
20
20
|
→ use register_tool from=<skill>/<recipe> defaults={...}
|
|
21
21
|
|
|
22
|
+
Retain a new named capability without exposing a tool
|
|
23
|
+
→ use register_tool expose=false with from or template
|
|
24
|
+
|
|
22
25
|
Register a trusted command directly
|
|
23
26
|
→ use register_tool template="..."
|
|
24
27
|
|
|
@@ -54,6 +57,12 @@ Run Control ≠ actor chat
|
|
|
54
57
|
|
|
55
58
|
A Skill Recipe is a maintained component addressed by `<skill>/<recipe>`. `spawn` creates a Run from a Recipe. `register_tool from=<skill>/<recipe>` persists compact logical delegation and activates from the resolved effective contract without copying, symlinking, or ambient re-resolution. A tool is callable in the current session only when activation evidence says `callable_now: true`.
|
|
56
59
|
|
|
60
|
+
Sync/async is an execution contract, not a usefulness or tool-exposure rule. A sync tool awaits its result; an async tool returns a Run launch receipt, with the eventual result delivered separately. A subagent-backed tool is a capability launcher, not a persisted live subagent. Register a stable, meaningfully parameterized capability when its name and contract save repeated planning; prefer one-off `spawn` over a redundant generic launcher or a new tool for every mission.
|
|
61
|
+
|
|
62
|
+
Use `register_tool expose=false` with a new name and one `template` or `from` source to retain a capability without requesting tool activation. Its `kind: retained_recipe`, `activation: not_requested` receipt is deliberate retention, not failed registration; it omits global callability flags because a same-named tool may already exist. Reuse the exact returned Recipe path through `spawn`, or deliberately expose it later with ordinary `register_tool from=<exact path>`. Retained update, delete and draft promotion are unsupported; do not hand-edit the live registry to bypass those guards.
|
|
63
|
+
|
|
64
|
+
Automatic retained lifecycle is not implemented. Current user-root tool discovery and automatic review actions are unchanged. `inspect target=recipes view=summary` distinguishes retained definitions under `recipes/library` from root tool candidates; verbose detail gives exact paths, resolved mode and invalid/disabled diagnostics. Storage is not proof of review, callability or a running Actor. `expose` is registration input, not a Recipe field or automatic-review action; do not invent retain/defer review commands. See [persistent tools](./references/persistent-tools.md) for current selection and activation proof.
|
|
65
|
+
|
|
57
66
|
`actors` owns generic Recipe/tool/Run mechanics. The owning capability Skill owns capability-specific selection and constraints. `swarm` owns multi-actor decomposition and integration methodology.
|
|
58
67
|
|
|
59
68
|
## Presentation Direction and Current Output
|
|
@@ -168,7 +177,7 @@ When a pi-actors operation fails:
|
|
|
168
177
|
3. Report resolver, registry, activation, Run, Trace, or Control truth exactly.
|
|
169
178
|
4. Retry only after the owning state is healthy.
|
|
170
179
|
|
|
171
|
-
Stop if spawn and registry resolve the same Recipe differently.
|
|
180
|
+
Stop if spawn and registry resolve the same Recipe differently. When tool exposure was requested, stop if registration persists but is not callable. A `retained_recipe` receipt with `activation: not_requested` instead permits ordinary Recipe reuse, not a claim of tool invocation. Stop if an operation cannot be proven through pi-actors surfaces.
|
|
172
181
|
|
|
173
182
|
Never recover by copying maintained Recipe args, defaults, Control, artifacts, or helper commands. Never hard-code a `{skill_dir}` replacement path. Never introduce `bash -lc`, `eval`, direct bundled-helper execution, or shell backgrounding to bypass resolution. Never call `spawn` and claim a tool call. Use [diagnostics](./references/diagnostics.md) for the safe next action.
|
|
174
183
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Use a persistent tool when the agent should call a trusted capability by name across turns or sessions. Use `spawn` instead for one-off Recipe execution.
|
|
4
4
|
|
|
5
|
+
## Select the capability, not the execution mode
|
|
6
|
+
|
|
7
|
+
Both sync and async Recipes can be tools. Sync invocation awaits the result; async invocation returns a launch receipt and the eventual result arrives separately. Neither synchrony nor a model-backed implementation establishes usefulness. A subagent-backed tool launches a capability, not a saved live subagent. Synchronous tools execute inline; non-singleton async calls launch Run executions, while singleton attachment follows its existing compatibility contract.
|
|
8
|
+
|
|
9
|
+
Prefer a stable purpose/name, caller-owned parameterized inputs, a clear result/acceptance boundary, understood effects and failure/cancellation limits, and meaningful leverage over existing tools or one-off `spawn`. Do not register every transient mission or a generic arbitrary-prompt alias merely to add a name. A successful registration is not permission for new effects, and a launch receipt is not successful task completion.
|
|
10
|
+
|
|
11
|
+
Current user-root Recipes are tool candidates by location; Skill components do not become tools automatically. Explicit create-only retention is supported through `register_tool expose=false`; it is separate from tool exposure. Automatic retained lifecycle is not implemented. Read-only `inspect target=recipes view=summary` reports retained counts for `recipes/library`; use `verbose=true` for exact `retained` paths, resolved mode, invalid/disabled state and diagnostics. Reuse a valid enabled entry through `spawn recipe=<exact path>` with required caller values, or deliberately register it with `from=<exact path>`. A stored definition is not review, activation or Run evidence; use `tool:<name>` for activation and `runtime view=runs` for running instances. A `library/<stem>` usage-lineage label is private accounting identity, not a callable name or Skill Recipe reference; reuse the exact file path from inspection. Do not populate or migrate the live registry manually. `expose` is a registration input, not a Recipe field; do not invent review actions or bypass unsupported retained mutations. Existing explicit `register_tool` works for both modes; automatic review remains under its existing policy. Launch counts and value-free structural review alone do not establish semantic purpose, a good name, or accepted useful reuse.
|
|
12
|
+
|
|
5
13
|
## Choose one source mode
|
|
6
14
|
|
|
7
15
|
```text
|
|
@@ -32,9 +40,19 @@ Resolution uses Pi's authoritative active-session Skill snapshot across every Sk
|
|
|
32
40
|
|
|
33
41
|
Use `description` to narrow agent-facing intent when useful. Every supplied default must name a caller-owned source arg and satisfy its type or enum. Never default runtime-owned origins.
|
|
34
42
|
|
|
43
|
+
## Retain without exposing a tool
|
|
44
|
+
|
|
45
|
+
Use `register_tool name=<name> expose=false from=<skill>/<recipe>` or a trusted `template` source to create `recipes/library/<name>.json`. A description is still required for authored command templates; source defaults/args/origins still belong to the maintained Recipe. This is create-only: retained `update=true`, null/empty deletion, `draft` mode and direct Recipe dependencies on managed disposable drafts are rejected, including Recipe-valued template leaves. A duplicate retained JSON/Markdown stem is a conflict; no older file is overwritten.
|
|
46
|
+
|
|
47
|
+
The `kind: retained_recipe`, `activation: not_requested` receipt confirms persistence/admission, not host activation or review. It returns the exact Recipe path and required/optional argument names, without global callability flags or default values. A same-named tool is left untouched. Use the exact returned path with `spawn`; if compact output omits the whole path, follow its read-only verbose Recipe inspection hint rather than guessing it. Retention records no launch and makes no self-contained-backup promise for referenced sources.
|
|
48
|
+
|
|
49
|
+
To expose a retained capability deliberately, call ordinary `register_tool name=<tool> from=<exact retained path>`, prove activation below, then call the actual tool. Deleting that admitted wrapper through the ordinary deletion path leaves the retained source; it does not stop active Runs. This is not a migration or safe-retention rule for old standalone tools.
|
|
50
|
+
|
|
51
|
+
Publication uses a complete atomic no-overwrite file link; unsupported filesystems refuse without an overwrite fallback. A storage, admission or lock-release failure can follow committed bytes. Inspect the retained inventory and preserve the original cause; do not blindly repeat the request or delete an uncertain file.
|
|
52
|
+
|
|
35
53
|
## Prove registration
|
|
36
54
|
|
|
37
|
-
|
|
55
|
+
When tool exposure was requested, read registration as a state transition, not one success word:
|
|
38
56
|
|
|
39
57
|
```text
|
|
40
58
|
resolved
|
|
@@ -46,7 +64,7 @@ resolved
|
|
|
46
64
|
→ callable_now
|
|
47
65
|
```
|
|
48
66
|
|
|
49
|
-
|
|
67
|
+
For tool exposure, when `callable_now` is true, call the actual generated tool and verify `launch_kind: "tool"` when provenance matters. When false, stop at the reported activation boundary and use tool/Recipe diagnosis. A Recipe spawn is not an activation test or tool invocation substitute.
|
|
50
68
|
|
|
51
69
|
Use:
|
|
52
70
|
|
|
@@ -7,6 +7,12 @@ description: Use only for internal automatic Recipe-memory review, diagnosis, or
|
|
|
7
7
|
|
|
8
8
|
Use this Skill only when automatic draft-memory or active-tool review needs diagnosis or bounded recovery. Normal agents must not spawn, register, specialize, or invoke `recipe-memory/draft-review` or `recipe-memory/tool-review`; they are package-owned internal reviewer components.
|
|
9
9
|
|
|
10
|
+
## Evidence limits
|
|
11
|
+
|
|
12
|
+
Automatic reviewers receive value-free contract shape, risk labels, usage counts, and batch-local equality groups, not executable content, descriptions, paths, or canonical names. Structural evidence and launch counts alone do not establish semantic purpose, good tool names, successful reuse, or accepted outcomes. Promotion preserves unchanged captured executable content; it does not generalize a one-off mission.
|
|
13
|
+
|
|
14
|
+
Explicit create-only retention is owned by `actors` through `register_tool expose=false`, not by these internal reviewers. Automatic retained lifecycle is not implemented. Current draft promote/discard and tool-review behavior are unchanged. Missing evidence is unknown; this guidance adds no retain/defer action, retry permission, automatic rewrite, or live migration. Do not bypass confidentiality or edit review state to simulate the future policy.
|
|
15
|
+
|
|
10
16
|
## Diagnose first
|
|
11
17
|
|
|
12
18
|
```text
|