@llblab/pi-actors 0.19.1 → 0.19.3
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/AGENTS.md +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +3 -3
- package/docs/async-runs.md +2 -2
- package/docs/template-recipes.md +16 -0
- package/lib/actor-recipe-context.ts +108 -0
- package/lib/async-runs.ts +27 -4
- package/lib/command-templates.ts +9 -0
- package/lib/execution.ts +13 -0
- package/lib/observability.ts +42 -2
- package/lib/prompts.ts +1 -1
- package/lib/recipe-references.ts +96 -7
- package/lib/tools.ts +2 -0
- package/package.json +1 -1
- package/scripts/async-runner.mjs +8 -1
- package/skills/actors/SKILL.md +6 -4
- package/skills/swarm/SKILL.md +1 -1
package/AGENTS.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
- `Knowledge surface separation`: pi-actors has distinct knowledge surfaces with different context-entry behavior: injected prompt is always present and should stay a tiny bootstrap/reminder; packaged `actors` skill is auto-matched by name/description and should signal that its body is the highest-density practical guide for operating the extension plus the shortest navigator to bundled recipes; packaged `swarm` skill is auto-matched for multi-agent methodology, strategies, standards, and portable examples; README is the human entrypoint explaining concept, rhythm, benefits, and scenarios but is not automatically in context; `/docs` are detailed transportable standards read on demand; `AGENTS.md` is project context and architectural constraints for agents changing the repo | Trigger: Editing prompt copy, README, docs, skills, or project context | Action: Keep each surface on its own wave, avoid duplicating prompt and skill headers, keep the actors skill recipe navigator compact and concrete, avoid duplicating scenario catalogs or changelog narratives in the actors skill, avoid turning the prompt into docs, keep multi-agent methodology in swarm-oriented guidance rather than the actors skill, keep packaged extension skill metadata versions synchronized with `package.json` version, and avoid extra colons in skill frontmatter scalar lines because skill formatters treat them poorly
|
|
41
41
|
- `Tool registry is executable muscle memory`: `~/.pi/agent/recipes/*.json` is the persistent user tool surface by location: every recipe in that agent root is automatically registered as an agent tool across sessions, and `register_tool` creates/updates/deletes recipe files there under the hood | Trigger: Any runtime registration, recipe discovery, migration, docs, skill, prompt, or recipe authoring work | Action: Treat the directory like `MEMORY.md` for executable habits; preserve filename identity, atomic writes, explicit operator-gated migration paths, and never author a recipe-owned `tool` property in repository recipes, docs, or fixtures; packaged/ad hoc recipes outside the agent root are components, not tools
|
|
42
|
-
- `Current runtime contract`: Register trusted command templates with tool names from registry keys, placeholder-derived args, progressive typed arg declarations, inline/default/`??`/ternary config fallback, placeholder-derived numeric node controls, split-first command-arg construction, sequential or `parallel: true` composition, direct no-shell execution, optional per-node `when`, optional per-node positive `timeout` disabled by default, lightweight warnings for obvious trusted-executable risk shapes, per-node `delay`, bounded leaf/node `retry`, `failure: "continue|branch|root"` propagation, `recover` cleanup between retry attempts, template recipes with explicit `async: true` detached mode, actor-oriented `spawn`/`message`/`inspect` tools with run-local JSONL outbox messages, Unix FIFO send, graceful cancel, and force kill, generic detached run primitives with process-group cancellation, injected async `{run_id}` and `{state_dir}` values, coordinator-scoped event-driven observability with at least one triangle per active async run and extra triangles for active parallel branches, runtime-inferred `command.done` bubbling for packaged multi-agent fanout,
|
|
42
|
+
- `Current runtime contract`: Register trusted command templates with tool names from registry keys, placeholder-derived args, progressive typed arg declarations, inline/default/`??`/ternary config fallback, placeholder-derived numeric node controls, split-first command-arg construction, sequential or `parallel: true` composition, direct no-shell execution, optional per-node `when`, optional per-node positive `timeout` disabled by default, lightweight warnings for obvious trusted-executable risk shapes, per-node `delay`, bounded leaf/node `retry`, `failure: "continue|branch|root"` propagation, `recover` cleanup between retry attempts, template recipes with explicit `async: true` detached mode, actor-oriented `spawn`/`message`/`inspect` tools with run-local JSONL outbox messages, Unix FIFO send, graceful cancel, and force kill, generic detached run primitives with process-group cancellation, injected async `{run_id}` and `{state_dir}` values, coordinator-scoped event-driven observability with at least one triangle per active async run and extra triangles for active parallel branches, runtime-inferred `command.done` bubbling for packaged multi-agent fanout, terminal follow-ups for `done`/`failed`/unhandled `killed`/`exited` states, recipe-persistence suggestions for successful direct inline/ad hoc `spawn` runs that are not already durable user recipes, named recipe `artifacts`, recipe `mailbox` metadata, `template` recipe references, recipe-layer `imports`, file-backed async recipe JSONL context bundles for child `pi -p` actors with raw entry/import recipes and `"you_are_here": true`, co-located recipe entries, `~/.pi/agent/recipes/*.json` template recipe files, run state under `~/.pi/agent/tmp/pi-actors/runs`, and `{file}` as the canonical local file path arg | Trigger: Changing registration or invocation behavior | Action: Keep README, command-template docs, template-recipe docs, async-run docs, actor-message docs, implementation, and migration notes aligned
|
|
43
43
|
- `Typed arg authoring`: Typed args support `string`, `path`, `int`, `number`, `bool`, and `enum(...)` plus two equivalent readability styles: metadata-first (`args` + `defaults` + simple `{name}` placeholders) for long command lines, and inline-first (`{name:type=default}` placeholders) for compact one-property templates | Trigger: Changing arg parsing, docs, schema generation, or registry serialization | Action: Preserve both styles, keep explicit `args` type declarations higher priority than inline placeholder types, and make breaking cleanup explicit when removing old arg shapes
|
|
44
44
|
- `Template recipe graph`: The valid execution chain is `tool → template → recipe → run → template`; file-backed and co-located recipes are storage variants of that chain | Trigger: Adding registry bindings, recipes, docs, or runtime shortcuts | Action: Keep command templates synchronous and portable, use `async: true` as the detached run switch, require every recipe to own `template` directly, and reject cyclic shortcuts such as recipe-owned `tool`
|
|
45
45
|
- `Layer boundary discipline`: Command-template evolution must be separated from template-recipe configuration and async-run lifecycle configuration | Trigger: Adding syntax, placeholders, imports, async controls, or docs | Action: Put portable execution graph semantics in `docs/command-templates.md`, recipe storage/import/default/reference behavior in `docs/template-recipes.md`, and detached lifecycle/state/IPC behavior in `docs/async-runs.md`; type imported recipes as command-template-shaped recipe definitions, not async-run instances
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.19.3: Spawn Recipe Persistence Suggestions
|
|
4
|
+
|
|
5
|
+
- `[Observability]` Added semi-active recipe persistence suggestions for successful direct `spawn` runs. Inline/ad hoc spawned actors now record `launch_source: "spawn"`, and their successful terminal follow-up asks the agent to offer saving the reusable pattern as a durable recipe/tool under `~/.pi/agent/recipes` without auto-saving.
|
|
6
|
+
- `[Runtime]` Recorded `launch_source` metadata for actor starts so observability can distinguish direct spawns from registered recipe-tool runs and avoid prompting for actors already backed by user-owned recipes.
|
|
7
|
+
- `[Docs/Prompt]` Updated onboarding prompt guidance, README, async-run docs, and project context around ask-first recipe persistence after successful transient actors.
|
|
8
|
+
- `[Tests]` Added regression coverage for successful transient spawn suggestions and suppression when the run already came from a saved user recipe.
|
|
9
|
+
- `[Package]` Bumped package metadata, lockfile metadata, and packaged skill metadata to `0.19.3` for the hotfix release.
|
|
10
|
+
|
|
11
|
+
## 0.19.2: Actor Recipe Context Bundle
|
|
12
|
+
|
|
13
|
+
- `[Actor Context]` Added a recipe context bundle for file-backed async recipes. The runtime now collects the raw authored entry recipe and resolved imports into deterministic JSONL records with filename-derived `name`, import alias/path metadata, role/depth, and raw recipe JSON so spawned LLM actors can understand the workflow composition behind their prompt.
|
|
14
|
+
- `[Actor Context]` Annotated command-template leaves with actor recipe context and appends the JSONL bundle to child `pi -p` prompts. The recipe record that launched the current child receives `"you_are_here": true` plus path metadata, enabling actors to give advisory feedback on their own recipe/composition fit; recipes can opt out with `"actor_context": false` / `"off"` when a minimal prompt is required.
|
|
15
|
+
- `[Tests]` Added coverage for raw recipe context record generation, import identity, `you_are_here` JSONL marking, prompt injection for `pi -p`, execution-time context propagation, async-run persistence, and recipe opt-out behavior.
|
|
16
|
+
- `[Package]` Bumped package metadata, lockfile metadata, and packaged skill metadata to `0.19.2` for the hotfix release.
|
|
17
|
+
|
|
3
18
|
## 0.19.1: Actor Inspector Hotfix
|
|
4
19
|
|
|
5
20
|
- `[Actor Inspector]` Fixed the live communications roster and row numbering controls after real swarm usage. `/actors-inspector-toggle <rows>` now keeps the room preview cap aligned with the requested row count, current-run sequence numbers are assigned before row limiting so the visible tail keeps its full-log positions, and roster role labels use concise `name/role` text instead of slugifying full role descriptions.
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ The terminal actor inspector is hidden by default. When opened without an explic
|
|
|
159
159
|
/actors-inspect 3
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
The table is compact and optimistic by default: bounded route/type/summary/body previews, capped noisy room rows, and an inline roster summary in the form `role
|
|
162
|
+
The table is compact and optimistic by default: bounded route/type/summary/body previews, capped noisy room rows, and an inline roster summary in the form `name/role` that wraps only when needed. Active roster members use the target color; members that sent `actor.leave` remain visible as inactive/muted participants from the current run. `/actors-inspect <number>` opens the selected row as a full-message view; toggle again to return to the table or close it. Actor display names come from room `actor.join` roster metadata or branch addresses, keeping debugger output plain and name-driven.
|
|
163
163
|
|
|
164
164
|
## Registry Model
|
|
165
165
|
|
|
@@ -218,7 +218,7 @@ Templates support:
|
|
|
218
218
|
- Retries, recovery, failure policy, delays, and guarded execution;
|
|
219
219
|
- Async run values such as `{run_id}`, `{state_dir}`, `{actor_address}`, `{default_room}`, and `{communication_file}`.
|
|
220
220
|
|
|
221
|
-
The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. The run actor owns detached lifecycle, state, messages, cancellation, and inspection.
|
|
221
|
+
The template owns execution shape. The recipe owns saved metadata, defaults, imports, mailbox, and artifacts. 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.
|
|
222
222
|
|
|
223
223
|
## Recipe Library
|
|
224
224
|
|
|
@@ -239,7 +239,7 @@ Packaged recipes are building blocks. Copy them into `~/.pi/agent/recipes/` or r
|
|
|
239
239
|
|
|
240
240
|
Use a foreground registered tool when the work is short, bounded, and does not need lifecycle.
|
|
241
241
|
|
|
242
|
-
Use an async recipe or `spawn` when the work is long-running, service-like, parallel, agentic, artifact-producing, or needs later control.
|
|
242
|
+
Use an async recipe or `spawn` when the work is long-running, service-like, parallel, agentic, artifact-producing, or needs later control. If a directly spawned inline/ad hoc actor completes successfully, pi-actors sends the launching agent a follow-up note to offer saving that pattern as a durable recipe/tool under `~/.pi/agent/recipes`; the agent should ask first and never auto-save.
|
|
243
243
|
|
|
244
244
|
Use `room:<run>` when multiple actors in the same run need shared context, roster discovery, or group-visible progress.
|
|
245
245
|
|
package/docs/async-runs.md
CHANGED
|
@@ -83,7 +83,7 @@ Use `run_id` on async recipe tools or `as: "run:<id>"` on `spawn` when the calle
|
|
|
83
83
|
|
|
84
84
|
Use ordinary files under the extension temp directory so status tools stay simple and inspectable:
|
|
85
85
|
|
|
86
|
-
- `run.json`: pid, optional source metadata (`tool`, `recipe`, `recipe_file`), command-template config, cwd, coordinator owner id, values, named `artifacts`, mailbox metadata, created time, and state dir.
|
|
86
|
+
- `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.
|
|
87
87
|
- `communication.json`: compact actor communication snapshot with self/root/parent, default-room, member, and contact hints for room-aware scripts and agents.
|
|
88
88
|
- `progress.json`: phase, active command count, completed count, failures, and updated time.
|
|
89
89
|
- `events.jsonl`: append-only implementation lifecycle log.
|
|
@@ -124,7 +124,7 @@ The core loop is:
|
|
|
124
124
|
{ "recipe": "music-player.json", "as": "run:music" }
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
2. Let terminal completion, `command.done`, and script-authored follow-up messages reach the launching coordinator automatically.
|
|
127
|
+
2. Let terminal completion, `command.done`, and script-authored follow-up messages reach the launching coordinator automatically. When a directly spawned inline/ad hoc actor completes successfully, the coordinator follow-up tells the agent to offer recipe persistence only as a question to the operator; it must not auto-save.
|
|
128
128
|
|
|
129
129
|
3. Respond with explicit run-local messages when needed:
|
|
130
130
|
|
package/docs/template-recipes.md
CHANGED
|
@@ -137,6 +137,22 @@ Use recipe-level `mailbox` to document the semantic messages a recipe actor acce
|
|
|
137
137
|
|
|
138
138
|
`mailbox` is contract metadata, not transport configuration. It should name semantic message types, not transport commands, file paths, or CLI fragments.
|
|
139
139
|
|
|
140
|
+
## Actor Recipe Context
|
|
141
|
+
|
|
142
|
+
File-backed async recipes automatically build a bounded recipe context bundle for child LLM actor launches. The bundle is appended to child `pi -p` prompts as JSONL: each line is one recipe/context record containing filename-derived `name`, source file, role/depth, import path/alias, and the raw authored recipe JSON. The record whose command-template node launched the current child is marked with `"you_are_here": true` and path metadata.
|
|
143
|
+
|
|
144
|
+
This context is provenance, not the task instruction. The authored prompt remains authoritative; the bundle explains the recipe/composition tree that produced the launch. A child actor can use it to give advisory feedback on whether its recipe, imports, mailbox metadata, and role boundaries fit the task, without needing a separate hand-written workflow explanation. Recipes that require a minimal child prompt may opt out:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"async": true,
|
|
149
|
+
"actor_context": false,
|
|
150
|
+
"template": "pi -p --model {model} {prompt}"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`"actor_context": "off"` is equivalent. Bundle generation uses the same recipe file-size and import-depth safety limits as normal recipe loading.
|
|
155
|
+
|
|
140
156
|
## Actor Message Delivery
|
|
141
157
|
|
|
142
158
|
Recipes do not declare a second event-delivery policy. A running actor emits addressed messages such as `command.done`, `run.done`, or `checkpoint.needs_input`; the coordinator/runtime decides whether a message stays diagnostic, becomes a notification, or re-enters the agent context. This keeps recipe metadata focused on the actor contract:
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actor recipe context prompt helpers.
|
|
3
|
+
* Zones: async runner prompt context, recipe provenance, LLM child launches
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
|
|
8
|
+
import type { CommandTemplateActorRecipeContext } from "./command-templates.ts";
|
|
9
|
+
import type { TemplateRecipeContextRecord } from "./recipe-references.ts";
|
|
10
|
+
|
|
11
|
+
export interface MarkedRecipeContextRecord extends TemplateRecipeContextRecord {
|
|
12
|
+
you_are_here?: true;
|
|
13
|
+
you_are_here_path?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function commandName(command: string): string {
|
|
17
|
+
return basename(command).toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPiCommand(command: string): boolean {
|
|
21
|
+
return commandName(command) === "pi";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findPrintPromptIndex(args: string[]): number | undefined {
|
|
25
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
26
|
+
const arg = args[index];
|
|
27
|
+
if ((arg === "-p" || arg === "--print") && index + 1 < args.length) {
|
|
28
|
+
return index + 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function matchesActorContext(
|
|
35
|
+
record: TemplateRecipeContextRecord,
|
|
36
|
+
context: CommandTemplateActorRecipeContext | undefined,
|
|
37
|
+
): boolean {
|
|
38
|
+
if (!context) return record.role === "entry";
|
|
39
|
+
if (context.file && context.file === record.file) return true;
|
|
40
|
+
if (context.name && context.name === record.name) return true;
|
|
41
|
+
if (context.alias && context.alias === record.alias) return true;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function contextPath(
|
|
46
|
+
record: TemplateRecipeContextRecord,
|
|
47
|
+
context: CommandTemplateActorRecipeContext | undefined,
|
|
48
|
+
): string {
|
|
49
|
+
return record.import_path.join(".") || context?.path || record.name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function markRecipeContextRecords(
|
|
53
|
+
records: TemplateRecipeContextRecord[],
|
|
54
|
+
context?: CommandTemplateActorRecipeContext,
|
|
55
|
+
): MarkedRecipeContextRecord[] {
|
|
56
|
+
let marked = false;
|
|
57
|
+
const result = records.map((record) => {
|
|
58
|
+
if (marked || !matchesActorContext(record, context)) return record;
|
|
59
|
+
marked = true;
|
|
60
|
+
return {
|
|
61
|
+
...record,
|
|
62
|
+
you_are_here: true as const,
|
|
63
|
+
you_are_here_path: contextPath(record, context),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatRecipeContextJsonl(
|
|
70
|
+
records: TemplateRecipeContextRecord[],
|
|
71
|
+
context?: CommandTemplateActorRecipeContext,
|
|
72
|
+
): string {
|
|
73
|
+
return markRecipeContextRecords(records, context)
|
|
74
|
+
.map((record) => JSON.stringify(record))
|
|
75
|
+
.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildRecipeContextPromptBlock(
|
|
79
|
+
records: TemplateRecipeContextRecord[],
|
|
80
|
+
context?: CommandTemplateActorRecipeContext,
|
|
81
|
+
): string {
|
|
82
|
+
const jsonl = formatRecipeContextJsonl(records, context);
|
|
83
|
+
if (!jsonl) return "";
|
|
84
|
+
return [
|
|
85
|
+
"Actor recipe context bundle follows as JSONL.",
|
|
86
|
+
'Each line is one recipe/context record; `"you_are_here": true` marks the recipe node that launched this actor.',
|
|
87
|
+
"Use this as workflow/composition context, while the task prompt remains authoritative.",
|
|
88
|
+
"```jsonl",
|
|
89
|
+
jsonl,
|
|
90
|
+
"```",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function appendRecipeContextToPiArgs(
|
|
95
|
+
command: string,
|
|
96
|
+
args: string[],
|
|
97
|
+
records: TemplateRecipeContextRecord[] | undefined,
|
|
98
|
+
context?: CommandTemplateActorRecipeContext,
|
|
99
|
+
): string[] {
|
|
100
|
+
if (!records || records.length === 0 || !isPiCommand(command)) return args;
|
|
101
|
+
const promptIndex = findPrintPromptIndex(args);
|
|
102
|
+
if (promptIndex === undefined) return args;
|
|
103
|
+
const block = buildRecipeContextPromptBlock(records, context);
|
|
104
|
+
if (!block) return args;
|
|
105
|
+
const next = [...args];
|
|
106
|
+
next[promptIndex] = `${next[promptIndex]}\n\n${block}`;
|
|
107
|
+
return next;
|
|
108
|
+
}
|
package/lib/async-runs.ts
CHANGED
|
@@ -34,9 +34,12 @@ import * as RecipeUsage from "./recipe-usage.ts";
|
|
|
34
34
|
|
|
35
35
|
const START_LOCK_MAX_AGE_MS = 5 * 60 * 1000;
|
|
36
36
|
|
|
37
|
+
export type AsyncRunLaunchSource = "spawn" | "tool";
|
|
38
|
+
|
|
37
39
|
export interface AsyncRunStartParams {
|
|
38
40
|
async?: boolean;
|
|
39
41
|
file?: string;
|
|
42
|
+
launch_source?: AsyncRunLaunchSource;
|
|
40
43
|
name?: string;
|
|
41
44
|
ownerId?: string;
|
|
42
45
|
run_id?: string;
|
|
@@ -59,6 +62,7 @@ export interface AsyncRunStartParams {
|
|
|
59
62
|
recover?: CommandTemplateValue;
|
|
60
63
|
repeat?: number;
|
|
61
64
|
values?: Record<string, unknown>;
|
|
65
|
+
actor_context?: boolean | string;
|
|
62
66
|
cwd?: string;
|
|
63
67
|
}
|
|
64
68
|
|
|
@@ -95,6 +99,7 @@ export interface AsyncRunMeta {
|
|
|
95
99
|
argv: string[];
|
|
96
100
|
createdAt: string;
|
|
97
101
|
cwd: string;
|
|
102
|
+
launch_source?: AsyncRunLaunchSource;
|
|
98
103
|
ownerId?: string;
|
|
99
104
|
pid: number;
|
|
100
105
|
recipe?: string;
|
|
@@ -107,6 +112,7 @@ export interface AsyncRunMeta {
|
|
|
107
112
|
values: Record<string, unknown>;
|
|
108
113
|
artifacts?: Record<string, string>;
|
|
109
114
|
mailbox?: RecipeReferences.TemplateRecipeMailbox;
|
|
115
|
+
recipe_context_records?: RecipeReferences.TemplateRecipeContextRecord[];
|
|
110
116
|
retire_when?: "children_terminal";
|
|
111
117
|
}
|
|
112
118
|
|
|
@@ -196,17 +202,25 @@ function isMutableUsageRecipeFile(file: string): boolean {
|
|
|
196
202
|
|
|
197
203
|
function readRecipeFile(file: string): AsyncRunStartParams {
|
|
198
204
|
const path = resolveRecipeFile(file);
|
|
199
|
-
const raw =
|
|
200
|
-
if (Object.hasOwn(raw, "tool")) {
|
|
205
|
+
const raw = RecipeReferences.readRawRecipeConfig(path);
|
|
206
|
+
if (raw && Object.hasOwn(raw, "tool")) {
|
|
201
207
|
throw new Error(
|
|
202
208
|
`Template recipe cannot define tool; use template in ${path}`,
|
|
203
209
|
);
|
|
204
210
|
}
|
|
205
|
-
const
|
|
211
|
+
const includeActorRecipeContext =
|
|
212
|
+
raw?.actor_context !== false && raw?.actor_context !== "off";
|
|
213
|
+
const config = RecipeReferences.readResolvedRecipeConfig(path, [], {
|
|
214
|
+
includeActorRecipeContext,
|
|
215
|
+
});
|
|
206
216
|
if (!config) {
|
|
207
217
|
throw new Error(`Template recipe must define template: ${path}`);
|
|
208
218
|
}
|
|
209
|
-
return {
|
|
219
|
+
return {
|
|
220
|
+
...(config as AsyncRunStartParams),
|
|
221
|
+
file: path,
|
|
222
|
+
...(includeActorRecipeContext ? {} : { actor_context: false }),
|
|
223
|
+
};
|
|
210
224
|
}
|
|
211
225
|
|
|
212
226
|
function getRunIdFromFile(file: string | undefined): string | undefined {
|
|
@@ -376,6 +390,11 @@ export function startRun(
|
|
|
376
390
|
? resolveRecipeFile(startParams.file)
|
|
377
391
|
: undefined;
|
|
378
392
|
const recipe = startParams.name || getRunIdFromFile(recipeFile);
|
|
393
|
+
const includeActorRecipeContext =
|
|
394
|
+
startParams.actor_context !== false && startParams.actor_context !== "off";
|
|
395
|
+
const recipeContextRecords = recipeFile && includeActorRecipeContext
|
|
396
|
+
? RecipeReferences.buildRecipeContextRecords(recipeFile)
|
|
397
|
+
: undefined;
|
|
379
398
|
if (recipeFile && isMutableUsageRecipeFile(recipeFile)) {
|
|
380
399
|
RecipeUsage.recordRecipeLaunch(recipeFile);
|
|
381
400
|
}
|
|
@@ -399,6 +418,7 @@ export function startRun(
|
|
|
399
418
|
argv: [process.execPath, ...argv],
|
|
400
419
|
createdAt: new Date().toISOString(),
|
|
401
420
|
cwd,
|
|
421
|
+
...(startParams.launch_source ? { launch_source: startParams.launch_source } : {}),
|
|
402
422
|
...(startParams.ownerId ? { ownerId: startParams.ownerId } : {}),
|
|
403
423
|
pid: 0,
|
|
404
424
|
...(recipe ? { recipe } : {}),
|
|
@@ -411,6 +431,9 @@ export function startRun(
|
|
|
411
431
|
values,
|
|
412
432
|
...(artifacts ? { artifacts } : {}),
|
|
413
433
|
...(startParams.mailbox ? { mailbox: startParams.mailbox } : {}),
|
|
434
|
+
...(recipeContextRecords && recipeContextRecords.length > 0
|
|
435
|
+
? { recipe_context_records: recipeContextRecords }
|
|
436
|
+
: {}),
|
|
414
437
|
...(startParams.retire_when === "children_terminal"
|
|
415
438
|
? { retire_when: "children_terminal" as const }
|
|
416
439
|
: {}),
|
package/lib/command-templates.ts
CHANGED
|
@@ -10,7 +10,16 @@ import { isAbsolute, resolve } from "node:path";
|
|
|
10
10
|
|
|
11
11
|
export type CommandTemplateFailureScope = "continue" | "branch" | "root";
|
|
12
12
|
|
|
13
|
+
export interface CommandTemplateActorRecipeContext {
|
|
14
|
+
alias?: string;
|
|
15
|
+
file?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
role?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
export interface CommandTemplateObjectConfig {
|
|
22
|
+
actorRecipeContext?: CommandTemplateActorRecipeContext;
|
|
14
23
|
label?: string;
|
|
15
24
|
parallel?: boolean;
|
|
16
25
|
when?: boolean | string;
|
package/lib/execution.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { formatFailureOutput, formatOutput, formatToolText } from "./output.ts";
|
|
|
10
10
|
import * as Schema from "./schema.ts";
|
|
11
11
|
|
|
12
12
|
export interface ToolExecOptions {
|
|
13
|
+
actorRecipeContext?: CommandTemplates.CommandTemplateActorRecipeContext;
|
|
13
14
|
cwd?: string;
|
|
14
15
|
signal?: AbortSignal;
|
|
15
16
|
stdin?: string;
|
|
@@ -331,6 +332,7 @@ async function executeRetriableTemplateConfig(
|
|
|
331
332
|
signal: AbortSignal | undefined,
|
|
332
333
|
stdin: string | undefined,
|
|
333
334
|
isRoot: boolean,
|
|
335
|
+
actorRecipeContext: CommandTemplates.CommandTemplateActorRecipeContext | undefined,
|
|
334
336
|
): Promise<TemplateExecution> {
|
|
335
337
|
const maxAttempts = normalizeRetry(normalized.retry, {
|
|
336
338
|
...(inherited.defaults ?? {}),
|
|
@@ -358,6 +360,7 @@ async function executeRetriableTemplateConfig(
|
|
|
358
360
|
signal,
|
|
359
361
|
stdin,
|
|
360
362
|
isRoot,
|
|
363
|
+
actorRecipeContext,
|
|
361
364
|
);
|
|
362
365
|
mergeExecution(aggregate, executed);
|
|
363
366
|
aggregate.result = executed.result;
|
|
@@ -376,6 +379,7 @@ async function executeRetriableTemplateConfig(
|
|
|
376
379
|
signal,
|
|
377
380
|
executed.result.stdout,
|
|
378
381
|
false,
|
|
382
|
+
actorRecipeContext,
|
|
379
383
|
);
|
|
380
384
|
mergeExecution(aggregate, recovered);
|
|
381
385
|
if (recovered.result.code === 0) continue;
|
|
@@ -403,6 +407,7 @@ async function executeTemplateConfig(
|
|
|
403
407
|
signal: AbortSignal | undefined,
|
|
404
408
|
stdin: string | undefined,
|
|
405
409
|
isRoot: boolean,
|
|
410
|
+
inheritedActorRecipeContext: CommandTemplates.CommandTemplateActorRecipeContext | undefined,
|
|
406
411
|
): Promise<TemplateExecution> {
|
|
407
412
|
const normalized = CommandTemplates.normalizeCommandTemplateConfig(config);
|
|
408
413
|
const normalizedDefaults = CommandTemplates.resolveInheritedDefaultReferences(
|
|
@@ -420,6 +425,7 @@ async function executeTemplateConfig(
|
|
|
420
425
|
? { defaults: mergeDefaults(inherited.defaults, normalizedDefaults) }
|
|
421
426
|
: {}),
|
|
422
427
|
};
|
|
428
|
+
const actorRecipeContext = normalized.actorRecipeContext ?? inheritedActorRecipeContext;
|
|
423
429
|
const controlValues = { ...(context.defaults ?? {}), ...params };
|
|
424
430
|
await applyDelay(normalized.delay, controlValues, signal);
|
|
425
431
|
if (
|
|
@@ -464,6 +470,7 @@ async function executeTemplateConfig(
|
|
|
464
470
|
signal,
|
|
465
471
|
stdin,
|
|
466
472
|
isRoot,
|
|
473
|
+
actorRecipeContext,
|
|
467
474
|
);
|
|
468
475
|
}
|
|
469
476
|
if (
|
|
@@ -479,6 +486,7 @@ async function executeTemplateConfig(
|
|
|
479
486
|
signal,
|
|
480
487
|
stdin,
|
|
481
488
|
isRoot,
|
|
489
|
+
actorRecipeContext,
|
|
482
490
|
);
|
|
483
491
|
}
|
|
484
492
|
if (
|
|
@@ -495,6 +503,7 @@ async function executeTemplateConfig(
|
|
|
495
503
|
signal,
|
|
496
504
|
stdin,
|
|
497
505
|
false,
|
|
506
|
+
actorRecipeContext,
|
|
498
507
|
);
|
|
499
508
|
}
|
|
500
509
|
if (!Array.isArray(normalized.template)) {
|
|
@@ -506,6 +515,7 @@ async function executeTemplateConfig(
|
|
|
506
515
|
{ emptyMessage: "Tool template produced an empty command." },
|
|
507
516
|
);
|
|
508
517
|
const result = await exec(invocation.command, invocation.args, {
|
|
518
|
+
...(actorRecipeContext ? { actorRecipeContext } : {}),
|
|
509
519
|
cwd,
|
|
510
520
|
signal,
|
|
511
521
|
stdin,
|
|
@@ -549,6 +559,7 @@ async function executeTemplateConfig(
|
|
|
549
559
|
signal,
|
|
550
560
|
stdin,
|
|
551
561
|
false,
|
|
562
|
+
actorRecipeContext,
|
|
552
563
|
),
|
|
553
564
|
),
|
|
554
565
|
);
|
|
@@ -640,6 +651,7 @@ async function executeTemplateConfig(
|
|
|
640
651
|
signal,
|
|
641
652
|
nextStdin,
|
|
642
653
|
false,
|
|
654
|
+
actorRecipeContext,
|
|
643
655
|
);
|
|
644
656
|
branches.push(...executed.branches);
|
|
645
657
|
commands.push(...executed.commands);
|
|
@@ -699,6 +711,7 @@ async function executeTemplateSteps(
|
|
|
699
711
|
signal,
|
|
700
712
|
undefined,
|
|
701
713
|
true,
|
|
714
|
+
undefined,
|
|
702
715
|
);
|
|
703
716
|
}
|
|
704
717
|
|
package/lib/observability.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import { basename, dirname, join, relative } from "node:path";
|
|
8
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
9
9
|
|
|
10
10
|
import * as AsyncRuns from "./async-runs.ts";
|
|
11
11
|
import * as Paths from "./paths.ts";
|
|
@@ -26,9 +26,12 @@ export interface RunObservation {
|
|
|
26
26
|
failures?: number;
|
|
27
27
|
ownerId?: string;
|
|
28
28
|
artifacts?: Record<string, string>;
|
|
29
|
+
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
30
|
+
recipeFile?: string;
|
|
29
31
|
terminalHandled?: boolean;
|
|
30
32
|
retireWhen?: string;
|
|
31
33
|
run: string;
|
|
34
|
+
tool?: string;
|
|
32
35
|
stateDir?: string;
|
|
33
36
|
status: RunObservedStatus;
|
|
34
37
|
updatedAt?: string;
|
|
@@ -57,8 +60,11 @@ export interface RunTransition {
|
|
|
57
60
|
run: string;
|
|
58
61
|
stateDir?: string;
|
|
59
62
|
artifacts?: Record<string, string>;
|
|
63
|
+
launchSource?: AsyncRuns.AsyncRunLaunchSource;
|
|
64
|
+
recipeFile?: string;
|
|
60
65
|
terminalHandled?: boolean;
|
|
61
66
|
to: RunObservedStatus;
|
|
67
|
+
tool?: string;
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export interface RunOutboxEvent {
|
|
@@ -132,6 +138,12 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
132
138
|
!Array.isArray(status.artifacts)
|
|
133
139
|
? { artifacts: status.artifacts as Record<string, string> }
|
|
134
140
|
: {}),
|
|
141
|
+
...(status.launch_source === "spawn" || status.launch_source === "tool"
|
|
142
|
+
? { launchSource: status.launch_source }
|
|
143
|
+
: {}),
|
|
144
|
+
...(typeof status.recipe_file === "string"
|
|
145
|
+
? { recipeFile: status.recipe_file }
|
|
146
|
+
: {}),
|
|
135
147
|
...(status.terminal_handled ? { terminalHandled: true } : {}),
|
|
136
148
|
...(typeof status.retire_when === "string"
|
|
137
149
|
? { retireWhen: status.retire_when }
|
|
@@ -139,6 +151,7 @@ function observeRun(stateDir: string): RunObservation | undefined {
|
|
|
139
151
|
run,
|
|
140
152
|
stateDir,
|
|
141
153
|
status: status.status as RunObservedStatus,
|
|
154
|
+
...(typeof status.tool === "string" ? { tool: status.tool } : {}),
|
|
142
155
|
updatedAt: getUpdatedAt(status),
|
|
143
156
|
};
|
|
144
157
|
} catch {
|
|
@@ -365,8 +378,11 @@ export function detectRunTransitions(
|
|
|
365
378
|
run: run.run,
|
|
366
379
|
...(run.stateDir ? { stateDir: run.stateDir } : {}),
|
|
367
380
|
...(run.artifacts ? { artifacts: run.artifacts } : {}),
|
|
381
|
+
...(run.launchSource ? { launchSource: run.launchSource } : {}),
|
|
382
|
+
...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
|
|
368
383
|
...(run.terminalHandled ? { terminalHandled: true } : {}),
|
|
369
384
|
to: run.status,
|
|
385
|
+
...(run.tool ? { tool: run.tool } : {}),
|
|
370
386
|
});
|
|
371
387
|
}
|
|
372
388
|
previous.set(run.run, run.status);
|
|
@@ -610,11 +626,35 @@ function getRunArtifacts(transition: RunTransition): string[] {
|
|
|
610
626
|
];
|
|
611
627
|
}
|
|
612
628
|
|
|
629
|
+
function isUserRecipeFile(file: string | undefined): boolean {
|
|
630
|
+
if (!file) return false;
|
|
631
|
+
const recipeRoot = resolve(Paths.getRecipeRoot());
|
|
632
|
+
const path = resolve(file);
|
|
633
|
+
return path === recipeRoot || path.startsWith(`${recipeRoot}/`);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function shouldSuggestRecipePersistence(
|
|
637
|
+
transition: RunTransition,
|
|
638
|
+
): boolean {
|
|
639
|
+
return (
|
|
640
|
+
transition.to === "done" &&
|
|
641
|
+
transition.launchSource === "spawn" &&
|
|
642
|
+
!transition.tool &&
|
|
643
|
+
!isUserRecipeFile(transition.recipeFile)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function formatRecipePersistenceSuggestion(transition: RunTransition): string {
|
|
648
|
+
if (!shouldSuggestRecipePersistence(transition)) return "";
|
|
649
|
+
return `\nAgent note: this actor was spawned directly and completed successfully. If this pattern looks reusable, 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.`;
|
|
650
|
+
}
|
|
651
|
+
|
|
613
652
|
export function formatRunTransitionMessage(transition: RunTransition): string {
|
|
614
653
|
const artifacts = formatNamedArtifacts(transition.artifacts);
|
|
615
654
|
const runFiles = formatRunFileList(getRunArtifacts(transition));
|
|
655
|
+
const persistenceSuggestion = formatRecipePersistenceSuggestion(transition);
|
|
616
656
|
if (transition.to === "done")
|
|
617
|
-
return `Run ${transition.run} completed successfully.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail if the result needs inspection
|
|
657
|
+
return `Run ${transition.run} completed successfully.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail if the result needs inspection.${persistenceSuggestion}`;
|
|
618
658
|
if (transition.to === "failed")
|
|
619
659
|
return `Run ${transition.run} failed.${artifacts}${runFiles}\nUse inspect target=run:${transition.run} view=status or view=tail for details.`;
|
|
620
660
|
if (transition.to === "cancelled")
|
package/lib/prompts.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const ONBOARDING_SYSTEM_PROMPT = `pi-actors quick model:
|
|
|
29
29
|
- Recipe imports are local variables; imported recipes are definitions, not nested async runs; parent async:true creates one run.
|
|
30
30
|
- Use spawn/message/inspect for actor-level start/send/observe; avoid runtime/FIFO/outbox vocabulary in public guidance.
|
|
31
31
|
- Run state lives under ~/.pi/agent/tmp/pi-actors/runs; inspect status/tail/messages/mailbox/files/artifacts intentionally and avoid busy-polling.
|
|
32
|
-
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones; packaged recipes are lower-priority components
|
|
32
|
+
- Maintain ~/.pi/agent/recipes like MEMORY.md for capabilities: keep useful tools, curate stale ones; packaged recipes are lower-priority components; offer to save successful direct spawn patterns only after confirmation.
|
|
33
33
|
- Foreground tools/templates fit short work; async recipes/runs fit subagents, services, fanout, media, and long pipelines.
|
|
34
34
|
- Long fanout = parent async recipe wrapping template(parallel:true) and imports; packaged fanout recipes bubble branch completion messages; grow recurring multi-agent workflows as packaged recipes/pipelines, not ad hoc external scripts.
|
|
35
35
|
- For deeper pi-actors guidance, inspect installed extension sources/docs/recipes; README and docs are not automatically in context.`;
|
package/lib/recipe-references.ts
CHANGED
|
@@ -70,6 +70,20 @@ interface ImportedRecipe {
|
|
|
70
70
|
values: Record<string, unknown>;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface TemplateRecipeContextRecord {
|
|
74
|
+
alias?: string;
|
|
75
|
+
depth: number;
|
|
76
|
+
file: string;
|
|
77
|
+
import_path: string[];
|
|
78
|
+
name: string;
|
|
79
|
+
recipe: Record<string, unknown>;
|
|
80
|
+
role: "entry" | "import";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface ReadResolvedRecipeConfigOptions {
|
|
84
|
+
includeActorRecipeContext?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
73
87
|
function hasWhitespace(value: string): boolean {
|
|
74
88
|
return /\s/.test(value);
|
|
75
89
|
}
|
|
@@ -218,7 +232,7 @@ function getRecipeCommandTemplate(
|
|
|
218
232
|
return normalizeRecipeTemplate({ ...envelope, template });
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
function readRawRecipeConfig(
|
|
235
|
+
export function readRawRecipeConfig(
|
|
222
236
|
path: string,
|
|
223
237
|
): Record<string, unknown> | undefined {
|
|
224
238
|
if (!existsSync(path)) return undefined;
|
|
@@ -437,14 +451,25 @@ function applyDefaultsToTemplate(
|
|
|
437
451
|
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
438
452
|
}
|
|
439
453
|
|
|
454
|
+
function withActorRecipeContext(
|
|
455
|
+
value: CommandTemplateValue,
|
|
456
|
+
context: CommandTemplates.CommandTemplateActorRecipeContext,
|
|
457
|
+
): CommandTemplateValue {
|
|
458
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
459
|
+
return { ...value, actorRecipeContext: context };
|
|
460
|
+
}
|
|
461
|
+
return { actorRecipeContext: context, template: value };
|
|
462
|
+
}
|
|
463
|
+
|
|
440
464
|
function expandImportNodes(
|
|
441
465
|
value: CommandTemplateValue,
|
|
442
466
|
imports: Record<string, ImportedRecipe>,
|
|
467
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
443
468
|
): CommandTemplateValue {
|
|
444
469
|
if (typeof value === "string") return value;
|
|
445
470
|
if (Array.isArray(value)) {
|
|
446
471
|
return value.map(
|
|
447
|
-
(item) => expandImportNodes(item, imports) as CommandTemplateConfig,
|
|
472
|
+
(item) => expandImportNodes(item, imports, options) as CommandTemplateConfig,
|
|
448
473
|
);
|
|
449
474
|
}
|
|
450
475
|
const record = value as Record<string, unknown>;
|
|
@@ -465,7 +490,16 @@ function expandImportNodes(
|
|
|
465
490
|
nodeDefaults,
|
|
466
491
|
nodeValues,
|
|
467
492
|
);
|
|
468
|
-
|
|
493
|
+
const expanded = applyDefaultsToTemplate(imported.config.template, defaults, record);
|
|
494
|
+
return options.includeActorRecipeContext
|
|
495
|
+
? withActorRecipeContext(expanded, {
|
|
496
|
+
alias: imported.alias,
|
|
497
|
+
file: imported.file,
|
|
498
|
+
name: imported.name,
|
|
499
|
+
path: imported.alias,
|
|
500
|
+
role: "import",
|
|
501
|
+
})
|
|
502
|
+
: expanded;
|
|
469
503
|
}
|
|
470
504
|
if (Array.isArray(record.template)) {
|
|
471
505
|
return {
|
|
@@ -475,6 +509,7 @@ function expandImportNodes(
|
|
|
475
509
|
expandImportNodes(
|
|
476
510
|
item as CommandTemplateValue,
|
|
477
511
|
imports,
|
|
512
|
+
options,
|
|
478
513
|
) as CommandTemplateConfig,
|
|
479
514
|
),
|
|
480
515
|
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
@@ -485,6 +520,7 @@ function expandImportNodes(
|
|
|
485
520
|
template: expandImportNodes(
|
|
486
521
|
record.template as CommandTemplateValue,
|
|
487
522
|
imports,
|
|
523
|
+
options,
|
|
488
524
|
),
|
|
489
525
|
} as CommandTemplates.CommandTemplateObjectConfig;
|
|
490
526
|
}
|
|
@@ -494,6 +530,7 @@ function expandImportNodes(
|
|
|
494
530
|
export function readResolvedRecipeConfig(
|
|
495
531
|
file: string,
|
|
496
532
|
stack: string[] = [],
|
|
533
|
+
options: ReadResolvedRecipeConfigOptions = {},
|
|
497
534
|
): TemplateRecipeConfig | undefined {
|
|
498
535
|
const path = resolveRecipePath(
|
|
499
536
|
file,
|
|
@@ -512,7 +549,7 @@ export function readResolvedRecipeConfig(
|
|
|
512
549
|
const imports: Record<string, ImportedRecipe> = {};
|
|
513
550
|
for (const [alias, binding] of Object.entries(getRecipeImports(raw))) {
|
|
514
551
|
const importPath = resolveRecipeImportPath(getImportFrom(binding), dirname(path));
|
|
515
|
-
const config = readResolvedRecipeConfig(importPath, [...stack, path]);
|
|
552
|
+
const config = readResolvedRecipeConfig(importPath, [...stack, path], options);
|
|
516
553
|
if (!config) throw new Error(`Recipe import not found: ${alias}`);
|
|
517
554
|
const bindingDefaults =
|
|
518
555
|
typeof binding === "string" ? undefined : binding.defaults;
|
|
@@ -533,9 +570,18 @@ export function readResolvedRecipeConfig(
|
|
|
533
570
|
>;
|
|
534
571
|
const template = getRecipeCommandTemplate(substituted);
|
|
535
572
|
if (!template) return undefined;
|
|
536
|
-
const expandedTemplate = expandImportNodes(template, imports);
|
|
573
|
+
const expandedTemplate = expandImportNodes(template, imports, options);
|
|
574
|
+
const recipeName = getRecipeIdFromPath(path);
|
|
575
|
+
const templateWithContext = options.includeActorRecipeContext
|
|
576
|
+
? withActorRecipeContext(expandedTemplate, {
|
|
577
|
+
file: path,
|
|
578
|
+
name: recipeName,
|
|
579
|
+
path: recipeName,
|
|
580
|
+
role: stack.length > 0 ? "import" : "entry",
|
|
581
|
+
})
|
|
582
|
+
: expandedTemplate;
|
|
537
583
|
return {
|
|
538
|
-
name:
|
|
584
|
+
name: recipeName,
|
|
539
585
|
...(typeof substituted.description === "string" &&
|
|
540
586
|
substituted.description.trim()
|
|
541
587
|
? { description: substituted.description.trim() }
|
|
@@ -554,7 +600,7 @@ export function readResolvedRecipeConfig(
|
|
|
554
600
|
...(Object.keys(imports).length > 0
|
|
555
601
|
? { imports: getRecipeImports(raw) }
|
|
556
602
|
: {}),
|
|
557
|
-
template:
|
|
603
|
+
template: templateWithContext,
|
|
558
604
|
...(Array.isArray(substituted.args)
|
|
559
605
|
? { args: substituted.args as string[] }
|
|
560
606
|
: {}),
|
|
@@ -635,6 +681,49 @@ export function readResolvedRecipeConfig(
|
|
|
635
681
|
};
|
|
636
682
|
}
|
|
637
683
|
|
|
684
|
+
function collectRecipeContextRecords(
|
|
685
|
+
file: string,
|
|
686
|
+
stack: string[],
|
|
687
|
+
importPath: string[],
|
|
688
|
+
alias?: string,
|
|
689
|
+
): TemplateRecipeContextRecord[] {
|
|
690
|
+
const path = resolveRecipePath(
|
|
691
|
+
file,
|
|
692
|
+
stack.length > 0 ? dirname(stack.at(-1)!) : Paths.getRecipeRoot(),
|
|
693
|
+
);
|
|
694
|
+
if (stack.includes(path)) {
|
|
695
|
+
throw new Error(`Cyclic recipe import: ${[...stack, path].join(" -> ")}`);
|
|
696
|
+
}
|
|
697
|
+
if (stack.length >= MAX_RECIPE_IMPORT_DEPTH) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`Recipe import depth exceeds limit ${MAX_RECIPE_IMPORT_DEPTH}: ${[...stack, path].join(" -> ")}`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
const raw = readRawRecipeConfig(path);
|
|
703
|
+
if (!raw || !Object.hasOwn(raw, "template")) return [];
|
|
704
|
+
const record: TemplateRecipeContextRecord = {
|
|
705
|
+
...(alias ? { alias } : {}),
|
|
706
|
+
depth: stack.length,
|
|
707
|
+
file: path,
|
|
708
|
+
import_path: importPath,
|
|
709
|
+
name: getRecipeIdFromPath(path),
|
|
710
|
+
recipe: raw,
|
|
711
|
+
role: stack.length === 0 ? "entry" : "import",
|
|
712
|
+
};
|
|
713
|
+
const imports = getRecipeImports(raw);
|
|
714
|
+
const children = Object.entries(imports).flatMap(([childAlias, binding]) => {
|
|
715
|
+
const importFile = resolveRecipeImportPath(getImportFrom(binding), dirname(path));
|
|
716
|
+
return collectRecipeContextRecords(importFile, [...stack, path], [...importPath, childAlias], childAlias);
|
|
717
|
+
});
|
|
718
|
+
return [record, ...children];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function buildRecipeContextRecords(
|
|
722
|
+
file: string,
|
|
723
|
+
): TemplateRecipeContextRecord[] {
|
|
724
|
+
return collectRecipeContextRecords(file, [], []);
|
|
725
|
+
}
|
|
726
|
+
|
|
638
727
|
export function getRecipeTemplate(
|
|
639
728
|
value: unknown,
|
|
640
729
|
): CommandTemplateValue | undefined {
|
package/lib/tools.ts
CHANGED
|
@@ -578,6 +578,7 @@ export function createSpawnToolDefinition<
|
|
|
578
578
|
: typeof input.recipe === "string"
|
|
579
579
|
? input.recipe
|
|
580
580
|
: undefined,
|
|
581
|
+
launch_source: "spawn",
|
|
581
582
|
ownerId: getRunOwnerId(ctx),
|
|
582
583
|
run_id: runId,
|
|
583
584
|
state_dir:
|
|
@@ -1301,6 +1302,7 @@ export function createRuntimeToolDefinition(
|
|
|
1301
1302
|
const meta = AsyncRuns.startRun(
|
|
1302
1303
|
{
|
|
1303
1304
|
...base,
|
|
1305
|
+
launch_source: "tool",
|
|
1304
1306
|
ownerId: getRunOwnerId(ctx),
|
|
1305
1307
|
run_id: runId,
|
|
1306
1308
|
tool: cfg.name,
|
package/package.json
CHANGED
package/scripts/async-runner.mjs
CHANGED
|
@@ -21,6 +21,7 @@ if (!stateDir) {
|
|
|
21
21
|
}
|
|
22
22
|
const { executeRegisteredTool } = await import("../lib/execution.ts");
|
|
23
23
|
const { execCommandTemplate } = await import("../lib/command-templates.ts");
|
|
24
|
+
const { appendRecipeContextToPiArgs } = await import("../lib/actor-recipe-context.ts");
|
|
24
25
|
const { writeJsonAtomic } = await import("../lib/file-state.ts");
|
|
25
26
|
const runPath = join(stateDir, "run.json");
|
|
26
27
|
const progressPath = join(stateDir, "progress.json");
|
|
@@ -90,10 +91,16 @@ function progressRunning() {
|
|
|
90
91
|
}
|
|
91
92
|
async function observedExec(command, args, options) {
|
|
92
93
|
const commandDetail = formatCommandDetail(command, args);
|
|
94
|
+
const execArgs = appendRecipeContextToPiArgs(
|
|
95
|
+
command,
|
|
96
|
+
args,
|
|
97
|
+
meta.recipe_context_records,
|
|
98
|
+
options?.actorRecipeContext,
|
|
99
|
+
);
|
|
93
100
|
activeSubagents += 1;
|
|
94
101
|
event("command.start", { activeSubagents, command: commandDetail });
|
|
95
102
|
progressRunning();
|
|
96
|
-
const result = await execCommandTemplate(command,
|
|
103
|
+
const result = await execCommandTemplate(command, execArgs, options);
|
|
97
104
|
activeSubagents = Math.max(0, activeSubagents - 1);
|
|
98
105
|
completedSubagents += 1;
|
|
99
106
|
if (result.code !== 0) {
|
package/skills/actors/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: actors
|
|
3
3
|
description: Highest-density practical guide for pi-actors. Read this skill whenever prompt and tools are not enough for spawn, message, inspect, actor runs, tools, recipes, command templates, async lifecycle, mailboxes, artifacts, and local orchestration mechanics.
|
|
4
4
|
metadata:
|
|
5
|
-
version: 0.19.
|
|
5
|
+
version: 0.19.3
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Actors (pi-actors)
|
|
@@ -63,6 +63,7 @@ Rules:
|
|
|
63
63
|
|
|
64
64
|
- Use `file`/`recipe` for saved recipes; bare names resolve under `~/.pi/agent/recipes`.
|
|
65
65
|
- Use inline `template` for one-off experiments; promote useful repeats to recipes.
|
|
66
|
+
- When a directly spawned inline/ad hoc actor completes successfully and the follow-up suggests persistence, offer to save it as a recipe/tool; ask before writing `~/.pi/agent/recipes`.
|
|
66
67
|
- Use stable `as` names when you will inspect or message the actor later.
|
|
67
68
|
- `async: true` on the recipe is the detached run switch.
|
|
68
69
|
|
|
@@ -124,7 +125,7 @@ Actor inspector commands:
|
|
|
124
125
|
- `/actors-inspector-filter all|room|direct|broadcast|mention <text>`: narrow table previews without changing room/run state.
|
|
125
126
|
- `/actors-inspect <number>`: open one visible row as a full-message view.
|
|
126
127
|
|
|
127
|
-
The table is compact and optimistic by default: bounded body previews, capped noisy room rows, and an inline roster summary in the form `role
|
|
128
|
+
The table is compact and optimistic by default: bounded body previews, capped noisy room rows, and an inline roster summary in the form `name/role` that wraps only when needed. Active roster members use the target color; members that sent `actor.leave` stay visible as inactive/muted participants from the current run. Actor display names come from `actor.join` bodies (`display`) or branch addresses, keeping debugger output plain and name-driven.
|
|
128
129
|
|
|
129
130
|
Let terminal notifications arrive; avoid sleep-poll loops except during diagnosis.
|
|
130
131
|
|
|
@@ -218,8 +219,9 @@ Rules:
|
|
|
218
219
|
5. Declare `mailbox` for actors that accept or emit meaningful messages.
|
|
219
220
|
6. Declare `artifacts` for durable outputs the coordinator should inspect.
|
|
220
221
|
7. File-backed recipe identity comes from the filename basename; legacy top-level `name` fields are ignored by loaders.
|
|
221
|
-
8.
|
|
222
|
-
9.
|
|
222
|
+
8. 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.
|
|
223
|
+
9. Keep packaged recipes generic: no machine-local paths, no private companion identities, no project-specific defaults unless the recipe is explicitly project-specific.
|
|
224
|
+
10. 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.
|
|
223
225
|
|
|
224
226
|
Priority for same-id recipes:
|
|
225
227
|
|
package/skills/swarm/SKILL.md
CHANGED