@llblab/pi-actors 0.57.1 → 0.58.1
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 +11 -3
- package/BACKLOG.md +14 -1
- package/CHANGELOG.md +13 -1
- 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/pi.js +1 -1
- 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 +19 -9
- package/dist/lib/tools-inspect.js +6 -3
- package/dist/lib/tools-register.js +1 -0
- package/dist/lib/tools-response.js +20 -16
- 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 +5 -6
- package/docs/capability-memory.md +121 -0
- package/docs/coordinator-delivery.md +2 -2
- package/docs/inspection.md +3 -1
- package/docs/output-projection-map.md +1 -1
- package/docs/releasing.md +5 -1
- package/docs/tool-registry.md +37 -1
- package/lib/file-state.ts +7 -3
- package/lib/pi.ts +1 -1
- 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 +22 -11
- package/lib/tools-inspect.ts +6 -3
- package/lib/tools-register.ts +1 -0
- package/lib/tools-response.ts +20 -13
- 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/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
|
}
|
package/lib/run-delivery.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface RunCompletionBatch {
|
|
|
69
69
|
/** Retained singleton capture; absence preserves older indexed bytes. Never a new-capture selector. */
|
|
70
70
|
readonly single_result?: true;
|
|
71
71
|
/** Captured record-list shape; no public format/version choice or source authority. */
|
|
72
|
-
readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1";
|
|
72
|
+
readonly result_layout?: "records" | "run_headers" | "headings" | "headings_flat_omissions" | "headings_outcomes" | "headings_shared_results" | "headings_shared_kinds" | "frontmatter_v1" | "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1" | "completion_yaml_files_v2";
|
|
73
73
|
/** Immutable scope/order/range data, not a caller-selected format or allocation authority. */
|
|
74
74
|
readonly numbering?: RunCompletionNumbering;
|
|
75
75
|
/** Formatting version/data only; no recipient ownership, read or publication capability. */
|
|
@@ -467,8 +467,12 @@ function isFrontmatterCompletionLayout(layout: unknown): boolean {
|
|
|
467
467
|
return layout === "frontmatter_v1" || layout === "frontmatter_v2";
|
|
468
468
|
}
|
|
469
469
|
|
|
470
|
+
function isRootedCompletionLayout(layout: unknown): boolean {
|
|
471
|
+
return layout === "completion_yaml_files_v1" || layout === "completion_yaml_files_v2";
|
|
472
|
+
}
|
|
473
|
+
|
|
470
474
|
function isYamlCompletionLayout(layout: unknown): boolean {
|
|
471
|
-
return layout === "completion_yaml_core_v1" || layout
|
|
475
|
+
return layout === "completion_yaml_core_v1" || isRootedCompletionLayout(layout);
|
|
472
476
|
}
|
|
473
477
|
|
|
474
478
|
function isSharedCompletionLayout(layout: unknown): boolean {
|
|
@@ -895,7 +899,7 @@ export function requestRunCompletionStream(input: { tempDir: string; ownerId: st
|
|
|
895
899
|
}
|
|
896
900
|
|
|
897
901
|
export function admitRunCompletionBatch(input: AdmitRunCompletionBatchInput): RunCompletionBatch {
|
|
898
|
-
return captureCompletionBatch(input, false, "
|
|
902
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v2");
|
|
899
903
|
}
|
|
900
904
|
|
|
901
905
|
/** Kernel-only opt-in; ordinary Pi capture stays fenced until session/lifecycle integration is admitted. */
|
|
@@ -905,10 +909,10 @@ export function captureRunCompletionStreamBatch(input: AdmitRunCompletionBatchIn
|
|
|
905
909
|
|
|
906
910
|
/** Private ordinary candidate composition; stream capture retains its separately gated codec. */
|
|
907
911
|
export function captureRunCompletionYamlCandidateBatch(input: AdmitRunCompletionBatchInput): RunCompletionBatch {
|
|
908
|
-
return captureCompletionBatch(input, false, "
|
|
912
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v2");
|
|
909
913
|
}
|
|
910
914
|
|
|
911
|
-
function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: boolean, layout: "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1" = "frontmatter_v2"): RunCompletionBatch {
|
|
915
|
+
function captureCompletionBatch(input: AdmitRunCompletionBatchInput, numbered: boolean, layout: "frontmatter_v2" | "completion_yaml_core_v1" | "completion_yaml_files_v1" | "completion_yaml_files_v2" = "frontmatter_v2"): RunCompletionBatch {
|
|
912
916
|
const now = (input.now ?? new Date()).toISOString();
|
|
913
917
|
if (Object.hasOwn(input, "presentationFormat")) throw new Error("Completion presentation selection is not supported; new batches use AX.");
|
|
914
918
|
if (Object.hasOwn(input, "numbering")) throw new Error("Completion numbering is assigned only by journal capture");
|
|
@@ -1869,12 +1873,13 @@ type PreparedCompletionYamlProjection =
|
|
|
1869
1873
|
function prepareCompletionYamlProjection(safe: RunCompletionBatch, candidates: readonly { row: CompletionRow; location?: CompletionPiece }[],
|
|
1870
1874
|
references: ReadonlySet<number>, ordered: readonly RunCompletionBatchMember[]): PreparedCompletionYamlProjection {
|
|
1871
1875
|
const packet: CompletionYamlPacket = { batchId: safe.batch_id, records: [] };
|
|
1872
|
-
const rooted = safe.result_layout
|
|
1876
|
+
const rooted = isRootedCompletionLayout(safe.result_layout);
|
|
1877
|
+
const localSingleton = safe.result_layout === "completion_yaml_files_v2" && !safe.numbering && candidates.length === 1;
|
|
1873
1878
|
if (!safe.batch_id.isWellFormed()) return { state: "unsupported" };
|
|
1874
1879
|
for (const [index, { row, location }] of candidates.entries()) {
|
|
1875
1880
|
const member = ordered[index]!;
|
|
1876
1881
|
const displayed = safe.numbering ? safe.numbering.kind === "stream" ? safe.numbering.first_index + index : "?" : index;
|
|
1877
|
-
const metadata: Record<string, CompletionYamlData> = { index: displayed };
|
|
1882
|
+
const metadata: Record<string, CompletionYamlData> = localSingleton ? {} : { index: displayed };
|
|
1878
1883
|
for (const piece of row.pieces) {
|
|
1879
1884
|
if (piece.block) continue;
|
|
1880
1885
|
if (!piece.field) return { state: "unsupported" };
|
|
@@ -1917,7 +1922,9 @@ function prepareCompletionYamlProjection(safe: RunCompletionBatch, candidates: r
|
|
|
1917
1922
|
}
|
|
1918
1923
|
packet.records.push({ metadata, parts });
|
|
1919
1924
|
}
|
|
1920
|
-
const service: Record<string, CompletionYamlData> = rooted ? {
|
|
1925
|
+
const service: Record<string, CompletionYamlData> = rooted ? {
|
|
1926
|
+
header_layout: safe.result_layout === "completion_yaml_files_v2" ? "rooted_files_v2" : "rooted_files_v1",
|
|
1927
|
+
} : {};
|
|
1921
1928
|
const omitted = safe.members.length - candidates.length;
|
|
1922
1929
|
if (omitted) {
|
|
1923
1930
|
service.omitted_results = omitted;
|
|
@@ -2349,7 +2356,7 @@ export function readRunCompletionPresentation(content: string, value: unknown):
|
|
|
2349
2356
|
!completionYamlKeys(hint, ["format", "content_sha256"]) || hint.format !== completionYamlCodec ||
|
|
2350
2357
|
hint.content_sha256 !== createHash("sha256").update(content).digest("hex")) return full;
|
|
2351
2358
|
const segments: NonNullable<RunCompletionPresentationView["segments"]>[number][] = [];
|
|
2352
|
-
const rooted = core.packet.service?.header_layout === "rooted_files_v1";
|
|
2359
|
+
const rooted = core.packet.service?.header_layout === "rooted_files_v1" || core.packet.service?.header_layout === "rooted_files_v2";
|
|
2353
2360
|
const facts = Object.fromEntries(Object.entries(core.packet.service ?? {}).filter(([key]) => key !== "header_layout"));
|
|
2354
2361
|
for (const [index, record] of core.packet.records.entries()) {
|
|
2355
2362
|
if (index) segments.push({ kind: "separator", text: "\n" });
|
|
@@ -2581,6 +2588,8 @@ export function encodeCompletionYamlCarrier(input: unknown): { state: "available
|
|
|
2581
2588
|
typeof packet.batchId !== "string" || !packet.batchId.length || !Array.isArray(packet.records) || packet.records.length > 64 ||
|
|
2582
2589
|
(Object.hasOwn(packet, "service") && (!completionYamlMapping(packet.service) || !Object.keys(packet.service).length)) ||
|
|
2583
2590
|
(!packet.records.length && !Object.hasOwn(packet, "service"))) return unavailable;
|
|
2591
|
+
const localSingleton = packet.records.length === 1 && completionYamlMapping(packet.service) &&
|
|
2592
|
+
packet.service.header_layout === "rooted_files_v2" && !Object.hasOwn(packet.service, "numbering");
|
|
2584
2593
|
const rows: string[] = [];
|
|
2585
2594
|
for (let index = 0; index < Math.max(1, packet.records.length); index++) {
|
|
2586
2595
|
const row: CompletionYamlData | undefined = packet.records[index];
|
|
@@ -2596,13 +2605,15 @@ export function encodeCompletionYamlCarrier(input: unknown): { state: "available
|
|
|
2596
2605
|
if (!completionYamlMapping(row) || !completionYamlKeys(row, ["metadata", "parts"]) ||
|
|
2597
2606
|
!completionYamlMapping(row.metadata) || !Array.isArray(row.parts)) return unavailable;
|
|
2598
2607
|
const meta = row.metadata;
|
|
2599
|
-
|
|
2608
|
+
const validIndex = localSingleton ? !Object.hasOwn(meta, "index")
|
|
2609
|
+
: meta.index === "?" || (typeof meta.index === "number" && Number.isSafeInteger(meta.index) && meta.index >= 0);
|
|
2610
|
+
if (!validIndex ||
|
|
2600
2611
|
typeof meta.run !== "string" || !meta.run.length || typeof meta.state !== "string" || !meta.state.length ||
|
|
2601
2612
|
(Object.hasOwn(meta, "kind") && meta.kind !== "primary" && meta.kind !== "diagnostic") ||
|
|
2602
2613
|
Object.hasOwn(meta, completionYamlServiceKey) ||
|
|
2603
2614
|
// JS enumerates array-index keys before semantic core fields; do not silently reorder the header.
|
|
2604
2615
|
Object.keys(meta).some(key => /^(0|[1-9]\d*)$/u.test(key) && Number(key) < 4294967295)) return unavailable;
|
|
2605
|
-
for (const key of ["index", "run", "state", ...(Object.hasOwn(meta, "kind") ? ["kind"] : []),
|
|
2616
|
+
for (const key of [...(localSingleton ? [] : ["index"]), "run", "state", ...(Object.hasOwn(meta, "kind") ? ["kind"] : []),
|
|
2606
2617
|
...Object.keys(meta).filter(key => !["index", "run", "state", "kind"].includes(key))]) {
|
|
2607
2618
|
Object.defineProperty(header, key, { value: meta[key]!, enumerable: true });
|
|
2608
2619
|
}
|
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 =
|
|
@@ -532,16 +538,18 @@ export function compactYamlDocument(value: Record<string, unknown>): string {
|
|
|
532
538
|
return `\n${text.replace(/\n$/u, "")}`;
|
|
533
539
|
}
|
|
534
540
|
|
|
541
|
+
function recipeInspectionHint(action: string): { view: "doctor" | "imports" | "summary"; verbose?: true } | undefined {
|
|
542
|
+
if (action === "inspect target=recipes view=doctor") return { view: "doctor" };
|
|
543
|
+
if (action === "inspect target=recipes view=imports verbose=true") return { view: "imports", verbose: true };
|
|
544
|
+
if (action === "inspect target=recipes view=summary verbose=true") return { view: "summary", verbose: true };
|
|
545
|
+
return undefined;
|
|
546
|
+
}
|
|
547
|
+
|
|
535
548
|
/** Only the closed read-only Recipe inspect literals become typed next; other advice stays opaque content. */
|
|
536
549
|
export function nextActionMetadata(action: string, byteBudget: number): YamlPart {
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
["inspect target=recipes view=summary verbose=true", "summary", true],
|
|
541
|
-
] as const;
|
|
542
|
-
const match = recognized.find(([literal]) => literal === action);
|
|
543
|
-
if (match) {
|
|
544
|
-
const typed = { next: { tool: "inspect", target: "recipes", view: match[1], ...(match[2] ? { verbose: true } : {}) } };
|
|
550
|
+
const hint = recipeInspectionHint(action);
|
|
551
|
+
if (hint) {
|
|
552
|
+
const typed = { next: { tool: "inspect", target: "recipes", ...hint } };
|
|
545
553
|
return { full: typed, selected: typed };
|
|
546
554
|
}
|
|
547
555
|
const instruction = selectYamlField("instruction", action, byteBudget);
|
|
@@ -550,10 +558,8 @@ export function nextActionMetadata(action: string, byteBudget: number): YamlPart
|
|
|
550
558
|
|
|
551
559
|
/** Only these existing read-only inspection hints become action nodes; other advice stays content. */
|
|
552
560
|
export function formatInspectionHint(action: string): string {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (action === "inspect target=recipes view=summary verbose=true") return formatInspectNextAction("recipes", "summary", true);
|
|
556
|
-
return formatMarkdownField("instruction", action);
|
|
561
|
+
const hint = recipeInspectionHint(action);
|
|
562
|
+
return hint ? formatInspectNextAction("recipes", hint.view, hint.verbose) : formatMarkdownField("instruction", action);
|
|
557
563
|
}
|
|
558
564
|
|
|
559
565
|
export function compactRecipeRegistry(
|
|
@@ -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
|
|
@@ -64,7 +73,7 @@ Rollout is partial: compact launch/attachment receipts use block YAML; other ope
|
|
|
64
73
|
|
|
65
74
|
Compact launches use bare block-YAML mappings for the Run/state/launch facts and nested observed locations, explicitly identify actual existing-Run reuse, and preserve multiline scalar contents. One leading response-separator LF supplies invocation spacing; it is outside the pure metadata emitter and is not a `---` wrapper. Model/thinking provenance groups are labeled model and thinking, without a redundant _policy suffix; structured data and saved text stay unchanged. A complete direct file-location row needs no repeated session-map hint; absent/unavailable/omitted rows retain one optional next node naming inspect, target=runtime, view=runs and verbose=true. It never executes automatically. Exact locations, verbose details and saved completion formats stay unchanged.
|
|
66
75
|
|
|
67
|
-
Fresh automatic completions have one AX capture path with no format selector: YAML frontmatter begins
|
|
76
|
+
Fresh automatic completions have one AX capture path with no format selector: YAML frontmatter begins run, state, then kind:primary for admitted primary/final Actor response or kind:diagnostic for explicit diagnostics. Markdown follows unindented outside the closed metadata block, without #N heading, result:/preview: wrapper or whole-body literal fence. Prepend index only when two or more results are shown; exactly one shown result omits it even if other results were omitted. Omission/inventory facts remain explicit. Display index is not member_index or generation; default indices are packet-local. This new-capture rule neither restyles saved messages nor changes numbered-stream presentation. Whole-result/reference omissions and non-done omitted states stay explicit. Known display loss appears at the actual cut as [N characters omitted], counting Unicode scalar characters of admitted retained text/field, never raw/private loss; no extra clipping ellipsis or body clipping flags/counts in YAML. Unknown upstream completeness/privacy remains separately visible at the body boundary, not a fabricated count or a known positive loss. Untracked fallback has no kind and cannot become primary. Full text fitting its share has no display-cut marker; upstream uncertainty still prevents a completeness claim. New packets share48KiB UTF-8 including references/framing across at most64candidate rows, with frozen equal shares and no unused-share redistribution or fixed16KiB result/2KiB preview cap. Captured members remain bounded at256. Text is admitted work output/final response or explicit error/failure diagnostics, never launch commands. Successful composition/retry/Pi text-print can retain text_basis:actor_response; scope and exact references remain technical, while original coverage facts remain structured retained data. The model receives one complete content with whole source/generation/location references or truthful omission/inventory, not a standing full-map instruction. The TUI renders compiler headers/markers as Text and each body as independent native Markdown; only whole validated header reference/location rows and framing disappear in compact view. Body-authored YAML, next/tool/batch and omission lookalikes remain content. Content-bound hashes/ranges/blocks are display consistency, not authentication, source/read/action or acceptance authority. Invalid/missing hints and expansion reveal full original text, without Run-source IO. Old saved codecs, budgets, labels, excerpts, counts and hints replay exactly with no refresh, repair, ACK/resend or restyling—including earlier #N packets and the first frontmatter encoding. Fresh unavailable indices use quoted YAML "?"; an old saved unquoted value is not silently corrected. Routine successful delivery adds no separate completions/delivery-accepted toast; actual failures and handled-generation fences remain. These source/package rules do not prove that an already loaded session was reloaded. Fresh explicit agent Trace previews append boundary … and [omitted: N chars] from normalized retained turn, with a separate upstream [omitted: unknown]. N counts only display loss after existing session privacy, not discarded original/source text. Short well-formed summaries stay unchanged, including authored older-looking markers; malformed retained text yields Agent turn [omitted: unknown] (malformed retained text), never a guessed count or repaired scalar. This is read-time display only, not a rewrite/version of saved packets or sources. Other legacy clipping is not yet uniformly covered. Follow retained references selectively without inventing completeness or permission. Agent Trace ids reference recorded entry provenance, not aggregate traversal order or chronological event numbers. Native entry keys survive sibling growth; bounded legacy/ambiguous/private-metadata fallbacks are not permanent identities, source immutability or new read/work permission.
|
|
68
77
|
|
|
69
78
|
Current completion index fields are packet-local, not persistent historical indices; earlier saved #N headings keep their original meaning. Session completion streams are adopted work in progress, not an available action; do not invent a start-stream command, initialize a missing counter or reinterpret old headings. A kernel-only path commits numbered snapshots/counters/activation atomically. Its internal runtime bridge requires trusted fresh-session opt-in plus current manager/generation binding; the default entrypoint does not opt in and no public tool/command exposes it. Unknown state does not fall back to local numbering on that path. Startup/reload/resume, including ambiguous imports, cannot resume a stream automatically; stored numbers remain data, not a new session grant. A pending target or projected scope is not an active stream. Missing state never authorizes bootstrap, and unknown/conflicting delivery never authorizes bypass or reallocation. Saved numbers stay exact; an admitted unavailable counter or unfitting whole range freezes an unknown index (quoted "?" in fresh YAML, #? in older layouts) rather than resetting or partially numbering a packet.
|
|
70
79
|
|
|
@@ -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
|
|