@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
@@ -2,7 +2,7 @@
2
2
  name: actors
3
3
  description: Required practical guide for non-trivial pi-actors use. Read before using or changing spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
4
4
  metadata:
5
- version: 0.37.1
5
+ version: 0.38.0
6
6
  ---
7
7
 
8
8
  # Actors (pi-actors)
@@ -199,9 +199,9 @@ Rules:
199
199
  7. Declare `mailbox` for actors that accept or emit meaningful messages.
200
200
  8. Declare `artifacts` for durable outputs the coordinator should inspect.
201
201
  9. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
202
- 10. File-backed async recipes pass child `pi -p` actors a bounded JSONL recipe context bundle by default: raw entry/import recipe records, derived `name`, import path/alias, and `"you_are_here": true` on the launching recipe node. Set `"actor_context": false` or `"off"` to suppress it for minimal prompts.
202
+ 10. File-backed async recipes pass child `pi -p` actors a bounded JSONL recipe context bundle by default: raw entry/import recipe records, derived `name`, import path/alias, and `"you_are_here": true` on the launching recipe node. The runner materializes child prompts under `prompts/command-NNN.md` and invokes Pi with `@file` args so large prompts and recipe context stay inspectable and argv-safe. Set `"actor_context": false` or `"off"` to suppress recipe context for minimal prompts.
203
203
  11. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
204
- 12. Do not ship concrete model-version defaults in packaged recipes; expose `model`, `models`, and stage-specific model args so the caller must choose current policy at launch.
204
+ 12. Do not ship concrete model-version defaults in packaged recipes. For review-oriented subagent/lens recipes, default model/thinking args through `{current_model}` and `{current_thinking}` so they inherit the selected Pi session policy; keep `model`, `models`, `thinking`, and stage-specific model args explicit so callers can override policy at launch.
205
205
 
206
206
  Priority for same-id recipes:
207
207
 
@@ -284,7 +284,7 @@ The user recipe root is the default tool set by location. It accepts canonical J
284
284
 
285
285
  Use packaged recipes by name with `spawn file=<name>` for async actors, or register/call them as tools when repeated use deserves a stable shortcut.
286
286
 
287
- Packaged review recipes are directly spawnable. Use `spawn file="pipeline-review-readiness" values={...}` for readiness review or `spawn file="subagent-review" values={...}` for one reviewer; pass model/thinking/tool policy through values, then inspect the run. Do not recreate their script commands, call packaged scripts directly, or create wrapper recipes just to launch the maintained recipe.
287
+ Packaged review recipes are directly spawnable. Use `spawn file="pipeline-review-readiness" values={...}` for readiness review or `spawn file="subagent-review" values={...}` for one reviewer; pass model/thinking/tool policy through values, then inspect the run. Review coordinators preflight stage models before fanout; `ACTOR_PREFLIGHT_FAILED` diagnostics identify the failed stage, selected policy, provider error class, prompt file, and override args. Quorum-aware review fanout exposes `subagent_ttl_ms`, `reviewer_concurrency`, `min_successful_reviewers`, and `merge_policy`; partial reviewer evidence is preserved and marked `complete`, `degraded`, or `insufficient_data`. Run status/progress exposes `model_policy` so inherited vs explicit model/thinking choices remain visible. Do not recreate their script commands, call packaged scripts directly, or create wrapper recipes just to launch the maintained recipe.
288
288
 
289
289
  - [`pipeline-room-swarm`](../../recipes/pipeline-room-swarm.json): room-visible swarm coordination with roles, rounds, optional locker, artifact synthesis, and `subagent_ttl_ms` for hard participant budgets.
290
290
  - [`pipeline-repo-health`](../../recipes/pipeline-repo-health.json): git/doc/validation evidence → normalized repository health report.
@@ -2,7 +2,7 @@
2
2
  name: swarm
3
3
  description: Subagent orchestration with scoped locks and quorum consensus. Use for multi-model review, parallel scoped work, delegated audit, and coordinated subagent execution.
4
4
  metadata:
5
- version: 0.37.1
5
+ version: 0.38.0
6
6
  ---
