@llblab/pi-actors 0.37.1 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +6 -5
  4. package/dist/index.js +23 -3
  5. package/dist/lib/async-runs.d.ts +5 -0
  6. package/dist/lib/async-runs.js +87 -8
  7. package/dist/lib/command-templates.d.ts +2 -0
  8. package/dist/lib/execution.d.ts +4 -0
  9. package/dist/lib/execution.js +154 -13
  10. package/dist/lib/model-context.d.ts +56 -0
  11. package/dist/lib/model-context.js +220 -0
  12. package/dist/lib/observability.d.ts +2 -0
  13. package/dist/lib/observability.js +32 -6
  14. package/dist/lib/preflight-diagnostics.d.ts +27 -0
  15. package/dist/lib/preflight-diagnostics.js +86 -0
  16. package/dist/lib/prompts.d.ts +1 -1
  17. package/dist/lib/prompts.js +2 -2
  18. package/dist/lib/recipes-context.d.ts +7 -0
  19. package/dist/lib/recipes-context.js +20 -2
  20. package/dist/lib/recipes-discovery.d.ts +1 -0
  21. package/dist/lib/recipes-discovery.js +63 -7
  22. package/dist/lib/recipes-references.d.ts +2 -0
  23. package/dist/lib/recipes-references.js +10 -0
  24. package/dist/lib/tools-local.d.ts +2 -1
  25. package/dist/lib/tools-local.js +6 -4
  26. package/dist/lib/tools-response.js +22 -1
  27. package/dist/lib/tools-spawn.d.ts +2 -1
  28. package/dist/lib/tools-spawn.js +2 -1
  29. package/dist/recipes/lens-swarm.json +15 -2
  30. package/dist/recipes/pipeline-release-readiness.json +22 -3
  31. package/dist/recipes/pipeline-review-readiness.json +22 -3
  32. package/dist/recipes/subagent-judge.json +2 -1
  33. package/dist/recipes/subagent-merge.json +5 -2
  34. package/dist/recipes/subagent-normalize.json +2 -1
  35. package/dist/recipes/subagent-preflight.json +29 -0
  36. package/dist/recipes/subagent-review-coordinator.json +79 -7
  37. package/dist/recipes/subagent-review.json +2 -1
  38. package/dist/recipes/subagent-verify.json +2 -1
  39. package/dist/scripts/async-runner.mjs +84 -10
  40. package/dist/scripts/conformance.mjs +1 -0
  41. package/dist/skills/actors/SKILL.md +4 -4
  42. package/dist/skills/swarm/SKILL.md +2 -2
  43. package/docs/async-runs.md +3 -1
  44. package/docs/command-templates.md +4 -1
  45. package/docs/recipe-library.md +3 -2
  46. package/docs/template-recipes.md +2 -2
  47. package/index.ts +24 -3
  48. package/lib/async-runs.ts +157 -8
  49. package/lib/command-templates.ts +2 -0
  50. package/lib/execution.ts +218 -24
  51. package/lib/model-context.ts +359 -0
  52. package/lib/observability.ts +35 -6
  53. package/lib/preflight-diagnostics.ts +132 -0
  54. package/lib/prompts.ts +2 -2
  55. package/lib/recipes-context.ts +29 -2
  56. package/lib/recipes-discovery.ts +76 -11
  57. package/lib/recipes-references.ts +12 -0
  58. package/lib/tools-local.ts +22 -8
  59. package/lib/tools-response.ts +26 -1
  60. package/lib/tools-spawn.ts +6 -2
  61. package/package.json +1 -1
  62. package/recipes/lens-swarm.json +15 -2
  63. package/recipes/pipeline-release-readiness.json +22 -3
  64. package/recipes/pipeline-review-readiness.json +22 -3
  65. package/recipes/subagent-judge.json +2 -1
  66. package/recipes/subagent-merge.json +5 -2
  67. package/recipes/subagent-normalize.json +2 -1
  68. package/recipes/subagent-preflight.json +29 -0
  69. package/recipes/subagent-review-coordinator.json +79 -7
  70. package/recipes/subagent-review.json +2 -1
  71. package/recipes/subagent-verify.json +2 -1
  72. package/scripts/async-runner.mjs +84 -10
  73. package/scripts/conformance.mjs +1 -0
  74. package/skills/actors/SKILL.md +4 -4
  75. 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
+ }
@@ -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; long fanout = parent async recipe wrapping template(parallel:true) and imports.
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 = {
@@ -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 findPrintPromptIndex(args: string[]): number | undefined {
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 = findPrintPromptIndex(args);
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
+ }