@llblab/pi-actors 0.37.1 → 0.38.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/BACKLOG.md +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +6 -5
- package/dist/index.js +23 -3
- package/dist/lib/async-runs.d.ts +5 -0
- package/dist/lib/async-runs.js +87 -8
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/execution.d.ts +4 -0
- package/dist/lib/execution.js +154 -13
- package/dist/lib/model-context.d.ts +56 -0
- package/dist/lib/model-context.js +220 -0
- package/dist/lib/observability.d.ts +2 -0
- package/dist/lib/observability.js +32 -6
- package/dist/lib/preflight-diagnostics.d.ts +27 -0
- package/dist/lib/preflight-diagnostics.js +86 -0
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipes-context.d.ts +7 -0
- package/dist/lib/recipes-context.js +20 -2
- package/dist/lib/recipes-discovery.js +15 -0
- package/dist/lib/recipes-references.d.ts +2 -0
- package/dist/lib/recipes-references.js +10 -0
- package/dist/lib/tools-local.d.ts +2 -1
- package/dist/lib/tools-local.js +6 -4
- package/dist/lib/tools-response.js +22 -1
- package/dist/lib/tools-spawn.d.ts +2 -1
- package/dist/lib/tools-spawn.js +2 -1
- package/dist/recipes/lens-swarm.json +15 -2
- package/dist/recipes/pipeline-release-readiness.json +22 -3
- package/dist/recipes/pipeline-review-readiness.json +22 -3
- package/dist/recipes/subagent-judge.json +2 -1
- package/dist/recipes/subagent-merge.json +5 -2
- package/dist/recipes/subagent-normalize.json +2 -1
- package/dist/recipes/subagent-preflight.json +29 -0
- package/dist/recipes/subagent-review-coordinator.json +79 -7
- package/dist/recipes/subagent-review.json +2 -1
- package/dist/recipes/subagent-verify.json +2 -1
- package/dist/scripts/async-runner.mjs +84 -10
- package/dist/scripts/conformance.mjs +1 -0
- package/dist/skills/actors/SKILL.md +4 -4
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/async-runs.md +3 -1
- package/docs/command-templates.md +4 -1
- package/docs/recipe-library.md +3 -2
- package/docs/template-recipes.md +2 -2
- package/index.ts +24 -3
- package/lib/async-runs.ts +157 -8
- package/lib/command-templates.ts +2 -0
- package/lib/execution.ts +218 -24
- package/lib/model-context.ts +359 -0
- package/lib/observability.ts +35 -6
- package/lib/preflight-diagnostics.ts +132 -0
- package/lib/prompts.ts +2 -2
- package/lib/recipes-context.ts +29 -2
- package/lib/recipes-discovery.ts +15 -0
- package/lib/recipes-references.ts +12 -0
- package/lib/tools-local.ts +22 -8
- package/lib/tools-response.ts +26 -1
- package/lib/tools-spawn.ts +6 -2
- package/package.json +1 -1
- package/recipes/lens-swarm.json +15 -2
- package/recipes/pipeline-release-readiness.json +22 -3
- package/recipes/pipeline-review-readiness.json +22 -3
- package/recipes/subagent-judge.json +2 -1
- package/recipes/subagent-merge.json +5 -2
- package/recipes/subagent-normalize.json +2 -1
- package/recipes/subagent-preflight.json +29 -0
- package/recipes/subagent-review-coordinator.json +79 -7
- package/recipes/subagent-review.json +2 -1
- package/recipes/subagent-verify.json +2 -1
- package/scripts/async-runner.mjs +84 -10
- package/scripts/conformance.mjs +1 -0
- package/skills/actors/SKILL.md +4 -4
- package/skills/swarm/SKILL.md +2 -2
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current model/thinking propagation helpers
|
|
3
|
+
* Zones: pi session context, recipe inheritance
|
|
4
|
+
* Owns current model/thinking extraction from Pi tool contexts and implicit command-template values.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface CurrentModelContext {
|
|
8
|
+
currentThinking?: unknown;
|
|
9
|
+
getThinkingLevel?: () => unknown;
|
|
10
|
+
model?: {
|
|
11
|
+
id?: unknown;
|
|
12
|
+
modelId?: unknown;
|
|
13
|
+
provider?: unknown;
|
|
14
|
+
};
|
|
15
|
+
sessionManager?: {
|
|
16
|
+
getBranch?: () => unknown[];
|
|
17
|
+
getSessionId?: () => string;
|
|
18
|
+
};
|
|
19
|
+
thinkingLevel?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const CURRENT_MODEL_VALUE_KEY = "current_model";
|
|
23
|
+
export const CURRENT_THINKING_VALUE_KEY = "current_thinking";
|
|
24
|
+
|
|
25
|
+
export type CurrentPolicySource =
|
|
26
|
+
| "explicit"
|
|
27
|
+
| "inherited"
|
|
28
|
+
| "mixed"
|
|
29
|
+
| "unresolved"
|
|
30
|
+
| "unused";
|
|
31
|
+
|
|
32
|
+
export interface CurrentPolicyAxisProvenance {
|
|
33
|
+
explicit_keys?: string[];
|
|
34
|
+
inherited_keys?: string[];
|
|
35
|
+
source: CurrentPolicySource;
|
|
36
|
+
unresolved_keys?: string[];
|
|
37
|
+
value?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CurrentPolicyProvenance {
|
|
41
|
+
model: CurrentPolicyAxisProvenance;
|
|
42
|
+
thinking: CurrentPolicyAxisProvenance;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CurrentPolicyRecipeSummary {
|
|
46
|
+
model?: {
|
|
47
|
+
inherited_defaults?: string[];
|
|
48
|
+
public_args?: string[];
|
|
49
|
+
};
|
|
50
|
+
thinking?: {
|
|
51
|
+
inherited_defaults?: string[];
|
|
52
|
+
public_args?: string[];
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getCurrentModelPattern(
|
|
57
|
+
ctx: CurrentModelContext | undefined,
|
|
58
|
+
): string | undefined {
|
|
59
|
+
const provider = ctx?.model?.provider;
|
|
60
|
+
const id = ctx?.model?.id ?? ctx?.model?.modelId;
|
|
61
|
+
if (typeof provider !== "string" || !provider.trim()) return undefined;
|
|
62
|
+
if (typeof id !== "string" || !id.trim()) return undefined;
|
|
63
|
+
return `${provider.trim()}/${id.trim()}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeThinkingLevel(value: unknown): string | undefined {
|
|
67
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getSessionThinkingLevel(
|
|
71
|
+
ctx: CurrentModelContext | undefined,
|
|
72
|
+
): string | undefined {
|
|
73
|
+
const branch = ctx?.sessionManager?.getBranch?.();
|
|
74
|
+
if (!Array.isArray(branch)) return undefined;
|
|
75
|
+
for (const entry of [...branch].reverse()) {
|
|
76
|
+
if (!entry || typeof entry !== "object") continue;
|
|
77
|
+
const record = entry as Record<string, unknown>;
|
|
78
|
+
if (record.type !== "thinking_level_change") continue;
|
|
79
|
+
const level = normalizeThinkingLevel(record.thinkingLevel);
|
|
80
|
+
if (level) return level;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getCurrentThinkingLevel(
|
|
86
|
+
ctx: CurrentModelContext | undefined,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
return (
|
|
89
|
+
normalizeThinkingLevel(ctx?.currentThinking) ??
|
|
90
|
+
normalizeThinkingLevel(ctx?.thinkingLevel) ??
|
|
91
|
+
normalizeThinkingLevel(ctx?.getThinkingLevel?.()) ??
|
|
92
|
+
getSessionThinkingLevel(ctx)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function withCurrentModelValues<T extends Record<string, unknown>>(
|
|
97
|
+
values: T,
|
|
98
|
+
ctx: CurrentModelContext | undefined,
|
|
99
|
+
): T & Record<string, unknown> {
|
|
100
|
+
const currentModel = getCurrentModelPattern(ctx);
|
|
101
|
+
const currentThinking = getCurrentThinkingLevel(ctx);
|
|
102
|
+
if (!currentModel && !currentThinking) return values;
|
|
103
|
+
return {
|
|
104
|
+
...values,
|
|
105
|
+
...(currentModel ? { [CURRENT_MODEL_VALUE_KEY]: currentModel } : {}),
|
|
106
|
+
...(currentThinking
|
|
107
|
+
? { [CURRENT_THINKING_VALUE_KEY]: currentThinking }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
113
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function stringReferencesPlaceholder(
|
|
117
|
+
value: string,
|
|
118
|
+
placeholder: string,
|
|
119
|
+
): boolean {
|
|
120
|
+
return new RegExp(`\\{\\s*${placeholder}\\s*\\}`).test(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function modelPolicyKeyMatches(key: string): boolean {
|
|
124
|
+
return /(^|_)models?$/.test(key);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function thinkingPolicyKeyMatches(key: string): boolean {
|
|
128
|
+
return /(^|_)(thinking|thinking_level)$/.test(key);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function argName(token: unknown): string | undefined {
|
|
132
|
+
if (typeof token !== "string") return undefined;
|
|
133
|
+
return token.split(":")[0]?.trim() || undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectPolicyDefaults(
|
|
137
|
+
value: unknown,
|
|
138
|
+
placeholder: string,
|
|
139
|
+
path = "template",
|
|
140
|
+
keys = new Set<string>(),
|
|
141
|
+
): Set<string> {
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
value.forEach((item, index) =>
|
|
144
|
+
collectPolicyDefaults(item, placeholder, `${path}[${index}]`, keys),
|
|
145
|
+
);
|
|
146
|
+
return keys;
|
|
147
|
+
}
|
|
148
|
+
if (!isRecord(value)) return keys;
|
|
149
|
+
for (const [key, child] of Object.entries(value)) {
|
|
150
|
+
if (key === "defaults" && isRecord(child)) {
|
|
151
|
+
for (const [defaultKey, defaultValue] of Object.entries(child)) {
|
|
152
|
+
if (
|
|
153
|
+
typeof defaultValue === "string" &&
|
|
154
|
+
stringReferencesPlaceholder(defaultValue, placeholder)
|
|
155
|
+
) {
|
|
156
|
+
keys.add(defaultKey);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
collectPolicyDefaults(child, placeholder, `${path}.${key}`, keys);
|
|
162
|
+
}
|
|
163
|
+
return keys;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function collectPolicyDirectRefs(
|
|
167
|
+
value: unknown,
|
|
168
|
+
placeholder: string,
|
|
169
|
+
path = "template",
|
|
170
|
+
refs = new Set<string>(),
|
|
171
|
+
): Set<string> {
|
|
172
|
+
if (typeof value === "string") {
|
|
173
|
+
if (stringReferencesPlaceholder(value, placeholder)) refs.add(path);
|
|
174
|
+
return refs;
|
|
175
|
+
}
|
|
176
|
+
if (Array.isArray(value)) {
|
|
177
|
+
value.forEach((item, index) =>
|
|
178
|
+
collectPolicyDirectRefs(item, placeholder, `${path}[${index}]`, refs),
|
|
179
|
+
);
|
|
180
|
+
return refs;
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(value)) return refs;
|
|
183
|
+
for (const [key, child] of Object.entries(value)) {
|
|
184
|
+
if (key === "defaults") continue;
|
|
185
|
+
collectPolicyDirectRefs(child, placeholder, `${path}.${key}`, refs);
|
|
186
|
+
}
|
|
187
|
+
return refs;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function summarizePolicyAxis(input: {
|
|
191
|
+
currentValue?: string;
|
|
192
|
+
directRefKey: string;
|
|
193
|
+
explicitKeyMatches: (key: string) => boolean;
|
|
194
|
+
explicitValues: Record<string, unknown>;
|
|
195
|
+
inheritedDefaultKeys: Set<string>;
|
|
196
|
+
directRefs: Set<string>;
|
|
197
|
+
}): CurrentPolicyAxisProvenance {
|
|
198
|
+
const explicitKeys = Object.keys(input.explicitValues)
|
|
199
|
+
.filter(
|
|
200
|
+
(key) =>
|
|
201
|
+
key !== CURRENT_MODEL_VALUE_KEY &&
|
|
202
|
+
key !== CURRENT_THINKING_VALUE_KEY &&
|
|
203
|
+
input.explicitKeyMatches(key),
|
|
204
|
+
)
|
|
205
|
+
.sort();
|
|
206
|
+
const inheritedKeys = [...input.inheritedDefaultKeys]
|
|
207
|
+
.filter((key) => !Object.hasOwn(input.explicitValues, key))
|
|
208
|
+
.sort();
|
|
209
|
+
const directRefs = [...input.directRefs].length > 0 ? [input.directRefKey] : [];
|
|
210
|
+
const inheritedOrDirect = [...new Set([...inheritedKeys, ...directRefs])].sort();
|
|
211
|
+
const unresolvedKeys = input.currentValue ? [] : inheritedOrDirect;
|
|
212
|
+
const inheritedResolvedKeys = input.currentValue ? inheritedOrDirect : [];
|
|
213
|
+
const hasExplicit = explicitKeys.length > 0;
|
|
214
|
+
const hasInherited = inheritedResolvedKeys.length > 0;
|
|
215
|
+
const source: CurrentPolicySource = unresolvedKeys.length
|
|
216
|
+
? "unresolved"
|
|
217
|
+
: hasExplicit && hasInherited
|
|
218
|
+
? "mixed"
|
|
219
|
+
: hasExplicit
|
|
220
|
+
? "explicit"
|
|
221
|
+
: hasInherited
|
|
222
|
+
? "inherited"
|
|
223
|
+
: "unused";
|
|
224
|
+
const explicitValue = explicitKeys
|
|
225
|
+
.map((key) => input.explicitValues[key])
|
|
226
|
+
.find((value) => typeof value === "string" && value.trim());
|
|
227
|
+
const value = hasInherited
|
|
228
|
+
? input.currentValue
|
|
229
|
+
: typeof explicitValue === "string"
|
|
230
|
+
? explicitValue
|
|
231
|
+
: undefined;
|
|
232
|
+
return {
|
|
233
|
+
...(explicitKeys.length ? { explicit_keys: explicitKeys } : {}),
|
|
234
|
+
...(inheritedResolvedKeys.length
|
|
235
|
+
? { inherited_keys: inheritedResolvedKeys }
|
|
236
|
+
: {}),
|
|
237
|
+
source,
|
|
238
|
+
...(unresolvedKeys.length ? { unresolved_keys: unresolvedKeys } : {}),
|
|
239
|
+
...(value ? { value } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function collectPolicyDefaultsAcross(
|
|
244
|
+
items: unknown[],
|
|
245
|
+
placeholder: string,
|
|
246
|
+
): Set<string> {
|
|
247
|
+
return items.reduce<Set<string>>(
|
|
248
|
+
(keys, item) => collectPolicyDefaults(item, placeholder, "template", keys),
|
|
249
|
+
new Set<string>(),
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function collectPolicyDirectRefsAcross(
|
|
254
|
+
items: unknown[],
|
|
255
|
+
placeholder: string,
|
|
256
|
+
): Set<string> {
|
|
257
|
+
return items.reduce<Set<string>>(
|
|
258
|
+
(refs, item) => collectPolicyDirectRefs(item, placeholder, "template", refs),
|
|
259
|
+
new Set<string>(),
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function describeCurrentPolicyProvenance(input: {
|
|
264
|
+
defaults?: Record<string, unknown>;
|
|
265
|
+
template: unknown;
|
|
266
|
+
values?: Record<string, unknown>;
|
|
267
|
+
}): CurrentPolicyProvenance {
|
|
268
|
+
const explicitValues = input.values ?? {};
|
|
269
|
+
const defaultsEnvelope = input.defaults ? { defaults: input.defaults } : {};
|
|
270
|
+
const templateAndDefaults = [defaultsEnvelope, input.template];
|
|
271
|
+
return {
|
|
272
|
+
model: summarizePolicyAxis({
|
|
273
|
+
currentValue:
|
|
274
|
+
typeof explicitValues[CURRENT_MODEL_VALUE_KEY] === "string"
|
|
275
|
+
? String(explicitValues[CURRENT_MODEL_VALUE_KEY])
|
|
276
|
+
: undefined,
|
|
277
|
+
directRefKey: CURRENT_MODEL_VALUE_KEY,
|
|
278
|
+
directRefs: collectPolicyDirectRefsAcross(
|
|
279
|
+
templateAndDefaults,
|
|
280
|
+
"current_model",
|
|
281
|
+
),
|
|
282
|
+
explicitKeyMatches: modelPolicyKeyMatches,
|
|
283
|
+
explicitValues,
|
|
284
|
+
inheritedDefaultKeys: collectPolicyDefaultsAcross(
|
|
285
|
+
templateAndDefaults,
|
|
286
|
+
"current_model",
|
|
287
|
+
),
|
|
288
|
+
}),
|
|
289
|
+
thinking: summarizePolicyAxis({
|
|
290
|
+
currentValue:
|
|
291
|
+
typeof explicitValues[CURRENT_THINKING_VALUE_KEY] === "string"
|
|
292
|
+
? String(explicitValues[CURRENT_THINKING_VALUE_KEY])
|
|
293
|
+
: undefined,
|
|
294
|
+
directRefKey: CURRENT_THINKING_VALUE_KEY,
|
|
295
|
+
directRefs: collectPolicyDirectRefsAcross(
|
|
296
|
+
templateAndDefaults,
|
|
297
|
+
"current_thinking",
|
|
298
|
+
),
|
|
299
|
+
explicitKeyMatches: thinkingPolicyKeyMatches,
|
|
300
|
+
explicitValues,
|
|
301
|
+
inheritedDefaultKeys: collectPolicyDefaultsAcross(
|
|
302
|
+
templateAndDefaults,
|
|
303
|
+
"current_thinking",
|
|
304
|
+
),
|
|
305
|
+
}),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function publicPolicyArgs(
|
|
310
|
+
args: unknown,
|
|
311
|
+
matches: (key: string) => boolean,
|
|
312
|
+
): string[] {
|
|
313
|
+
return Array.isArray(args)
|
|
314
|
+
? args
|
|
315
|
+
.map(argName)
|
|
316
|
+
.filter((key): key is string => Boolean(key && matches(key)))
|
|
317
|
+
.sort()
|
|
318
|
+
: [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function nonEmptyAxisRecipeSummary(
|
|
322
|
+
inheritedDefaults: string[],
|
|
323
|
+
publicArgs: string[],
|
|
324
|
+
): { inherited_defaults?: string[]; public_args?: string[] } | undefined {
|
|
325
|
+
if (inheritedDefaults.length === 0 && publicArgs.length === 0)
|
|
326
|
+
return undefined;
|
|
327
|
+
return {
|
|
328
|
+
...(inheritedDefaults.length
|
|
329
|
+
? { inherited_defaults: inheritedDefaults }
|
|
330
|
+
: {}),
|
|
331
|
+
...(publicArgs.length ? { public_args: publicArgs } : {}),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function describeRecipeCurrentPolicy(input: {
|
|
336
|
+
args?: unknown;
|
|
337
|
+
defaults?: Record<string, unknown>;
|
|
338
|
+
template: unknown;
|
|
339
|
+
}): CurrentPolicyRecipeSummary | undefined {
|
|
340
|
+
const templateAndDefaults = [
|
|
341
|
+
input.defaults ? { defaults: input.defaults } : {},
|
|
342
|
+
input.template,
|
|
343
|
+
];
|
|
344
|
+
const model = nonEmptyAxisRecipeSummary(
|
|
345
|
+
[...collectPolicyDefaultsAcross(templateAndDefaults, "current_model")].sort(),
|
|
346
|
+
publicPolicyArgs(input.args, modelPolicyKeyMatches),
|
|
347
|
+
);
|
|
348
|
+
const thinking = nonEmptyAxisRecipeSummary(
|
|
349
|
+
[
|
|
350
|
+
...collectPolicyDefaultsAcross(templateAndDefaults, "current_thinking"),
|
|
351
|
+
].sort(),
|
|
352
|
+
publicPolicyArgs(input.args, thinkingPolicyKeyMatches),
|
|
353
|
+
);
|
|
354
|
+
if (!model && !thinking) return undefined;
|
|
355
|
+
return {
|
|
356
|
+
...(model ? { model } : {}),
|
|
357
|
+
...(thinking ? { thinking } : {}),
|
|
358
|
+
};
|
|
359
|
+
}
|
package/lib/observability.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface RunObservation {
|
|
|
42
42
|
ownerId?: string;
|
|
43
43
|
artifacts?: Record<string, string>;
|
|
44
44
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
45
|
+
modelPolicy?: Record<string, unknown>;
|
|
45
46
|
recipeFile?: string;
|
|
46
47
|
terminalHandled?: boolean;
|
|
47
48
|
retireWhen?: string;
|
|
@@ -247,6 +248,7 @@ export interface RunTransition {
|
|
|
247
248
|
stateDir?: string;
|
|
248
249
|
artifacts?: Record<string, string>;
|
|
249
250
|
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
251
|
+
modelPolicy?: Record<string, unknown>;
|
|
250
252
|
recipeFile?: string;
|
|
251
253
|
terminalHandled?: boolean;
|
|
252
254
|
to: RunObservedStatus;
|
|
@@ -352,6 +354,11 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
352
354
|
...(status.launch_source === "spawn" || status.launch_source === "tool"
|
|
353
355
|
? { launchSource: status.launch_source }
|
|
354
356
|
: {}),
|
|
357
|
+
...(status.model_policy &&
|
|
358
|
+
typeof status.model_policy === "object" &&
|
|
359
|
+
!Array.isArray(status.model_policy)
|
|
360
|
+
? { modelPolicy: status.model_policy as Record<string, unknown> }
|
|
361
|
+
: {}),
|
|
355
362
|
...(typeof status.recipe_file === "string"
|
|
356
363
|
? { recipeFile: status.recipe_file }
|
|
357
364
|
: {}),
|
|
@@ -747,6 +754,7 @@ export function detectRunTransitions(
|
|
|
747
754
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
748
755
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
749
756
|
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
757
|
+
...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
|
|
750
758
|
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
751
759
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
752
760
|
to: run.status,
|
|
@@ -1021,6 +1029,26 @@ function formatRecipePersistenceSuggestion(transition: RunTransition): string {
|
|
|
1021
1029
|
return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern fits this machine's recurring workflow, ask the operator whether to save it as a durable recipe/tool under ~/.pi/agent/recipes with register_tool. Do not auto-save without confirmation.`;
|
|
1022
1030
|
}
|
|
1023
1031
|
|
|
1032
|
+
function formatTransitionPolicy(transition: RunTransition): string {
|
|
1033
|
+
if (!transition.modelPolicy) return "";
|
|
1034
|
+
const axis = (key: "model" | "thinking", label: string): string | undefined => {
|
|
1035
|
+
const value = transition.modelPolicy?.[key];
|
|
1036
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1037
|
+
const record = value as Record<string, unknown>;
|
|
1038
|
+
const source = typeof record.source === "string" ? record.source : "unused";
|
|
1039
|
+
if (source === "unused") return undefined;
|
|
1040
|
+
const renderedValue =
|
|
1041
|
+
typeof record.value === "string" && record.value.trim()
|
|
1042
|
+
? ` (${record.value.trim()})`
|
|
1043
|
+
: "";
|
|
1044
|
+
return `${label}: ${source}${renderedValue}`;
|
|
1045
|
+
};
|
|
1046
|
+
const lines = [axis("model", "Model"), axis("thinking", "Thinking")].filter(
|
|
1047
|
+
(line): line is string => Boolean(line),
|
|
1048
|
+
);
|
|
1049
|
+
return lines.length ? `\nPolicy:\n- ${lines.join("\n- ")}` : "";
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1024
1052
|
function formatTransitionNextActions(transition: RunTransition): string {
|
|
1025
1053
|
const actions = [
|
|
1026
1054
|
`inspect target=run:${transition.run} view=status`,
|
|
@@ -1036,16 +1064,17 @@ export function formatRunTransitionMessage(transition: RunTransition): string {
|
|
|
1036
1064
|
const artifacts = formatNamedArtifacts(transition.artifacts);
|
|
1037
1065
|
const runFiles = formatRunFileList(getRunArtifacts(transition));
|
|
1038
1066
|
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
1067
|
+
const policy = formatTransitionPolicy(transition);
|
|
1039
1068
|
const nextActions = formatTransitionNextActions(transition);
|
|
1040
1069
|
if (transition.to === "done")
|
|
1041
|
-
return `Run ${transition.run} completed successfully.${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1070
|
+
return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
|
|
1042
1071
|
if (transition.to === "failed")
|
|
1043
|
-
return `Run ${transition.run} failed.${artifacts}${runFiles}${nextActions}`;
|
|
1072
|
+
return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
|
|
1044
1073
|
if (transition.to === "cancelled")
|
|
1045
|
-
return `Run ${transition.run} was cancelled.${nextActions}`;
|
|
1074
|
+
return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
|
|
1046
1075
|
if (transition.to === "killed")
|
|
1047
|
-
return `Run ${transition.run} was force-killed.${nextActions}`;
|
|
1076
|
+
return `Run ${transition.run} was force-killed.${policy}${nextActions}`;
|
|
1048
1077
|
if (transition.to === "exited")
|
|
1049
|
-
return `Run ${transition.run} exited before writing a result.${nextActions}`;
|
|
1050
|
-
return `Run ${transition.run} finished with status ${transition.to}.${nextActions}`;
|
|
1078
|
+
return `Run ${transition.run} exited before writing a result.${policy}${nextActions}`;
|
|
1079
|
+
return `Run ${transition.run} finished with status ${transition.to}.${policy}${nextActions}`;
|
|
1051
1080
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review preflight diagnostics
|
|
3
|
+
* Zones: recipe diagnostics, subagent launch policy
|
|
4
|
+
* Owns compact classification of failed review-pipeline preflight commands.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type ReviewPreflightErrorClass =
|
|
8
|
+
| "auth_or_key"
|
|
9
|
+
| "model_unavailable"
|
|
10
|
+
| "quota_or_balance"
|
|
11
|
+
| "rate_limited"
|
|
12
|
+
| "timeout"
|
|
13
|
+
| "transport"
|
|
14
|
+
| "unknown_provider_failure";
|
|
15
|
+
|
|
16
|
+
export interface ReviewPreflightDiagnosticInput {
|
|
17
|
+
args: readonly string[];
|
|
18
|
+
code?: number;
|
|
19
|
+
killed?: boolean;
|
|
20
|
+
promptFile?: string;
|
|
21
|
+
promptText?: string;
|
|
22
|
+
stderr?: string;
|
|
23
|
+
stdout?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReviewPreflightDiagnostic {
|
|
27
|
+
errorClass: ReviewPreflightErrorClass;
|
|
28
|
+
model?: string;
|
|
29
|
+
promptFile?: string;
|
|
30
|
+
stage: string;
|
|
31
|
+
suggestedOverrideArgs: string;
|
|
32
|
+
thinking?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const PREFLIGHT_STAGE_PATTERN = /Preflight check for stage\s+([^\s.]+)\.?/i;
|
|
36
|
+
|
|
37
|
+
function getFlagValue(args: readonly string[], flag: string): string | undefined {
|
|
38
|
+
const inlinePrefix = `${flag}=`;
|
|
39
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
40
|
+
const value = args[index];
|
|
41
|
+
if (value === flag) return args[index + 1];
|
|
42
|
+
if (value?.startsWith(inlinePrefix)) return value.slice(inlinePrefix.length);
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getToolPolicy(args: readonly string[]): string {
|
|
48
|
+
if (args.includes("--no-tools")) return "<tool-policy>";
|
|
49
|
+
return getFlagValue(args, "--tools") ?? "<tool-policy>";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function extractReviewPreflightStage(
|
|
53
|
+
promptText: string | undefined,
|
|
54
|
+
): string | undefined {
|
|
55
|
+
return promptText?.match(PREFLIGHT_STAGE_PATTERN)?.[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function classifyReviewPreflightError(
|
|
59
|
+
text: string,
|
|
60
|
+
killed = false,
|
|
61
|
+
): ReviewPreflightErrorClass {
|
|
62
|
+
if (killed) return "timeout";
|
|
63
|
+
const normalized = text.toLowerCase();
|
|
64
|
+
if (/insufficient[_ -]?quota|\bquota\b|balance|billing|credits?|payment/.test(normalized)) {
|
|
65
|
+
return "quota_or_balance";
|
|
66
|
+
}
|
|
67
|
+
if (/api[_ -]?key|unauthori[sz]ed|forbidden|permission|\b401\b|\b403\b|auth/.test(normalized)) {
|
|
68
|
+
return "auth_or_key";
|
|
69
|
+
}
|
|
70
|
+
if (/\b404\b|not found|unknown model|model .*not (exist|found)|invalid model|unsupported model/.test(normalized)) {
|
|
71
|
+
return "model_unavailable";
|
|
72
|
+
}
|
|
73
|
+
if (/rate limit|\b429\b|too many requests/.test(normalized)) {
|
|
74
|
+
return "rate_limited";
|
|
75
|
+
}
|
|
76
|
+
if (/network|econn|enotfound|etimedout|fetch failed|socket|\btls\b/.test(normalized)) {
|
|
77
|
+
return "transport";
|
|
78
|
+
}
|
|
79
|
+
return "unknown_provider_failure";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getStageModelArg(stage: string): string {
|
|
83
|
+
if (
|
|
84
|
+
stage === "reviewer" ||
|
|
85
|
+
stage === "verifier" ||
|
|
86
|
+
stage === "merger" ||
|
|
87
|
+
stage === "judge"
|
|
88
|
+
) {
|
|
89
|
+
return `${stage}_model`;
|
|
90
|
+
}
|
|
91
|
+
return "model";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildSuggestedOverrideArgs(stage: string, args: readonly string[]): string {
|
|
95
|
+
return `${getStageModelArg(stage)}=<working-model> thinking=<supported-level> tools=${getToolPolicy(args)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildReviewPreflightDiagnostic(
|
|
99
|
+
input: ReviewPreflightDiagnosticInput,
|
|
100
|
+
): ReviewPreflightDiagnostic | undefined {
|
|
101
|
+
const stage = extractReviewPreflightStage(input.promptText);
|
|
102
|
+
if (!stage) return undefined;
|
|
103
|
+
return {
|
|
104
|
+
errorClass: classifyReviewPreflightError(
|
|
105
|
+
[input.stderr, input.stdout].filter(Boolean).join("\n"),
|
|
106
|
+
input.killed,
|
|
107
|
+
),
|
|
108
|
+
...(getFlagValue(input.args, "--model")
|
|
109
|
+
? { model: getFlagValue(input.args, "--model") }
|
|
110
|
+
: {}),
|
|
111
|
+
...(input.promptFile ? { promptFile: input.promptFile } : {}),
|
|
112
|
+
stage,
|
|
113
|
+
suggestedOverrideArgs: buildSuggestedOverrideArgs(stage, input.args),
|
|
114
|
+
...(getFlagValue(input.args, "--thinking")
|
|
115
|
+
? { thinking: getFlagValue(input.args, "--thinking") }
|
|
116
|
+
: {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatReviewPreflightDiagnostic(
|
|
121
|
+
diagnostic: ReviewPreflightDiagnostic,
|
|
122
|
+
): string {
|
|
123
|
+
return [
|
|
124
|
+
"ACTOR_PREFLIGHT_FAILED",
|
|
125
|
+
`stage=${diagnostic.stage}`,
|
|
126
|
+
...(diagnostic.model ? [`model=${diagnostic.model}`] : []),
|
|
127
|
+
...(diagnostic.thinking ? [`thinking=${diagnostic.thinking}`] : []),
|
|
128
|
+
`error_class=${diagnostic.errorClass}`,
|
|
129
|
+
...(diagnostic.promptFile ? [`prompt_file=${diagnostic.promptFile}`] : []),
|
|
130
|
+
`suggested_override_args=\"${diagnostic.suggestedOverrideArgs}\"`,
|
|
131
|
+
].join(" ");
|
|
132
|
+
}
|
package/lib/prompts.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const REGISTER_TOOL_GUIDELINES = [
|
|
|
22
22
|
export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
23
23
|
- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
|
|
24
24
|
- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
|
|
25
|
-
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.
|
|
25
|
+
- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, concurrency, min_successful, when, timeout, delay, retry, failure, recover, repeat, output.
|
|
26
26
|
- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
|
|
27
27
|
- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
|
|
28
28
|
- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
|
|
@@ -31,7 +31,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
31
31
|
- Use spawn/message/inspect for actor-level start/send/observe; short foreground checks can stay ordinary tools/templates; avoid runtime/FIFO/outbox vocabulary in public guidance.
|
|
32
32
|
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
|
|
33
33
|
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones, and fix/remove/disable invalid recipes flagged by registry warnings; packaged/ad hoc recipes are lower-priority components; offer to save successful recurring patterns only after confirmation.
|
|
34
|
-
- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers;
|
|
34
|
+
- Prefer maintained packaged recipes/pipelines with spawn file=<recipe> before ad hoc scripts/wrappers; review swarms inherit current model/thinking, preflight before fanout, and expose quorum/concurrency/TTL knobs unless explicit args are passed.
|
|
35
35
|
- For any non-trivial actor use or pi-actors change, read the bundled actors skill first; for deeper guidance, inspect installed extension sources/docs/recipes because README/docs are not automatically in context.`;
|
|
36
36
|
|
|
37
37
|
export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
|
package/lib/recipes-context.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Owns compact actor recipe context records appended to child-agent prompts.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { writeFileSync } from "node:fs";
|
|
7
8
|
import { basename } from "node:path";
|
|
8
9
|
|
|
9
10
|
import type { CommandTemplateActorRecipeContext } from "./command-templates.ts";
|
|
@@ -62,7 +63,7 @@ function isPiFileArgument(arg: string): boolean {
|
|
|
62
63
|
return arg.startsWith("@") && arg.length > 1;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
function
|
|
66
|
+
export function findPiPrintPromptIndex(args: string[]): number | undefined {
|
|
66
67
|
let printMode = false;
|
|
67
68
|
let positionalOnly = false;
|
|
68
69
|
let promptIndex: number | undefined;
|
|
@@ -153,7 +154,7 @@ export function appendRecipeContextToPiArgs(
|
|
|
153
154
|
context?: CommandTemplateActorRecipeContext,
|
|
154
155
|
): string[] {
|
|
155
156
|
if (!records || records.length === 0 || !isPiCommand(command)) return args;
|
|
156
|
-
const promptIndex =
|
|
157
|
+
const promptIndex = findPiPrintPromptIndex(args);
|
|
157
158
|
if (promptIndex === undefined) return args;
|
|
158
159
|
const block = buildRecipeContextPromptBlock(records, context);
|
|
159
160
|
if (!block) return args;
|
|
@@ -161,3 +162,29 @@ export function appendRecipeContextToPiArgs(
|
|
|
161
162
|
next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
|
|
162
163
|
return next;
|
|
163
164
|
}
|
|
165
|
+
|
|
166
|
+
export interface MaterializedPiPrintPromptArgs {
|
|
167
|
+
args: string[];
|
|
168
|
+
promptBytes?: number;
|
|
169
|
+
promptFile?: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function materializePiPrintPromptArg(
|
|
173
|
+
command: string,
|
|
174
|
+
args: string[],
|
|
175
|
+
promptFile: string | (() => string),
|
|
176
|
+
): MaterializedPiPrintPromptArgs {
|
|
177
|
+
if (!isPiCommand(command)) return { args };
|
|
178
|
+
const promptIndex = findPiPrintPromptIndex(args);
|
|
179
|
+
if (promptIndex === undefined) return { args };
|
|
180
|
+
const prompt = args[promptIndex];
|
|
181
|
+
const path = typeof promptFile === "function" ? promptFile() : promptFile;
|
|
182
|
+
writeFileSync(path, prompt, "utf8");
|
|
183
|
+
const next = [...args];
|
|
184
|
+
next[promptIndex] = `@${path}`;
|
|
185
|
+
return {
|
|
186
|
+
args: next,
|
|
187
|
+
promptBytes: Buffer.byteLength(prompt),
|
|
188
|
+
promptFile: path,
|
|
189
|
+
};
|
|
190
|
+
}
|
package/lib/recipes-discovery.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
|
|
11
11
|
import * as CommandTemplates from "./command-templates.ts";
|
|
12
12
|
import type { RegisteredTool } from "./config.ts";
|
|
13
|
+
import * as ModelContext from "./model-context.ts";
|
|
13
14
|
import type { TemplateRecipeConfig } from "./recipes-references.ts";
|
|
14
15
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
15
16
|
import * as Schema from "./schema.ts";
|
|
@@ -701,6 +702,20 @@ export function summarizeDiscovery(
|
|
|
701
702
|
invalid: entry.invalid,
|
|
702
703
|
shadows: entry.shadows,
|
|
703
704
|
...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
|
|
705
|
+
...(entry.config &&
|
|
706
|
+
ModelContext.describeRecipeCurrentPolicy({
|
|
707
|
+
args: entry.config.args,
|
|
708
|
+
defaults: entry.config.defaults,
|
|
709
|
+
template: entry.config.template,
|
|
710
|
+
})
|
|
711
|
+
? {
|
|
712
|
+
current_policy: ModelContext.describeRecipeCurrentPolicy({
|
|
713
|
+
args: entry.config.args,
|
|
714
|
+
defaults: entry.config.defaults,
|
|
715
|
+
template: entry.config.template,
|
|
716
|
+
}),
|
|
717
|
+
}
|
|
718
|
+
: {}),
|
|
704
719
|
...(entry.config?.imports ? { imports: entry.config.imports } : {}),
|
|
705
720
|
...(recipeUsage(entry.config)
|
|
706
721
|
? { usage: recipeUsage(entry.config) }
|