7
7
 
8
8
  # Swarm
@@ -162,7 +162,7 @@ Use [`references/development-swarm.md`](./references/development-swarm.md) for c
162
162
 
163
163
  Purpose: turn one result into many risk lenses and a decision-grade verdict.
164
164
 
165
- Use lens swarm for broad coverage, quorum for confidence on one critical judgement, or both for high-stakes releases. The final report should separate consensus findings, minority findings, merger findings, risks, and recommended next actions.
165
+ Use lens swarm for broad coverage, quorum for confidence on one critical judgement, or both for high-stakes releases. In adapters that expose current session model/thinking policy, default ordinary same-policy review swarms to that current policy and require explicit args only when intentionally varying models or thinking levels. Run a cheap model/tool preflight before launching expensive reviewer fanout; if it fails, use the `ACTOR_PREFLIGHT_FAILED` stage/model/error-class/prompt-file diagnostic to choose explicit override args instead of rerunning blindly. For packaged review swarms, tune `min_successful_reviewers`, `reviewer_concurrency`, `subagent_ttl_ms`, and `merge_policy` instead of manual reruns; preserve partial reports and label the outcome `complete`, `degraded`, or `insufficient_data`. The final report should separate consensus findings, minority findings, merger findings, risks, and recommended next actions.
166
166
 
167
167
  A review swarm synthesis must not fabricate claims. Every final finding should trace to a reviewer note, checked artifact, command output, source, or explicit merger rationale. Devil's Advocate critical findings must be preserved or explicitly disproved with evidence.
168
168
 
@@ -87,10 +87,11 @@ Use ordinary files under the extension temp directory so status tools stay simpl
87
87
 
88
88
  - `run.json`: pid, optional source metadata (`launch_source`, `tool`, `recipe`, `recipe_file`), command-template config, cwd, coordinator owner id, values, named `artifacts`, mailbox metadata, created time, and state dir.
89
89
  - `communication.json`: compact actor communication snapshot with self/root/parent, default-room, member, and contact hints for room-aware scripts and agents.
90
- - `progress.json`: phase, active command count, completed count, failures, and updated time.
90
+ - `progress.json`: phase, active command count, completed count, failures, updated time, and optional `model_policy` provenance for inherited/explicit model and thinking values.
91
91
  - `events.jsonl`: append-only implementation lifecycle log.
92
92
  - `outbox.jsonl`: implementation storage for actor-message envelopes used by `inspect view=messages`, coordinator notifications, or follow-up context. Coordinator follow-ups preserve bounded `body` previews plus message metadata for decision points.
93
93
  - `stdout.log` and `stderr.log`: detached process output.
94
+ - `prompts/command-NNN.md`: state-owned prompt files materialized for child `pi -p` commands so long prompts and appended recipe context travel as `@file` arguments instead of fragile inline argv text.
94
95
  - `result.json`: final code, killed flag, output selector, and optional full-output path.
95
96
 
96
97
  For pi-actors, actor run state defaults to:
@@ -109,6 +110,7 @@ State files use this shape:
109
110
  ~/.pi/agent/tmp/pi-actors/runs/<run>/outbox.jsonl
110
111
  ~/.pi/agent/tmp/pi-actors/runs/<run>/stdout.log
111
112
  ~/.pi/agent/tmp/pi-actors/runs/<run>/stderr.log
113
+ ~/.pi/agent/tmp/pi-actors/runs/<run>/prompts/command-001.md
112
114
  ~/.pi/agent/tmp/pi-actors/runs/<run>/result.json
