@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/dist/lib/registry.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Zones: registry mutations, persistence, runtime activation
|
|
4
4
|
* Owns register/update/delete validation, persistence, runtime side effects, and result payloads
|
|
5
5
|
*/
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
7
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
8
8
|
import * as CommandTemplates from "./command-templates.js";
|
|
9
9
|
import * as Config from "./config.js";
|
|
10
10
|
import * as ExecutionOutput from "./execution-output.js";
|
|
@@ -560,6 +560,14 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
560
560
|
? params
|
|
561
561
|
: {};
|
|
562
562
|
assertRegisterToolInputModes(rawInput);
|
|
563
|
+
if (Object.hasOwn(rawInput, "expose")) {
|
|
564
|
+
const exposure = rawInput.expose;
|
|
565
|
+
if (typeof exposure !== "boolean")
|
|
566
|
+
throw new RetainedRecipeRefusal("expose", "expose must be Boolean.");
|
|
567
|
+
// Select under the existing name reservation; acquire retention locks only after it releases.
|
|
568
|
+
if (exposure === false)
|
|
569
|
+
return () => retainRecipe(rawInput, ctx, deps);
|
|
570
|
+
}
|
|
563
571
|
let input = rawInput;
|
|
564
572
|
if (!input.name)
|
|
565
573
|
return listTools(deps);
|
|
@@ -702,12 +710,196 @@ function executeRegisterToolUnlocked(params, ctx, deps) {
|
|
|
702
710
|
},
|
|
703
711
|
};
|
|
704
712
|
}
|
|
713
|
+
class RetainedRecipeRefusal extends Error {
|
|
714
|
+
reason;
|
|
715
|
+
constructor(field, reason, cause) {
|
|
716
|
+
super(ExecutionOutput.formatToolText(registryMetadataText({ field, result: reason })), { cause });
|
|
717
|
+
this.reason = reason;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function retainedDirectory(path) {
|
|
721
|
+
const stat = lstatSync(path, { bigint: true });
|
|
722
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
723
|
+
throw new RetainedRecipeRefusal("storage", "Retained storage must be a regular non-symlink directory.");
|
|
724
|
+
}
|
|
725
|
+
return { path, realpath: realpathSync.native(path), dev: stat.dev, ino: stat.ino };
|
|
726
|
+
}
|
|
727
|
+
function recheckRetainedDirectory(frame) {
|
|
728
|
+
const current = retainedDirectory(frame.path);
|
|
729
|
+
if (current.realpath !== frame.realpath || current.dev !== frame.dev || current.ino !== frame.ino) {
|
|
730
|
+
throw new RetainedRecipeRefusal("storage", "Retained storage identity changed.");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function retainedSlotAbsent(path, ownTargetPublished = false) {
|
|
734
|
+
for (const candidate of ownTargetPublished ? [path.replace(/\.json$/u, ".md")] : [path, path.replace(/\.json$/u, ".md")]) {
|
|
735
|
+
try {
|
|
736
|
+
lstatSync(candidate);
|
|
737
|
+
}
|
|
738
|
+
catch (error) {
|
|
739
|
+
let code;
|
|
740
|
+
try {
|
|
741
|
+
code = error?.code;
|
|
742
|
+
}
|
|
743
|
+
catch (observationError) {
|
|
744
|
+
throw new AggregateError([error, observationError], "Retained slot observation failed.", { cause: error });
|
|
745
|
+
}
|
|
746
|
+
if (code === "ENOENT")
|
|
747
|
+
continue;
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
throw new RetainedRecipeRefusal("name", "Retained Recipe already exists; this path is create-only.");
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function retainedFile(path) {
|
|
754
|
+
const stat = lstatSync(path, { bigint: true });
|
|
755
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
756
|
+
throw new RetainedRecipeRefusal("storage", "Retained target must be a regular non-symlink file.");
|
|
757
|
+
}
|
|
758
|
+
if (stat.size > BigInt(RecipesReferences.MAX_RECIPE_FILE_BYTES))
|
|
759
|
+
throw new RetainedRecipeRefusal("storage", "Retained target exceeds the source size limit.");
|
|
760
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size, mtime: stat.mtimeNs, ctime: stat.ctimeNs };
|
|
761
|
+
}
|
|
762
|
+
function withinRetainedSource(root, path) {
|
|
763
|
+
const relation = relative(resolve(root), resolve(path));
|
|
764
|
+
return relation === "" || (!isAbsolute(relation) && !/^\.\.(?:[\\/]|$)/u.test(relation));
|
|
765
|
+
}
|
|
766
|
+
function assertRetainedSourceLifetime(source, root, field) {
|
|
767
|
+
const drafts = join(root, "drafts");
|
|
768
|
+
if (withinRetainedSource(drafts, source) ||
|
|
769
|
+
(existsSync(drafts) && existsSync(source) && withinRetainedSource(realpathSync.native(drafts), realpathSync.native(source)))) {
|
|
770
|
+
throw new RetainedRecipeRefusal(field, "Retained creation cannot reference a managed disposable draft.");
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function assertRetainedTemplateLifetimes(value, path, root, context) {
|
|
774
|
+
if (typeof value === "string") {
|
|
775
|
+
const source = RecipesReferences.resolveRecipeReferencePath(value, dirname(path), context.activeSkills);
|
|
776
|
+
if (source)
|
|
777
|
+
assertRetainedSourceLifetime(source, root, "template");
|
|
778
|
+
}
|
|
779
|
+
else if (Array.isArray(value)) {
|
|
780
|
+
for (const child of value)
|
|
781
|
+
assertRetainedTemplateLifetimes(child, path, root, context);
|
|
782
|
+
}
|
|
783
|
+
else if (value && typeof value === "object") {
|
|
784
|
+
const node = value;
|
|
785
|
+
assertRetainedTemplateLifetimes(node.template, path, root, context);
|
|
786
|
+
assertRetainedTemplateLifetimes(node.recover, path, root, context);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
function retainRecipe(input, ctx, deps) {
|
|
790
|
+
assertRegisterToolInputModes(input);
|
|
791
|
+
if (typeof input.name !== "string" || !input.name.trim()) {
|
|
792
|
+
throw new RetainedRecipeRefusal("name", "Retained creation requires a name.");
|
|
793
|
+
}
|
|
794
|
+
const name = Identity.normalizeToolName(input.name);
|
|
795
|
+
if (!name || deps.reservedToolNames.has(name)) {
|
|
796
|
+
throw new RetainedRecipeRefusal("name", "Invalid or reserved retained Recipe name.");
|
|
797
|
+
}
|
|
798
|
+
const blocker = deps.getToolNameBlocker(name);
|
|
799
|
+
if (blocker)
|
|
800
|
+
throw new Error(ExecutionOutput.formatToolText(blocker));
|
|
801
|
+
if (input.update || Object.hasOwn(input, "draft")) {
|
|
802
|
+
throw new RetainedRecipeRefusal("expose", "Retained creation does not support update or draft promotion.");
|
|
803
|
+
}
|
|
804
|
+
const template = getInputTemplate(input.template);
|
|
805
|
+
if (template === null || template === "") {
|
|
806
|
+
throw new RetainedRecipeRefusal("template", "Retained deletion is not supported.");
|
|
807
|
+
}
|
|
808
|
+
if (template === undefined && !input.from) {
|
|
809
|
+
throw new RetainedRecipeRefusal("source", "Retained creation requires template or from.");
|
|
810
|
+
}
|
|
811
|
+
const root = resolve(getRecipeRoot(deps)), library = join(root, "library"), path = join(library, `${name}.json`);
|
|
812
|
+
const context = getRegistrationResolutionContext(ctx, deps);
|
|
813
|
+
const from = input.from === undefined ? undefined : normalizeFromReference(input.from, context);
|
|
814
|
+
if (from) {
|
|
815
|
+
const source = from.endsWith(".json") || from.endsWith(".md") ? from
|
|
816
|
+
: RecipesReferences.resolveRecipeReferencePath(from, context.cwd, context.activeSkills);
|
|
817
|
+
if (source)
|
|
818
|
+
assertRetainedSourceLifetime(source, root, "from");
|
|
819
|
+
}
|
|
820
|
+
const authored = buildAuthoredRecipe({ ...input, ...(from ? { from } : {}) }, undefined, undefined);
|
|
821
|
+
if (!from)
|
|
822
|
+
assertRetainedTemplateLifetimes(authored.template, path, root, context);
|
|
823
|
+
const bytes = `${JSON.stringify(authored, null, 2)}\n`;
|
|
824
|
+
if (Buffer.byteLength(bytes) > RecipesReferences.MAX_RECIPE_FILE_BYTES) {
|
|
825
|
+
throw new RetainedRecipeRefusal("source", "Retained Recipe exceeds the 1 MiB source limit.");
|
|
826
|
+
}
|
|
827
|
+
const candidate = RecipesDiscovery.admitUserRecipe(path, context, authored);
|
|
828
|
+
if (!candidate.validated || !candidate.tool) {
|
|
829
|
+
throw new RetainedRecipeRefusal("source", "Retained Recipe admission failed; check the source and caller argument contract.", candidate.diagnostics);
|
|
830
|
+
}
|
|
831
|
+
let stage = "storage";
|
|
832
|
+
try {
|
|
833
|
+
return withFileMutationLock(root, () => {
|
|
834
|
+
mkdirSync(root, { recursive: true });
|
|
835
|
+
const rootFrame = retainedDirectory(root);
|
|
836
|
+
return withFileMutationLock(library, () => {
|
|
837
|
+
recheckRetainedDirectory(rootFrame);
|
|
838
|
+
mkdirSync(library, { recursive: true });
|
|
839
|
+
const libraryFrame = retainedDirectory(library);
|
|
840
|
+
const relation = relative(rootFrame.realpath, libraryFrame.realpath);
|
|
841
|
+
if ((process.platform === "win32" ? relation.toLowerCase() : relation) !== "library") {
|
|
842
|
+
throw new RetainedRecipeRefusal("storage", "Retained library escapes its Recipe root.");
|
|
843
|
+
}
|
|
844
|
+
const recheck = () => { recheckRetainedDirectory(rootFrame); recheckRetainedDirectory(libraryFrame); };
|
|
845
|
+
return withFileMutationLock(path, () => {
|
|
846
|
+
recheck();
|
|
847
|
+
retainedSlotAbsent(path);
|
|
848
|
+
stage = "persistence";
|
|
849
|
+
writeTextAtomic(path, bytes, { exclusive: true, onBeforeReplace: () => { recheck(); retainedSlotAbsent(path); } });
|
|
850
|
+
stage = "persisted_admission";
|
|
851
|
+
recheck();
|
|
852
|
+
retainedSlotAbsent(path, true);
|
|
853
|
+
const before = retainedFile(path);
|
|
854
|
+
if (readFileSync(path, "utf8") !== bytes)
|
|
855
|
+
throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
|
|
856
|
+
const admitted = RecipesDiscovery.admitUserRecipe(path, context);
|
|
857
|
+
if (!admitted.validated || !admitted.tool)
|
|
858
|
+
throw new RetainedRecipeRefusal("source", "Persisted retained Recipe admission failed.", admitted.diagnostics);
|
|
859
|
+
const after = retainedFile(path);
|
|
860
|
+
if (Object.keys(before).some(key => before[key] !== after[key]) ||
|
|
861
|
+
readFileSync(path, "utf8") !== bytes)
|
|
862
|
+
throw new RetainedRecipeRefusal("storage", "Persisted retained content changed.");
|
|
863
|
+
recheck();
|
|
864
|
+
retainedSlotAbsent(path, true);
|
|
865
|
+
stage = "receipt";
|
|
866
|
+
const args = RecipesDiscovery.summarizeRegisteredToolArgs(admitted.tool);
|
|
867
|
+
const source = from ? from.endsWith(".json") || from.endsWith(".md") ? `<explicit-file:${basename(from)}>` : from : "template";
|
|
868
|
+
const recipe = registrySelected("recipe", path), selectedName = registrySelected("tool", name);
|
|
869
|
+
const selectedSource = registrySelected("source", source);
|
|
870
|
+
const required = ToolsResponse.selectYamlField("required_args", args.required, REGISTRY_METADATA_BUDGET);
|
|
871
|
+
const optional = ToolsResponse.selectYamlField("optional_args", args.optional, REGISTRY_METADATA_BUDGET);
|
|
872
|
+
const facts = { kind: "retained_recipe", result: "retained", persisted: true, resolved: true,
|
|
873
|
+
validated: true, activation: "not_requested", async: admitted.async };
|
|
874
|
+
const text = growRegistryDocument(() => ({ ...selectedName.selected, ...facts, ...recipe.selected, ...selectedSource.selected,
|
|
875
|
+
...required.selected, ...optional.selected, next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } }), [recipe, selectedName, selectedSource, required, optional]);
|
|
876
|
+
const result = {
|
|
877
|
+
content: [textContent(ExecutionOutput.formatToolText(text))],
|
|
878
|
+
details: { ...facts, tool: name, recipe: path, source, required_args: args.required, optional_args: args.optional,
|
|
879
|
+
next_actions: ["inspect target=recipes view=summary verbose=true"] },
|
|
880
|
+
};
|
|
881
|
+
stage = "release";
|
|
882
|
+
return result;
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
const reason = error instanceof RetainedRecipeRefusal ? { reason: error.reason } : {};
|
|
889
|
+
throw new Error(ExecutionOutput.formatToolText(registryMetadataText({ operation: "retain", stage, result: "unconfirmed",
|
|
890
|
+
effects: "unknown; inspect retained inventory before retry", ...reason,
|
|
891
|
+
next: { tool: "inspect", target: "recipes", view: "summary", verbose: true } })), { cause: error });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
705
894
|
export async function executeRegisterTool(params, ctx, deps) {
|
|
706
895
|
const input = params;
|
|
896
|
+
let result;
|
|
707
897
|
if (!input.name)
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
898
|
+
result = executeRegisterToolUnlocked(params, ctx, deps);
|
|
899
|
+
else {
|
|
900
|
+
const name = Identity.normalizeToolName(input.name);
|
|
901
|
+
result = !name ? executeRegisterToolUnlocked(params, ctx, deps)
|
|
902
|
+
: withFileMutationLock(getToolRecipePath(deps, name), () => executeRegisterToolUnlocked(params, ctx, deps));
|
|
903
|
+
}
|
|
904
|
+
return typeof result === "function" ? result() : result;
|
|
713
905
|
}
|
|
@@ -66,7 +66,7 @@ export interface RunCompletionBatch {
|
|
|
66
66
|
/** Retained singleton capture; absence preserves older indexed bytes. Never a new-capture selector. */
|
|
67
67
|
readonly single_result?: true;
|
|
68
68
|
/** Captured record-list shape; no public format/version choice or source authority. */
|
|
69
|
-
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";
|
|
69
|
+
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";
|
|
70
70
|
/** Immutable scope/order/range data, not a caller-selected format or allocation authority. */
|
|
71
71
|
readonly numbering?: RunCompletionNumbering;
|
|
72
72
|
/** Formatting version/data only; no recipient ownership, read or publication capability. */
|
package/dist/lib/run-delivery.js
CHANGED
|
@@ -278,8 +278,11 @@ function parseInventorySource(value, batch) {
|
|
|
278
278
|
function isFrontmatterCompletionLayout(layout) {
|
|
279
279
|
return layout === "frontmatter_v1" || layout === "frontmatter_v2";
|
|
280
280
|
}
|
|
281
|
+
function isYamlCompletionLayout(layout) {
|
|
282
|
+
return layout === "completion_yaml_core_v1" || layout === "completion_yaml_files_v1";
|
|
283
|
+
}
|
|
281
284
|
function isSharedCompletionLayout(layout) {
|
|
282
|
-
return layout === "headings_shared_results" || layout === "headings_shared_kinds" || layout
|
|
285
|
+
return layout === "headings_shared_results" || layout === "headings_shared_kinds" || isYamlCompletionLayout(layout) || isFrontmatterCompletionLayout(layout);
|
|
283
286
|
}
|
|
284
287
|
function parseBatch(value) {
|
|
285
288
|
const record = asRecord(value, "Completion batch");
|
|
@@ -322,7 +325,7 @@ function parseBatch(value) {
|
|
|
322
325
|
throw new Error("Completion shared text budget is invalid");
|
|
323
326
|
}
|
|
324
327
|
const numberingDescriptor = Object.getOwnPropertyDescriptor(record, "numbering");
|
|
325
|
-
if (numberingDescriptor && (resultLayout
|
|
328
|
+
if (numberingDescriptor && (isYamlCompletionLayout(resultLayout) || !Object.hasOwn(numberingDescriptor, "value") || presentationFormat !== "ax_records" || (resultLayout !== "headings" && resultLayout !== "headings_flat_omissions" && resultLayout !== "headings_outcomes" && !isSharedCompletionLayout(resultLayout)))) {
|
|
326
329
|
throw new Error("Completion numbering requires a canonical captured heading layout");
|
|
327
330
|
}
|
|
328
331
|
const numbering = numberingDescriptor ? parseCompletionNumbering(numberingDescriptor.value, members.length) : undefined;
|
|
@@ -685,7 +688,7 @@ export function requestRunCompletionStream(input) {
|
|
|
685
688
|
});
|
|
686
689
|
}
|
|
687
690
|
export function admitRunCompletionBatch(input) {
|
|
688
|
-
return captureCompletionBatch(input, false, "
|
|
691
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v1");
|
|
689
692
|
}
|
|
690
693
|
/** Kernel-only opt-in; ordinary Pi capture stays fenced until session/lifecycle integration is admitted. */
|
|
691
694
|
export function captureRunCompletionStreamBatch(input) {
|
|
@@ -693,7 +696,7 @@ export function captureRunCompletionStreamBatch(input) {
|
|
|
693
696
|
}
|
|
694
697
|
/** Private ordinary candidate composition; stream capture retains its separately gated codec. */
|
|
695
698
|
export function captureRunCompletionYamlCandidateBatch(input) {
|
|
696
|
-
return captureCompletionBatch(input, false, "
|
|
699
|
+
return captureCompletionBatch(input, false, "completion_yaml_files_v1");
|
|
697
700
|
}
|
|
698
701
|
function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
|
|
699
702
|
const now = (input.now ?? new Date()).toISOString();
|
|
@@ -710,7 +713,7 @@ function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
|
|
|
710
713
|
members: input.members,
|
|
711
714
|
phase: "pending",
|
|
712
715
|
});
|
|
713
|
-
const draft = layout
|
|
716
|
+
const draft = isYamlCompletionLayout(layout)
|
|
714
717
|
? parseBatch({ ...captured, result_layout: layout }) : { ...captured, result_layout: layout };
|
|
715
718
|
return mutateJournal(input.tempDir, input.ownerId, (journal) => {
|
|
716
719
|
let batch = draft;
|
|
@@ -741,10 +744,10 @@ function captureCompletionBatch(input, numbered, layout = "frontmatter_v2") {
|
|
|
741
744
|
else if (journal.completion_stream) {
|
|
742
745
|
throw new Error("Completion stream capture requires the numbered kernel path; stream state is retained");
|
|
743
746
|
}
|
|
744
|
-
if (layout
|
|
747
|
+
if (!isYamlCompletionLayout(layout))
|
|
745
748
|
batch = retainSharedCompletionTexts(batch);
|
|
746
749
|
if (projectCompletionBatchMessage(batch).membersShown === batch.members.length) {
|
|
747
|
-
if (layout
|
|
750
|
+
if (isYamlCompletionLayout(layout))
|
|
748
751
|
batch = retainSharedCompletionTexts(batch);
|
|
749
752
|
journal.completion_batch = batch;
|
|
750
753
|
return batch;
|
|
@@ -1281,7 +1284,7 @@ function isRoutineCompletionSummary(member) {
|
|
|
1281
1284
|
return member.summary === (member.status === "done" ? "Run completed." : `Run ${member.status}.`);
|
|
1282
1285
|
}
|
|
1283
1286
|
function completionRecordRow(member, index, byIdentity, flat = false, layout, textBudget) {
|
|
1284
|
-
const frontmatter = isFrontmatterCompletionLayout(layout) || layout
|
|
1287
|
+
const frontmatter = isFrontmatterCompletionLayout(layout) || isYamlCompletionLayout(layout);
|
|
1285
1288
|
const simpleKinds = frontmatter || layout === "headings_shared_kinds", shared = isSharedCompletionLayout(layout);
|
|
1286
1289
|
const outcomes = shared || layout === "headings_outcomes", directOmission = outcomes || layout === "headings_flat_omissions";
|
|
1287
1290
|
const run = member.run.startsWith("run:") ? member.run.slice(4) : member.run;
|
|
@@ -1604,8 +1607,97 @@ function completionYamlReferences(row) {
|
|
|
1604
1607
|
})) };
|
|
1605
1608
|
return technical;
|
|
1606
1609
|
}
|
|
1610
|
+
/** Split selected paths without normalize/resolve/relative: dot and symlink routes must not be rewritten. */
|
|
1611
|
+
function rootedCompletionPath(path, runRoot) {
|
|
1612
|
+
if (typeof path !== "string" || !path.isWellFormed())
|
|
1613
|
+
return undefined;
|
|
1614
|
+
const windows = /^(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+[\\/])/u.test(path);
|
|
1615
|
+
const separator = windows ? /[\\/]/u : /\//u;
|
|
1616
|
+
if (!windows && !path.startsWith("/"))
|
|
1617
|
+
return undefined;
|
|
1618
|
+
if (path.split(separator).some(part => part === "." || part === ".."))
|
|
1619
|
+
return undefined;
|
|
1620
|
+
if (runRoot && path.startsWith(runRoot)) {
|
|
1621
|
+
const tail = path.slice(runRoot.length);
|
|
1622
|
+
if (separator.test(runRoot.slice(-1)) ? tail.length > 0 : separator.test(tail[0] ?? "")) {
|
|
1623
|
+
const relative = separator.test(runRoot.slice(-1)) ? tail : tail.slice(1);
|
|
1624
|
+
if (relative && !separator.test(relative[0]))
|
|
1625
|
+
return { root: runRoot, path: relative };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
let split = -1;
|
|
1629
|
+
for (let i = 0; i < path.length; i++)
|
|
1630
|
+
if (separator.test(path[i]))
|
|
1631
|
+
split = i;
|
|
1632
|
+
if (split < 0 || split === path.length - 1)
|
|
1633
|
+
return undefined;
|
|
1634
|
+
const end = split === 0 || (windows && split === 2 && path[1] === ":") ? split + 1 : split;
|
|
1635
|
+
return { root: path.slice(0, end), path: path.slice(split + 1) };
|
|
1636
|
+
}
|
|
1637
|
+
/** Body-free, read-free navigation over already selected references. Roots are presentation bases, not containment proof. */
|
|
1638
|
+
function completionRootedFiles(row, member, locations) {
|
|
1639
|
+
const technical = completionYamlReferences(row), files = {};
|
|
1640
|
+
const groups = [];
|
|
1641
|
+
const runRoot = locations?.state === "available" ? locations.directory : undefined;
|
|
1642
|
+
const group = (root) => {
|
|
1643
|
+
const found = groups.findIndex(item => item.root === root);
|
|
1644
|
+
if (found >= 0)
|
|
1645
|
+
return found;
|
|
1646
|
+
groups.push({ root, entries: [] });
|
|
1647
|
+
return groups.length - 1;
|
|
1648
|
+
};
|
|
1649
|
+
let priority = Infinity;
|
|
1650
|
+
const add = (path, entry, rank = Infinity, base) => {
|
|
1651
|
+
const selected = rootedCompletionPath(path, runRoot);
|
|
1652
|
+
const index = group(selected?.root ?? (base === undefined ? null : completionYamlString(base)));
|
|
1653
|
+
const selectedPath = selected?.path ?? path;
|
|
1654
|
+
groups[index].entries.push({ ...entry, path: selectedPath,
|
|
1655
|
+
...(!selected && base === undefined ? { path_basis: "as_recorded", base_unavailable: true } : {}) });
|
|
1656
|
+
if (selected && rank < priority) {
|
|
1657
|
+
priority = rank;
|
|
1658
|
+
files.read_first = { group: index, entry: groups[index].entries.length - 1 };
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
for (const [role, purpose, rank] of [
|
|
1662
|
+
["primary", "Full result text", 0], ["diagnostic_metadata", "Recorded error and command metadata", 1],
|
|
1663
|
+
["artifact_inventory", "Declared artifact inventory; target availability unproven", 2],
|
|
1664
|
+
]) {
|
|
1665
|
+
const detail = technical[role];
|
|
1666
|
+
if (!detail)
|
|
1667
|
+
continue;
|
|
1668
|
+
const { source: rawSource, ...facts } = detail;
|
|
1669
|
+
const { path, ...binding } = rawSource;
|
|
1670
|
+
add(path, { role, purpose, ...facts, source: binding }, rank);
|
|
1671
|
+
delete technical[role];
|
|
1672
|
+
}
|
|
1673
|
+
const artifacts = technical.artifacts;
|
|
1674
|
+
if (artifacts) {
|
|
1675
|
+
for (const item of artifacts.declarations)
|
|
1676
|
+
add(item.path, { role: "artifact", purpose: "Declared artifact", name: item.name, scope: artifacts.scope, state: "declared" });
|
|
1677
|
+
delete technical.artifacts;
|
|
1678
|
+
}
|
|
1679
|
+
if (locations?.state === "available") {
|
|
1680
|
+
const { directory, files: selectedFiles, ...observation } = locations;
|
|
1681
|
+
files.observation = { ...observation, run: completionYamlString(locations.run), run_instance_id: completionYamlString(locations.run_instance_id),
|
|
1682
|
+
group: group(completionYamlString(directory)) };
|
|
1683
|
+
for (const item of selectedFiles) {
|
|
1684
|
+
const { path, ...facts } = item;
|
|
1685
|
+
const purpose = item.role === "result" ? "Recorded result metadata" : item.role === "execution" ? "Recorded execution and capture locations" : "Recorded Run file";
|
|
1686
|
+
add(typeof path === "string" ? completionYamlString(path) : null, { ...facts, purpose }, item.state === "present" && item.role === "result" ? 3 : item.state === "present" && item.role === "execution" ? 4 : Infinity, directory);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
else if (locations)
|
|
1690
|
+
files.observation = { ...locations, run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id) };
|
|
1691
|
+
if (groups.length) {
|
|
1692
|
+
files.groups = groups;
|
|
1693
|
+
}
|
|
1694
|
+
if (Object.keys(technical).length)
|
|
1695
|
+
files.notes = technical;
|
|
1696
|
+
return files;
|
|
1697
|
+
}
|
|
1607
1698
|
function prepareCompletionYamlProjection(safe, candidates, references, ordered) {
|
|
1608
1699
|
const packet = { batchId: safe.batch_id, records: [] };
|
|
1700
|
+
const rooted = safe.result_layout === "completion_yaml_files_v1";
|
|
1609
1701
|
if (!safe.batch_id.isWellFormed())
|
|
1610
1702
|
return { state: "unsupported" };
|
|
1611
1703
|
for (const [index, { row, location }] of candidates.entries()) {
|
|
@@ -1630,18 +1722,24 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
|
|
|
1630
1722
|
else
|
|
1631
1723
|
metadata[key] = completionYamlString(value);
|
|
1632
1724
|
}
|
|
1633
|
-
const
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1725
|
+
const navigation = references.has(index) && location
|
|
1726
|
+
? safe.file_navigation.find(item => safe.members[item.member_index] === member).locations : undefined;
|
|
1727
|
+
if (rooted) {
|
|
1728
|
+
const files = completionRootedFiles(row, member, navigation);
|
|
1729
|
+
if (Object.keys(files).length)
|
|
1730
|
+
metadata.files = files;
|
|
1731
|
+
}
|
|
1732
|
+
else {
|
|
1733
|
+
const technical = completionYamlReferences(row);
|
|
1734
|
+
if (Object.keys(technical).length)
|
|
1735
|
+
metadata.references = technical;
|
|
1736
|
+
if (navigation) {
|
|
1737
|
+
metadata.run_files = { run: completionYamlString(member.run), run_instance_id: completionYamlString(member.run_instance_id),
|
|
1738
|
+
locations: navigation.state === "available" ? { ...navigation, run: completionYamlString(navigation.run),
|
|
1739
|
+
run_instance_id: completionYamlString(navigation.run_instance_id), directory: completionYamlString(navigation.directory),
|
|
1740
|
+
files: navigation.files.map(file => ({ ...file, ...(typeof file.path === "string" ? { path: completionYamlString(file.path) } : {}) })) }
|
|
1741
|
+
: { ...navigation } };
|
|
1742
|
+
}
|
|
1645
1743
|
}
|
|
1646
1744
|
const parts = [];
|
|
1647
1745
|
const add = (part) => { if (parts.length)
|
|
@@ -1650,13 +1748,22 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
|
|
|
1650
1748
|
if (piece.block) {
|
|
1651
1749
|
if (!piece.text.isWellFormed())
|
|
1652
1750
|
return { state: "unsupported" };
|
|
1653
|
-
|
|
1751
|
+
if (rooted && piece.block === "marker" && !/^\[[0-9]+ characters omitted\]$/u.test(piece.text)) {
|
|
1752
|
+
metadata.text_warnings ??= [];
|
|
1753
|
+
metadata.text_warnings.push(piece.text);
|
|
1754
|
+
}
|
|
1755
|
+
else
|
|
1756
|
+
add(piece.block === "body" ? { body: piece.text } : { marker: piece.text });
|
|
1654
1757
|
}
|
|
1655
|
-
if (safe.file_navigation !== undefined && !references.has(index))
|
|
1656
|
-
|
|
1758
|
+
if (safe.file_navigation !== undefined && !references.has(index)) {
|
|
1759
|
+
if (rooted)
|
|
1760
|
+
metadata.run_files_omission = { reason: "packet_byte_limit", shown: 0, omitted: 1, total: 1 };
|
|
1761
|
+
else
|
|
1762
|
+
add({ marker: "Run-file reference omitted (packet byte limit)." });
|
|
1763
|
+
}
|
|
1657
1764
|
packet.records.push({ metadata, parts });
|
|
1658
1765
|
}
|
|
1659
|
-
const service = {};
|
|
1766
|
+
const service = rooted ? { header_layout: "rooted_files_v1" } : {};
|
|
1660
1767
|
const omitted = safe.members.length - candidates.length;
|
|
1661
1768
|
if (omitted) {
|
|
1662
1769
|
service.omitted_results = omitted;
|
|
@@ -1676,8 +1783,11 @@ function prepareCompletionYamlProjection(safe, candidates, references, ordered)
|
|
|
1676
1783
|
if (safe.numbering.kind === "unavailable")
|
|
1677
1784
|
service.numbering_unavailable = safe.numbering.reason;
|
|
1678
1785
|
}
|
|
1679
|
-
if (safe.inventory_source)
|
|
1680
|
-
|
|
1786
|
+
if (safe.inventory_source) {
|
|
1787
|
+
const path = rooted ? rootedCompletionPath(safe.inventory_source.path) : undefined;
|
|
1788
|
+
service.result_inventory = { ...safe.inventory_source,
|
|
1789
|
+
...(path ? { root: path.root, path: path.path } : { path: completionYamlString(safe.inventory_source.path) }) };
|
|
1790
|
+
}
|
|
1681
1791
|
if (Object.keys(service).length)
|
|
1682
1792
|
packet.service = service;
|
|
1683
1793
|
const chunks = [];
|
|
@@ -1737,7 +1847,7 @@ export function projectRunCompletionYamlPrototype(batch) {
|
|
|
1737
1847
|
throw error;
|
|
1738
1848
|
}
|
|
1739
1849
|
}
|
|
1740
|
-
function projectCompletionRecords(safe, assembler = safe.result_layout
|
|
1850
|
+
function projectCompletionRecords(safe, assembler = isYamlCompletionLayout(safe.result_layout) ? "yaml" : "legacy") {
|
|
1741
1851
|
const frontmatter = isFrontmatterCompletionLayout(safe.result_layout) || assembler === "yaml";
|
|
1742
1852
|
const shared = isSharedCompletionLayout(safe.result_layout);
|
|
1743
1853
|
const maxBytes = shared ? Limits.RUN_COMPLETION_MODEL_MAX_BYTES : Limits.RUN_DELIVERY_MODEL_MAX_BYTES;
|
|
@@ -2024,7 +2134,7 @@ export function projectRunCompletionBatchPresentation(batch) {
|
|
|
2024
2134
|
if (safe.presentation_format !== "ax_records" && safe.presentation_format !== "ax_records_v3")
|
|
2025
2135
|
return { content: projectCompletionBatchMessage(safe).message };
|
|
2026
2136
|
const result = safe.result_layout !== undefined ? projectCompletionRecords(safe) : projectRetainedRecordCompletionBatch(safe);
|
|
2027
|
-
if (safe.result_layout
|
|
2137
|
+
if (isYamlCompletionLayout(safe.result_layout))
|
|
2028
2138
|
return { content: result.message, presentation: Object.freeze({
|
|
2029
2139
|
format: "completion_yaml_core_v1", content_sha256: createHash("sha256").update(result.message).digest("hex"),
|
|
2030
2140
|
}) };
|
|
@@ -2192,10 +2302,14 @@ export function readRunCompletionPresentation(content, value) {
|
|
|
2192
2302
|
hint.content_sha256 !== createHash("sha256").update(content).digest("hex"))
|
|
2193
2303
|
return full;
|
|
2194
2304
|
const segments = [];
|
|
2305
|
+
const rooted = core.packet.service?.header_layout === "rooted_files_v1";
|
|
2306
|
+
const facts = Object.fromEntries(Object.entries(core.packet.service ?? {}).filter(([key]) => key !== "header_layout"));
|
|
2195
2307
|
for (const [index, record] of core.packet.records.entries()) {
|
|
2196
2308
|
if (index)
|
|
2197
2309
|
segments.push({ kind: "separator", text: "\n" });
|
|
2198
|
-
const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => key !== "references" && key !== "run_files"));
|
|
2310
|
+
const metadata = Object.fromEntries(Object.entries(record.metadata).filter(([key]) => rooted || (key !== "references" && key !== "run_files")));
|
|
2311
|
+
if (rooted && index === 0 && Object.keys(facts).length)
|
|
2312
|
+
metadata.delivery = facts;
|
|
2199
2313
|
const header = renderYamlMetadata(metadata, completionYamlMaxBytes);
|
|
2200
2314
|
if (header.state !== "available")
|
|
2201
2315
|
return full;
|
|
@@ -2203,13 +2317,13 @@ export function readRunCompletionPresentation(content, value) {
|
|
|
2203
2317
|
for (const part of record.parts)
|
|
2204
2318
|
segments.push("body" in part ? { kind: "body", text: part.body } : { kind: "marker", text: part.marker });
|
|
2205
2319
|
}
|
|
2206
|
-
if (core.packet.service) {
|
|
2207
|
-
const service = renderYamlMetadata(core.packet.service, completionYamlMaxBytes);
|
|
2320
|
+
if (core.packet.service && (!rooted || !core.packet.records.length)) {
|
|
2321
|
+
const service = renderYamlMetadata(rooted ? facts : core.packet.service, completionYamlMaxBytes);
|
|
2208
2322
|
if (service.state !== "available")
|
|
2209
2323
|
return full;
|
|
2210
2324
|
if (segments.length)
|
|
2211
2325
|
segments.push({ kind: "separator", text: "\n" });
|
|
2212
|
-
segments.push({ kind: "service", text: service.text.replace(/\n$/u, "") });
|
|
2326
|
+
segments.push({ kind: rooted ? "header" : "service", text: service.text.replace(/\n$/u, "") });
|
|
2213
2327
|
}
|
|
2214
2328
|
return { text: segments.map(segment => segment.text).join("\n"), compact: true, records: true, yaml: true,
|
|
2215
2329
|
segments: Object.freeze(segments.map(segment => Object.freeze(segment))) };
|
|
@@ -364,9 +364,6 @@ function inspectRecipes(requestedView, input, deps, context) {
|
|
|
364
364
|
if (view === "reviews") {
|
|
365
365
|
return ReviewDiagnostics.readAutomaticReviewDiagnostics({ recipeRoot });
|
|
366
366
|
}
|
|
367
|
-
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
368
|
-
{ root: recipeRoot, defaultTool: true, mutableUsage: true },
|
|
369
|
-
]);
|
|
370
367
|
const resolutionContext = context && typeof context === "object" && "recipeResolutionContext" in context
|
|
371
368
|
? context.recipeResolutionContext
|
|
372
369
|
: undefined;
|
|
@@ -399,7 +396,11 @@ function inspectRecipes(requestedView, input, deps, context) {
|
|
|
399
396
|
skill: diagnostic.skill,
|
|
400
397
|
...(diagnostic.stem ? { stem: diagnostic.stem } : {}),
|
|
401
398
|
}));
|
|
399
|
+
const discovered = RecipesDiscovery.discoverRecipeSources([
|
|
400
|
+
{ root: recipeRoot, defaultTool: true, mutableUsage: true, resolutionContext },
|
|
401
|
+
]);
|
|
402
402
|
const discoverySummary = RecipesDiscovery.summarizeDiscovery(discovered);
|
|
403
|
+
const retained = RecipesDiscovery.inventoryRetainedRecipes(recipeRoot, resolutionContext);
|
|
403
404
|
const summary = {
|
|
404
405
|
...discoverySummary,
|
|
405
406
|
...(registryStatus
|
|
@@ -425,6 +426,8 @@ function inspectRecipes(requestedView, input, deps, context) {
|
|
|
425
426
|
error: `Ambiguous active Skill Recipe namespace ${name}`,
|
|
426
427
|
})),
|
|
427
428
|
drafts: RecipesDiscovery.listDraftRecipes(join(recipeRoot, "drafts")),
|
|
429
|
+
retained: retained.recipes,
|
|
430
|
+
retained_diagnostics: retained.diagnostics,
|
|
428
431
|
};
|
|
429
432
|
return {
|
|
430
433
|
...summary,
|
|
@@ -26,6 +26,7 @@ export function createRegisterToolDefinition(deps) {
|
|
|
26
26
|
description: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.description),
|
|
27
27
|
defaults: looseObjectSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.defaults),
|
|
28
28
|
draft: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.draft),
|
|
29
|
+
expose: booleanSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.expose),
|
|
29
30
|
from: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.from),
|
|
30
31
|
name: stringSchema(Prompts.REGISTER_TOOL_PARAM_DESCRIPTIONS.name),
|
|
31
32
|
template: unionSchema([
|
|
@@ -512,6 +512,10 @@ export function recipeRegistryNextActions(summary, view) {
|
|
|
512
512
|
if (summary.skill_recipe_catalog_partial === true) {
|
|
513
513
|
actions.push("inspect target=recipes view=imports verbose=true");
|
|
514
514
|
}
|
|
515
|
+
if ((Array.isArray(summary.retained) && summary.retained.length > 0) ||
|
|
516
|
+
(Array.isArray(summary.retained_diagnostics) && summary.retained_diagnostics.length > 0)) {
|
|
517
|
+
actions.push("inspect target=recipes view=summary verbose=true");
|
|
518
|
+
}
|
|
515
519
|
if (drafts.length > 0) {
|
|
516
520
|
actions.push("inspect target=recipes view=summary verbose=true");
|
|
517
521
|
const firstPath = typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
|
|
@@ -555,6 +559,7 @@ export function compactRecipeRegistry(summary) {
|
|
|
555
559
|
const active = count("active", summary.active), shadowed = count("shadowed", summary.shadowed);
|
|
556
560
|
const invalid = count("invalid", summary.invalid), disabled = count("disabled", summary.disabled);
|
|
557
561
|
const diagnostics = count("diagnostics", summary.diagnostics), drafts = count("drafts", summary.drafts);
|
|
562
|
+
const retained = count("retained", summary.retained);
|
|
558
563
|
const recommendations = count("recommendations", summary.recommendations);
|
|
559
564
|
const skillCatalogPartial = summary.skill_recipe_catalog_partial === true;
|
|
560
565
|
const currentPolicy = Array.isArray(summary.active)
|
|
@@ -564,7 +569,7 @@ export function compactRecipeRegistry(summary) {
|
|
|
564
569
|
? summary.next_actions
|
|
565
570
|
: [];
|
|
566
571
|
const hint = nextActions.length ? nextActionMetadata(nextActions[0], Limits.RUN_DELIVERY_MODEL_MAX_BYTES) : undefined;
|
|
567
|
-
const render = () => ({ recipes: "registry", active, drafts, shadowed, invalid, disabled,
|
|
572
|
+
const render = () => ({ recipes: "registry", active, drafts, retained, shadowed, invalid, disabled,
|
|
568
573
|
current_policy: currentPolicy, skill_catalog_partial: skillCatalogPartial, recommendations, diagnostics,
|
|
569
574
|
...(hint ? hint.selected : {}) });
|
|
570
575
|
return hint
|