@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.
Files changed (74) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +14 -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.js +15 -0
  21. package/dist/lib/recipes-references.d.ts +2 -0
  22. package/dist/lib/recipes-references.js +10 -0
  23. package/dist/lib/tools-local.d.ts +2 -1
  24. package/dist/lib/tools-local.js +6 -4
  25. package/dist/lib/tools-response.js +22 -1
  26. package/dist/lib/tools-spawn.d.ts +2 -1
  27. package/dist/lib/tools-spawn.js +2 -1
  28. package/dist/recipes/lens-swarm.json +15 -2
  29. package/dist/recipes/pipeline-release-readiness.json +22 -3
  30. package/dist/recipes/pipeline-review-readiness.json +22 -3
  31. package/dist/recipes/subagent-judge.json +2 -1
  32. package/dist/recipes/subagent-merge.json +5 -2
  33. package/dist/recipes/subagent-normalize.json +2 -1
  34. package/dist/recipes/subagent-preflight.json +29 -0
  35. package/dist/recipes/subagent-review-coordinator.json +79 -7
  36. package/dist/recipes/subagent-review.json +2 -1
  37. package/dist/recipes/subagent-verify.json +2 -1
  38. package/dist/scripts/async-runner.mjs +84 -10
  39. package/dist/scripts/conformance.mjs +1 -0
  40. package/dist/skills/actors/SKILL.md +4 -4
  41. package/dist/skills/swarm/SKILL.md +2 -2
  42. package/docs/async-runs.md +3 -1
  43. package/docs/command-templates.md +4 -1
  44. package/docs/recipe-library.md +3 -2
  45. package/docs/template-recipes.md +2 -2
  46. package/index.ts +24 -3
  47. package/lib/async-runs.ts +157 -8
  48. package/lib/command-templates.ts +2 -0
  49. package/lib/execution.ts +218 -24
  50. package/lib/model-context.ts +359 -0
  51. package/lib/observability.ts +35 -6
  52. package/lib/preflight-diagnostics.ts +132 -0
  53. package/lib/prompts.ts +2 -2
  54. package/lib/recipes-context.ts +29 -2
  55. package/lib/recipes-discovery.ts +15 -0
  56. package/lib/recipes-references.ts +12 -0
  57. package/lib/tools-local.ts +22 -8
  58. package/lib/tools-response.ts +26 -1
  59. package/lib/tools-spawn.ts +6 -2
  60. package/package.json +1 -1
  61. package/recipes/lens-swarm.json +15 -2
  62. package/recipes/pipeline-release-readiness.json +22 -3
  63. package/recipes/pipeline-review-readiness.json +22 -3
  64. package/recipes/subagent-judge.json +2 -1
  65. package/recipes/subagent-merge.json +5 -2
  66. package/recipes/subagent-normalize.json +2 -1
  67. package/recipes/subagent-preflight.json +29 -0
  68. package/recipes/subagent-review-coordinator.json +79 -7
  69. package/recipes/subagent-review.json +2 -1
  70. package/recipes/subagent-verify.json +2 -1
  71. package/scripts/async-runner.mjs +84 -10
  72. package/scripts/conformance.mjs +1 -0
  73. package/skills/actors/SKILL.md +4 -4
  74. package/skills/swarm/SKILL.md +2 -2