113
115
  ```
114
116
 
@@ -50,6 +50,8 @@ Common object fields:
50
50
 
51
51
  - `label`: Optional human label for diagnostics and parallel branch reports.
52
52
  - `parallel`: Optional boolean for array templates. Default is `false` for sequence; `true` runs children concurrently.
53
+ - `concurrency`: Optional positive integer cap for a `parallel: true` node. Omit it to launch all children at once.
54
+ - `min_successful`: Optional non-negative integer evidence threshold for a `parallel: true` node. Usable branches are successful branches with non-empty stdout; joins include a `parallel_status` header when this is set.
53
55
  - `when`: Optional node guard. A false guard skips the node; strings may be `flag`, `!flag`, or `{flag?yes:no}` style expressions.
54
56
  - `args`: Optional placeholder declarations. Untyped names remain valid; compact typed forms such as `file:path`, `request_timeout:int`, `speed:number`, `dry_run:bool`, `prompts:array`, and `mode:enum(check,fix)` are valid when the host supports typed tool schemas. Defaults belong in `defaults` or inline placeholder defaults; hosts may normalize interactive shorthand such as `request_timeout:int=60000` before persistence.
55
57
  - `defaults`: Placeholder default values by name.
@@ -187,6 +189,7 @@ Composition rules:
187
189
  - Skipped nodes preserve current stdin/stdout flow and do not execute commands
188
190
  - Each parallel child receives the same stdin, and child stdout values are joined in stable array order before flowing to the next sequence leaf
189
191
  - Parallel branch joins include branch label and status, and tool details include branch metadata plus coverage summary
192
+ - `min_successful` adds a join header with `complete`, `degraded`, or `insufficient_data`; with `failure: "branch"` or `"root"`, an unmet threshold fails at that scope
190
193
  - Each leaf still applies its own inline defaults
191
194
 
192
195
  ```json
@@ -398,7 +401,7 @@ string → leaf command
398
401
  string[] → sequential composition
399
402
  { template } → leaf command object
400
403
  { parallel, template } → sequence or parallel subtree
