@llblab/pi-actors 0.43.1 → 0.45.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 +6 -6
- package/BACKLOG.md +1 -189
- package/CHANGELOG.md +419 -402
- package/README.md +8 -6
- package/dist/index.js +7 -3
- package/dist/lib/async-runs.d.ts +2 -0
- package/dist/lib/async-runs.js +41 -34
- package/dist/lib/config.js +1 -0
- package/dist/lib/file-state.d.ts +3 -1
- package/dist/lib/file-state.js +25 -7
- package/dist/lib/inspector-overlay.js +46 -30
- package/dist/lib/inspector.js +8 -2
- package/dist/lib/limits.d.ts +10 -0
- package/dist/lib/limits.js +10 -0
- package/dist/lib/observability.d.ts +4 -2
- package/dist/lib/observability.js +43 -36
- package/dist/lib/recipes-discovery.js +20 -3
- package/dist/lib/recipes-references.d.ts +10 -0
- package/dist/lib/recipes-references.js +168 -5
- package/dist/lib/run-evidence-policy.d.ts +95 -0
- package/dist/lib/run-evidence-policy.js +177 -0
- package/dist/lib/run-ui-runtime.js +2 -0
- package/dist/lib/runs-controls.d.ts +7 -5
- package/dist/lib/runs-controls.js +186 -49
- package/dist/lib/runs-retention.js +27 -14
- package/dist/lib/runs-trace.d.ts +25 -1
- package/dist/lib/runs-trace.js +410 -21
- package/dist/lib/runtime-triage.js +6 -22
- package/dist/lib/schema.d.ts +3 -0
- package/dist/lib/schema.js +34 -1
- package/dist/lib/tools-inspect.js +58 -14
- package/dist/lib/tools-local.js +27 -10
- package/dist/lib/tools-spawn.js +2 -2
- package/dist/lib/trace-projection.js +90 -44
- package/dist/scripts/conformance.mjs +5 -0
- package/dist/scripts/locker.mjs +31 -74
- package/dist/scripts/music-player.mjs +41 -129
- package/dist/scripts/release-gates.mjs +26 -4
- package/dist/skills/actors/SKILL.md +9 -8
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/README.md +0 -1
- package/docs/actor-inspector.md +3 -2
- package/docs/async-runs.md +10 -8
- package/docs/command-templates.md +2 -2
- package/docs/recipe-library.md +4 -2
- package/docs/template-recipes.md +10 -4
- package/docs/tool-registry.md +1 -1
- package/index.ts +9 -3
- package/lib/async-runs.ts +51 -54
- package/lib/config.ts +4 -0
- package/lib/file-state.ts +28 -7
- package/lib/inspector-overlay.ts +29 -17
- package/lib/inspector.ts +16 -2
- package/lib/limits.ts +10 -0
- package/lib/observability.ts +55 -57
- package/lib/recipes-discovery.ts +23 -3
- package/lib/recipes-references.ts +227 -7
- package/lib/run-evidence-policy.ts +242 -0
- package/lib/run-ui-runtime.ts +2 -0
- package/lib/runs-controls.ts +177 -108
- package/lib/runs-retention.ts +28 -20
- package/lib/runs-trace.ts +496 -21
- package/lib/runtime-triage.ts +11 -25
- package/lib/schema.ts +53 -1
- package/lib/tools-inspect.ts +56 -15
- package/lib/tools-local.ts +49 -19
- package/lib/tools-spawn.ts +2 -2
- package/lib/trace-projection.ts +137 -75
- package/package.json +1 -1
- package/scripts/conformance.mjs +5 -0
- package/scripts/locker.mjs +31 -74
- package/scripts/music-player.mjs +41 -129
- package/scripts/release-gates.mjs +26 -4
- package/skills/actors/SKILL.md +9 -8
- package/skills/swarm/SKILL.md +2 -2
- package/dist/lib/runtime-notifier.d.ts +0 -48
- package/dist/lib/runtime-notifier.js +0 -138
- package/docs/0.43-baseline.md +0 -39
- package/lib/runtime-notifier.ts +0 -211
package/lib/recipes-discovery.ts
CHANGED
|
@@ -122,6 +122,25 @@ function getRecipeCommandTemplateConfig(
|
|
|
122
122
|
: config) as CommandTemplates.CommandTemplateConfig;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
function getRecipeDiagnosticTemplateConfig(
|
|
126
|
+
config: TemplateRecipeConfig,
|
|
127
|
+
): CommandTemplates.CommandTemplateObjectConfig {
|
|
128
|
+
const templateConfig = getRecipeCommandTemplateConfig(config);
|
|
129
|
+
return typeof templateConfig === "object" && !Array.isArray(templateConfig)
|
|
130
|
+
? {
|
|
131
|
+
...templateConfig,
|
|
132
|
+
defaults: {
|
|
133
|
+
...(templateConfig.defaults ?? {}),
|
|
134
|
+
...(config.defaults ?? {}),
|
|
135
|
+
...(config.values ?? {}),
|
|
136
|
+
},
|
|
137
|
+
}
|
|
138
|
+
: {
|
|
139
|
+
defaults: { ...(config.defaults ?? {}), ...(config.values ?? {}) },
|
|
140
|
+
template: templateConfig,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
function getRecipeConfigDiagnostics(
|
|
126
145
|
file: string,
|
|
127
146
|
config: TemplateRecipeConfig | undefined,
|
|
@@ -131,7 +150,7 @@ function getRecipeConfigDiagnostics(
|
|
|
131
150
|
return [`Invalid recipe: ${file}${reason ? `: ${reason}` : ""}`];
|
|
132
151
|
}
|
|
133
152
|
return CommandTemplates.getCommandTemplateWarnings(
|
|
134
|
-
|
|
153
|
+
getRecipeDiagnosticTemplateConfig(config),
|
|
135
154
|
).map((warning) => `Recipe ${file}: ${warning}`);
|
|
136
155
|
}
|
|
137
156
|
|
|
@@ -141,7 +160,7 @@ function getRecipeRiskLabels(
|
|
|
141
160
|
if (!config) return [];
|
|
142
161
|
const labels = new Set(
|
|
143
162
|
CommandTemplates.getCommandTemplateRiskLabels(
|
|
144
|
-
|
|
163
|
+
getRecipeDiagnosticTemplateConfig(config),
|
|
145
164
|
),
|
|
146
165
|
);
|
|
147
166
|
if (config.async === true) labels.add("risk.long_running");
|
|
@@ -846,9 +865,10 @@ export function toRegisteredTool(
|
|
|
846
865
|
assertToolSafeRepeatConfig(
|
|
847
866
|
argTemplateConfig,
|
|
848
867
|
explicitArgTypes,
|
|
849
|
-
cfg.defaults ?? {},
|
|
868
|
+
{ ...(cfg.defaults ?? {}), ...(cfg.values ?? {}) },
|
|
850
869
|
);
|
|
851
870
|
const argTypes = Schema.getTemplateArgTypes(argTemplateConfig);
|
|
871
|
+
Schema.assertCompatibleToolArgTypes(explicitArgTypes, argTypes);
|
|
852
872
|
return {
|
|
853
873
|
name: entry.id,
|
|
854
874
|
description,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { basename, dirname, extname, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, isAbsolute, relative, resolve } from "node:path";
|
|
10
10
|
|
|
11
11
|
import type {
|
|
12
12
|
CommandTemplateConfig,
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
import * as CommandTemplates from "./command-templates.ts";
|
|
16
16
|
import * as Paths from "./paths.ts";
|
|
17
17
|
import * as RecipeControl from "./recipe-control.ts";
|
|
18
|
+
import * as Schema from "./schema.ts";
|
|
18
19
|
|
|
19
20
|
const MAX_RECIPE_FILE_BYTES = 1024 * 1024;
|
|
20
21
|
const MAX_RECIPE_IMPORT_DEPTH = 32;
|
|
@@ -57,6 +58,8 @@ export interface TemplateRecipeDefinition {
|
|
|
57
58
|
|
|
58
59
|
export interface TemplateRecipeConfig extends TemplateRecipeDefinition {
|
|
59
60
|
async?: boolean;
|
|
61
|
+
recipe_dir?: string;
|
|
62
|
+
skill_dir?: string;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
interface ImportedRecipe {
|
|
@@ -74,6 +77,7 @@ export interface TemplateRecipeContextRecord {
|
|
|
74
77
|
file: string;
|
|
75
78
|
import_path: string[];
|
|
76
79
|
name: string;
|
|
80
|
+
qualified_name?: string;
|
|
77
81
|
recipe: Record<string, unknown>;
|
|
78
82
|
role: "entry" | "import";
|
|
79
83
|
}
|
|
@@ -82,10 +86,120 @@ export interface ReadResolvedRecipeConfigOptions {
|
|
|
82
86
|
includeActorRecipeContext?: boolean;
|
|
83
87
|
}
|
|
84
88
|
|
|
89
|
+
const RUNTIME_RECIPE_VALUE_NAMES = new Set(["recipe_dir", "skill_dir"]);
|
|
90
|
+
const activeSkillRecipeRoots = new Map<string, Set<string>>();
|
|
91
|
+
|
|
92
|
+
export interface ActiveSkillRecipeSource {
|
|
93
|
+
name: string;
|
|
94
|
+
filePath?: string;
|
|
95
|
+
baseDir?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function setActiveSkillRecipeSources(
|
|
99
|
+
skills: ActiveSkillRecipeSource[],
|
|
100
|
+
): void {
|
|
101
|
+
activeSkillRecipeRoots.clear();
|
|
102
|
+
for (const skill of skills) {
|
|
103
|
+
const name = skill.name.trim();
|
|
104
|
+
const skillDir = skill.baseDir
|
|
105
|
+
? resolve(skill.baseDir)
|
|
106
|
+
: skill.filePath
|
|
107
|
+
? dirname(resolve(skill.filePath))
|
|
108
|
+
: undefined;
|
|
109
|
+
if (!name || !skillDir) continue;
|
|
110
|
+
const roots = activeSkillRecipeRoots.get(name) ?? new Set<string>();
|
|
111
|
+
roots.add(resolve(skillDir, "recipes"));
|
|
112
|
+
activeSkillRecipeRoots.set(name, roots);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getActiveSkillRecipeNamespaces(): Record<string, string[]> {
|
|
117
|
+
return Object.fromEntries(
|
|
118
|
+
[...activeSkillRecipeRoots.entries()]
|
|
119
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
120
|
+
.map(([name, roots]) => [name, [...roots].sort()]),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function qualifiedRecipeNameForFile(file: string): string | undefined {
|
|
125
|
+
const path = resolve(file);
|
|
126
|
+
const stdRelation = relative(Paths.getPackagedRecipeRoot(), path);
|
|
127
|
+
if (
|
|
128
|
+
stdRelation &&
|
|
129
|
+
!stdRelation.startsWith("..") &&
|
|
130
|
+
!isAbsolute(stdRelation)
|
|
131
|
+
) {
|
|
132
|
+
return `std:${stdRelation.replace(/\.(?:json|md)$/u, "")}`;
|
|
133
|
+
}
|
|
134
|
+
for (const [skillName, roots] of activeSkillRecipeRoots) {
|
|
135
|
+
if (roots.size !== 1) continue;
|
|
136
|
+
const root = [...roots][0];
|
|
137
|
+
const relation = relative(root, path);
|
|
138
|
+
if (relation && !relation.startsWith("..") && !isAbsolute(relation))
|
|
139
|
+
return `skill:${skillName}/${relation.replaceAll("\\", "/").replace(/\.(?:json|md)$/u, "")}`;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function qualifiedRecipeCandidates(value: string): string[] | undefined {
|
|
145
|
+
if (value.startsWith("std:")) {
|
|
146
|
+
return recipeNameFiles(value.slice(4)).map((file) =>
|
|
147
|
+
resolve(Paths.getPackagedRecipeRoot(), file),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!value.startsWith("skill:")) return undefined;
|
|
151
|
+
const qualified = value.slice(6);
|
|
152
|
+
const separator = qualified.indexOf("/");
|
|
153
|
+
if (separator <= 0 || separator === qualified.length - 1)
|
|
154
|
+
throw new Error(`Invalid Skill Recipe reference: ${value}`);
|
|
155
|
+
const skillName = qualified.slice(0, separator);
|
|
156
|
+
const recipeName = qualified.slice(separator + 1);
|
|
157
|
+
const roots = [...(activeSkillRecipeRoots.get(skillName) ?? [])];
|
|
158
|
+
if (roots.length === 0)
|
|
159
|
+
throw new Error(`Active Skill Recipe namespace not found: ${skillName}`);
|
|
160
|
+
if (roots.length > 1)
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Ambiguous active Skill Recipe namespace ${skillName}: ${roots.join(", ")}`,
|
|
163
|
+
);
|
|
164
|
+
const root = roots[0];
|
|
165
|
+
return recipeNameFiles(recipeName).map((file) => {
|
|
166
|
+
const candidate = resolve(root, file);
|
|
167
|
+
const relation = relative(root, candidate);
|
|
168
|
+
if (!relation || relation.startsWith("..") || isAbsolute(relation))
|
|
169
|
+
throw new Error(`Skill Recipe reference escapes its recipes root: ${value}`);
|
|
170
|
+
return candidate;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
85
174
|
function hasWhitespace(value: string): boolean {
|
|
86
175
|
return /\s/.test(value);
|
|
87
176
|
}
|
|
88
177
|
|
|
178
|
+
function findOwningSkillDir(recipeFile: string): string | undefined {
|
|
179
|
+
let current = dirname(recipeFile);
|
|
180
|
+
while (true) {
|
|
181
|
+
if (existsSync(resolve(current, "SKILL.md"))) return current;
|
|
182
|
+
const parent = dirname(current);
|
|
183
|
+
if (parent === current) return undefined;
|
|
184
|
+
current = parent;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function assertRuntimeRecipeValuesNotDeclared(raw: Record<string, unknown>): void {
|
|
189
|
+
const args = Array.isArray(raw.args)
|
|
190
|
+
? Schema.getExplicitToolArgNames(raw.args.map(String))
|
|
191
|
+
: [];
|
|
192
|
+
for (const name of RUNTIME_RECIPE_VALUE_NAMES) {
|
|
193
|
+
if (
|
|
194
|
+
args.includes(name) ||
|
|
195
|
+
(isRecord(raw.defaults) && Object.hasOwn(raw.defaults, name)) ||
|
|
196
|
+
(isRecord(raw.values) && Object.hasOwn(raw.values, name))
|
|
197
|
+
) {
|
|
198
|
+
throw new Error(`Recipe runtime value is reserved: ${name}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
89
203
|
export function resolveRecipePath(
|
|
90
204
|
value: string,
|
|
91
205
|
recipeRoot = Paths.getRecipeRoot(),
|
|
@@ -125,6 +239,8 @@ function recipeCandidatePaths(
|
|
|
125
239
|
value: string,
|
|
126
240
|
currentRecipeRoot: string,
|
|
127
241
|
): string[] {
|
|
242
|
+
const qualified = qualifiedRecipeCandidates(value.trim());
|
|
243
|
+
if (qualified) return qualified;
|
|
128
244
|
if (!isBareRecipeName(value))
|
|
129
245
|
return [resolveRecipePath(value, currentRecipeRoot)];
|
|
130
246
|
const roots = [
|
|
@@ -646,6 +762,63 @@ function mergeDefaults(
|
|
|
646
762
|
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
647
763
|
}
|
|
648
764
|
|
|
765
|
+
function assertRecipeDefaultsDeclared(
|
|
766
|
+
args: string[] | undefined,
|
|
767
|
+
defaults: Record<string, unknown> | undefined,
|
|
768
|
+
label: string,
|
|
769
|
+
): void {
|
|
770
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
771
|
+
if (spec.error) throw new Error(spec.error);
|
|
772
|
+
if (spec.args.length === 0) return;
|
|
773
|
+
const declared = new Set(spec.args);
|
|
774
|
+
for (const key of Object.keys(defaults ?? {})) {
|
|
775
|
+
if (!declared.has(key)) throw new Error(`Unknown ${label} argument: ${key}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function assertRecipeArgumentContract(
|
|
780
|
+
config: Pick<
|
|
781
|
+
TemplateRecipeDefinition,
|
|
782
|
+
"args" | "defaults" | "values" | "template"
|
|
783
|
+
>,
|
|
784
|
+
label = "Recipe default",
|
|
785
|
+
): void {
|
|
786
|
+
assertRecipeDefaultsDeclared(config.args, config.defaults, label);
|
|
787
|
+
const spec = Schema.parseToolArgDeclarationList(config.args ?? []);
|
|
788
|
+
if (spec.error) throw new Error(spec.error);
|
|
789
|
+
Schema.assertCompatibleToolArgTypes(
|
|
790
|
+
spec.argTypes,
|
|
791
|
+
Schema.getTemplateArgTypes({ template: config.template }),
|
|
792
|
+
);
|
|
793
|
+
const staticValues = Object.fromEntries(
|
|
794
|
+
Object.entries({ ...config.defaults, ...config.values }).filter(
|
|
795
|
+
([, value]) =>
|
|
796
|
+
typeof value !== "string" || !/\{[^{}]+\}/.test(value),
|
|
797
|
+
),
|
|
798
|
+
);
|
|
799
|
+
Schema.normalizeRuntimeValues(staticValues, spec.argTypes);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function resolveRecipeValueLayers(
|
|
803
|
+
args: string[] | undefined,
|
|
804
|
+
...layers: Array<Record<string, unknown> | undefined>
|
|
805
|
+
): Record<string, unknown> | undefined {
|
|
806
|
+
const spec = Schema.parseToolArgDeclarationList(args ?? []);
|
|
807
|
+
if (spec.error) throw new Error(spec.error);
|
|
808
|
+
const merged = mergeDefaults(spec.defaults, ...layers);
|
|
809
|
+
if (!merged) return undefined;
|
|
810
|
+
const staticValues = Object.fromEntries(
|
|
811
|
+
Object.entries(merged).filter(
|
|
812
|
+
([, value]) =>
|
|
813
|
+
typeof value !== "string" || !/\{[^{}]+\}/.test(value),
|
|
814
|
+
),
|
|
815
|
+
);
|
|
816
|
+
return {
|
|
817
|
+
...merged,
|
|
818
|
+
...Schema.normalizeRuntimeValues(staticValues, spec.argTypes),
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
649
822
|
function applyDefaultsToTemplate(
|
|
650
823
|
template: CommandTemplateValue,
|
|
651
824
|
defaults: Record<string, unknown> | undefined,
|
|
@@ -655,6 +828,7 @@ function applyDefaultsToTemplate(
|
|
|
655
828
|
delete cleanOverrides.name;
|
|
656
829
|
delete cleanOverrides.template;
|
|
657
830
|
delete cleanOverrides.values;
|
|
831
|
+
delete cleanOverrides.defaults;
|
|
658
832
|
if (typeof template === "object" && !Array.isArray(template)) {
|
|
659
833
|
return {
|
|
660
834
|
...template,
|
|
@@ -716,11 +890,22 @@ function applyDelegatedRecipeToNode(
|
|
|
716
890
|
delegated: TemplateRecipeConfig,
|
|
717
891
|
overrides: Record<string, unknown> = {},
|
|
718
892
|
): CommandTemplateValue {
|
|
719
|
-
|
|
720
|
-
|
|
893
|
+
const nodeDefaults = isRecord(overrides.defaults)
|
|
894
|
+
? overrides.defaults
|
|
895
|
+
: undefined;
|
|
896
|
+
const nodeValues = isRecord(overrides.values) ? overrides.values : undefined;
|
|
897
|
+
const values = resolveRecipeValueLayers(
|
|
898
|
+
delegated.args,
|
|
899
|
+
delegated.defaults,
|
|
721
900
|
delegated.values,
|
|
722
|
-
|
|
901
|
+
nodeDefaults,
|
|
902
|
+
nodeValues,
|
|
903
|
+
{
|
|
904
|
+
...(delegated.recipe_dir ? { recipe_dir: delegated.recipe_dir } : {}),
|
|
905
|
+
...(delegated.skill_dir ? { skill_dir: delegated.skill_dir } : {}),
|
|
906
|
+
},
|
|
723
907
|
);
|
|
908
|
+
return applyDefaultsToTemplate(delegated.template, values, overrides);
|
|
724
909
|
}
|
|
725
910
|
|
|
726
911
|
function expandRecipeDelegations(
|
|
@@ -830,7 +1015,8 @@ function expandImportNodes(
|
|
|
830
1015
|
? record.defaults
|
|
831
1016
|
: undefined;
|
|
832
1017
|
const nodeValues = isRecord(record.values) ? record.values : undefined;
|
|
833
|
-
const defaults =
|
|
1018
|
+
const defaults = resolveRecipeValueLayers(
|
|
1019
|
+
imported.config.args,
|
|
834
1020
|
imported.defaults,
|
|
835
1021
|
imported.values,
|
|
836
1022
|
nodeDefaults,
|
|
@@ -897,6 +1083,8 @@ export function readResolvedRecipeConfig(
|
|
|
897
1083
|
const raw = readRawRecipeConfig(path);
|
|
898
1084
|
if (!raw || !Object.hasOwn(raw, "template")) return undefined;
|
|
899
1085
|
RecipeControl.assertRecipeHasNoMailbox(raw);
|
|
1086
|
+
assertRuntimeRecipeValuesNotDeclared(raw);
|
|
1087
|
+
assertRecipeArgumentContract(raw as unknown as TemplateRecipeDefinition);
|
|
900
1088
|
const imports: Record<string, ImportedRecipe> = {};
|
|
901
1089
|
for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
|
|
902
1090
|
const importPath = resolveRecipeImportPath(
|
|
@@ -913,13 +1101,22 @@ export function readResolvedRecipeConfig(
|
|
|
913
1101
|
typeof binding === "string" ? undefined : binding.defaults;
|
|
914
1102
|
const bindingValues =
|
|
915
1103
|
typeof binding === "string" ? undefined : binding.values;
|
|
1104
|
+
assertRecipeDefaultsDeclared(
|
|
1105
|
+
config.args,
|
|
1106
|
+
bindingDefaults,
|
|
1107
|
+
`Recipe import ${alias} default`,
|
|
1108
|
+
);
|
|
916
1109
|
imports[alias] = {
|
|
917
1110
|
alias,
|
|
918
1111
|
file: importPath,
|
|
919
1112
|
name: config.name ?? alias,
|
|
920
1113
|
config,
|
|
921
1114
|
defaults: { ...(config.defaults ?? {}), ...(bindingDefaults ?? {}) },
|
|
922
|
-
values: {
|
|
1115
|
+
values: {
|
|
1116
|
+
...(bindingValues ?? {}),
|
|
1117
|
+
...(config.recipe_dir ? { recipe_dir: config.recipe_dir } : {}),
|
|
1118
|
+
...(config.skill_dir ? { skill_dir: config.skill_dir } : {}),
|
|
1119
|
+
},
|
|
923
1120
|
};
|
|
924
1121
|
}
|
|
925
1122
|
const substituted = substituteImportRefs(raw, imports) as Record<
|
|
@@ -945,6 +1142,21 @@ export function readResolvedRecipeConfig(
|
|
|
945
1142
|
)
|
|
946
1143
|
: expandRecipeDelegations(expandedImportsTemplate, path, stack, options);
|
|
947
1144
|
const recipeName = getRecipeIdFromPath(path);
|
|
1145
|
+
const recipeDir = dirname(path);
|
|
1146
|
+
const skillDir = findOwningSkillDir(path);
|
|
1147
|
+
if (
|
|
1148
|
+
!skillDir &&
|
|
1149
|
+
JSON.stringify({
|
|
1150
|
+
artifacts: substituted.artifacts,
|
|
1151
|
+
defaults: substituted.defaults,
|
|
1152
|
+
template: substituted.template,
|
|
1153
|
+
values: substituted.values,
|
|
1154
|
+
}).includes("{skill_dir}")
|
|
1155
|
+
) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
`Recipe uses {skill_dir} outside an owning Skill directory: ${path}`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
948
1160
|
const templateWithContext = options.includeActorRecipeContext
|
|
949
1161
|
? withActorRecipeContext(expandedTemplate, {
|
|
950
1162
|
file: path,
|
|
@@ -967,6 +1179,8 @@ export function readResolvedRecipeConfig(
|
|
|
967
1179
|
);
|
|
968
1180
|
return {
|
|
969
1181
|
name: recipeName,
|
|
1182
|
+
recipe_dir: recipeDir,
|
|
1183
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
970
1184
|
...(typeof substituted.description === "string" &&
|
|
971
1185
|
substituted.description.trim()
|
|
972
1186
|
? { description: substituted.description.trim() }
|
|
@@ -1059,7 +1273,11 @@ export function readResolvedRecipeConfig(
|
|
|
1059
1273
|
...(typeof substituted.repeat === "number"
|
|
1060
1274
|
? { repeat: substituted.repeat }
|
|
1061
1275
|
: {}),
|
|
1062
|
-
|
|
1276
|
+
values: {
|
|
1277
|
+
...(isRecord(substituted.values) ? substituted.values : {}),
|
|
1278
|
+
recipe_dir: recipeDir,
|
|
1279
|
+
...(skillDir ? { skill_dir: skillDir } : {}),
|
|
1280
|
+
},
|
|
1063
1281
|
...(isRecord(substituted.usage) ? { usage: substituted.usage } : {}),
|
|
1064
1282
|
};
|
|
1065
1283
|
}
|
|
@@ -1084,12 +1302,14 @@ function collectRecipeContextRecords(
|
|
|
1084
1302
|
}
|
|
1085
1303
|
const raw = readRawRecipeConfig(path);
|
|
1086
1304
|
if (!raw || !Object.hasOwn(raw, "template")) return [];
|
|
1305
|
+
const qualifiedName = qualifiedRecipeNameForFile(path);
|
|
1087
1306
|
const record: TemplateRecipeContextRecord = {
|
|
1088
1307
|
...(alias ? { alias } : {}),
|
|
1089
1308
|
depth: stack.length,
|
|
1090
1309
|
file: path,
|
|
1091
1310
|
import_path: importPath,
|
|
1092
1311
|
name: getRecipeIdFromPath(path),
|
|
1312
|
+
...(qualifiedName ? { qualified_name: qualifiedName } : {}),
|
|
1093
1313
|
recipe: raw,
|
|
1094
1314
|
role: stack.length === 0 ? "entry" : "import",
|
|
1095
1315
|
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure bounded Run-evidence policy.
|
|
3
|
+
* Zones: Trace retention, compaction accounting, Control capacity and admission
|
|
4
|
+
* Owns deterministic limits policy; journal I/O, locks, lifecycle authorization, and projection stay in adapters.
|
|
5
|
+
* Trace keeps the newest physical suffix because semantic priority would silently
|
|
6
|
+
* redefine causal history. Admitted Controls never expire: only a real terminal
|
|
7
|
+
* transition or generation replacement proves that durable work is finished.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as Limits from "./limits.ts";
|
|
11
|
+
|
|
12
|
+
export const TRACE_COMPACTION_KIND = "runtime.trace_compacted";
|
|
13
|
+
export const TRACE_COMPACTION_VERSION = 1;
|
|
14
|
+
export type TraceJournalUsage = { bytes: number; events: number };
|
|
15
|
+
export type TraceRetentionCandidate<T> = { encodedBytes: number; value: T };
|
|
16
|
+
export type TraceRetentionSelection<T> = {
|
|
17
|
+
droppedEvents: number; retained: TraceRetentionCandidate<T>[];
|
|
18
|
+
retainedBytes: number; retainedEvents: number;
|
|
19
|
+
};
|
|
20
|
+
export interface TraceCompactionData {
|
|
21
|
+
version: typeof TRACE_COMPACTION_VERSION;
|
|
22
|
+
compactions_total: number;
|
|
23
|
+
dropped_valid_events_total: number;
|
|
24
|
+
dropped_malformed_lines_total: number;
|
|
25
|
+
dropped_bytes_total: number;
|
|
26
|
+
dropped_event_count_exact: boolean;
|
|
27
|
+
retained_events: number;
|
|
28
|
+
retained_bytes: number;
|
|
29
|
+
history_complete: false;
|
|
30
|
+
}
|
|
31
|
+
export interface TraceCompactionDelta {
|
|
32
|
+
dropped_valid_events: number;
|
|
33
|
+
dropped_malformed_lines: number;
|
|
34
|
+
dropped_bytes: number;
|
|
35
|
+
dropped_event_count_exact: boolean;
|
|
36
|
+
retained_events: number;
|
|
37
|
+
retained_bytes: number;
|
|
38
|
+
}
|
|
39
|
+
export type RunControlStatus = "queued" | "delivered" | "claimed" | "handled" | "failed";
|
|
40
|
+
export type RunControlCapacity = {
|
|
41
|
+
available: number; backpressured: boolean; limit: number;
|
|
42
|
+
oldest_pending_at?: string; pending: number;
|
|
43
|
+
};
|
|
44
|
+
export type RunControlAdmissionIntegrity =
|
|
45
|
+
| "valid" | "unreadable" | "malformed" | "noncanonical"
|
|
46
|
+
| "invalid_record" | "invalid_status_timestamp" | "generation_mismatch"
|
|
47
|
+
| "journal_bytes";
|
|
48
|
+
export interface RunControlAdmissionErrorDetails {
|
|
49
|
+
reason: "control_backpressure" | "control_journal_integrity";
|
|
50
|
+
pending: number;
|
|
51
|
+
limit: number;
|
|
52
|
+
journal_bytes: number;
|
|
53
|
+
journal_limit: number;
|
|
54
|
+
oldest_pending_at?: string;
|
|
55
|
+
run_instance_id: string;
|
|
56
|
+
next_actions: string[];
|
|
57
|
+
integrity_reason?: string;
|
|
58
|
+
}
|
|
59
|
+
export type RunControlAdmissionDecision =
|
|
60
|
+
{ admitted: true; capacity: RunControlCapacity } |
|
|
61
|
+
{ admitted: false; capacity: RunControlCapacity; error: RunControlAdmissionErrorDetails };
|
|
62
|
+
export type RunControlAdmissionInput = {
|
|
63
|
+
integrity: RunControlAdmissionIntegrity; journalBytes: number;
|
|
64
|
+
newRecordBytes: number; records: readonly unknown[];
|
|
65
|
+
retainedJournalBytes: number; runInstanceId: string;
|
|
66
|
+
capacity?: RunControlCapacity;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const PENDING_CONTROL_STATUSES = new Set<RunControlStatus>(["queued", "delivered", "claimed"]);
|
|
70
|
+
const TERMINAL_CONTROL_STATUSES = new Set<RunControlStatus>(["handled", "failed"]);
|
|
71
|
+
const CONTROL_ADMISSION_NEXT_ACTIONS = [
|
|
72
|
+
"inspect target=run:<id> view=control",
|
|
73
|
+
"inspect target=runtime view=triage",
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function isCount(value: number): boolean {
|
|
77
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
78
|
+
}
|
|
79
|
+
function assertCount(value: number, name: string): void {
|
|
80
|
+
if (!isCount(value)) throw new RangeError(`${name} must be a non-negative safe integer`);
|
|
81
|
+
}
|
|
82
|
+
function boundedAdd(left: number, right: number): number {
|
|
83
|
+
return Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
84
|
+
}
|
|
85
|
+
export function traceAppendFits(usage: TraceJournalUsage, encodedEventBytes: number): boolean {
|
|
86
|
+
if (!isCount(usage.events) || !isCount(usage.bytes) || !isCount(encodedEventBytes)) return false;
|
|
87
|
+
return usage.events + 1 <= Limits.TRACE_JOURNAL_MAX_EVENTS &&
|
|
88
|
+
usage.bytes + encodedEventBytes <= Limits.TRACE_JOURNAL_MAX_BYTES;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function selectNewestTraceSuffix<T>(
|
|
92
|
+
candidates: readonly TraceRetentionCandidate<T>[],
|
|
93
|
+
reserve: { bytes?: number; events?: number } = {},
|
|
94
|
+
): TraceRetentionSelection<T> {
|
|
95
|
+
const reservedBytes = reserve.bytes ?? 0;
|
|
96
|
+
const reservedEvents = reserve.events ?? 0;
|
|
97
|
+
assertCount(reservedBytes, "reserved Trace bytes");
|
|
98
|
+
assertCount(reservedEvents, "reserved Trace events");
|
|
99
|
+
const byteBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_BYTES - reservedBytes);
|
|
100
|
+
const eventBudget = Math.max(0, Limits.TRACE_JOURNAL_TARGET_EVENTS - reservedEvents);
|
|
101
|
+
let retainedBytes = 0;
|
|
102
|
+
let start = candidates.length;
|
|
103
|
+
while (start > 0 && candidates.length - start < eventBudget) {
|
|
104
|
+
const candidate = candidates[start - 1]!;
|
|
105
|
+
assertCount(candidate.encodedBytes, "encoded Trace bytes");
|
|
106
|
+
if (retainedBytes + candidate.encodedBytes > byteBudget) break;
|
|
107
|
+
retainedBytes += candidate.encodedBytes;
|
|
108
|
+
start -= 1;
|
|
109
|
+
}
|
|
110
|
+
const retained = candidates.slice(start);
|
|
111
|
+
return {
|
|
112
|
+
droppedEvents: candidates.length - retained.length,
|
|
113
|
+
retained,
|
|
114
|
+
retainedBytes,
|
|
115
|
+
retainedEvents: retained.length,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function accumulateTraceCompactionStatistics(
|
|
120
|
+
previous: TraceCompactionData | undefined,
|
|
121
|
+
delta: TraceCompactionDelta,
|
|
122
|
+
): TraceCompactionData {
|
|
123
|
+
for (const [name, value] of Object.entries(delta)) {
|
|
124
|
+
if (name !== "dropped_event_count_exact") assertCount(value as number, name);
|
|
125
|
+
}
|
|
126
|
+
const prior = previous ?? {
|
|
127
|
+
compactions_total: 0,
|
|
128
|
+
dropped_bytes_total: 0,
|
|
129
|
+
dropped_event_count_exact: true,
|
|
130
|
+
dropped_malformed_lines_total: 0,
|
|
131
|
+
dropped_valid_events_total: 0,
|
|
132
|
+
};
|
|
133
|
+
const eventCountOverflow = prior.dropped_valid_events_total +
|
|
134
|
+
delta.dropped_valid_events > Number.MAX_SAFE_INTEGER;
|
|
135
|
+
return {
|
|
136
|
+
version: TRACE_COMPACTION_VERSION,
|
|
137
|
+
compactions_total: boundedAdd(prior.compactions_total, 1),
|
|
138
|
+
dropped_valid_events_total: boundedAdd(prior.dropped_valid_events_total, delta.dropped_valid_events),
|
|
139
|
+
dropped_malformed_lines_total: boundedAdd(prior.dropped_malformed_lines_total, delta.dropped_malformed_lines),
|
|
140
|
+
dropped_bytes_total: boundedAdd(prior.dropped_bytes_total, delta.dropped_bytes),
|
|
141
|
+
dropped_event_count_exact: prior.dropped_event_count_exact &&
|
|
142
|
+
delta.dropped_event_count_exact && !eventCountOverflow,
|
|
143
|
+
retained_events: delta.retained_events,
|
|
144
|
+
retained_bytes: delta.retained_bytes,
|
|
145
|
+
history_complete: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function traceCompactionMarkerInput(data: TraceCompactionData): {
|
|
150
|
+
data: TraceCompactionData; kind: typeof TRACE_COMPACTION_KIND; level: "warning";
|
|
151
|
+
} {
|
|
152
|
+
return { data, kind: TRACE_COMPACTION_KIND, level: "warning" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function classifyRunControlRecord(value: unknown): "pending" | "terminal" | undefined {
|
|
156
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
157
|
+
const status = (value as { status?: unknown }).status as RunControlStatus;
|
|
158
|
+
if (PENDING_CONTROL_STATUSES.has(status)) return "pending";
|
|
159
|
+
if (TERMINAL_CONTROL_STATUSES.has(status)) return "terminal";
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function runControlStatusTimestamp(
|
|
164
|
+
record: Record<string, unknown>, status: RunControlStatus,
|
|
165
|
+
): unknown {
|
|
166
|
+
if (status === "failed") return record.failed_at;
|
|
167
|
+
if (status === "handled") return record.handled_at;
|
|
168
|
+
if (status === "claimed") return record.claimed_at;
|
|
169
|
+
if (status === "delivered") return record.delivered_at;
|
|
170
|
+
return record.queued_at;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function computeRunControlCapacity(records: readonly unknown[]): RunControlCapacity {
|
|
174
|
+
let oldest: { timestamp: string; timestampMs: number } | undefined;
|
|
175
|
+
let pending = 0;
|
|
176
|
+
for (const value of records) {
|
|
177
|
+
if (classifyRunControlRecord(value) !== "pending") continue;
|
|
178
|
+
pending += 1;
|
|
179
|
+
const record = value as Record<string, unknown>;
|
|
180
|
+
const timestamp = runControlStatusTimestamp(record, record.status as RunControlStatus);
|
|
181
|
+
const timestampMs = typeof timestamp === "string" ? Date.parse(timestamp) : Number.NaN;
|
|
182
|
+
if (typeof timestamp === "string" && Number.isFinite(timestampMs) &&
|
|
183
|
+
(!oldest || timestampMs < oldest.timestampMs)) {
|
|
184
|
+
oldest = { timestamp, timestampMs };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
available: Math.max(0, Limits.RUN_CONTROL_PENDING_LIMIT - pending),
|
|
189
|
+
backpressured: pending >= Limits.RUN_CONTROL_PENDING_LIMIT,
|
|
190
|
+
limit: Limits.RUN_CONTROL_PENDING_LIMIT,
|
|
191
|
+
...(oldest ? { oldest_pending_at: oldest.timestamp } : {}),
|
|
192
|
+
pending,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function decideRunControlAdmission(
|
|
197
|
+
input: RunControlAdmissionInput,
|
|
198
|
+
): RunControlAdmissionDecision {
|
|
199
|
+
const capacity = input.capacity ?? computeRunControlCapacity(input.records);
|
|
200
|
+
const reject = (
|
|
201
|
+
reason: RunControlAdmissionErrorDetails["reason"],
|
|
202
|
+
integrityReason?: string,
|
|
203
|
+
): RunControlAdmissionDecision => ({
|
|
204
|
+
admitted: false,
|
|
205
|
+
capacity,
|
|
206
|
+
error: {
|
|
207
|
+
reason,
|
|
208
|
+
pending: capacity.pending,
|
|
209
|
+
limit: capacity.limit,
|
|
210
|
+
journal_bytes: input.journalBytes,
|
|
211
|
+
journal_limit: Limits.RUN_CONTROL_JOURNAL_MAX_BYTES,
|
|
212
|
+
...(capacity.oldest_pending_at ? { oldest_pending_at: capacity.oldest_pending_at } : {}),
|
|
213
|
+
run_instance_id: input.runInstanceId,
|
|
214
|
+
next_actions: [...CONTROL_ADMISSION_NEXT_ACTIONS],
|
|
215
|
+
...(integrityReason ? { integrity_reason: integrityReason } : {}),
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
if (input.integrity !== "valid") return reject("control_journal_integrity", input.integrity);
|
|
219
|
+
if (!isCount(input.journalBytes) || !isCount(input.retainedJournalBytes) ||
|
|
220
|
+
!isCount(input.newRecordBytes) ||
|
|
221
|
+
input.journalBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
222
|
+
return reject("control_journal_integrity", "journal_bytes");
|
|
223
|
+
}
|
|
224
|
+
for (const value of input.records) {
|
|
225
|
+
const classification = classifyRunControlRecord(value);
|
|
226
|
+
if (!classification) return reject("control_journal_integrity", "invalid_record");
|
|
227
|
+
if (classification !== "pending") continue;
|
|
228
|
+
const record = value as Record<string, unknown>;
|
|
229
|
+
const timestamp = runControlStatusTimestamp(record, record.status as RunControlStatus);
|
|
230
|
+
if (typeof timestamp !== "string" || !Number.isFinite(Date.parse(timestamp))) {
|
|
231
|
+
return reject("control_journal_integrity", "invalid_status_timestamp");
|
|
232
|
+
}
|
|
233
|
+
if (record.run_instance_id !== input.runInstanceId) {
|
|
234
|
+
return reject("control_journal_integrity", "generation_mismatch");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (capacity.backpressured ||
|
|
238
|
+
input.retainedJournalBytes + input.newRecordBytes > Limits.RUN_CONTROL_JOURNAL_MAX_BYTES) {
|
|
239
|
+
return reject("control_backpressure");
|
|
240
|
+
}
|
|
241
|
+
return { admitted: true, capacity };
|
|
242
|
+
}
|
package/lib/run-ui-runtime.ts
CHANGED
|
@@ -106,6 +106,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
106
106
|
ownerId: deps.getRunOwnerId(ctx),
|
|
107
107
|
sink: Pi.createNotificationSink(deps.pi, ctx),
|
|
108
108
|
state: observation,
|
|
109
|
+
includeAttention: true,
|
|
109
110
|
});
|
|
110
111
|
reportDiagnostics(ctx);
|
|
111
112
|
},
|
|
@@ -141,6 +142,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
|
|
|
141
142
|
},
|
|
142
143
|
start(ctx) {
|
|
143
144
|
close();
|
|
145
|
+
Observability.primeRunAttentionState(observation, deps.getRunOwnerId(ctx));
|
|
144
146
|
update(ctx, true, true);
|
|
145
147
|
watcher.refresh();
|
|
146
148
|
reconciliation.start();
|