@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.
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +19 -0
- package/README.md +6 -5
- package/dist/index.js +23 -3
- package/dist/lib/async-runs.d.ts +5 -0
- package/dist/lib/async-runs.js +87 -8
- package/dist/lib/command-templates.d.ts +2 -0
- package/dist/lib/execution.d.ts +4 -0
- package/dist/lib/execution.js +154 -13
- package/dist/lib/model-context.d.ts +56 -0
- package/dist/lib/model-context.js +220 -0
- package/dist/lib/observability.d.ts +2 -0
- package/dist/lib/observability.js +32 -6
- package/dist/lib/preflight-diagnostics.d.ts +27 -0
- package/dist/lib/preflight-diagnostics.js +86 -0
- package/dist/lib/prompts.d.ts +1 -1
- package/dist/lib/prompts.js +2 -2
- package/dist/lib/recipes-context.d.ts +7 -0
- package/dist/lib/recipes-context.js +20 -2
- package/dist/lib/recipes-discovery.d.ts +1 -0
- package/dist/lib/recipes-discovery.js +63 -7
- package/dist/lib/recipes-references.d.ts +2 -0
- package/dist/lib/recipes-references.js +10 -0
- package/dist/lib/tools-local.d.ts +2 -1
- package/dist/lib/tools-local.js +6 -4
- package/dist/lib/tools-response.js +22 -1
- package/dist/lib/tools-spawn.d.ts +2 -1
- package/dist/lib/tools-spawn.js +2 -1
- package/dist/recipes/lens-swarm.json +15 -2
- package/dist/recipes/pipeline-release-readiness.json +22 -3
- package/dist/recipes/pipeline-review-readiness.json +22 -3
- package/dist/recipes/subagent-judge.json +2 -1
- package/dist/recipes/subagent-merge.json +5 -2
- package/dist/recipes/subagent-normalize.json +2 -1
- package/dist/recipes/subagent-preflight.json +29 -0
- package/dist/recipes/subagent-review-coordinator.json +79 -7
- package/dist/recipes/subagent-review.json +2 -1
- package/dist/recipes/subagent-verify.json +2 -1
- package/dist/scripts/async-runner.mjs +84 -10
- package/dist/scripts/conformance.mjs +1 -0
- package/dist/skills/actors/SKILL.md +4 -4
- package/dist/skills/swarm/SKILL.md +2 -2
- package/docs/async-runs.md +3 -1
- package/docs/command-templates.md +4 -1
- package/docs/recipe-library.md +3 -2
- package/docs/template-recipes.md +2 -2
- package/index.ts +24 -3
- package/lib/async-runs.ts +157 -8
- package/lib/command-templates.ts +2 -0
- package/lib/execution.ts +218 -24
- package/lib/model-context.ts +359 -0
- package/lib/observability.ts +35 -6
- package/lib/preflight-diagnostics.ts +132 -0
- package/lib/prompts.ts +2 -2
- package/lib/recipes-context.ts +29 -2
- package/lib/recipes-discovery.ts +76 -11
- package/lib/recipes-references.ts +12 -0
- package/lib/tools-local.ts +22 -8
- package/lib/tools-response.ts +26 -1
- package/lib/tools-spawn.ts +6 -2
- package/package.json +1 -1
- package/recipes/lens-swarm.json +15 -2
- package/recipes/pipeline-release-readiness.json +22 -3
- package/recipes/pipeline-review-readiness.json +22 -3
- package/recipes/subagent-judge.json +2 -1
- package/recipes/subagent-merge.json +5 -2
- package/recipes/subagent-normalize.json +2 -1
- package/recipes/subagent-preflight.json +29 -0
- package/recipes/subagent-review-coordinator.json +79 -7
- package/recipes/subagent-review.json +2 -1
- package/recipes/subagent-verify.json +2 -1
- package/scripts/async-runner.mjs +84 -10
- package/scripts/conformance.mjs +1 -0
- package/skills/actors/SKILL.md +4 -4
- package/skills/swarm/SKILL.md +2 -2
package/BACKLOG.md
CHANGED
|
@@ -71,4 +71,4 @@ These are valid ideas but not current focus. Reintroduce only with concrete evid
|
|
|
71
71
|
|
|
72
72
|
## Suggested Milestone Order
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
1. Re-curate after the next real packaged review-swarm dogfood run.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.38.1: Windows Recipe ACL Hotfix
|
|
6
|
+
|
|
7
|
+
- `[Registry]` Replaced POSIX mode-bit recipe-root writability checks on Windows with ACL-aware diagnostics, avoiding false `world-writable` and `group-writable` startup warnings from Node's NTFS mode emulation while still flagging broad Windows write grants.
|
|
8
|
+
- `[Tests]` Added portable coverage for broad Windows ACL parsing and skipped the POSIX chmod diagnostic regression on Windows.
|
|
9
|
+
|
|
10
|
+
## 0.38.0: Review Swarm Pipeline Hardening
|
|
11
|
+
|
|
12
|
+
- `[Recipes]` Added current model/thinking inheritance for packaged review/lens-swarm recipes through `{current_model}` and `{current_thinking}`, so normal subagent review swarms use the selected Pi session policy by default while explicit args still override it.
|
|
13
|
+
- `[Runtime]` Injected the current Pi model and thinking level into spawn/runtime tool recipe values and fail fast before async fanout when a current placeholder cannot be resolved.
|
|
14
|
+
- `[Runtime]` Persisted `model_policy` provenance in async run status, progress, terminal results, compact status text, and terminal follow-ups so operators can distinguish inherited, explicit, mixed, and unresolved model/thinking policy.
|
|
15
|
+
- `[Runtime]` Materialized child `pi -p` prompts into run-local `prompts/command-NNN.md` files and invoked Pi with `@file` arguments, preserving long quoted prompts and recipe context without fragile inline argv transport.
|
|
16
|
+
- `[Runtime]` Expanded parallel branch diagnostics with stdout/stderr byte counts, tail previews, failure reasons, prompt-file paths, soft-quorum usability, and failed-run result/progress details; branch-failure fanouts now fail when every branch fails or emits empty output.
|
|
17
|
+
- `[Recipes]` Added `subagent-preflight` and wired the review coordinator to run stage model/thinking/tool smoke checks before reviewer fanout, so unavailable providers fail before expensive reviewer branches launch.
|
|
18
|
+
- `[Recipes]` Made review pipelines quorum-aware with `subagent_ttl_ms`, `reviewer_concurrency`, `min_successful_reviewers`, and `merge_policy` knobs; partial reviewer evidence is preserved, verifier/merger stages run only after the evidence threshold is met, and normalized reports must mark `complete`, `degraded`, or `insufficient_data`.
|
|
19
|
+
- `[Diagnostics]` Preflight command failures now emit `ACTOR_PREFLIGHT_FAILED` diagnostics with stage, selected model/thinking, provider error class, prompt file, and suggested override args; fake-`pi` coverage proves reviewer fanout starts only after preflight passes.
|
|
20
|
+
- `[Tests]` Added a deterministic packaged review-readiness dogfood fixture to `npm run conformance`, using a fake local `pi` executable to verify preflight, degraded reviewer fanout, prompt-file artifacts, branch failure capture, merge gating, and terminal status without external model APIs.
|
|
21
|
+
- `[Guidance]` Updated README, actor/swarm skills, recipe docs, async-run docs, and onboarding copy to steer review swarms away from `models.json` rediscovery and toward maintained recipes with current model/thinking inheritance, policy provenance, prompt-file transport, preflight, quorum controls, and inspectable branch diagnostics.
|
|
22
|
+
- `[Backlog]` Captured the remaining `pi-telegram` review-swarm dogfood lessons as concrete `pi-actors` hardening work: review-pipeline preflight/quorum resilience, model-provenance visibility, and a deterministic review-swarm fixture.
|
|
23
|
+
|
|
5
24
|
## 0.37.1: Subagent Recipe Prompt Injection Hotfix
|
|
6
25
|
|
|
7
26
|
- `[Recipes]` Fixed actor recipe context injection for child `pi -p` launches with options after the print flag, so packaged subagent recipes such as `subagent-review` and `pipeline-review-readiness` append context to the actual prompt instead of corrupting `--model` or other option values.
|
package/README.md
CHANGED
|
@@ -111,22 +111,23 @@ cat > ~/.pi/agent/recipes/docs_review.json <<'JSON'
|
|
|
111
111
|
{
|
|
112
112
|
"description": "Start an async docs review actor",
|
|
113
113
|
"async": true,
|
|
114
|
-
"args": ["scope:path", "model:string"],
|
|
114
|
+
"args": ["scope:path", "model:string", "thinking:string"],
|
|
115
|
+
"defaults": { "model": "{current_model}", "thinking": "{current_thinking}" },
|
|
115
116
|
"mailbox": {
|
|
116
117
|
"accepts": ["control.kill", "control.continue"],
|
|
117
118
|
"emits": ["review.completed", "run.failed"]
|
|
118
119
|
},
|
|
119
|
-
"template": "pi -p --model {model} --no-tools \"Review {scope} for unclear actor-runtime onboarding. Return concise findings.\""
|
|
120
|
+
"template": "pi -p --model {model} --thinking {thinking} --no-tools \"Review {scope} for unclear actor-runtime onboarding. Return concise findings.\""
|
|
120
121
|
}
|
|
121
122
|
JSON
|
|
122
123
|
```
|
|
123
124
|
|
|
124
|
-
Because it lives under `~/.pi/agent/recipes/`, the file becomes a persistent agent tool by location. The filename is the tool id.
|
|
125
|
+
Because it lives under `~/.pi/agent/recipes/`, the file becomes a persistent agent tool by location. The filename is the tool id. `{current_model}` and `{current_thinking}` inherit the selected Pi session model/thinking level; pass `model=...` or `thinking=...` only when a run should intentionally diverge. Run status/progress and terminal follow-ups include `model_policy` provenance so inherited, explicit, and unresolved policy choices stay inspectable.
|
|
125
126
|
|
|
126
127
|
Start it:
|
|
127
128
|
|
|
128
129
|
```text
|
|
129
|
-
docs_review scope="README.md"
|
|
130
|
+
docs_review scope="README.md" run_id=docs_review
|
|
130
131
|
```
|
|
131
132
|
|
|
132
133
|
Inspect only when there is a reason:
|
|
@@ -258,7 +259,7 @@ Templates support:
|
|
|
258
259
|
- Retries, recovery, failure policy, delays, and guarded execution;
|
|
259
260
|
- Async run values such as `{run_id}`, `{state_dir}`, `{actor_address}`, `{default_room}`, and `{communication_file}`.
|
|
260
261
|
|
|
261
|
-
The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. JSON is the canonical precise recipe format; Markdown recipes use frontmatter plus fenced `template`/`json recipe` blocks for literate authoring and compile into the same model. The run actor owns detached lifecycle, state, messages, cancellation, and inspection. File-backed async recipes also provide child `pi -p` actors with a bounded JSONL recipe context bundle by default, including raw entry/import recipe records and a `"you_are_here": true` marker for the recipe node that launched the child. Set `"actor_context": false` or `"off"` in a recipe to suppress that context for minimal prompts.
|
|
262
|
+
The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. JSON is the canonical precise recipe format; Markdown recipes use frontmatter plus fenced `template`/`json recipe` blocks for literate authoring and compile into the same model. The run actor owns detached lifecycle, state, messages, cancellation, and inspection. File-backed async recipes also provide child `pi -p` actors with a bounded JSONL recipe context bundle by default, including raw entry/import recipe records and a `"you_are_here": true` marker for the recipe node that launched the child; the runner materializes child prompts under `prompts/` and invokes Pi with `@file` arguments. Set `"actor_context": false` or `"off"` in a recipe to suppress that context for minimal prompts.
|
|
262
263
|
|
|
263
264
|
## Recipe Library
|
|
264
265
|
|
package/dist/index.js
CHANGED
|
@@ -84,13 +84,33 @@ export default function toolRegistryExtension(pi) {
|
|
|
84
84
|
onChange: () => activeRunContext && scheduleRunEventUpdate(activeRunContext),
|
|
85
85
|
});
|
|
86
86
|
const actorToolDefinitions = new Map();
|
|
87
|
+
const withCurrentThinkingContext = (definition) => {
|
|
88
|
+
if (typeof definition.execute !== "function")
|
|
89
|
+
return definition;
|
|
90
|
+
const execute = definition.execute;
|
|
91
|
+
return {
|
|
92
|
+
...definition,
|
|
93
|
+
execute: (...args) => {
|
|
94
|
+
const nextArgs = [...args];
|
|
95
|
+
const ctx = nextArgs[4];
|
|
96
|
+
if (ctx && typeof ctx === "object") {
|
|
97
|
+
nextArgs[4] = {
|
|
98
|
+
...ctx,
|
|
99
|
+
getThinkingLevel: () => pi.getThinkingLevel(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return execute(...nextArgs);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
};
|
|
87
106
|
const runtime = Runtime.createAutoToolsRuntime({
|
|
88
107
|
configPath: Paths.EXTENSION_RUNTIME_PATHS.configPath,
|
|
89
108
|
exec: CommandTemplates.execCommandTemplate,
|
|
90
109
|
getActiveTools: () => pi.getActiveTools(),
|
|
91
110
|
registerTool: (definition) => {
|
|
92
|
-
|
|
93
|
-
|
|
111
|
+
const wrapped = withCurrentThinkingContext(definition);
|
|
112
|
+
actorToolDefinitions.set(wrapped.name, wrapped);
|
|
113
|
+
pi.registerTool(wrapped);
|
|
94
114
|
},
|
|
95
115
|
reservedToolNames: Tools.RESERVED_TOOL_NAMES,
|
|
96
116
|
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
@@ -161,5 +181,5 @@ export default function toolRegistryExtension(pi) {
|
|
|
161
181
|
getRuntimeTool: (name) => actorToolDefinitions.get(name),
|
|
162
182
|
registryRuntime: runtime,
|
|
163
183
|
setActiveTools: (toolNames) => pi.setActiveTools(toolNames),
|
|
164
|
-
}));
|
|
184
|
+
}).map(withCurrentThinkingContext));
|
|
165
185
|
}
|
package/dist/lib/async-runs.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Owns launch, state observation, listing, message/control facade methods, and retention while runs-* subdomains own narrower run internals.
|
|
4
4
|
*/
|
|
5
5
|
import type { CommandTemplateFailureScope, CommandTemplateValue } from "./command-templates.ts";
|
|
6
|
+
import { type CurrentPolicyProvenance } from "./model-context.ts";
|
|
6
7
|
import * as RecipesReferences from "./recipes-references.ts";
|
|
7
8
|
import { type RunArtifactDeclaration } from "./runs-artifacts.ts";
|
|
8
9
|
import { type RunOutboxEvent } from "./runs-outbox.ts";
|
|
@@ -28,6 +29,8 @@ export interface AsyncRunStartParams {
|
|
|
28
29
|
args?: string[];
|
|
29
30
|
defaults?: Record<string, unknown>;
|
|
30
31
|
parallel?: boolean;
|
|
32
|
+
concurrency?: number | string;
|
|
33
|
+
min_successful?: number | string;
|
|
31
34
|
label?: string;
|
|
32
35
|
when?: boolean | string;
|
|
33
36
|
timeout?: number | string;
|
|
@@ -41,6 +44,7 @@ export interface AsyncRunStartParams {
|
|
|
41
44
|
recover?: CommandTemplateValue;
|
|
42
45
|
repeat?: number;
|
|
43
46
|
values?: Record<string, unknown>;
|
|
47
|
+
policy_values?: Record<string, unknown>;
|
|
44
48
|
actor_context?: boolean | string;
|
|
45
49
|
cwd?: string;
|
|
46
50
|
}
|
|
@@ -63,6 +67,7 @@ export interface AsyncRunMeta {
|
|
|
63
67
|
artifacts?: Record<string, RunArtifactDeclaration>;
|
|
64
68
|
control?: AsyncRunControlEndpoint;
|
|
65
69
|
mailbox?: RecipesReferences.TemplateRecipeMailbox;
|
|
70
|
+
model_policy?: CurrentPolicyProvenance;
|
|
66
71
|
recipe_context_records?: RecipesReferences.TemplateRecipeContextRecord[];
|
|
67
72
|
retire_when?: "children_terminal";
|
|
68
73
|
}
|
package/dist/lib/async-runs.js
CHANGED
|
@@ -7,6 +7,7 @@ import { closeSync, existsSync, mkdirSync, openSync, writeFileSync, } from "node
|
|
|
7
7
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { writeJsonAtomic } from "./file-state.js";
|
|
10
|
+
import { CURRENT_MODEL_VALUE_KEY, CURRENT_THINKING_VALUE_KEY, describeCurrentPolicyProvenance, } from "./model-context.js";
|
|
10
11
|
import * as Paths from "./paths.js";
|
|
11
12
|
import * as RecipesReferences from "./recipes-references.js";
|
|
12
13
|
import * as RecipesUsage from "./recipes-usage.js";
|
|
@@ -50,6 +51,8 @@ function resolveRunTemplate(params) {
|
|
|
50
51
|
"args",
|
|
51
52
|
"defaults",
|
|
52
53
|
"parallel",
|
|
54
|
+
"concurrency",
|
|
55
|
+
"min_successful",
|
|
53
56
|
"label",
|
|
54
57
|
"when",
|
|
55
58
|
"timeout",
|
|
@@ -128,6 +131,74 @@ function resolveStartParams(params) {
|
|
|
128
131
|
function readJson(path) {
|
|
129
132
|
return readJsonFileResilient(path, undefined).value;
|
|
130
133
|
}
|
|
134
|
+
function isRecord(value) {
|
|
135
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
136
|
+
}
|
|
137
|
+
function isFalsyValue(value) {
|
|
138
|
+
if (value === undefined || value === null || value === false)
|
|
139
|
+
return true;
|
|
140
|
+
const normalized = String(value).trim().toLowerCase();
|
|
141
|
+
return (normalized === "" ||
|
|
142
|
+
normalized === "0" ||
|
|
143
|
+
normalized === "false" ||
|
|
144
|
+
normalized === "no");
|
|
145
|
+
}
|
|
146
|
+
function stringReferencesPlaceholder(value, placeholder) {
|
|
147
|
+
return new RegExp(`\\{\\s*${placeholder}\\s*\\}`).test(value);
|
|
148
|
+
}
|
|
149
|
+
function collectUnresolvedCurrentPlaceholderReferences(value, values, placeholder, valueKey, path = "template", refs = []) {
|
|
150
|
+
if (typeof value === "string") {
|
|
151
|
+
if (stringReferencesPlaceholder(value, placeholder) &&
|
|
152
|
+
isFalsyValue(values[valueKey])) {
|
|
153
|
+
refs.push(path);
|
|
154
|
+
}
|
|
155
|
+
return refs;
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
value.forEach((item, index) => collectUnresolvedCurrentPlaceholderReferences(item, values, placeholder, valueKey, `${path}[${index}]`, refs));
|
|
159
|
+
return refs;
|
|
160
|
+
}
|
|
161
|
+
if (!isRecord(value))
|
|
162
|
+
return refs;
|
|
163
|
+
for (const [key, child] of Object.entries(value)) {
|
|
164
|
+
if (key === "defaults" && isRecord(child)) {
|
|
165
|
+
for (const [defaultKey, defaultValue] of Object.entries(child)) {
|
|
166
|
+
if (typeof defaultValue === "string" &&
|
|
167
|
+
stringReferencesPlaceholder(defaultValue, placeholder) &&
|
|
168
|
+
isFalsyValue(values[defaultKey]) &&
|
|
169
|
+
isFalsyValue(values[valueKey])) {
|
|
170
|
+
refs.push(`${path}.defaults.${defaultKey}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
collectUnresolvedCurrentPlaceholderReferences(child, values, placeholder, valueKey, `${path}.${key}`, refs);
|
|
176
|
+
}
|
|
177
|
+
return refs;
|
|
178
|
+
}
|
|
179
|
+
function assertCurrentPlaceholderReferencesResolved(template, defaults, values, modelPolicy) {
|
|
180
|
+
const checks = [
|
|
181
|
+
{
|
|
182
|
+
label: "model",
|
|
183
|
+
placeholder: "current_model",
|
|
184
|
+
valueKey: CURRENT_MODEL_VALUE_KEY,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
label: "thinking level",
|
|
188
|
+
placeholder: "current_thinking",
|
|
189
|
+
valueKey: CURRENT_THINKING_VALUE_KEY,
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
for (const check of checks) {
|
|
193
|
+
const refs = collectUnresolvedCurrentPlaceholderReferences(template, values, check.placeholder, check.valueKey);
|
|
194
|
+
if (defaults) {
|
|
195
|
+
collectUnresolvedCurrentPlaceholderReferences({ defaults }, values, check.placeholder, check.valueKey, "recipe", refs);
|
|
196
|
+
}
|
|
197
|
+
if (refs.length === 0)
|
|
198
|
+
continue;
|
|
199
|
+
throw Object.assign(new Error(`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(",")}`), { model_policy: modelPolicy });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
131
202
|
function acquireStateStartLock(stateDir) {
|
|
132
203
|
return RunsStart.acquireStateStartLock(stateDir);
|
|
133
204
|
}
|
|
@@ -139,6 +210,20 @@ export function startRun(params, cwd) {
|
|
|
139
210
|
const resolved = resolveRunTemplate(startParams);
|
|
140
211
|
const run = safeRunId(startParams.run_id);
|
|
141
212
|
const stateDir = resolveStateDir(startParams, run);
|
|
213
|
+
const values = {
|
|
214
|
+
...(startParams.values || {}),
|
|
215
|
+
actor_address: `run:${run}`,
|
|
216
|
+
communication_file: join(stateDir, "communication.json"),
|
|
217
|
+
default_room: `room:${run}`,
|
|
218
|
+
run_id: run,
|
|
219
|
+
state_dir: stateDir,
|
|
220
|
+
};
|
|
221
|
+
const modelPolicy = describeCurrentPolicyProvenance({
|
|
222
|
+
defaults: startParams.defaults,
|
|
223
|
+
template: resolved.template,
|
|
224
|
+
values: startParams.policy_values ?? startParams.values ?? {},
|
|
225
|
+
});
|
|
226
|
+
assertCurrentPlaceholderReferencesResolved(resolved.template, startParams.defaults, values, modelPolicy);
|
|
142
227
|
assertNoActiveRunState(stateDir);
|
|
143
228
|
mkdirSync(stateDir, { recursive: true });
|
|
144
229
|
const releaseStartLock = acquireStateStartLock(stateDir);
|
|
@@ -162,14 +247,6 @@ export function startRun(params, cwd) {
|
|
|
162
247
|
const outFd = openSync(stdout, "a");
|
|
163
248
|
const errFd = openSync(stderr, "a");
|
|
164
249
|
const argv = asyncRunnerArgv(stateDir);
|
|
165
|
-
const values = {
|
|
166
|
-
...(startParams.values || {}),
|
|
167
|
-
actor_address: `run:${run}`,
|
|
168
|
-
communication_file: join(stateDir, "communication.json"),
|
|
169
|
-
default_room: `room:${run}`,
|
|
170
|
-
run_id: run,
|
|
171
|
-
state_dir: stateDir,
|
|
172
|
-
};
|
|
173
250
|
const outputValues = {
|
|
174
251
|
...(startParams.defaults || {}),
|
|
175
252
|
...values,
|
|
@@ -192,6 +269,7 @@ export function startRun(params, cwd) {
|
|
|
192
269
|
...(startParams.tool ? { tool: startParams.tool } : {}),
|
|
193
270
|
template: resolved.template,
|
|
194
271
|
values,
|
|
272
|
+
model_policy: modelPolicy,
|
|
195
273
|
...(artifacts ? { artifacts } : {}),
|
|
196
274
|
...(startParams.control ? { control: startParams.control } : {}),
|
|
197
275
|
...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
|
|
@@ -215,6 +293,7 @@ export function startRun(params, cwd) {
|
|
|
215
293
|
writeJsonAtomic(join(stateDir, "progress.json"), {
|
|
216
294
|
completed: 0,
|
|
217
295
|
failures: [],
|
|
296
|
+
model_policy: modelPolicy,
|
|
218
297
|
phase: "starting",
|
|
219
298
|
updatedAt: new Date().toISOString(),
|
|
220
299
|
});
|
|
@@ -15,6 +15,8 @@ export interface CommandTemplateObjectConfig {
|
|
|
15
15
|
actorRecipeContext?: CommandTemplateActorRecipeContext;
|
|
16
16
|
label?: string;
|
|
17
17
|
parallel?: boolean;
|
|
18
|
+
concurrency?: number | string;
|
|
19
|
+
min_successful?: number | string;
|
|
18
20
|
when?: boolean | string;
|
|
19
21
|
template?: CommandTemplateValue;
|
|
20
22
|
args?: string[];
|
package/dist/lib/execution.d.ts
CHANGED
|
@@ -22,10 +22,13 @@ export interface ToolExecResult {
|
|
|
22
22
|
export interface BranchReport {
|
|
23
23
|
code: number;
|
|
24
24
|
command: string;
|
|
25
|
+
failureReason?: string;
|
|
25
26
|
killed: boolean;
|
|
26
27
|
label: string;
|
|
27
28
|
status: "done" | "failed" | "timeout";
|
|
28
29
|
stderr?: string;
|
|
30
|
+
stderrBytes: number;
|
|
31
|
+
stdout?: string;
|
|
29
32
|
stdoutBytes: number;
|
|
30
33
|
}
|
|
31
34
|
export interface SoftQuorumReport {
|
|
@@ -53,6 +56,7 @@ export interface RegisteredToolExecutionResult {
|
|
|
53
56
|
killed: boolean;
|
|
54
57
|
}>;
|
|
55
58
|
softQuorum?: SoftQuorumReport;
|
|
59
|
+
failureReason?: string;
|
|
56
60
|
template: CommandTemplates.CommandTemplateValue;
|
|
57
61
|
templateWarnings?: string[];
|
|
58
62
|
tool: string;
|
package/dist/lib/execution.js
CHANGED
|
@@ -37,6 +37,15 @@ function formatInvocationDetail(invocation) {
|
|
|
37
37
|
function formatCommandDetail(commands) {
|
|
38
38
|
return commands.length === 1 ? commands[0] : commands.join(" && ");
|
|
39
39
|
}
|
|
40
|
+
function getExecutionFailureReason(branches, softQuorum, stderr) {
|
|
41
|
+
if (/parallel branch quorum not met/.test(stderr)) {
|
|
42
|
+
return "parallel_quorum_not_met";
|
|
43
|
+
}
|
|
44
|
+
if (branches.length > 0 && softQuorum?.usable === false) {
|
|
45
|
+
return "all_branches_unusable";
|
|
46
|
+
}
|
|
47
|
+
return "command_failed";
|
|
48
|
+
}
|
|
40
49
|
function mergeDefaults(inherited, own) {
|
|
41
50
|
if (!inherited && !own)
|
|
42
51
|
return undefined;
|
|
@@ -53,17 +62,32 @@ function getBranchStatus(result) {
|
|
|
53
62
|
return "done";
|
|
54
63
|
return result.killed ? "timeout" : "failed";
|
|
55
64
|
}
|
|
65
|
+
function getBranchFailureReason(result) {
|
|
66
|
+
if (result.code === 0) {
|
|
67
|
+
return result.stdout.trim() ? undefined : "empty_output";
|
|
68
|
+
}
|
|
69
|
+
if (result.killed)
|
|
70
|
+
return "timeout_or_killed";
|
|
71
|
+
return result.stderr.trim() ? "nonzero_exit" : "nonzero_exit_empty_stderr";
|
|
72
|
+
}
|
|
56
73
|
function createBranchReport(label, command, result) {
|
|
74
|
+
const failureReason = getBranchFailureReason(result);
|
|
57
75
|
return {
|
|
58
76
|
code: result.code,
|
|
59
77
|
command,
|
|
78
|
+
...(failureReason ? { failureReason } : {}),
|
|
60
79
|
killed: result.killed,
|
|
61
80
|
label,
|
|
62
81
|
status: getBranchStatus(result),
|
|
63
|
-
...(result.stderr ? { stderr: result.stderr.slice(
|
|
82
|
+
...(result.stderr ? { stderr: result.stderr.slice(-1000) } : {}),
|
|
83
|
+
stderrBytes: Buffer.byteLength(result.stderr),
|
|
84
|
+
...(result.stdout ? { stdout: result.stdout.slice(-1000) } : {}),
|
|
64
85
|
stdoutBytes: Buffer.byteLength(result.stdout),
|
|
65
86
|
};
|
|
66
87
|
}
|
|
88
|
+
function isUsableBranch(branch) {
|
|
89
|
+
return branch.status === "done" && branch.stdoutBytes > 0;
|
|
90
|
+
}
|
|
67
91
|
function createSoftQuorum(branches) {
|
|
68
92
|
if (branches.length === 0)
|
|
69
93
|
return undefined;
|
|
@@ -75,9 +99,12 @@ function createSoftQuorum(branches) {
|
|
|
75
99
|
done,
|
|
76
100
|
expected: branches.length,
|
|
77
101
|
failed,
|
|
78
|
-
usable:
|
|
102
|
+
usable: branches.some(isUsableBranch),
|
|
79
103
|
};
|
|
80
104
|
}
|
|
105
|
+
function countUsableBranches(branches) {
|
|
106
|
+
return branches.filter(isUsableBranch).length;
|
|
107
|
+
}
|
|
81
108
|
function getMaxParallelBranches() {
|
|
82
109
|
const raw = Number(process.env.PI_ACTORS_MAX_PARALLEL_BRANCHES ?? "");
|
|
83
110
|
return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_PARALLEL_BRANCHES;
|
|
@@ -111,6 +138,24 @@ function normalizeRetry(value, values) {
|
|
|
111
138
|
throw new Error("Command template retry must be a positive integer.");
|
|
112
139
|
return resolved;
|
|
113
140
|
}
|
|
141
|
+
function normalizeConcurrency(value, values, branchCount) {
|
|
142
|
+
const resolved = resolveNumericControlField(value, values, "concurrency");
|
|
143
|
+
if (resolved === undefined)
|
|
144
|
+
return branchCount;
|
|
145
|
+
if (!Number.isInteger(resolved) || resolved < 1)
|
|
146
|
+
throw new Error("Command template concurrency must be a positive integer.");
|
|
147
|
+
return Math.min(resolved, branchCount);
|
|
148
|
+
}
|
|
149
|
+
function normalizeMinSuccessful(value, values, branchCount) {
|
|
150
|
+
const resolved = resolveNumericControlField(value, values, "min_successful");
|
|
151
|
+
if (resolved === undefined)
|
|
152
|
+
return undefined;
|
|
153
|
+
if (!Number.isInteger(resolved) || resolved < 0)
|
|
154
|
+
throw new Error("Command template min_successful must be a non-negative integer.");
|
|
155
|
+
if (resolved > branchCount)
|
|
156
|
+
throw new Error(`Command template min_successful ${resolved} exceeds branch count ${branchCount}.`);
|
|
157
|
+
return resolved;
|
|
158
|
+
}
|
|
114
159
|
function getRecoverConfig(config) {
|
|
115
160
|
const recovered = Array.isArray(config) ? { template: config } : config;
|
|
116
161
|
const normalized = CommandTemplates.normalizeCommandTemplateConfig(recovered);
|
|
@@ -174,8 +219,20 @@ async function applyDelay(delay, values, signal) {
|
|
|
174
219
|
return;
|
|
175
220
|
await sleep(resolved, signal);
|
|
176
221
|
}
|
|
177
|
-
function
|
|
178
|
-
|
|
222
|
+
function getParallelStatus(branches, minSuccessful) {
|
|
223
|
+
const usable = countUsableBranches(branches);
|
|
224
|
+
if (minSuccessful !== undefined && usable < minSuccessful) {
|
|
225
|
+
return "insufficient_data";
|
|
226
|
+
}
|
|
227
|
+
return branches.every(isUsableBranch) ? "complete" : "degraded";
|
|
228
|
+
}
|
|
229
|
+
function formatParallelStatusHeader(branches, minSuccessful) {
|
|
230
|
+
if (minSuccessful === undefined)
|
|
231
|
+
return undefined;
|
|
232
|
+
return `--- parallel_status: ${getParallelStatus(branches, minSuccessful)} usable: ${countUsableBranches(branches)} expected: ${branches.length} minimum: ${minSuccessful} ---`;
|
|
233
|
+
}
|
|
234
|
+
function joinParallelStdout(branches, results, minSuccessful) {
|
|
235
|
+
const body = results
|
|
179
236
|
.map((result, index) => {
|
|
180
237
|
const branch = branches[index];
|
|
181
238
|
const header = `--- branch: ${branch.label} status: ${branch.status} ---`;
|
|
@@ -185,6 +242,24 @@ function joinParallelStdout(branches, results) {
|
|
|
185
242
|
return `${header}\nexit: ${branch.code}${stderr}`;
|
|
186
243
|
})
|
|
187
244
|
.join("\n");
|
|
245
|
+
return [formatParallelStatusHeader(branches, minSuccessful), body]
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.join("\n");
|
|
248
|
+
}
|
|
249
|
+
async function mapConcurrent(items, concurrency, fn) {
|
|
250
|
+
const results = new Array(items.length);
|
|
251
|
+
let nextIndex = 0;
|
|
252
|
+
async function worker() {
|
|
253
|
+
for (;;) {
|
|
254
|
+
const index = nextIndex;
|
|
255
|
+
nextIndex += 1;
|
|
256
|
+
if (index >= items.length)
|
|
257
|
+
return;
|
|
258
|
+
results[index] = await fn(items[index], index);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
262
|
+
return results;
|
|
188
263
|
}
|
|
189
264
|
async function executeRetriableTemplateConfig(normalized, inherited, params, exec, cwd, signal, stdin, isRoot, actorRecipeContext) {
|
|
190
265
|
const maxAttempts = normalizeRetry(normalized.retry, {
|
|
@@ -270,7 +345,17 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
270
345
|
},
|
|
271
346
|
};
|
|
272
347
|
});
|
|
273
|
-
return executeTemplateConfig({
|
|
348
|
+
return executeTemplateConfig({
|
|
349
|
+
parallel: normalized.parallel === true,
|
|
350
|
+
...(normalized.concurrency !== undefined
|
|
351
|
+
? { concurrency: normalized.concurrency }
|
|
352
|
+
: {}),
|
|
353
|
+
...(normalized.min_successful !== undefined
|
|
354
|
+
? { min_successful: normalized.min_successful }
|
|
355
|
+
: {}),
|
|
356
|
+
...(normalized.failure !== undefined ? { failure: normalized.failure } : {}),
|
|
357
|
+
template: repeatedSteps,
|
|
358
|
+
}, context, params, exec, cwd, signal, stdin, isRoot, actorRecipeContext);
|
|
274
359
|
}
|
|
275
360
|
if (normalized.retry !== undefined &&
|
|
276
361
|
(Array.isArray(normalized.template) || normalized.recover !== undefined)) {
|
|
@@ -310,10 +395,17 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
310
395
|
throw new Error(formatToolText("Tool template produced no command steps."));
|
|
311
396
|
if (normalized.parallel === true) {
|
|
312
397
|
assertParallelBranchLimit(steps.length);
|
|
313
|
-
const
|
|
398
|
+
const concurrency = normalizeConcurrency(normalized.concurrency, controlValues, steps.length);
|
|
399
|
+
const minSuccessful = normalizeMinSuccessful(normalized.min_successful, controlValues, steps.length);
|
|
400
|
+
const branchResults = await mapConcurrent(steps, concurrency, (step) => executeTemplateConfig(step, context, params, exec, cwd, signal, stdin, false, actorRecipeContext));
|
|
314
401
|
const commands = branchResults.flatMap((item) => item.commands);
|
|
315
402
|
const failures = branchResults.flatMap((item) => item.failures);
|
|
316
403
|
const branches = branchResults.map((item, index) => createBranchReport(getNodeLabel(steps[index], index), item.commands.at(-1) ?? "<template>", item.result));
|
|
404
|
+
const usableBranches = countUsableBranches(branches);
|
|
405
|
+
const quorumUnmet = minSuccessful !== undefined && usableBranches < minSuccessful;
|
|
406
|
+
const quorumFailureMessage = quorumUnmet
|
|
407
|
+
? `parallel branch quorum not met: usable ${usableBranches}/${branches.length}, minimum ${minSuccessful}`
|
|
408
|
+
: "";
|
|
317
409
|
const nodeFailure = getFailureScope(normalized);
|
|
318
410
|
const rootFailure = branchResults.find((item, index) => {
|
|
319
411
|
if (item.result.code === 0)
|
|
@@ -335,6 +427,7 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
335
427
|
};
|
|
336
428
|
}
|
|
337
429
|
const firstFailedBranch = branchResults.find((item) => item.result.code !== 0);
|
|
430
|
+
const allBranchesUnusable = branches.every((branch) => !isUsableBranch(branch));
|
|
338
431
|
const successful = branchResults.map((item) => {
|
|
339
432
|
if (item.result.code === 0)
|
|
340
433
|
return item.result;
|
|
@@ -348,9 +441,31 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
348
441
|
.map((item) => item.stderr)
|
|
349
442
|
.filter(Boolean)
|
|
350
443
|
.join("\n"),
|
|
351
|
-
stdout: joinParallelStdout(branches, successful),
|
|
444
|
+
stdout: joinParallelStdout(branches, successful, minSuccessful),
|
|
352
445
|
};
|
|
353
|
-
if (
|
|
446
|
+
if (quorumUnmet && nodeFailure === "root") {
|
|
447
|
+
return {
|
|
448
|
+
commands,
|
|
449
|
+
branches: [
|
|
450
|
+
...branchResults.flatMap((item) => item.branches),
|
|
451
|
+
...branches,
|
|
452
|
+
],
|
|
453
|
+
criticalFailure: true,
|
|
454
|
+
failureScope: "root",
|
|
455
|
+
failures,
|
|
456
|
+
result: {
|
|
457
|
+
...result,
|
|
458
|
+
code: firstFailedBranch?.result.code || 1,
|
|
459
|
+
stderr: [result.stderr, quorumFailureMessage]
|
|
460
|
+
.filter(Boolean)
|
|
461
|
+
.join("\n"),
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
const branchFailureTriggered = minSuccessful === undefined
|
|
466
|
+
? firstFailedBranch || allBranchesUnusable
|
|
467
|
+
: quorumUnmet;
|
|
468
|
+
if (branchFailureTriggered && nodeFailure === "branch") {
|
|
354
469
|
return {
|
|
355
470
|
commands,
|
|
356
471
|
branches: [
|
|
@@ -359,7 +474,19 @@ async function executeTemplateConfig(config, inherited, params, exec, cwd, signa
|
|
|
359
474
|
],
|
|
360
475
|
failureScope: "branch",
|
|
361
476
|
failures,
|
|
362
|
-
result: {
|
|
477
|
+
result: {
|
|
478
|
+
...result,
|
|
479
|
+
code: firstFailedBranch?.result.code || 1,
|
|
480
|
+
stderr: [
|
|
481
|
+
result.stderr,
|
|
482
|
+
allBranchesUnusable
|
|
483
|
+
? "all parallel branches failed or produced empty output"
|
|
484
|
+
: "",
|
|
485
|
+
quorumFailureMessage,
|
|
486
|
+
]
|
|
487
|
+
.filter(Boolean)
|
|
488
|
+
.join("\n"),
|
|
489
|
+
},
|
|
363
490
|
};
|
|
364
491
|
}
|
|
365
492
|
return {
|
|
@@ -421,9 +548,25 @@ export async function executeRegisteredTool(cfg, params, exec, cwd, signal) {
|
|
|
421
548
|
const executed = await executeTemplateSteps(cfg, Schema.normalizeRuntimeValues(params, cfg.argTypes), exec, cwd, signal);
|
|
422
549
|
const command = formatCommandDetail(executed.commands);
|
|
423
550
|
const result = executed.result;
|
|
551
|
+
const softQuorum = createSoftQuorum(executed.branches);
|
|
424
552
|
if (result.code !== 0) {
|
|
425
553
|
const formatted = formatFailureOutput(cfg.name, result.code, result.killed, result.stdout, result.stderr);
|
|
426
|
-
throw new Error(formatted.text)
|
|
554
|
+
throw Object.assign(new Error(formatted.text), {
|
|
555
|
+
details: {
|
|
556
|
+
branches: executed.branches,
|
|
557
|
+
code: result.code,
|
|
558
|
+
command,
|
|
559
|
+
killed: result.killed,
|
|
560
|
+
...(executed.failures.length > 0
|
|
561
|
+
? { nonCriticalFailures: executed.failures }
|
|
562
|
+
: {}),
|
|
563
|
+
...(softQuorum ? { softQuorum } : {}),
|
|
564
|
+
failureReason: getExecutionFailureReason(executed.branches, softQuorum, result.stderr),
|
|
565
|
+
template: cfg.template,
|
|
566
|
+
tool: cfg.name,
|
|
567
|
+
truncated: formatted.truncated,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
427
570
|
}
|
|
428
571
|
const formatted = formatOutput(cfg.name, "stdout", result.stdout);
|
|
429
572
|
const templateWarnings = CommandTemplates.getCommandTemplateWarnings(createTemplateConfig(cfg));
|
|
@@ -438,9 +581,7 @@ export async function executeRegisteredTool(cfg, params, exec, cwd, signal) {
|
|
|
438
581
|
...(executed.failures.length > 0
|
|
439
582
|
? { nonCriticalFailures: executed.failures }
|
|
440
583
|
: {}),
|
|
441
|
-
...(
|
|
442
|
-
? { softQuorum: createSoftQuorum(executed.branches) }
|
|
443
|
-
: {}),
|
|
584
|
+
...(softQuorum ? { softQuorum } : {}),
|
|
444
585
|
template: cfg.template,
|
|
445
586
|
...(templateWarnings.length > 0 ? { templateWarnings } : {}),
|
|
446
587
|
tool: cfg.name,
|
|
@@ -0,0 +1,56 @@
|
|
|
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 interface CurrentModelContext {
|
|
7
|
+
currentThinking?: unknown;
|
|
8
|
+
getThinkingLevel?: () => unknown;
|
|
9
|
+
model?: {
|
|
10
|
+
id?: unknown;
|
|
11
|
+
modelId?: unknown;
|
|
12
|
+
provider?: unknown;
|
|
13
|
+
};
|
|
14
|
+
sessionManager?: {
|
|
15
|
+
getBranch?: () => unknown[];
|
|
16
|
+
getSessionId?: () => string;
|
|
17
|
+
};
|
|
18
|
+
thinkingLevel?: unknown;
|
|
19
|
+
}
|
|
20
|
+
export declare const CURRENT_MODEL_VALUE_KEY = "current_model";
|
|
21
|
+
export declare const CURRENT_THINKING_VALUE_KEY = "current_thinking";
|
|
22
|
+
export type CurrentPolicySource = "explicit" | "inherited" | "mixed" | "unresolved" | "unused";
|
|
23
|
+
export interface CurrentPolicyAxisProvenance {
|
|
24
|
+
explicit_keys?: string[];
|
|
25
|
+
inherited_keys?: string[];
|
|
26
|
+
source: CurrentPolicySource;
|
|
27
|
+
unresolved_keys?: string[];
|
|
28
|
+
value?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface CurrentPolicyProvenance {
|
|
31
|
+
model: CurrentPolicyAxisProvenance;
|
|
32
|
+
thinking: CurrentPolicyAxisProvenance;
|
|
33
|
+
}
|
|
34
|
+
export interface CurrentPolicyRecipeSummary {
|
|
35
|
+
model?: {
|
|
36
|
+
inherited_defaults?: string[];
|
|
37
|
+
public_args?: string[];
|
|
38
|
+
};
|
|
39
|
+
thinking?: {
|
|
40
|
+
inherited_defaults?: string[];
|
|
41
|
+
public_args?: string[];
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export declare function getCurrentModelPattern(ctx: CurrentModelContext | undefined): string | undefined;
|
|
45
|
+
export declare function getCurrentThinkingLevel(ctx: CurrentModelContext | undefined): string | undefined;
|
|
46
|
+
export declare function withCurrentModelValues<T extends Record<string, unknown>>(values: T, ctx: CurrentModelContext | undefined): T & Record<string, unknown>;
|
|
47
|
+
export declare function describeCurrentPolicyProvenance(input: {
|
|
48
|
+
defaults?: Record<string, unknown>;
|
|
49
|
+
template: unknown;
|
|
50
|
+
values?: Record<string, unknown>;
|
|
51
|
+
}): CurrentPolicyProvenance;
|
|
52
|
+
export declare function describeRecipeCurrentPolicy(input: {
|
|
53
|
+
args?: unknown;
|
|
54
|
+
defaults?: Record<string, unknown>;
|
|
55
|
+
template: unknown;
|
|
56
|
+
}): CurrentPolicyRecipeSummary | undefined;
|