@llblab/pi-actors 0.33.0 → 0.34.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/AGENTS.md +22 -11
- package/BACKLOG.md +3 -34
- package/CHANGELOG.md +14 -0
- package/dist/index.js +10 -10
- package/dist/lib/async-runs.d.ts +23 -75
- package/dist/lib/async-runs.js +49 -801
- package/dist/lib/command-templates.d.ts +3 -3
- package/dist/lib/command-templates.js +3 -3
- package/dist/lib/config.d.ts +2 -2
- package/dist/lib/config.js +5 -5
- package/dist/lib/{output.d.ts → execution-output.d.ts} +3 -3
- package/dist/lib/{output.js → execution-output.js} +3 -3
- package/dist/lib/execution.js +1 -1
- package/dist/lib/{actor-inspector-tui.d.ts → inspector.d.ts} +3 -2
- package/dist/lib/{actor-inspector-tui.js → inspector.js} +3 -2
- package/dist/lib/mailbox-loop.d.ts +3 -3
- package/dist/lib/mailbox-loop.js +3 -3
- package/dist/lib/{actor-messages.d.ts → messages.d.ts} +3 -3
- package/dist/lib/{actor-messages.js → messages.js} +3 -3
- package/dist/lib/{actor-recipe-context.d.ts → recipes-context.d.ts} +3 -2
- package/dist/lib/{actor-recipe-context.js → recipes-context.js} +2 -1
- package/dist/lib/{recipe-discovery.d.ts → recipes-discovery.d.ts} +8 -8
- package/dist/lib/{recipe-discovery.js → recipes-discovery.js} +6 -6
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +22 -22
- package/dist/lib/{actor-rooms.d.ts → rooms.d.ts} +4 -4
- package/dist/lib/{actor-rooms.js → rooms.js} +3 -3
- package/dist/lib/runs-artifacts.d.ts +21 -0
- package/dist/lib/runs-artifacts.js +68 -0
- package/dist/lib/runs-control.d.ts +13 -0
- package/dist/lib/runs-control.js +59 -0
- package/dist/lib/runs-identity.d.ts +5 -0
- package/dist/lib/runs-identity.js +11 -0
- package/dist/lib/runs-index.d.ts +18 -0
- package/dist/lib/runs-index.js +119 -0
- package/dist/lib/runs-mailbox.d.ts +25 -0
- package/dist/lib/runs-mailbox.js +146 -0
- package/dist/lib/runs-messages.d.ts +15 -0
- package/dist/lib/runs-messages.js +179 -0
- package/dist/lib/runs-outbox.d.ts +41 -0
- package/dist/lib/runs-outbox.js +87 -0
- package/dist/lib/runs-process.d.ts +7 -0
- package/dist/lib/runs-process.js +36 -0
- package/dist/lib/runs-retention.d.ts +8 -0
- package/dist/lib/runs-retention.js +56 -0
- package/dist/lib/runs-start.d.ts +9 -0
- package/dist/lib/runs-start.js +67 -0
- package/dist/lib/runs-status.d.ts +8 -0
- package/dist/lib/runs-status.js +60 -0
- package/dist/lib/runtime.d.ts +2 -2
- package/dist/lib/runtime.js +5 -5
- package/dist/lib/schema.d.ts +9 -0
- package/dist/lib/schema.js +40 -0
- package/dist/lib/tools-access.d.ts +19 -0
- package/dist/lib/tools-access.js +44 -0
- package/dist/lib/tools-inspect.d.ts +11 -0
- package/dist/lib/tools-inspect.js +534 -0
- package/dist/lib/tools-local.d.ts +14 -0
- package/dist/lib/tools-local.js +156 -0
- package/dist/lib/tools-mailbox.d.ts +8 -0
- package/dist/lib/tools-mailbox.js +48 -0
- package/dist/lib/tools-message.d.ts +9 -0
- package/dist/lib/tools-message.js +428 -0
- package/dist/lib/tools-register.d.ts +18 -0
- package/dist/lib/tools-register.js +40 -0
- package/dist/lib/tools-response.d.ts +21 -0
- package/dist/lib/tools-response.js +219 -0
- package/dist/lib/tools-spawn.d.ts +12 -0
- package/dist/lib/tools-spawn.js +159 -0
- package/dist/lib/tools.d.ts +8 -39
- package/dist/lib/tools.js +11 -1490
- package/dist/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/dist/scripts/actor-worker.mjs +190 -7
- package/dist/scripts/async-runner.mjs +171 -8
- package/dist/scripts/build-dist.mjs +1 -1
- package/dist/scripts/conformance.mjs +3 -3
- package/dist/scripts/coordinator.mjs +733 -11
- package/dist/scripts/locker.mjs +345 -16
- package/dist/scripts/recipe-utils.mjs +478 -14
- package/dist/scripts/validate-recipe.mjs +110 -10
- package/dist/skills/actors/SKILL.md +1 -1
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/recipe-library.md +1 -1
- package/docs/template-recipes.md +1 -1
- package/index.ts +11 -18
- package/lib/async-runs.ts +124 -1016
- package/lib/command-templates.ts +3 -3
- package/lib/config.ts +7 -7
- package/lib/{output.ts → execution-output.ts} +3 -3
- package/lib/execution.ts +5 -1
- package/lib/{actor-inspector-tui.ts → inspector.ts} +4 -3
- package/lib/mailbox-loop.ts +3 -3
- package/lib/{actor-messages.ts → messages.ts} +3 -3
- package/lib/{actor-recipe-context.ts → recipes-context.ts} +3 -2
- package/lib/{recipe-discovery.ts → recipes-discovery.ts} +19 -19
- package/lib/{recipe-usage.ts → recipes-usage.ts} +2 -2
- package/lib/registry.ts +37 -24
- package/lib/{actor-rooms.ts → rooms.ts} +4 -4
- package/lib/runs-artifacts.ts +96 -0
- package/lib/runs-control.ts +85 -0
- package/lib/runs-identity.ts +14 -0
- package/lib/runs-index.ts +156 -0
- package/lib/runs-mailbox.ts +208 -0
- package/lib/runs-messages.ts +252 -0
- package/lib/runs-outbox.ts +144 -0
- package/lib/runs-process.ts +51 -0
- package/lib/runs-retention.ts +77 -0
- package/lib/runs-start.ts +98 -0
- package/lib/runs-status.ts +78 -0
- package/lib/runtime.ts +6 -6
- package/lib/schema.ts +55 -0
- package/lib/tools-access.ts +68 -0
- package/lib/tools-inspect.ts +735 -0
- package/lib/tools-local.ts +234 -0
- package/lib/tools-mailbox.ts +56 -0
- package/lib/tools-message.ts +569 -0
- package/lib/tools-register.ts +64 -0
- package/lib/tools-response.ts +267 -0
- package/lib/tools-spawn.ts +240 -0
- package/lib/tools.ts +20 -2040
- package/package.json +1 -1
- package/recipes/utility-coordinator-lock-snapshot.json +1 -1
- package/scripts/actor-worker.mjs +190 -7
- package/scripts/async-runner.mjs +171 -8
- package/scripts/build-dist.mjs +1 -1
- package/scripts/conformance.mjs +3 -3
- package/scripts/coordinator.mjs +733 -11
- package/scripts/locker.mjs +345 -16
- package/scripts/recipe-utils.mjs +478 -14
- package/scripts/validate-recipe.mjs +110 -10
- package/skills/actors/SKILL.md +1 -1
- package/skills/swarm/SKILL.md +1 -1
- package/dist/lib/actor-worker.d.ts +0 -16
- package/dist/lib/actor-worker.js +0 -134
- package/dist/lib/async-runner.d.ts +0 -5
- package/dist/lib/async-runner.js +0 -134
- package/dist/lib/coordinator.d.ts +0 -5
- package/dist/lib/coordinator.js +0 -660
- package/dist/lib/locker.d.ts +0 -5
- package/dist/lib/locker.js +0 -310
- package/dist/lib/recipe-utils.d.ts +0 -5
- package/dist/lib/recipe-utils.js +0 -385
- package/dist/lib/validate-recipe.d.ts +0 -6
- package/dist/lib/validate-recipe.js +0 -104
- package/lib/actor-worker.ts +0 -174
- package/lib/async-runner.ts +0 -173
- package/lib/coordinator.ts +0 -742
- package/lib/locker.ts +0 -340
- package/lib/recipe-utils.ts +0 -486
- package/lib/validate-recipe.ts +0 -110
- /package/dist/lib/{recipe-references.d.ts → recipes-references.d.ts} +0 -0
- /package/dist/lib/{recipe-references.js → recipes-references.js} +0 -0
- /package/dist/lib/{recipe-usage.d.ts → recipes-usage.d.ts} +0 -0
- /package/dist/lib/{recipe-usage.js → recipes-usage.js} +0 -0
- /package/lib/{recipe-references.ts → recipes-references.ts} +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public tool response formatting
|
|
3
|
+
* Zones: compact text summaries, verbose JSON switching, next-action rendering
|
|
4
|
+
* Owns model-facing response helpers shared by public tool execution paths
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Limits from "./limits.ts";
|
|
8
|
+
import * as ToolsMailbox from "./tools-mailbox.ts";
|
|
9
|
+
|
|
10
|
+
export function asRecord(value: unknown): Record<string, unknown> {
|
|
11
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
12
|
+
? (value as Record<string, unknown>)
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function jsonText(value: unknown): string {
|
|
17
|
+
return `\n${JSON.stringify(value, null, 2)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compactPreview(
|
|
21
|
+
value: unknown,
|
|
22
|
+
maxLength = Limits.COMPACT_PREVIEW_CHARS,
|
|
23
|
+
): string | undefined {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
const text =
|
|
26
|
+
typeof value === "string" ? value : JSON.stringify(value, undefined, 0);
|
|
27
|
+
const compact = text.replaceAll(/\s+/g, "_");
|
|
28
|
+
return compact.length > maxLength
|
|
29
|
+
? `${compact.slice(0, Math.max(0, maxLength - 1))}…`
|
|
30
|
+
: compact;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function compactNextActions(actions: string[]): string {
|
|
34
|
+
return actions.length
|
|
35
|
+
? ` next=${actions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`
|
|
36
|
+
: "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function formatFailureCount(value: unknown): number | undefined {
|
|
40
|
+
return Array.isArray(value) ? value.length : undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function actorRunNextActions(run: unknown): string[] {
|
|
44
|
+
const id = String(run ?? "").trim();
|
|
45
|
+
if (!id) return [];
|
|
46
|
+
return [
|
|
47
|
+
`inspect target=run:${id} view=status`,
|
|
48
|
+
`inspect target=run:${id} view=messages`,
|
|
49
|
+
`message to=run:${id} type=<actor.action>`,
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function compactAsyncRunStatus(value: unknown): string {
|
|
54
|
+
const status = asRecord(value);
|
|
55
|
+
const progress = asRecord(status.progress);
|
|
56
|
+
const result = asRecord(status.result);
|
|
57
|
+
const run = String(status.run ?? "<unknown>");
|
|
58
|
+
const tokens = [`run=${run}`, `status=${String(status.status ?? "unknown")}`];
|
|
59
|
+
if (status.tool) tokens.push(`tool=${String(status.tool)}`);
|
|
60
|
+
if (status.recipe) tokens.push(`recipe=${String(status.recipe)}`);
|
|
61
|
+
if (status.retire_when)
|
|
62
|
+
tokens.push(`retire_when=${String(status.retire_when)}`);
|
|
63
|
+
if (Number(status.pid) > 0) tokens.push(`pid=${Number(status.pid)}`);
|
|
64
|
+
if (progress.phase && progress.phase !== status.status)
|
|
65
|
+
tokens.push(`phase=${String(progress.phase)}`);
|
|
66
|
+
if (Number(progress.activeSubagents) > 0)
|
|
67
|
+
tokens.push(`active=${Number(progress.activeSubagents)}`);
|
|
68
|
+
if (Number(progress.completed) > 0)
|
|
69
|
+
tokens.push(`completed=${Number(progress.completed)}`);
|
|
70
|
+
const failures = formatFailureCount(progress.failures);
|
|
71
|
+
if (failures !== undefined && failures > 0)
|
|
72
|
+
tokens.push(`failures=${failures}`);
|
|
73
|
+
if (result.code !== undefined) tokens.push(`code=${String(result.code)}`);
|
|
74
|
+
if (result.killed === true) tokens.push("killed=true");
|
|
75
|
+
const draftRecipe = status.draft_recipe;
|
|
76
|
+
if (draftRecipe) tokens.push(`draft_recipe=${String(draftRecipe)}`);
|
|
77
|
+
const nextActions = actorRunNextActions(run);
|
|
78
|
+
if (nextActions.length > 0)
|
|
79
|
+
tokens.push(
|
|
80
|
+
`next=${nextActions.map((action) => action.replaceAll(/\s+/g, "_")).join("|")}`,
|
|
81
|
+
);
|
|
82
|
+
return `\n${tokens.join(" ")}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function compactInboxMessages(
|
|
86
|
+
messages: Array<Record<string, unknown>>,
|
|
87
|
+
emptyLabel: string,
|
|
88
|
+
): string {
|
|
89
|
+
if (messages.length === 0) return `\n(no ${emptyLabel} messages)`;
|
|
90
|
+
return `\n${messages
|
|
91
|
+
.map((message) =>
|
|
92
|
+
[
|
|
93
|
+
...(message.id ? [`id=${String(message.id)}`] : []),
|
|
94
|
+
`status=${String(message.status ?? "")}`,
|
|
95
|
+
`type=${String(message.type ?? "")}`,
|
|
96
|
+
`from=${String(message.from ?? "")}`,
|
|
97
|
+
`to=${String(message.to ?? "")}`,
|
|
98
|
+
...(message.queued_at
|
|
99
|
+
? [`queued_at=${String(message.queued_at)}`]
|
|
100
|
+
: []),
|
|
101
|
+
...(message.sent_at ? [`sent_at=${String(message.sent_at)}`] : []),
|
|
102
|
+
...(message.claimed_at
|
|
103
|
+
? [`claimed_at=${String(message.claimed_at)}`]
|
|
104
|
+
: []),
|
|
105
|
+
...(message.handled_at
|
|
106
|
+
? [`handled_at=${String(message.handled_at)}`]
|
|
107
|
+
: []),
|
|
108
|
+
...(message.failed_at
|
|
109
|
+
? [`failed_at=${String(message.failed_at)}`]
|
|
110
|
+
: []),
|
|
111
|
+
].join(" "),
|
|
112
|
+
)
|
|
113
|
+
.join("\n")}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function compactBranchInbox(
|
|
117
|
+
messages: Array<Record<string, unknown>>,
|
|
118
|
+
): string {
|
|
119
|
+
return compactInboxMessages(messages, "branch inbox");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function compactRunMailbox(
|
|
123
|
+
run: string,
|
|
124
|
+
mailbox: Record<string, unknown>,
|
|
125
|
+
messages: Array<Record<string, unknown>>,
|
|
126
|
+
): string {
|
|
127
|
+
const normalized = ToolsMailbox.normalizeMailboxContracts(mailbox);
|
|
128
|
+
return `\nrun=${run} accepts=${ToolsMailbox.mailboxTypes(normalized.accepts).join(",")} emits=${ToolsMailbox.mailboxTypes(normalized.emits).join(",")}${compactInboxMessages(messages, "run inbox")}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function artifactNextActions(
|
|
132
|
+
run: unknown,
|
|
133
|
+
artifacts: Record<string, unknown>,
|
|
134
|
+
): string[] {
|
|
135
|
+
const id = String(run ?? "").trim();
|
|
136
|
+
if (!id || Object.keys(artifacts).length === 0) return [];
|
|
137
|
+
return [
|
|
138
|
+
`inspect target=run:${id} view=artifacts verbose=true`,
|
|
139
|
+
`inspect target=run:${id} view=messages`,
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function maybeJsonText(
|
|
144
|
+
value: unknown,
|
|
145
|
+
verbose: boolean | undefined,
|
|
146
|
+
compact: string,
|
|
147
|
+
): string {
|
|
148
|
+
return verbose ? jsonText(value) : compact;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function compactRecipeImports(summary: Record<string, unknown>): string {
|
|
152
|
+
const active = Array.isArray(summary.active)
|
|
153
|
+
? (summary.active as Array<Record<string, unknown>>)
|
|
154
|
+
: [];
|
|
155
|
+
const lines = active.flatMap((entry) => {
|
|
156
|
+
const imports = asRecord(entry.imports);
|
|
157
|
+
return Object.entries(imports).map(([alias, value]) => {
|
|
158
|
+
const binding =
|
|
159
|
+
typeof value === "string" ? { from: value } : asRecord(value);
|
|
160
|
+
return `recipe=${String(entry.id ?? "<unknown>")} alias=${alias} from=${String(binding.from ?? value)}`;
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
return lines.length ? `\n${lines.join("\n")}` : "\n(no recipe imports)";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function compactRecipeDoctor(summary: Record<string, unknown>): string {
|
|
167
|
+
const details = Array.isArray(summary.diagnostic_details)
|
|
168
|
+
? (summary.diagnostic_details as Array<Record<string, unknown>>)
|
|
169
|
+
: [];
|
|
170
|
+
const remediations = Array.isArray(summary.remediations)
|
|
171
|
+
? (summary.remediations as Array<Record<string, unknown>>)
|
|
172
|
+
: [];
|
|
173
|
+
const recommendations = Array.isArray(summary.recommendations)
|
|
174
|
+
? (summary.recommendations as Array<Record<string, unknown>>)
|
|
175
|
+
: [];
|
|
176
|
+
const counts = { error: 0, info: 0, warning: 0 };
|
|
177
|
+
for (const detail of details) {
|
|
178
|
+
const severity = String(detail.severity ?? "info");
|
|
179
|
+
if (severity === "error" || severity === "warning" || severity === "info")
|
|
180
|
+
counts[severity] += 1;
|
|
181
|
+
}
|
|
182
|
+
const topAction = asRecord(summary.top_action);
|
|
183
|
+
const lines = [
|
|
184
|
+
`recipes doctor errors=${counts.error} warnings=${counts.warning} info=${counts.info} actions=${remediations.length} recommendations=${recommendations.length}`,
|
|
185
|
+
];
|
|
186
|
+
if (Object.keys(topAction).length > 0) {
|
|
187
|
+
const action = compactPreview(
|
|
188
|
+
topAction.action,
|
|
189
|
+
Limits.DOCTOR_ACTION_PREVIEW_CHARS,
|
|
190
|
+
);
|
|
191
|
+
lines.push(
|
|
192
|
+
`top severity=${String(topAction.severity ?? "info")} kind=${String(topAction.kind ?? "inspect")} id=${String(topAction.id ?? "root")} action=${action ?? "inspect"}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
for (const item of remediations.slice(0, 8)) {
|
|
196
|
+
const action = compactPreview(
|
|
197
|
+
item.action,
|
|
198
|
+
Limits.DOCTOR_ACTION_PREVIEW_CHARS,
|
|
199
|
+
);
|
|
200
|
+
const blocked = item.blocked_fallback
|
|
201
|
+
? ` blocked=${compactPreview(item.blocked_fallback, Limits.DOCTOR_ACTION_PREVIEW_CHARS)}`
|
|
202
|
+
: "";
|
|
203
|
+
lines.push(
|
|
204
|
+
`${String(item.severity ?? "info")} kind=${String(item.kind ?? "inspect")} id=${String(item.id ?? "root")}${blocked} action=${action ?? "inspect"}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
208
|
+
? (summary.next_actions as string[])
|
|
209
|
+
: [];
|
|
210
|
+
if (nextActions.length > 0)
|
|
211
|
+
lines[0] = `${lines[0]}${compactNextActions(nextActions)}`;
|
|
212
|
+
return `\n${lines.join("\n")}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function recipeRegistryNextActions(
|
|
216
|
+
summary: Record<string, unknown>,
|
|
217
|
+
view: string,
|
|
218
|
+
): string[] {
|
|
219
|
+
const actions: string[] = [];
|
|
220
|
+
const drafts = Array.isArray(summary.drafts)
|
|
221
|
+
? (summary.drafts as Array<Record<string, unknown>>)
|
|
222
|
+
: [];
|
|
223
|
+
const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
|
|
224
|
+
const diagnostics = Array.isArray(summary.diagnostics)
|
|
225
|
+
? summary.diagnostics.length
|
|
226
|
+
: 0;
|
|
227
|
+
const topAction = asRecord(summary.top_action);
|
|
228
|
+
if (view !== "doctor" && (invalid > 0 || diagnostics > 0)) {
|
|
229
|
+
actions.push("inspect target=recipes view=doctor");
|
|
230
|
+
}
|
|
231
|
+
if (view === "doctor" && typeof topAction.action === "string") {
|
|
232
|
+
actions.push(String(topAction.action));
|
|
233
|
+
}
|
|
234
|
+
if (drafts.length > 0) {
|
|
235
|
+
actions.push("inspect target=recipes view=summary verbose=true");
|
|
236
|
+
const firstPath =
|
|
237
|
+
typeof drafts[0]?.path === "string" ? drafts[0].path : undefined;
|
|
238
|
+
if (firstPath) actions.push(`spawn file=${firstPath}`);
|
|
239
|
+
}
|
|
240
|
+
return [...new Set(actions)].slice(0, 4);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function compactRecipeRegistry(
|
|
244
|
+
summary: Record<string, unknown>,
|
|
245
|
+
): string {
|
|
246
|
+
const active = Array.isArray(summary.active) ? summary.active.length : 0;
|
|
247
|
+
const shadowed = Array.isArray(summary.shadowed)
|
|
248
|
+
? summary.shadowed.length
|
|
249
|
+
: 0;
|
|
250
|
+
const invalid = Array.isArray(summary.invalid) ? summary.invalid.length : 0;
|
|
251
|
+
const disabled = Array.isArray(summary.disabled)
|
|
252
|
+
? summary.disabled.length
|
|
253
|
+
: 0;
|
|
254
|
+
const diagnostics = Array.isArray(summary.diagnostics)
|
|
255
|
+
? summary.diagnostics.length
|
|
256
|
+
: 0;
|
|
257
|
+
const drafts = Array.isArray(summary.drafts) ? summary.drafts.length : 0;
|
|
258
|
+
const recommendations = Array.isArray(summary.recommendations)
|
|
259
|
+
? summary.recommendations.length
|
|
260
|
+
: 0;
|
|
261
|
+
const nextActions = Array.isArray(summary.next_actions)
|
|
262
|
+
? (summary.next_actions as string[])
|
|
263
|
+
: [];
|
|
264
|
+
return `\nrecipes active=${active} drafts=${drafts} shadowed=${shadowed} invalid=${invalid} disabled=${disabled} recommendations=${recommendations} diagnostics=${diagnostics}${compactNextActions(nextActions)}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const DEFAULT_INSPECT_LINES = Limits.DEFAULT_INSPECT_LINES;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public spawn tool behavior
|
|
3
|
+
* Zones: actor launch, draft recipe capture, launch diagnostics
|
|
4
|
+
* Owns the public spawn execution path for run-backed actors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
import * as AsyncRuns from "./async-runs.ts";
|
|
11
|
+
import * as Messages from "./messages.ts";
|
|
12
|
+
import * as Paths from "./paths.ts";
|
|
13
|
+
import * as RecipesDiscovery from "./recipes-discovery.ts";
|
|
14
|
+
import * as Rooms from "./rooms.ts";
|
|
15
|
+
import * as Schema from "./schema.ts";
|
|
16
|
+
import * as ToolsResponse from "./tools-response.ts";
|
|
17
|
+
|
|
18
|
+
export interface SpawnToolContext {
|
|
19
|
+
cwd: string;
|
|
20
|
+
sessionManager?: { getSessionId?: () => string };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const asRecord = ToolsResponse.asRecord;
|
|
24
|
+
const maybeJsonText = ToolsResponse.maybeJsonText;
|
|
25
|
+
|
|
26
|
+
function getRunOwnerId(ctx: SpawnToolContext): string | undefined {
|
|
27
|
+
return ctx.sessionManager?.getSessionId?.();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function shadowedRecipeLaunchDiagnostic(
|
|
31
|
+
recipe: unknown,
|
|
32
|
+
): Record<string, unknown> | undefined {
|
|
33
|
+
if (
|
|
34
|
+
typeof recipe !== "string" ||
|
|
35
|
+
recipe.includes("/") ||
|
|
36
|
+
recipe.includes("~")
|
|
37
|
+
)
|
|
38
|
+
return undefined;
|
|
39
|
+
const discovery = RecipesDiscovery.discoverRecipeSources([
|
|
40
|
+
{ root: Paths.getRecipeRoot(), defaultTool: true, mutableUsage: true },
|
|
41
|
+
{ root: Paths.getPackagedRecipeRoot(), defaultTool: false },
|
|
42
|
+
]);
|
|
43
|
+
return RecipesDiscovery.getShadowedLaunchDiagnostic(discovery, recipe);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function draftRecipeName(run: string): string {
|
|
47
|
+
return `${run.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "spawn"}.json`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function draftRecipeDefaults(
|
|
51
|
+
values: Record<string, unknown>,
|
|
52
|
+
): Record<string, unknown> | undefined {
|
|
53
|
+
const ignored = new Set([
|
|
54
|
+
"actor_address",
|
|
55
|
+
"communication_file",
|
|
56
|
+
"default_room",
|
|
57
|
+
"run_id",
|
|
58
|
+
"state_dir",
|
|
59
|
+
]);
|
|
60
|
+
const defaults = Object.fromEntries(
|
|
61
|
+
Object.entries(values).filter(([key]) => !ignored.has(key)),
|
|
62
|
+
);
|
|
63
|
+
return Object.keys(defaults).length > 0 ? defaults : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function writeSpawnDraftRecipe(
|
|
67
|
+
input: Record<string, unknown>,
|
|
68
|
+
meta: AsyncRuns.AsyncRunMeta,
|
|
69
|
+
): string | undefined {
|
|
70
|
+
if (
|
|
71
|
+
process.env.NODE_TEST_CONTEXT &&
|
|
72
|
+
process.env.PI_ACTORS_ENABLE_SPAWN_DRAFTS_IN_TEST !== "1"
|
|
73
|
+
)
|
|
74
|
+
return undefined;
|
|
75
|
+
if (
|
|
76
|
+
input.template === undefined ||
|
|
77
|
+
input.file !== undefined ||
|
|
78
|
+
input.recipe !== undefined
|
|
79
|
+
)
|
|
80
|
+
return undefined;
|
|
81
|
+
const root = Paths.getRecipeDraftRoot();
|
|
82
|
+
mkdirSync(root, { recursive: true });
|
|
83
|
+
const path = join(root, draftRecipeName(String(meta.run)));
|
|
84
|
+
const defaults = draftRecipeDefaults(meta.values);
|
|
85
|
+
const recipe = {
|
|
86
|
+
async: true,
|
|
87
|
+
description: `Draft recipe captured from spawn run ${String(meta.run)}`,
|
|
88
|
+
...(meta.artifacts ? { artifacts: meta.artifacts } : {}),
|
|
89
|
+
...(defaults ? { defaults } : {}),
|
|
90
|
+
template: input.template,
|
|
91
|
+
};
|
|
92
|
+
writeFileSync(path, `${JSON.stringify(recipe, null, 2)}\n`, { flag: "wx" });
|
|
93
|
+
return path;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function enhanceSpawnRecipeError(error: unknown, recipe: unknown): Error {
|
|
97
|
+
const diagnostic = shadowedRecipeLaunchDiagnostic(recipe);
|
|
98
|
+
if (!diagnostic)
|
|
99
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
100
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
101
|
+
return Object.assign(
|
|
102
|
+
new Error(
|
|
103
|
+
`${original} reason=${diagnostic.reason} active_path=${diagnostic.active_path} blocked_fallback=${diagnostic.blocked_fallback} hint=${diagnostic.hint}`,
|
|
104
|
+
),
|
|
105
|
+
{
|
|
106
|
+
...diagnostic,
|
|
107
|
+
original_error: original,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function runIdFromActorAddress(
|
|
113
|
+
address: string | undefined,
|
|
114
|
+
): string | undefined {
|
|
115
|
+
if (!address) return undefined;
|
|
116
|
+
const parsed = Messages.parseActorAddress(address);
|
|
117
|
+
if (parsed.kind !== "run" || !parsed.value) {
|
|
118
|
+
throw new Error(`Expected run:<id> actor address, received: ${address}`);
|
|
119
|
+
}
|
|
120
|
+
return parsed.value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function createSpawnToolDefinition<
|
|
124
|
+
TContext extends SpawnToolContext,
|
|
125
|
+
>(): any {
|
|
126
|
+
return {
|
|
127
|
+
name: "spawn",
|
|
128
|
+
label: "Spawn",
|
|
129
|
+
description:
|
|
130
|
+
"Create an addressable actor from a recipe file or inline command template. Use instead of ad hoc shell backgrounding for work that may outlive this turn, needs steering/follow-up/artifacts, runs as a service, fans out, or should be inspected later. Currently spawns run:<id> actors backed by async runs.",
|
|
131
|
+
parameters: Schema.objectSchema(
|
|
132
|
+
{
|
|
133
|
+
artifacts: Schema.looseObjectSchema(
|
|
134
|
+
"Optional named artifact paths for the spawned actor.",
|
|
135
|
+
),
|
|
136
|
+
as: Schema.stringSchema(
|
|
137
|
+
"Optional actor address for the spawned run, e.g. run:<id>.",
|
|
138
|
+
),
|
|
139
|
+
file: Schema.stringSchema(
|
|
140
|
+
"Optional template recipe JSON file. Bare names resolve under ~/.pi/agent/recipes.",
|
|
141
|
+
),
|
|
142
|
+
recipe: Schema.stringSchema(
|
|
143
|
+
"Alias for file; template recipe JSON file/name to spawn.",
|
|
144
|
+
),
|
|
145
|
+
state_dir: Schema.stringSchema(
|
|
146
|
+
"Optional explicit run state directory.",
|
|
147
|
+
),
|
|
148
|
+
template: Schema.unionSchema([
|
|
149
|
+
Schema.stringSchema("Inline command template string"),
|
|
150
|
+
Schema.arraySchema(
|
|
151
|
+
"Inline command-template sequence or parallel tree",
|
|
152
|
+
),
|
|
153
|
+
Schema.looseObjectSchema(
|
|
154
|
+
"Inline command-template object with flags such as parallel, repeat, retry, failure, and nested template.",
|
|
155
|
+
),
|
|
156
|
+
]),
|
|
157
|
+
values: Schema.looseObjectSchema(
|
|
158
|
+
"Runtime placeholder values passed to the actor.",
|
|
159
|
+
),
|
|
160
|
+
verbose: Schema.booleanSchema(
|
|
161
|
+
"Return full JSON instead of compact text.",
|
|
162
|
+
),
|
|
163
|
+
},
|
|
164
|
+
[],
|
|
165
|
+
),
|
|
166
|
+
async execute(
|
|
167
|
+
_toolCallId: string,
|
|
168
|
+
params: unknown,
|
|
169
|
+
_signal: AbortSignal | undefined,
|
|
170
|
+
_onUpdate: unknown,
|
|
171
|
+
ctx: TContext,
|
|
172
|
+
) {
|
|
173
|
+
const input = asRecord(params);
|
|
174
|
+
const runId = runIdFromActorAddress(
|
|
175
|
+
typeof input.as === "string" ? input.as : undefined,
|
|
176
|
+
);
|
|
177
|
+
const recipe =
|
|
178
|
+
typeof input.file === "string"
|
|
179
|
+
? input.file
|
|
180
|
+
: typeof input.recipe === "string"
|
|
181
|
+
? input.recipe
|
|
182
|
+
: undefined;
|
|
183
|
+
let meta: AsyncRuns.AsyncRunMeta;
|
|
184
|
+
try {
|
|
185
|
+
meta = AsyncRuns.startRun(
|
|
186
|
+
{
|
|
187
|
+
file: recipe,
|
|
188
|
+
launch_source: "spawn",
|
|
189
|
+
ownerId: getRunOwnerId(ctx),
|
|
190
|
+
run_id: runId,
|
|
191
|
+
state_dir:
|
|
192
|
+
typeof input.state_dir === "string" ? input.state_dir : undefined,
|
|
193
|
+
...(input.template !== undefined
|
|
194
|
+
? {
|
|
195
|
+
template:
|
|
196
|
+
input.template as AsyncRuns.AsyncRunStartParams["template"],
|
|
197
|
+
}
|
|
198
|
+
: {}),
|
|
199
|
+
values: asRecord(input.values),
|
|
200
|
+
...(input.artifacts &&
|
|
201
|
+
typeof input.artifacts === "object" &&
|
|
202
|
+
!Array.isArray(input.artifacts)
|
|
203
|
+
? {
|
|
204
|
+
artifacts: input.artifacts as Record<
|
|
205
|
+
string,
|
|
206
|
+
AsyncRuns.RunArtifactDeclaration
|
|
207
|
+
>,
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
},
|
|
211
|
+
ctx.cwd,
|
|
212
|
+
);
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw enhanceSpawnRecipeError(error, recipe);
|
|
215
|
+
}
|
|
216
|
+
const draftRecipe = writeSpawnDraftRecipe(input, meta);
|
|
217
|
+
const nextActions = ToolsResponse.actorRunNextActions(meta.run);
|
|
218
|
+
const details = {
|
|
219
|
+
...meta,
|
|
220
|
+
...(draftRecipe ? { draft_recipe: draftRecipe } : {}),
|
|
221
|
+
next_actions: nextActions,
|
|
222
|
+
};
|
|
223
|
+
Rooms.ensureDefaultRoom(meta.state_dir, String(meta.run));
|
|
224
|
+
Rooms.writeCommunicationSnapshot(meta.state_dir, String(meta.run));
|
|
225
|
+
return {
|
|
226
|
+
content: [
|
|
227
|
+
{
|
|
228
|
+
type: "text" as const,
|
|
229
|
+
text: maybeJsonText(
|
|
230
|
+
details,
|
|
231
|
+
input.verbose === true,
|
|
232
|
+
ToolsResponse.compactAsyncRunStatus(details),
|
|
233
|
+
),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
details,
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|