@llblab/pi-actors 0.57.0 → 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 +9 -1
- package/BACKLOG.md +14 -1
- package/CHANGELOG.md +11 -0
- package/README.md +9 -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/run-delivery.d.ts +1 -1
- package/dist/lib/run-delivery.js +146 -32
- 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 +11 -2
- package/dist/skills/actors/references/persistent-tools.md +20 -2
- package/dist/skills/actors/references/runs.md +1 -1
- package/dist/skills/recipe-memory/SKILL.md +6 -0
- package/docs/README.md +1 -0
- package/docs/agent-experience.md +42 -3
- package/docs/capability-memory.md +166 -0
- package/docs/coordinator-delivery.md +2 -2
- package/docs/inspection.md +3 -1
- package/docs/output-projection-map.md +4 -4
- 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/run-delivery.ts +130 -31
- 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 +11 -2
- package/skills/actors/references/persistent-tools.md +20 -2
- package/skills/actors/references/runs.md +1 -1
- package/skills/recipe-memory/SKILL.md +6 -0
package/lib/prompts.ts
CHANGED
|
@@ -7,15 +7,18 @@
|
|
|
7
7
|
export const REGISTER_TOOL_DESCRIPTION =
|
|
8
8
|
"Register a persistent custom tool from one maintained Recipe, command template, or captured draft. " +
|
|
9
9
|
"Use from for Recipe specialization, template for trusted commands, or draft for promotion. " +
|
|
10
|
+
"Set expose=false to retain a new template/from Recipe without tool activation. " +
|
|
10
11
|
"Definitions persist under ~/.pi/agent/recipes; activation is reported separately.";
|
|
11
12
|
|
|
12
13
|
export const REGISTER_TOOL_PROMPT_SNIPPET =
|
|
13
|
-
"
|
|
14
|
+
"Persist command templates or Recipes with register_tool; use expose=false to retain without tool activation";
|
|
14
15
|
|
|
15
16
|
export const REGISTER_TOOL_GUIDELINES = [
|
|
16
17
|
"Use register_tool from=<skill>/<recipe> with defaults={...} to specialize a maintained Recipe without copying its contract.",
|
|
17
18
|
"Use register_tool template only for trusted command templates, and use register_tool draft only for captured draft promotion.",
|
|
18
|
-
"
|
|
19
|
+
"Use register_tool expose=false to retain a new named template/from Recipe without activation; update, delete and draft promotion are unsupported in this mode.",
|
|
20
|
+
"A register_tool retained_recipe receipt with activation=not_requested is deliberate retention, not failed activation; reuse its exact Recipe path through spawn.",
|
|
21
|
+
"When register_tool requests tool exposure, trust callable_now and activation; persistence alone is not callability.",
|
|
19
22
|
'Set template=null or template="" in register_tool to delete a persisted tool.',
|
|
20
23
|
"Set update=true in register_tool to overwrite an existing tool registration.",
|
|
21
24
|
];
|
|
@@ -42,6 +45,8 @@ export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
|
|
42
45
|
"Optional caller-owned defaults. Keys and values must satisfy the effective source or command-template argument contract.",
|
|
43
46
|
draft:
|
|
44
47
|
"Promote a captured draft Recipe path from ~/.pi/agent/recipes/drafts. This is a source mode; do not combine it with from or template.",
|
|
48
|
+
expose:
|
|
49
|
+
"Set false to retain a new named template/from Recipe in recipes/library without registering or activating a tool. Default true preserves ordinary tool registration. Retained update, delete and draft promotion are unsupported.",
|
|
45
50
|
async:
|
|
46
51
|
"Set true for a co-located async template recipe. Omit for ordinary command templates or file-backed recipe references.",
|
|
47
52
|
template:
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -756,6 +756,32 @@ function templatePreview(value: unknown): string | undefined {
|
|
|
756
756
|
: rendered;
|
|
757
757
|
}
|
|
758
758
|
|
|
759
|
+
export function inventoryRetainedRecipes(
|
|
760
|
+
recipeRoot: string,
|
|
761
|
+
resolutionContext?: RecipeResolutionContext,
|
|
762
|
+
): { recipes: Array<Record<string, unknown>>; diagnostics: string[] } {
|
|
763
|
+
const discovery = discoverRecipeSources([
|
|
764
|
+
{ root: join(recipeRoot, "library"), resolutionContext },
|
|
765
|
+
]);
|
|
766
|
+
return {
|
|
767
|
+
recipes: [...discovery.active.values()]
|
|
768
|
+
.map((entry) => ({
|
|
769
|
+
id: entry.id,
|
|
770
|
+
path: entry.path,
|
|
771
|
+
source_kind: "user_retained_recipe",
|
|
772
|
+
tool: false,
|
|
773
|
+
invalid: entry.invalid,
|
|
774
|
+
disabled: entry.disabled,
|
|
775
|
+
...(entry.config ? { async: entry.config.async === true } : {}),
|
|
776
|
+
...(entry.config?.description ? { description: entry.config.description } : {}),
|
|
777
|
+
...(entry.diagnostics.length ? { diagnostics: entry.diagnostics } : {}),
|
|
778
|
+
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
779
|
+
}))
|
|
780
|
+
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
781
|
+
diagnostics: discovery.diagnostics,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
759
785
|
export function listDraftRecipes(root: string): Array<Record<string, unknown>> {
|
|
760
786
|
return listRecipeFiles(root).map((path) => {
|
|
761
787
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -16,7 +16,7 @@ import * as Paths from "./paths.ts";
|
|
|
16
16
|
import * as RecipeControl from "./recipe-control.ts";
|
|
17
17
|
import * as Schema from "./schema.ts";
|
|
18
18
|
|
|
19
|
-
const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
19
|
+
export const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
20
20
|
const MAX_RECIPE_IMPORT_DEPTH = 32;
|
|
21
21
|
|
|
22
22
|
export interface TemplateRecipeImportBinding {
|
package/lib/recipes-usage.ts
CHANGED
|
@@ -101,7 +101,7 @@ function readRecipeSource(path: string): { fingerprint: string } | undefined {
|
|
|
101
101
|
|
|
102
102
|
function inferRecipeRoot(path: string): string {
|
|
103
103
|
const parent = dirname(resolve(path));
|
|
104
|
-
return basename(parent)
|
|
104
|
+
return ["drafts", "library"].includes(basename(parent)) ? dirname(parent) : parent;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function pathKey(path: string, recipeRoot: string): string {
|
|
@@ -112,8 +112,9 @@ function pathKey(path: string, recipeRoot: string): string {
|
|
|
112
112
|
return relation.replaceAll("\\", "/");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
function recipeName(path: string): string {
|
|
116
|
-
|
|
115
|
+
function recipeName(path: string, recipeRoot: string): string {
|
|
116
|
+
const name = basename(path).replace(/\.(?:json|md)$/u, "");
|
|
117
|
+
return dirname(resolve(path)) === resolve(recipeRoot, "library") ? `library/${name}` : name;
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
function lineageFilename(lineageName: string): string {
|
|
@@ -232,8 +233,10 @@ function findRelocatedRecipe(
|
|
|
232
233
|
index: RecipeUsageIndex,
|
|
233
234
|
fingerprint: string,
|
|
234
235
|
recipeRoot: string,
|
|
236
|
+
newPath: string,
|
|
235
237
|
): { indexedName: string; path: string; record: RecipeUsageRecord } | undefined {
|
|
236
238
|
const candidates = Object.entries(index.paths).flatMap(([oldPath, indexedName]) => {
|
|
239
|
+
if (oldPath.startsWith("library/") !== newPath.startsWith("library/")) return [];
|
|
237
240
|
if (existsSync(join(recipeRoot, oldPath))) return [];
|
|
238
241
|
const found = readIndexedLedger(indexedName, recipeRoot);
|
|
239
242
|
return found?.record.fingerprint === fingerprint
|
|
@@ -288,7 +291,7 @@ export function ensureRecipeLineage(
|
|
|
288
291
|
const source = readRecipeSource(path);
|
|
289
292
|
if (!source) return false;
|
|
290
293
|
const key = pathKey(path, recipeRoot);
|
|
291
|
-
const name = recipeName(path);
|
|
294
|
+
const name = recipeName(path, recipeRoot);
|
|
292
295
|
const index = readIndex(recipeRoot);
|
|
293
296
|
const indexedName = index.paths[key];
|
|
294
297
|
if (indexedName) {
|
|
@@ -392,11 +395,11 @@ export function recordRecipeLaunch(
|
|
|
392
395
|
if (!source) return false;
|
|
393
396
|
const { fingerprint } = source;
|
|
394
397
|
const key = pathKey(path, recipeRoot);
|
|
395
|
-
const desiredName = recipeName(path);
|
|
398
|
+
const desiredName = recipeName(path, recipeRoot);
|
|
396
399
|
const index = readIndex(recipeRoot);
|
|
397
400
|
const relocated = index.paths[key]
|
|
398
401
|
? undefined
|
|
399
|
-
: findRelocatedRecipe(index, fingerprint, recipeRoot);
|
|
402
|
+
: findRelocatedRecipe(index, fingerprint, recipeRoot, key);
|
|
400
403
|
const indexedName = index.paths[key] ?? relocated?.indexedName;
|
|
401
404
|
const found = indexedName
|
|
402
405
|
? readIndexedLedger(indexedName, recipeRoot)
|
|
@@ -436,7 +439,7 @@ export function recordRecipeLaunch(
|
|
|
436
439
|
former_names: renamed
|
|
437
440
|
? appendBounded(stored?.former_names ?? [], stored!.lineage_name)
|
|
438
441
|
: relocated
|
|
439
|
-
? appendBounded(stored?.former_names ?? [], recipeName(relocated.path))
|
|
442
|
+
? appendBounded(stored?.former_names ?? [], recipeName(join(recipeRoot, relocated.path), recipeRoot))
|
|
440
443
|
: stored?.former_names ?? [],
|
|
441
444
|
former_paths: relocated
|
|
442
445
|
? appendBounded(stored?.former_paths ?? [], relocated.path)
|
|
@@ -591,7 +594,7 @@ export function moveRecipeUsage(
|
|
|
591
594
|
const toKey = pathKey(toPath, recipeRoot);
|
|
592
595
|
const index = readIndex(recipeRoot);
|
|
593
596
|
const oldName = index.paths[fromKey];
|
|
594
|
-
const newName = recipeName(toPath);
|
|
597
|
+
const newName = recipeName(toPath, recipeRoot);
|
|
595
598
|
if (!oldName) {
|
|
596
599
|
return index.paths[toKey] === newName && Boolean(readLedger(newName, recipeRoot));
|
|
597
600
|
}
|
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
|
}
|