@@ -0,0 +1,220 @@
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
+ export const CURRENT_MODEL_VALUE_KEY = "current_model";
7
+ export const CURRENT_THINKING_VALUE_KEY = "current_thinking";
8
+ export function getCurrentModelPattern(ctx) {
9
+ const provider = ctx?.model?.provider;
10
+ const id = ctx?.model?.id ?? ctx?.model?.modelId;
11
+ if (typeof provider !== "string" || !provider.trim())
12
+ return undefined;
13
+ if (typeof id !== "string" || !id.trim())
14
+ return undefined;
15
+ return `${provider.trim()}/${id.trim()}`;
16
+ }
17
+ function normalizeThinkingLevel(value) {
18
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
19
+ }
20
+ function getSessionThinkingLevel(ctx) {
21
+ const branch = ctx?.sessionManager?.getBranch?.();
22
+ if (!Array.isArray(branch))
23
+ return undefined;
24
+ for (const entry of [...branch].reverse()) {
25
+ if (!entry || typeof entry !== "object")
26
+ continue;
27
+ const record = entry;
28
+ if (record.type !== "thinking_level_change")
29
+ continue;
30
+ const level = normalizeThinkingLevel(record.thinkingLevel);
31
+ if (level)
32
+ return level;
33
+ }
34
+ return undefined;
35
+ }
36
+ export function getCurrentThinkingLevel(ctx) {
37
+ return (normalizeThinkingLevel(ctx?.currentThinking) ??
38
+ normalizeThinkingLevel(ctx?.thinkingLevel) ??
39
+ normalizeThinkingLevel(ctx?.getThinkingLevel?.()) ??
40
+ getSessionThinkingLevel(ctx));
41
+ }
42
+ export function withCurrentModelValues(values, ctx) {
43
+ const currentModel = getCurrentModelPattern(ctx);
44
+ const currentThinking = getCurrentThinkingLevel(ctx);
45
+ if (!currentModel && !currentThinking)
46
+ return values;
47
+ return {
48
+ ...values,
49
+ ...(currentModel ? { [CURRENT_MODEL_VALUE_KEY]: currentModel } : {}),
50
+ ...(currentThinking
51
+ ? { [CURRENT_THINKING_VALUE_KEY]: currentThinking }
52
+ : {}),
53
+ };
54
+ }
55
+ function isRecord(value) {
56
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
57
+ }
58
+ function stringReferencesPlaceholder(value, placeholder) {
59
+ return new RegExp(`\\{\\s*${placeholder}\\s*\\}`).test(value);
60
+ }
61
+ function modelPolicyKeyMatches(key) {
62
+ return /(^|_)models?$/.test(key);
63
+ }
64
+ function thinkingPolicyKeyMatches(key) {
65
+ return /(^|_)(thinking|thinking_level)$/.test(key);
66
+ }
67
+ function argName(token) {
68
+ if (typeof token !== "string")
69
+ return undefined;
70
+ return token.split(":")[0]?.trim() || undefined;
71
+ }
72
+ function collectPolicyDefaults(value, placeholder, path = "template", keys = new Set()) {
73
+ if (Array.isArray(value)) {
74
+ value.forEach((item, index) => collectPolicyDefaults(item, placeholder, `${path}[${index}]`, keys));
75
+ return keys;
76
+ }
77
+ if (!isRecord(value))
78
+ return keys;
79
+ for (const [key, child] of Object.entries(value)) {
80
+ if (key === "defaults" && isRecord(child)) {
81
+ for (const [defaultKey, defaultValue] of Object.entries(child)) {
82
+ if (typeof defaultValue === "string" &&
83
+ stringReferencesPlaceholder(defaultValue, placeholder)) {
84
+ keys.add(defaultKey);
85
+ }
86
+ }
87
+ continue;
88
+ }
89
+ collectPolicyDefaults(child, placeholder, `${path}.${key}`, keys);
90
+ }
91
+ return keys;
92
+ }
93
+ function collectPolicyDirectRefs(value, placeholder, path = "template", refs = new Set()) {
94
+ if (typeof value === "string") {
95
+ if (stringReferencesPlaceholder(value, placeholder))
96
+ refs.add(path);
97
+ return refs;
98
+ }
99
+ if (Array.isArray(value)) {
100
+ value.forEach((item, index) => collectPolicyDirectRefs(item, placeholder, `${path}[${index}]`, refs));
101
+ return refs;
102
+ }
103
+ if (!isRecord(value))
104
+ return refs;
105
+ for (const [key, child] of Object.entries(value)) {
106
+ if (key === "defaults")
107
+ continue;
108
+ collectPolicyDirectRefs(child, placeholder, `${path}.${key}`, refs);
109
+ }
110
+ return refs;
111
+ }
112
+ function summarizePolicyAxis(input) {
113
+ const explicitKeys = Object.keys(input.explicitValues)
114
+ .filter((key) => key !== CURRENT_MODEL_VALUE_KEY &&
115
+ key !== CURRENT_THINKING_VALUE_KEY &&
116
+ input.explicitKeyMatches(key))
117
+ .sort();
118
+ const inheritedKeys = [...input.inheritedDefaultKeys]
119
+ .filter((key) => !Object.hasOwn(input.explicitValues, key))
120
+ .sort();
121
+ const directRefs = [...input.directRefs].length > 0 ? [input.directRefKey] : [];
122
+ const inheritedOrDirect = [...new Set([...inheritedKeys, ...directRefs])].sort();
123
+ const unresolvedKeys = input.currentValue ? [] : inheritedOrDirect;
124
+ const inheritedResolvedKeys = input.currentValue ? inheritedOrDirect : [];
125
+ const hasExplicit = explicitKeys.length > 0;
126
+ const hasInherited = inheritedResolvedKeys.length > 0;
127
+ const source = unresolvedKeys.length
128
+ ? "unresolved"
129
+ : hasExplicit && hasInherited
130
+ ? "mixed"
131
+ : hasExplicit
132
+ ? "explicit"
133
+ : hasInherited
134
+ ? "inherited"
135
+ : "unused";
136
+ const explicitValue = explicitKeys
137
+ .map((key) => input.explicitValues[key])
138
+ .find((value) => typeof value === "string" && value.trim());
139
+ const value = hasInherited
140
+ ? input.currentValue
141
+ : typeof explicitValue === "string"
142
+ ? explicitValue
143
+ : undefined;
144
+ return {
145
+ ...(explicitKeys.length ? { explicit_keys: explicitKeys } : {}),
146
+ ...(inheritedResolvedKeys.length
147
+ ? { inherited_keys: inheritedResolvedKeys }
148
+ : {}),
149
+ source,
150
+ ...(unresolvedKeys.length ? { unresolved_keys: unresolvedKeys } : {}),
151
+ ...(value ? { value } : {}),
152
+ };
153
+ }
154
+ function collectPolicyDefaultsAcross(items, placeholder) {
155
+ return items.reduce((keys, item) => collectPolicyDefaults(item, placeholder, "template", keys), new Set());
156
+ }
157
+ function collectPolicyDirectRefsAcross(items, placeholder) {
158
+ return items.reduce((refs, item) => collectPolicyDirectRefs(item, placeholder, "template", refs), new Set());
159
+ }
160
+ export function describeCurrentPolicyProvenance(input) {
161
+ const explicitValues = input.values ?? {};
162
+ const defaultsEnvelope = input.defaults ? { defaults: input.defaults } : {};
163
+ const templateAndDefaults = [defaultsEnvelope, input.template];
164
+ return {
165
+ model: summarizePolicyAxis({
166
+ currentValue: typeof explicitValues[CURRENT_MODEL_VALUE_KEY] === "string"
167
+ ? String(explicitValues[CURRENT_MODEL_VALUE_KEY])
168
+ : undefined,
169
+ directRefKey: CURRENT_MODEL_VALUE_KEY,
170
+ directRefs: collectPolicyDirectRefsAcross(templateAndDefaults, "current_model"),
171
+ explicitKeyMatches: modelPolicyKeyMatches,
172
+ explicitValues,
173
+ inheritedDefaultKeys: collectPolicyDefaultsAcross(templateAndDefaults, "current_model"),
174
+ }),
175
+ thinking: summarizePolicyAxis({
176
+ currentValue: typeof explicitValues[CURRENT_THINKING_VALUE_KEY] === "string"
177
+ ? String(explicitValues[CURRENT_THINKING_VALUE_KEY])
178
+ : undefined,
179
+ directRefKey: CURRENT_THINKING_VALUE_KEY,
180
+ directRefs: collectPolicyDirectRefsAcross(templateAndDefaults, "current_thinking"),
181
+ explicitKeyMatches: thinkingPolicyKeyMatches,
182
+ explicitValues,
183
+ inheritedDefaultKeys: collectPolicyDefaultsAcross(templateAndDefaults, "current_thinking"),
184
+ }),
185
+ };
186
+ }
187
+ function publicPolicyArgs(args, matches) {
188
+ return Array.isArray(args)
189
+ ? args
190
+ .map(argName)
191
+ .filter((key) => Boolean(key && matches(key)))
192
+ .sort()
193
+ : [];
194
+ }
195
+ function nonEmptyAxisRecipeSummary(inheritedDefaults, publicArgs) {
196
+ if (inheritedDefaults.length === 0 && publicArgs.length === 0)
197
+ return undefined;
198
+ return {
199
+ ...(inheritedDefaults.length
200
+ ? { inherited_defaults: inheritedDefaults }
201
+ : {}),
202
+ ...(publicArgs.length ? { public_args: publicArgs } : {}),
203
+ };
204
+ }
205
+ export function describeRecipeCurrentPolicy(input) {
206
+ const templateAndDefaults = [
207
+ input.defaults ? { defaults: input.defaults } : {},
208
+ input.template,
209
+ ];
210
+ const model = nonEmptyAxisRecipeSummary([...collectPolicyDefaultsAcross(templateAndDefaults, "current_model")].sort(), publicPolicyArgs(input.args, modelPolicyKeyMatches));
211
+ const thinking = nonEmptyAxisRecipeSummary([
212
+ ...collectPolicyDefaultsAcross(templateAndDefaults, "current_thinking"),
213
+ ].sort(), publicPolicyArgs(input.args, thinkingPolicyKeyMatches));
214
+ if (!model && !thinking)
215
+ return undefined;
216
+ return {
217
+ ...(model ? { model } : {}),
218
+ ...(thinking ? { thinking } : {}),
219
+ };
220
+ }
@@ -15,6 +15,7 @@ export interface RunObservation {
15
15
  ownerId?: string;
16
16
  artifacts?: Record<string, string>;
17
17
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
18
+ modelPolicy?: Record<string, unknown>;
18
19
  recipeFile?: string;
19
20
  terminalHandled?: boolean;
20
21
  retireWhen?: string;
@@ -95,6 +96,7 @@ export interface RunTransition {
95
96
  stateDir?: string;
96
97
  artifacts?: Record<string, string>;
97
98
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
99
+ modelPolicy?: Record<string, unknown>;
98
100
  recipeFile?: string;
99
101
  terminalHandled?: boolean;
100
102
  to: RunObservedStatus;
@@ -185,6 +185,11 @@ function observeRun(stateDir) {
185
185
  ...(status.launch_source === "spawn" || status.launch_source === "tool"
186
186
  ? { launchSource: status.launch_source }
187
187
  : {}),
188
+ ...(status.model_policy &&
189
+ typeof status.model_policy === "object" &&
190
+ !Array.isArray(status.model_policy)
191
+ ? { modelPolicy: status.model_policy }
192
+ : {}),
188
193
  ...(typeof status.recipe_file === "string"
189
194
  ? { recipeFile: status.recipe_file }
190
195
  : {}),
@@ -496,6 +501,7 @@ export function detectRunTransitions(previous, summary) {
496
501
  ...(run.stateDir ? { stateDir: run.stateDir } : {}),
497
502
  ...(run.artifacts ? { artifacts: run.artifacts } : {}),
498
503
  ...(run.launchSource ? { launchSource: run.launchSource } : {}),
504
+ ...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
499
505
  ...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
500
506
  ...(run.terminalHandled ? { terminalHandled: true } : {}),
501
507
  to: run.status,
@@ -721,6 +727,25 @@ function formatRecipePersistenceSuggestion(transition) {
721
727
  }
722
728
  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.`;
723
729
  }
730
+ function formatTransitionPolicy(transition) {
731
+ if (!transition.modelPolicy)
732
+ return "";
733
+ const axis = (key, label) => {
734
+ const value = transition.modelPolicy?.[key];
735
+ if (!value || typeof value !== "object" || Array.isArray(value))
736
+ return undefined;
737
+ const record = value;
738
+ const source = typeof record.source === "string" ? record.source : "unused";
739
+ if (source === "unused")
740
+ return undefined;
741
+ const renderedValue = typeof record.value === "string" && record.value.trim()
742
+ ? ` (${record.value.trim()})`
743
+ : "";
744
+ return `${label}: ${source}${renderedValue}`;
745
+ };
746
+ const lines = [axis("model", "Model"), axis("thinking", "Thinking")].filter((line) => Boolean(line));
747
+ return lines.length ? `\nPolicy:\n- ${lines.join("\n- ")}` : "";
748
+ }
724
749
  function formatTransitionNextActions(transition) {
725
750
  const actions = [
726
751
  `inspect target=run:${transition.run} view=status`,
@@ -735,16 +760,17 @@ export function formatRunTransitionMessage(transition) {
735
760
  const artifacts = formatNamedArtifacts(transition.artifacts);
736
761
  const runFiles = formatRunFileList(getRunArtifacts(transition));
737
762
  const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
763
+ const policy = formatTransitionPolicy(transition);
738
764
  const nextActions = formatTransitionNextActions(transition);
739
765
  if (transition.to === "done")
740
- return `Run ${transition.run} completed successfully.${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
766
+ return `Run ${transition.run} completed successfully.${policy}${artifacts}${runFiles}${nextActions}${persistenceSuggestion}`;
741
767
  if (transition.to === "failed")
742
- return `Run ${transition.run} failed.${artifacts}${runFiles}${nextActions}`;
768
+ return `Run ${transition.run} failed.${policy}${artifacts}${runFiles}${nextActions}`;
743
769
  if (transition.to === "cancelled")
744
- return `Run ${transition.run} was cancelled.${nextActions}`;
770
+ return `Run ${transition.run} was cancelled.${policy}${nextActions}`;
745
771
  if (transition.to === "killed")
746
- return `Run ${transition.run} was force-killed.${nextActions}`;
772
+ return `Run ${transition.run} was force-killed.${policy}${nextActions}`;
747
773
  if (transition.to === "exited")
748
- return `Run ${transition.run} exited before writing a result.${nextActions}`;
749
- return `Run ${transition.run} finished with status ${transition.to}.${nextActions}`;
774
+ return `Run ${transition.run} exited before writing a result.${policy}${nextActions}`;
775
+ return `Run ${transition.run} finished with status ${transition.to}.${policy}${nextActions}`;
750
776
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Review preflight diagnostics
3
+ * Zones: recipe diagnostics, subagent launch policy
4
+ * Owns compact classification of failed review-pipeline preflight commands.
5
+ */
6
+ export type ReviewPreflightErrorClass = "auth_or_key" | "model_unavailable" | "quota_or_balance" | "rate_limited" | "timeout" | "transport" | "unknown_provider_failure";
7
+ export interface ReviewPreflightDiagnosticInput {
8
+ args: readonly string[];
9
+ code?: number;
10
+ killed?: boolean;
11
+ promptFile?: string;
12
+ promptText?: string;
13
+ stderr?: string;
14
+ stdout?: string;
15
+ }
16
+ export interface ReviewPreflightDiagnostic {
17
+ errorClass: ReviewPreflightErrorClass;
18
+ model?: string;
19
+ promptFile?: string;
20
+ stage: string;
21
+ suggestedOverrideArgs: string;
22
+ thinking?: string;
23
+ }
24
+ export declare function extractReviewPreflightStage(promptText: string | undefined): string | undefined;
25
+ export declare function classifyReviewPreflightError(text: string, killed?: boolean): ReviewPreflightErrorClass;
26
+ export declare function buildReviewPreflightDiagnostic(input: ReviewPreflightDiagnosticInput): ReviewPreflightDiagnostic | undefined;
27
+ export declare function formatReviewPreflightDiagnostic(diagnostic: ReviewPreflightDiagnostic): string;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Review preflight diagnostics
3
+ * Zones: recipe diagnostics, subagent launch policy
4
+ * Owns compact classification of failed review-pipeline preflight commands.
5
+ */
6
+ const PREFLIGHT_STAGE_PATTERN = /Preflight check for stage\s+([^\s.]+)\.?/i;
7
+ function getFlagValue(args, flag) {
8
+ const inlinePrefix = `${flag}=`;
9
+ for (let index = 0; index < args.length; index += 1) {
10
+ const value = args[index];
11
+ if (value === flag)
12
+ return args[index + 1];
13
+ if (value?.startsWith(inlinePrefix))
14
+ return value.slice(inlinePrefix.length);
15
+ }
16
+ return undefined;
17
+ }
18
+ function getToolPolicy(args) {
19
+ if (args.includes("--no-tools"))
20
+ return "<tool-policy>";
21
+ return getFlagValue(args, "--tools") ?? "<tool-policy>";
22
+ }
23
+ export function extractReviewPreflightStage(promptText) {
24
+ return promptText?.match(PREFLIGHT_STAGE_PATTERN)?.[1];
25
+ }
26
+ export function classifyReviewPreflightError(text, killed = false) {
27
+ if (killed)
28
+ return "timeout";
29
+ const normalized = text.toLowerCase();
30
+ if (/insufficient[_ -]?quota|\bquota\b|balance|billing|credits?|payment/.test(normalized)) {
31
+ return "quota_or_balance";
32
+ }
33
+ if (/api[_ -]?key|unauthori[sz]ed|forbidden|permission|\b401\b|\b403\b|auth/.test(normalized)) {
34
+ return "auth_or_key";
35
+ }
36
+ if (/\b404\b|not found|unknown model|model .*not (exist|found)|invalid model|unsupported model/.test(normalized)) {
37
+ return "model_unavailable";
38
+ }
39
+ if (/rate limit|\b429\b|too many requests/.test(normalized)) {
40
+ return "rate_limited";
41
+ }
42
+ if (/network|econn|enotfound|etimedout|fetch failed|socket|\btls\b/.test(normalized)) {
43
+ return "transport";
44
+ }
45
+ return "unknown_provider_failure";
46
+ }
47
+ function getStageModelArg(stage) {
48
+ if (stage === "reviewer" ||
49
+ stage === "verifier" ||
50
+ stage === "merger" ||
51
+ stage === "judge") {
52
+ return `${stage}_model`;
53
+ }
54
+ return "model";
55
+ }
56
+ function buildSuggestedOverrideArgs(stage, args) {
57
+ return `${getStageModelArg(stage)}=<working-model> thinking=<supported-level> tools=${getToolPolicy(args)}`;
58
+ }
59
+ export function buildReviewPreflightDiagnostic(input) {
60
+ const stage = extractReviewPreflightStage(input.promptText);
61
+ if (!stage)
62
+ return undefined;
63
+ return {
64
+ errorClass: classifyReviewPreflightError([input.stderr, input.stdout].filter(Boolean).join("\n"), input.killed),
65
+ ...(getFlagValue(input.args, "--model")
66
+ ? { model: getFlagValue(input.args, "--model") }
67
+ : {}),
68
+ ...(input.promptFile ? { promptFile: input.promptFile } : {}),
69
+ stage,
70
+ suggestedOverrideArgs: buildSuggestedOverrideArgs(stage, input.args),
71
+ ...(getFlagValue(input.args, "--thinking")
72
+ ? { thinking: getFlagValue(input.args, "--thinking") }
73
+ : {}),
74
+ };
75
+ }
76
+ export function formatReviewPreflightDiagnostic(diagnostic) {
77
+ return [
78
+ "ACTOR_PREFLIGHT_FAILED",
79
+ `stage=${diagnostic.stage}`,
80
+ ...(diagnostic.model ? [`model=${diagnostic.model}`] : []),
81
+ ...(diagnostic.thinking ? [`thinking=${diagnostic.thinking}`] : []),
82
+ `error_class=${diagnostic.errorClass}`,
83
+ ...(diagnostic.promptFile ? [`prompt_file=${diagnostic.promptFile}`] : []),
84
+ `suggested_override_args=\"${diagnostic.suggestedOverrideArgs}\"`,
85
+ ].join(" ");
86
+ }
@@ -6,7 +6,7 @@
6
6
  export declare const REGISTER_TOOL_DESCRIPTION: string;
7
7
  export declare const REGISTER_TOOL_PROMPT_SNIPPET = "Register persistent command templates as agent-callable tools";
8
8
  export declare const REGISTER_TOOL_GUIDELINES: string[];
9
- export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- 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.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.\n- 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.\n- 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.\n- 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.";
9
+ export declare const ONBOARDING_SYSTEM_PROMPT = "pi-actors quick model:\n- Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.\n- Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.\n- 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.\n- Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.\n- ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.\n- Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.\n- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.\n- Actor-mode trigger: if work may outlive this turn, need steering/follow-up/artifacts, run as a service, fan out, or be resumed/inspected later, use spawn -> message -> inspect instead of ad hoc shell backgrounding.\n- 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.\n- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.\n- 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.\n- 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.\n- 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.";
10
10
  export declare const REGISTER_TOOL_PARAM_DESCRIPTIONS: {
11
11
  readonly name: "Tool name in snake_case (e.g., 'transcribe')";
12
12
  readonly description: "Describe what the tool does for the LLM. Required unless deleting; omitted updates keep the old description.";
@@ -16,7 +16,7 @@ export const REGISTER_TOOL_GUIDELINES = [
16
16
  export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
17
17
  - Local-first actor memory: persist trusted local capabilities instead of rebuilding shell recipes.
18
18
  - Layers: task -> command template -> recipe/tool -> spawn -> run:<id>; tool:<name> wraps registered capabilities.
19
- - Command templates stay sync: string leaf, array sequence, object node; flags include args/defaults, parallel, when, timeout, delay, retry, failure, recover, repeat, output.
19
+ - 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.
20
20
  - Placeholders support typed/default args plus {value??fallback} and {flag?yes:no}.
21
21
  - ~/.pi/agent/recipes/*.json is actor muscle memory: every recipe there is auto-registered as an agent tool across sessions; register_tool writes there.
22
22
  - Recipes own template directly and may declare metadata/defaults/imports/mailbox/artifacts; files >1 MiB or import depth >32 fail closed.
@@ -25,7 +25,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
25
25
  - 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.
26
26
  - Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
27
27
  - 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.
28
- - 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.
28
+ - 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.
29
29
  - 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.`;
30
30
  export const REGISTER_TOOL_PARAM_DESCRIPTIONS = {
31
31
  name: "Tool name in snake_case (e.g., 'transcribe')",
@@ -9,7 +9,14 @@ export interface MarkedRecipeContextRecord extends TemplateRecipeContextRecord {
9
9
  you_are_here?: true;
10
10
  you_are_here_path?: string;
11
11
  }
12
+ export declare function findPiPrintPromptIndex(args: string[]): number | undefined;
12
13
  export declare function markRecipeContextRecords(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): MarkedRecipeContextRecord[];
13
14
  export declare function formatRecipeContextJsonl(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
14
15
  export declare function buildRecipeContextPromptBlock(records: TemplateRecipeContextRecord[], context?: CommandTemplateActorRecipeContext): string;
15
16
  export declare function appendRecipeContextToPiArgs(command: string, args: string[], records: TemplateRecipeContextRecord[] | undefined, context?: CommandTemplateActorRecipeContext): string[];
17
+ export interface MaterializedPiPrintPromptArgs {
18
+ args: string[];
19
+ promptBytes?: number;
20
+ promptFile?: string;
21
+ }
22
+ export declare function materializePiPrintPromptArg(command: string, args: string[], promptFile: string | (() => string)): MaterializedPiPrintPromptArgs;
@@ -3,6 +3,7 @@
3
3
  * Zones: async runner prompt context, recipe provenance, LLM child launches
4
4
  * Owns compact actor recipe context records appended to child-agent prompts.
5
5
  */
6
+ import { writeFileSync } from "node:fs";
6
7
  import { basename } from "node:path";
7
8
  function commandName(command) {
8
9
  return basename(command).toLowerCase();
@@ -46,7 +47,7 @@ function piOptionConsumesNextArg(arg) {
46
47
  function isPiFileArgument(arg) {
47
48
  return arg.startsWith("@") && arg.length > 1;
48
49
  }
49
- function findPrintPromptIndex(args) {
50
+ export function findPiPrintPromptIndex(args) {
50
51
  let printMode = false;
51
52
  let positionalOnly = false;
52
53
  let promptIndex;
@@ -120,7 +121,7 @@ export function buildRecipeContextPromptBlock(records, context) {
120
121
  export function appendRecipeContextToPiArgs(command, args, records, context) {
121
122
  if (!records || records.length === 0 || !isPiCommand(command))
122
123
  return args;
123
- const promptIndex = findPrintPromptIndex(args);
124
+ const promptIndex = findPiPrintPromptIndex(args);
124
125
  if (promptIndex === undefined)
125
126
  return args;
126
127
  const block = buildRecipeContextPromptBlock(records, context);
@@ -130,3 +131,20 @@ export function appendRecipeContextToPiArgs(command, args, records, context) {
130
131
  next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
131
132
  return next;
132
133
  }
134
+ export function materializePiPrintPromptArg(command, args, promptFile) {
135
+ if (!isPiCommand(command))
136
+ return { args };
137
+ const promptIndex = findPiPrintPromptIndex(args);
138
+ if (promptIndex === undefined)
139
+ return { args };
140
+ const prompt = args[promptIndex];
141
+ const path = typeof promptFile === "function" ? promptFile() : promptFile;
142
+ writeFileSync(path, prompt, "utf8");
143
+ const next = [...args];
144
+ next[promptIndex] = `@${path}`;
145
+ return {
146
+ args: next,
147
+ promptBytes: Buffer.byteLength(prompt),
148
+ promptFile: path,
149
+ };
150
+ }
@@ -7,6 +7,7 @@ import { createHash } from "node:crypto";
7
7
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
8
8
  import { join } from "node:path";
9
9
  import * as CommandTemplates from "./command-templates.js";
10
+ import * as ModelContext from "./model-context.js";
10
11
  import * as RecipesReferences from "./recipes-references.js";
11
12
  import * as Schema from "./schema.js";
12
13
  function assertToolSafeRepeatConfig(config, argTypes, defaults) {
@@ -524,6 +525,20 @@ export function summarizeDiscovery(result) {
524
525
  invalid: entry.invalid,
525
526
  shadows: entry.shadows,
526
527
  ...(entry.riskLabels.length ? { risk_labels: entry.riskLabels } : {}),
528
+ ...(entry.config &&
529
+ ModelContext.describeRecipeCurrentPolicy({
530
+ args: entry.config.args,
531
+ defaults: entry.config.defaults,
532
+ template: entry.config.template,
533
+ })
534
+ ? {
535
+ current_policy: ModelContext.describeRecipeCurrentPolicy({
536
+ args: entry.config.args,
537
+ defaults: entry.config.defaults,
538
+ template: entry.config.template,
539
+ }),
540
+ }
541
+ : {}),
527
542
  ...(entry.config?.imports ? { imports: entry.config.imports } : {}),
528
543
  ...(recipeUsage(entry.config)
529
544
  ? { usage: recipeUsage(entry.config) }
@@ -33,6 +33,8 @@ export interface TemplateRecipeDefinition {
33
33
  args?: string[];
34
34
  defaults?: Record<string, unknown>;
35
35
  parallel?: boolean;
36
+ concurrency?: number | string;
37
+ min_successful?: number | string;
36
38
  label?: string;
37
39
  when?: boolean | string;
38
40
  timeout?: number | string;
@@ -135,6 +135,8 @@ function getRecipeCommandTemplate(raw) {
135
135
  "args",
136
136
  "defaults",
137
137
  "parallel",
138
+ "concurrency",
139
+ "min_successful",
138
140
  "label",
139
141
  "when",
140
142
  "timeout",
@@ -711,6 +713,14 @@ export function readResolvedRecipeConfig(file, stack = [], options = {}) {
711
713
  ...(typeof substituted.parallel === "boolean"
712
714
  ? { parallel: substituted.parallel }
713
715
  : {}),
716
+ ...(typeof substituted.concurrency === "number" ||
717
+ typeof substituted.concurrency === "string"
718
+ ? { concurrency: substituted.concurrency }
719
+ : {}),
720
+ ...(typeof substituted.min_successful === "number" ||
721
+ typeof substituted.min_successful === "string"
722
+ ? { min_successful: substituted.min_successful }
723
+ : {}),
714
724
  ...(typeof substituted.label === "string"
715
725
  ? { label: substituted.label }
716
726
  : {}),
@@ -3,9 +3,10 @@
3
3
  * Zones: user recipe tools, generated schemas, async recipe launch
4
4
  * Owns wrapping saved local capabilities as executable pi tools
5
5
  */
6
+ import * as ModelContext from "./model-context.ts";
6
7
  import type { RegisteredTool } from "./config.ts";
7
8
  import * as Execution from "./execution.ts";
8
- export interface RuntimeToolContext {
9
+ export interface RuntimeToolContext extends ModelContext.CurrentModelContext {
9
10
  cwd: string;
10
11
  sessionManager?: {
11
12
  getSessionId?: () => string;
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import * as Rooms from "./rooms.js";
7
7
  import * as AsyncRuns from "./async-runs.js";
8
+ import * as ModelContext from "./model-context.js";
8
9
  import * as Execution from "./execution.js";
9
10
  import * as Prompts from "./prompts.js";
10
11
  import * as RecipesReferences from "./recipes-references.js";
@@ -124,7 +125,8 @@ export function createRuntimeToolDefinition(cfg, exec) {
124
125
  ownerId: getRunOwnerId(ctx),
125
126
  run_id: runId,
126
127
  tool: cfg.name,
127
- values: Schema.normalizeRuntimeValues({ ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values }, cfg.argTypes),
128
+ policy_values: ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...values }, ctx),
129
+ values: Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues({ ...(cfg.recipe?.values ?? {}), ...cfg.defaults, ...values }, ctx), cfg.argTypes),
128
130
  }, ctx.cwd);
129
131
  Rooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
130
132
  Rooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
@@ -139,14 +141,14 @@ export function createRuntimeToolDefinition(cfg, exec) {
139
141
  };
140
142
  }
141
143
  if (isRecipe && recipeTemplate) {
142
- const paramsWithDefaults = {
144
+ const paramsWithDefaults = ModelContext.withCurrentModelValues({
143
145
  ...(cfg.recipe?.values ?? {}),
144
146
  ...cfg.defaults,
145
147
  ...params,
146
- };
148
+ }, ctx);
147
149
  return await Execution.executeRegisteredTool({ ...cfg, template: recipeTemplate }, Schema.normalizeRuntimeValues(paramsWithDefaults, cfg.argTypes), exec, ctx.cwd, signal);
148
150
  }
149
- return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(params, cfg.argTypes), exec, ctx.cwd, signal);
151
+ return await Execution.executeRegisteredTool(cfg, Schema.normalizeRuntimeValues(ModelContext.withCurrentModelValues(params, ctx), cfg.argTypes), exec, ctx.cwd, signal);
150
152
  }
151
153
  catch (error) {
152
154
  throw formatRuntimeToolArgumentError(cfg, error, required, isAsyncRecipe);