401
- { parallel, when, args, defaults, delay, retry, failure, recover, output, template } → full node
404
+ { parallel, concurrency, min_successful, when, args, defaults, delay, retry, failure, recover, output, template } → full node
402
405
  ```
403
406
 
404
407
  Start with a string. Add composition when needed. Add `parallel: true` when independent work can run concurrently. Add `when` when a node is conditional. Add delay when launch pacing matters. Add retry when flaky. Add `failure` when propagation scope matters. Add `recover` when a retried node needs cleanup before another attempt. Same contract, growing capability, no dead weight.
@@ -29,6 +29,7 @@ Core subagent recipes:
29
29
  - `recipes/subagent-prompt.json`: Start one prompt-driven subagent.
30
30
  - `recipes/subagent-tools.json`: Start a subagent with an explicit tool allowlist.
31
31
  - `recipes/subagents-prompts.json`: Run prompt fanout with one imported subagent component.
32
+ - `recipes/subagent-preflight.json`: Tiny model/thinking/tool-policy smoke check before expensive fanout; failures surface `ACTOR_PREFLIGHT_FAILED` with stage, selected policy, provider error class, prompt file, and override args.
32
33
  - `recipes/subagent-review.json`: Evidence-grounded review lens.
33
34
  - `recipes/subagent-critic.json`: Assumption and failure-mode critique.
34
35
  - `recipes/subagent-plan.json`: Bounded plan slices and validation gates.
@@ -46,7 +47,7 @@ Core subagent recipes:
46
47
  - `recipes/subagent-followup.json`: Same-context or degraded continuation.
47
48
  - `recipes/subagent-judge.json`: Post-merge/report quality judge.
48
49
 
49
- Most atoms expose policy knobs such as `model`, `thinking`, `tools`, `output_format`, `evidence_policy`, `risk_policy`, source policy, continuity policy, handoff format, or model pools. Packaged recipes intentionally do not ship concrete model-version defaults: callers must pass current model policy at launch, which keeps reusable recipe components from aging around old provider aliases. The generic prompt launchers, including `subagent-tools` and `subagents-prompts`, expose the same core model/thinking/tool/output knobs so callers do not need separate recipe families for policy tuning. Interactive async atoms also declare mailbox metadata for their basic control, completion, and domain-result message surface. Higher-level recipes pass these knobs through instead of hard-coding local policy.
50
+ Most atoms expose policy knobs such as `model`, `thinking`, `tools`, `output_format`, `evidence_policy`, `risk_policy`, source policy, continuity policy, handoff format, or model pools. Packaged recipes intentionally do not ship concrete model-version defaults: review-oriented subagent and lens-swarm recipes default model/thinking args through `{current_model}` and `{current_thinking}` so they inherit the selected Pi session policy, and callers can still pass explicit values when a run should diverge. Recipe inspection marks these inherited policy defaults as `current_policy`, and run status/progress records whether launch policy was inherited, explicit, mixed, or unresolved. Generic prompt launchers, including `subagent-tools` and `subagents-prompts`, expose the same core model/thinking/tool/output knobs so callers do not need separate recipe families for policy tuning. Interactive async atoms also declare mailbox metadata for their basic control, completion, and domain-result message surface. Higher-level recipes pass these knobs through instead of hard-coding local policy.
50
51
 
51
52
  For one-off packaged subagent reviews, launch the recipe directly with `spawn file="subagent-review" values={...}` or `spawn file="pipeline-review-readiness" values={...}`. Do not copy the underlying `pi -p` command or wrap the recipe unless you are creating a durable operator tool with a narrower interface.
52
53
 
@@ -73,7 +74,7 @@ inspect target=run:docs_review view=tail
73
74
  Pipeline recipes demonstrate second-order composition:
74
75
 
75
76
  - `recipes/coordinator-locker.json`: Long-lived coordinator cell with queue, acquire/renew/release lease locks, journal, actor messages for worker coordination, and platform-adapted control metadata.
76
- - `recipes/subagent-review-coordinator.json`: Lens reviewers → verifier → merger → judge → normalizer.
77
+ - `recipes/subagent-review-coordinator.json`: Model/tool preflight with compact provider diagnostics → quorum-aware lens reviewers → verifier → merger → judge → normalizer. Review pipelines expose `subagent_ttl_ms`, `reviewer_concurrency`, `min_successful_reviewers`, and `merge_policy` knobs; reviewer joins preserve partial evidence and mark `complete`, `degraded`, or `insufficient_data`. `npm run conformance` includes a fake-`pi` review-readiness dogfood fixture for this packaged path.
77
78
  - `recipes/pipeline-release-readiness.json`: Task-first release cell: changelog section → package summary → packaged skill summary → validation → release review → artifact report.
78
79
  - `recipes/pipeline-release-summary.json`: Evidence-only release summary cell: changelog section → package summary → packaged skill summary → validation → release summary / risks / PR body draft artifact. It does not commit, open a PR, merge, tag, publish, or perform external release side effects.
79
80
  - `recipes/pipeline-repo-health.json`: Task-first repository-health cell: git status/log → docs index → validation → normalized artifact report.
@@ -208,7 +208,7 @@ Top-level command-template flags may sit beside recipe metadata such as `async`:
208
208
  }
209
209
  ```
210
210
 
211
- Valid command-template flags include `args`, `defaults`, `parallel`, `when`, `label`, `timeout`, `delay`, `output`, `retry`, `failure`, `recover`, and `repeat`.
211
+ Valid command-template flags include `args`, `defaults`, `parallel`, `concurrency`, `min_successful`, `when`, `label`, `timeout`, `delay`, `output`, `retry`, `failure`, `recover`, and `repeat`.
212
212
 
213
213
  Timeout is disabled by default. Set a positive `timeout` when a recipe should fail closed after a bounded runtime; omit it, or set `0`, for intentionally open-ended runs that will be stopped by async cancellation, such as background audio playback.
214
214
 
@@ -260,7 +260,7 @@ If a tool recipe contains `async: true`, calling the tool starts a detached acto
260
260
 
261
261
  ## Values And Public Args
262
262
 
263
- Recipe placeholders come from runtime values, recipe `defaults`, inline placeholder defaults, and registered-tool defaults.
263
+ Recipe placeholders come from runtime values, recipe `defaults`, inline placeholder defaults, and registered-tool defaults. Pi tool launches also inject `{current_model}` and `{current_thinking}` when the active session exposes a selected model and thinking level; recipes that require those placeholders fail before fanout when the current value is unavailable unless the caller supplies an explicit override such as `model` or `thinking`. Async runs persist `model_policy` provenance in run status, progress, and terminal results so operators can tell whether model/thinking values were inherited, explicit, mixed, or unresolved.
264
264
 
265
265
  Recipe tools derive public arguments from the referenced or co-located command template when the recipe is available locally. Explicit `args` is still available when the public tool surface should be narrower than the recipe internals.
266
266
 
package/index.ts CHANGED
@@ -102,13 +102,34 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
102
102
  activeRunContext && scheduleRunEventUpdate(activeRunContext),
103
103
  });
104
104
  const actorToolDefinitions = new Map<string, Tools.ActorToolDefinition>();
105
+ const withCurrentThinkingContext = <T extends Tools.ActorToolDefinition>(
106
+ definition: T,
107
+ ): T => {
108
+ if (typeof definition.execute !== "function") return definition;
109
+ const execute = definition.execute as (...args: unknown[]) => unknown;
110
+ return {
111
+ ...definition,
112
+ execute: (...args: unknown[]) => {
113
+ const nextArgs = [...args];
114
+ const ctx = nextArgs[4];
115
+ if (ctx && typeof ctx === "object") {
116
+ nextArgs[4] = {
117
+ ...(ctx as Record<string, unknown>),
118
+ getThinkingLevel: () => pi.getThinkingLevel(),
119
+ };
120
+ }
121
+ return execute(...nextArgs);
122
+ },
123
+ } as T;
124
+ };
105
125
  const runtime = Runtime.createAutoToolsRuntime({
106
126
  configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
107
127
  exec: CommandTemplates.execCommandTemplate,
108
128
  getActiveTools: () => pi.getActiveTools(),
109
129
  registerTool: (definition) => {
110
- actorToolDefinitions.set(definition.name, definition);
111
- pi.registerTool(definition);
130
+ const wrapped = withCurrentThinkingContext(definition);
131
+ actorToolDefinitions.set(wrapped.name, wrapped);
132
+ pi.registerTool(wrapped);
112
133
  },
113
134
  reservedToolNames: Tools.RESERVED_TOOL_NAMES,
114
135
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
@@ -186,6 +207,6 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
186
207
  getRuntimeTool: (name) => actorToolDefinitions.get(name),
187
208
  registryRuntime: runtime,
188
209
  setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
189
- }),
210
+ }).map(withCurrentThinkingContext),
190
211
  );
191
212
  }
package/lib/async-runs.ts CHANGED
@@ -22,6 +22,12 @@ import type {
22
22
  CommandTemplateValue,
23
23
  } from "./command-templates.ts";
24
24
  import { writeJsonAtomic } from "./file-state.ts";
25
+ import {
26
+ CURRENT_MODEL_VALUE_KEY,
27
+ CURRENT_THINKING_VALUE_KEY,
28
+ describeCurrentPolicyProvenance,
29
+ type CurrentPolicyProvenance,
30
+ } from "./model-context.ts";
25
31
  import * as Paths from "./paths.ts";
26
32
  import * as RecipesReferences from "./recipes-references.ts";
27
33
  import * as RecipesUsage from "./recipes-usage.ts";
@@ -93,6 +99,8 @@ export interface AsyncRunStartParams {
93
99
  args?: string[];
94
100
  defaults?: Record<string, unknown>;
95
101
  parallel?: boolean;
102
+ concurrency?: number | string;
103
+ min_successful?: number | string;
96
104
  label?: string;
97
105
  when?: boolean | string;
98
106
  timeout?: number | string;
@@ -106,6 +114,7 @@ export interface AsyncRunStartParams {
106
114
  recover?: CommandTemplateValue;
107
115
  repeat?: number;
108
116
  values?: Record<string, unknown>;
117
+ policy_values?: Record<string, unknown>;
109
118
  actor_context?: boolean | string;
110
119
  cwd?: string;
111
120
  }
@@ -136,6 +145,7 @@ export interface AsyncRunMeta {
136
145
  artifacts?: Record<string, RunArtifactDeclaration>;
137
146
  control?: AsyncRunControlEndpoint;
138
147
  mailbox?: RecipesReferences.TemplateRecipeMailbox;
148
+ model_policy?: CurrentPolicyProvenance;
139
149
  recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
140
150
  retire_when?: "children_terminal";
141
151
  }
@@ -180,6 +190,8 @@ function resolveRunTemplate(params: AsyncRunStartParams): {
180
190
  "args",
181
191
  "defaults",
182
192
  "parallel",
193
+ "concurrency",
194
+ "min_successful",
183
195
  "label",
184
196
  "when",
185
197
  "timeout",
@@ -270,6 +282,130 @@ function readJson(path: string): Record<string, unknown> | undefined {
270
282
  ).value;
271
283
  }
272
284
 
285
+ function isRecord(value: unknown): value is Record<string, unknown> {
286
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
287
+ }
288
+
289
+ function isFalsyValue(value: unknown): boolean {
290
+ if (value === undefined || value === null || value === false) return true;
291
+ const normalized = String(value).trim().toLowerCase();
292
+ return (
293
+ normalized === "" ||
294
+ normalized === "0" ||
295
+ normalized === "false" ||
296
+ normalized === "no"
297
+ );
298
+ }
299
+
300
+ function stringReferencesPlaceholder(
301
+ value: string,
302
+ placeholder: string,
303
+ ): boolean {
304
+ return new RegExp(`\\{\\s*${placeholder}\\s*\\}`).test(value);
305
+ }
306
+
307
+ function collectUnresolvedCurrentPlaceholderReferences(
308
+ value: unknown,
309
+ values: Record<string, unknown>,
310
+ placeholder: string,
311
+ valueKey: string,
312
+ path = "template",
313
+ refs: string[] = [],
314
+ ): string[] {
315
+ if (typeof value === "string") {
316
+ if (
317
+ stringReferencesPlaceholder(value, placeholder) &&
318
+ isFalsyValue(values[valueKey])
319
+ ) {
320
+ refs.push(path);
321
+ }
322
+ return refs;
323
+ }
324
+ if (Array.isArray(value)) {
325
+ value.forEach((item, index) =>
326
+ collectUnresolvedCurrentPlaceholderReferences(
327
+ item,
328
+ values,
329
+ placeholder,
330
+ valueKey,
331
+ `${path}[${index}]`,
332
+ refs,
333
+ ),
334
+ );
335
+ return refs;
336
+ }
337
+ if (!isRecord(value)) return refs;
338
+ for (const [key, child] of Object.entries(value)) {
339
+ if (key === "defaults" && isRecord(child)) {
340
+ for (const [defaultKey, defaultValue] of Object.entries(child)) {
341
+ if (
342
+ typeof defaultValue === "string" &&
343
+ stringReferencesPlaceholder(defaultValue, placeholder) &&
344
+ isFalsyValue(values[defaultKey]) &&
345
+ isFalsyValue(values[valueKey])
346
+ ) {
347
+ refs.push(`${path}.defaults.${defaultKey}`);
348
+ }
349
+ }
350
+ continue;
351
+ }
352
+ collectUnresolvedCurrentPlaceholderReferences(
353
+ child,
354
+ values,
355
+ placeholder,
356
+ valueKey,
357
+ `${path}.${key}`,
358
+ refs,
359
+ );
360
+ }
361
+ return refs;
362
+ }
363
+
364
+ function assertCurrentPlaceholderReferencesResolved(
365
+ template: CommandTemplateValue,
366
+ defaults: Record<string, unknown> | undefined,
367
+ values: Record<string, unknown>,
368
+ modelPolicy: CurrentPolicyProvenance,
369
+ ): void {
370
+ const checks = [
371
+ {
372
+ label: "model",
373
+ placeholder: "current_model",
374
+ valueKey: CURRENT_MODEL_VALUE_KEY,
375
+ },
376
+ {
377
+ label: "thinking level",
378
+ placeholder: "current_thinking",
379
+ valueKey: CURRENT_THINKING_VALUE_KEY,
380
+ },
381
+ ];
382
+ for (const check of checks) {
383
+ const refs = collectUnresolvedCurrentPlaceholderReferences(
384
+ template,
385
+ values,
386
+ check.placeholder,
387
+ check.valueKey,
388
+ );
389
+ if (defaults) {
390
+ collectUnresolvedCurrentPlaceholderReferences(
391
+ { defaults },
392
+ values,
393
+ check.placeholder,
394
+ check.valueKey,
395
+ "recipe",
396
+ refs,
397
+ );
398
+ }
399
+ if (refs.length === 0) continue;
400
+ throw Object.assign(
401
+ new Error(
402
+ `Template recipe requires the current Pi ${check.label} for inheritance, but no current ${check.label} was available. Pass explicit values or launch from a Pi session with a selected ${check.label}. unresolved=${refs.slice(0, 4).join(",")}`,
403
+ ),
404
+ { model_policy: modelPolicy },
405
+ );
406
+ }
407
+ }
408
+
273
409
  function acquireStateStartLock(stateDir: string): () => void {
274
410
  return RunsStart.acquireStateStartLock(stateDir);
275
411
  }
@@ -286,6 +422,25 @@ export function startRun(
286
422
  const resolved = resolveRunTemplate(startParams);
287
423
  const run = safeRunId(startParams.run_id);
288
424
  const stateDir = resolveStateDir(startParams, run);
425
+ const values = {
426
+ ...(startParams.values || {}),
427
+ actor_address: `run:${run}`,
428
+ communication_file: join(stateDir, "communication.json"),
429
+ default_room: `room:${run}`,
430
+ run_id: run,
431
+ state_dir: stateDir,
432
+ };
433
+ const modelPolicy = describeCurrentPolicyProvenance({
434
+ defaults: startParams.defaults,
435
+ template: resolved.template,
436
+ values: startParams.policy_values ?? startParams.values ?? {},
437
+ });
438
+ assertCurrentPlaceholderReferencesResolved(
439
+ resolved.template,
440
+ startParams.defaults,
441
+ values,
442
+ modelPolicy,
443
+ );
289
444
  assertNoActiveRunState(stateDir);
290
445
  mkdirSync(stateDir, { recursive: true });
291
446
  const releaseStartLock = acquireStateStartLock(stateDir);
@@ -315,14 +470,6 @@ export function startRun(
315
470
  const outFd = openSync(stdout, "a");
316
471
  const errFd = openSync(stderr, "a");
317
472
  const argv = asyncRunnerArgv(stateDir);
318
- const values = {
319
- ...(startParams.values || {}),
320
- actor_address: `run:${run}`,
321
- communication_file: join(stateDir, "communication.json"),
322
- default_room: `room:${run}`,
323
- run_id: run,
324
- state_dir: stateDir,
325
- };
326
473
  const outputValues = {
327
474
  ...(startParams.defaults || {}),
328
475
  ...values,
@@ -345,6 +492,7 @@ export function startRun(
345
492
  ...(startParams.tool ? { tool: startParams.tool } : {}),
346
493
  template: resolved.template,
347
494
  values,
495
+ model_policy: modelPolicy,
348
496
  ...(artifacts ? { artifacts } : {}),
349
497
  ...(startParams.control ? { control: startParams.control } : {}),
350
498
  ...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
@@ -368,6 +516,7 @@ export function startRun(
368
516
  writeJsonAtomic(join(stateDir, "progress.json"), {
369
517
  completed: 0,
370
518
  failures: [],
519
+ model_policy: modelPolicy,
371
520
  phase: "starting",
372
521
  updatedAt: new Date().toISOString(),
373
522
  });
@@ -22,6 +22,8 @@ export interface CommandTemplateObjectConfig {
22
22
  actorRecipeContext?: CommandTemplateActorRecipeContext;
23
23
  label?: string;
24
24
  parallel?: boolean;
25
+ concurrency?: number | string;
26
+ min_successful?: number | string;
25
27
  when?: boolean | string;
26
28
  template?: CommandTemplateValue;
27
29
  args?: string[];