@introspection-ai/recipes 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +191 -0
- package/README.md +184 -0
- package/dist/agent-tool.d.ts +42 -0
- package/dist/agent-tool.d.ts.map +1 -0
- package/dist/agent-tool.js +159 -0
- package/dist/agent-tool.js.map +1 -0
- package/dist/child-agent-completions.d.ts +61 -0
- package/dist/child-agent-completions.d.ts.map +1 -0
- package/dist/child-agent-completions.js +135 -0
- package/dist/child-agent-completions.js.map +1 -0
- package/dist/child-agent-store.d.ts +48 -0
- package/dist/child-agent-store.d.ts.map +1 -0
- package/dist/child-agent-store.js +98 -0
- package/dist/child-agent-store.js.map +1 -0
- package/dist/child-agent.d.ts +43 -0
- package/dist/child-agent.d.ts.map +1 -0
- package/dist/child-agent.js +268 -0
- package/dist/child-agent.js.map +1 -0
- package/dist/direct-cli.d.ts +9 -0
- package/dist/direct-cli.d.ts.map +1 -0
- package/dist/direct-cli.js +22 -0
- package/dist/direct-cli.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/inspect.d.ts +26 -0
- package/dist/inspect.d.ts.map +1 -0
- package/dist/inspect.js +70 -0
- package/dist/inspect.js.map +1 -0
- package/dist/interactions.d.ts +204 -0
- package/dist/interactions.d.ts.map +1 -0
- package/dist/interactions.js +358 -0
- package/dist/interactions.js.map +1 -0
- package/dist/mcp-catalog.d.ts +8 -0
- package/dist/mcp-catalog.d.ts.map +1 -0
- package/dist/mcp-catalog.js +60 -0
- package/dist/mcp-catalog.js.map +1 -0
- package/dist/mcp-cli-core.d.ts +79 -0
- package/dist/mcp-cli-core.d.ts.map +1 -0
- package/dist/mcp-cli-core.js +1717 -0
- package/dist/mcp-cli-core.js.map +1 -0
- package/dist/mcp-cli-help.d.ts +2 -0
- package/dist/mcp-cli-help.d.ts.map +1 -0
- package/dist/mcp-cli-help.js +36 -0
- package/dist/mcp-cli-help.js.map +1 -0
- package/dist/mcp-cli-policy.d.ts +17 -0
- package/dist/mcp-cli-policy.d.ts.map +1 -0
- package/dist/mcp-cli-policy.js +230 -0
- package/dist/mcp-cli-policy.js.map +1 -0
- package/dist/mcp-cli.d.ts +3 -0
- package/dist/mcp-cli.d.ts.map +1 -0
- package/dist/mcp-cli.js +33 -0
- package/dist/mcp-cli.js.map +1 -0
- package/dist/mcp-client.d.ts +3 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +86 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/mcp-command-context.d.ts +13 -0
- package/dist/mcp-command-context.d.ts.map +1 -0
- package/dist/mcp-command-context.js +29 -0
- package/dist/mcp-command-context.js.map +1 -0
- package/dist/mcp-contract.d.ts +16 -0
- package/dist/mcp-contract.d.ts.map +1 -0
- package/dist/mcp-contract.js +306 -0
- package/dist/mcp-contract.js.map +1 -0
- package/dist/mcp-daemon-client.d.ts +10 -0
- package/dist/mcp-daemon-client.d.ts.map +1 -0
- package/dist/mcp-daemon-client.js +119 -0
- package/dist/mcp-daemon-client.js.map +1 -0
- package/dist/mcp-daemon-protocol.d.ts +64 -0
- package/dist/mcp-daemon-protocol.d.ts.map +1 -0
- package/dist/mcp-daemon-protocol.js +6 -0
- package/dist/mcp-daemon-protocol.js.map +1 -0
- package/dist/mcp-daemon.d.ts +3 -0
- package/dist/mcp-daemon.d.ts.map +1 -0
- package/dist/mcp-daemon.js +76880 -0
- package/dist/mcp-daemon.js.map +1 -0
- package/dist/mcp-run-worker.d.ts +2 -0
- package/dist/mcp-run-worker.d.ts.map +1 -0
- package/dist/mcp-run-worker.js +76579 -0
- package/dist/mcp-run-worker.js.map +1 -0
- package/dist/mcp.d.ts +168 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +558 -0
- package/dist/mcp.js.map +1 -0
- package/dist/pi-extension.d.ts +30 -0
- package/dist/pi-extension.d.ts.map +1 -0
- package/dist/pi-extension.js +880 -0
- package/dist/pi-extension.js.map +1 -0
- package/dist/provider-env.d.ts +2 -0
- package/dist/provider-env.d.ts.map +1 -0
- package/dist/provider-env.js +21 -0
- package/dist/provider-env.js.map +1 -0
- package/dist/recipe/resolve.d.ts +30 -0
- package/dist/recipe/resolve.d.ts.map +1 -0
- package/dist/recipe/resolve.js +131 -0
- package/dist/recipe/resolve.js.map +1 -0
- package/dist/recipe-agent.d.ts +71 -0
- package/dist/recipe-agent.d.ts.map +1 -0
- package/dist/recipe-agent.js +555 -0
- package/dist/recipe-agent.js.map +1 -0
- package/dist/recipe-extensions.d.ts +9 -0
- package/dist/recipe-extensions.d.ts.map +1 -0
- package/dist/recipe-extensions.js +81 -0
- package/dist/recipe-extensions.js.map +1 -0
- package/dist/recipe-mcp-config.d.ts +15 -0
- package/dist/recipe-mcp-config.d.ts.map +1 -0
- package/dist/recipe-mcp-config.js +82 -0
- package/dist/recipe-mcp-config.js.map +1 -0
- package/dist/recipe-model.d.ts +49 -0
- package/dist/recipe-model.d.ts.map +1 -0
- package/dist/recipe-model.js +299 -0
- package/dist/recipe-model.js.map +1 -0
- package/dist/recipe-package.d.ts +79 -0
- package/dist/recipe-package.d.ts.map +1 -0
- package/dist/recipe-package.js +479 -0
- package/dist/recipe-package.js.map +1 -0
- package/dist/recipe-skills.d.ts +7 -0
- package/dist/recipe-skills.d.ts.map +1 -0
- package/dist/recipe-skills.js +46 -0
- package/dist/recipe-skills.js.map +1 -0
- package/dist/run-controller.d.ts +32 -0
- package/dist/run-controller.d.ts.map +1 -0
- package/dist/run-controller.js +259 -0
- package/dist/run-controller.js.map +1 -0
- package/dist/run.d.ts +28 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +84 -0
- package/dist/run.js.map +1 -0
- package/dist/session.d.ts +140 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +363 -0
- package/dist/session.js.map +1 -0
- package/dist/test-utils.d.ts +32 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +259 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +96 -0
- package/dist/testing.js.map +1 -0
- package/docs/agent-composition.md +196 -0
- package/docs/deployment-configuration.md +65 -0
- package/docs/index.md +49 -0
- package/docs/interactions.md +210 -0
- package/docs/mcp-auth.md +80 -0
- package/docs/mcp-configuration.md +126 -0
- package/docs/migration.md +70 -0
- package/docs/pi-extension.md +97 -0
- package/docs/python-bindings-release.md +47 -0
- package/docs/recipe-evals.md +70 -0
- package/docs/recipe-flow.md +41 -0
- package/docs/recipe-format.md +186 -0
- package/docs/recipe-judges.md +152 -0
- package/docs/runtime-library.md +171 -0
- package/package.json +106 -0
- package/vendor/mcp-client/darwin-arm64/mcp-client +0 -0
- package/vendor/mcp-client/darwin-x64/mcp-client +0 -0
- package/vendor/mcp-client/linux-arm64/mcp-client +0 -0
- package/vendor/mcp-client/linux-x64/mcp-client +0 -0
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
5
|
+
import { createRecipeChildAgentRunner, promptResultText, } from "./child-agent.js";
|
|
6
|
+
import { loadRecipeExtensionFactory } from "./recipe-extensions.js";
|
|
7
|
+
import { ChildAgentRunStore, } from "./child-agent-store.js";
|
|
8
|
+
import { ChildCompletionQueue, envelopeFromRun, renderCompletionNotice, } from "./child-agent-completions.js";
|
|
9
|
+
import { clearMcpSession, configureMcpLocalConfigPath, formatMcpConfigurationDiagnostics, materializeMcpSession, materializeSessionMcpCli, resolveAgentMcpSelections, stopMcpDaemon, } from "./mcp.js";
|
|
10
|
+
import { clearMcpCatalogPreload, preloadMcpCatalogs, } from "./mcp-catalog.js";
|
|
11
|
+
import { applyRecipeAgentModelConfigToModel } from "./recipe-model.js";
|
|
12
|
+
import { resolveRecipe, } from "./recipe/resolve.js";
|
|
13
|
+
import { createAgentTool, } from "./agent-tool.js";
|
|
14
|
+
/** Mid-turn completion delivery retries on this cadence until the session idles. */
|
|
15
|
+
const COMPLETION_DELIVERY_RETRY_MS = 100;
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
class RecipeLaunchError extends Error {
|
|
18
|
+
constructor(message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "RecipeLaunchError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function stringFlag(value) {
|
|
24
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
25
|
+
}
|
|
26
|
+
function recipeNotFoundMessage(input, resolvedPath) {
|
|
27
|
+
const lines = [`Recipe "${input}" was not found.`];
|
|
28
|
+
lines.push(`Resolved path: ${resolvedPath}`);
|
|
29
|
+
lines.push("Pass a local Recipe directory containing package.json with a pi block.");
|
|
30
|
+
lines.push("Then launch again with `pi --recipe <recipe>`.");
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
function recipeLoadErrorMessage(input, reason) {
|
|
34
|
+
return [
|
|
35
|
+
`Recipe "${input}" could not be loaded.`,
|
|
36
|
+
reason,
|
|
37
|
+
"Run `introspection check` from the repository for a validation report.",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
function modelParts(spec) {
|
|
41
|
+
const index = spec.indexOf("/");
|
|
42
|
+
if (index < 0) {
|
|
43
|
+
throw new Error(`Invalid recipe model "${spec}" - expected "<provider>/<model_id>"`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
provider: spec.slice(0, index),
|
|
47
|
+
model: spec.slice(index + 1),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function visibleSubagents(state) {
|
|
51
|
+
return [...state.resolved.subagents.values()];
|
|
52
|
+
}
|
|
53
|
+
function mcpSelectionsForAgent(agent) {
|
|
54
|
+
return resolveAgentMcpSelections(agent.mcp);
|
|
55
|
+
}
|
|
56
|
+
function scopedMcpSelections(state) {
|
|
57
|
+
return [state.resolved.agent, ...visibleSubagents(state)].flatMap(mcpSelectionsForAgent);
|
|
58
|
+
}
|
|
59
|
+
function textResult(text, details) {
|
|
60
|
+
return {
|
|
61
|
+
content: [{ type: "text", text }],
|
|
62
|
+
details,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function asRecord(value) {
|
|
66
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
67
|
+
? value
|
|
68
|
+
: null;
|
|
69
|
+
}
|
|
70
|
+
function contentText(content) {
|
|
71
|
+
if (typeof content === "string")
|
|
72
|
+
return content;
|
|
73
|
+
if (!Array.isArray(content))
|
|
74
|
+
return "";
|
|
75
|
+
return content
|
|
76
|
+
.map((part) => {
|
|
77
|
+
const record = asRecord(part);
|
|
78
|
+
return record?.type === "text" && typeof record.text === "string"
|
|
79
|
+
? record.text
|
|
80
|
+
: "";
|
|
81
|
+
})
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
function resultText(result) {
|
|
86
|
+
const record = asRecord(result);
|
|
87
|
+
return contentText(record?.content).trim();
|
|
88
|
+
}
|
|
89
|
+
function themeFg(theme, name, text) {
|
|
90
|
+
return theme?.fg ? theme.fg(name, text) : text;
|
|
91
|
+
}
|
|
92
|
+
function themeBold(theme, text) {
|
|
93
|
+
return theme?.bold ? theme.bold(text) : text;
|
|
94
|
+
}
|
|
95
|
+
function truncateLine(text, max = 120) {
|
|
96
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
97
|
+
return singleLine.length > max ? `${singleLine.slice(0, max - 3)}...` : singleLine;
|
|
98
|
+
}
|
|
99
|
+
function formatAgentCall(args, theme) {
|
|
100
|
+
const action = args.action ?? "start";
|
|
101
|
+
const agent = args.name ?? args.id ?? "agent";
|
|
102
|
+
const label = args.label ? ` ${themeFg(theme, "muted", `(${args.label})`)}` : "";
|
|
103
|
+
return `${themeFg(theme, "toolTitle", themeBold(theme, `agent ${action}`))} ${themeFg(theme, "accent", agent)}${label}`;
|
|
104
|
+
}
|
|
105
|
+
// Keep the legacy wire value so resumed transcripts retain their renderer.
|
|
106
|
+
const AGENT_COMPLETIONS_TYPE = "recipe-agent-completions";
|
|
107
|
+
function formatDurationMs(ms) {
|
|
108
|
+
const seconds = Math.round(ms / 1000);
|
|
109
|
+
if (seconds < 60)
|
|
110
|
+
return `${seconds}s`;
|
|
111
|
+
const minutes = Math.floor(seconds / 60);
|
|
112
|
+
const rest = seconds % 60;
|
|
113
|
+
return rest ? `${minutes}m ${rest}s` : `${minutes}m`;
|
|
114
|
+
}
|
|
115
|
+
/** Compact TUI rendering for a completion wake-up notice (pi-subagents style). */
|
|
116
|
+
function formatCompletionMessage(batch, options, theme) {
|
|
117
|
+
const blocks = batch.map((envelope) => {
|
|
118
|
+
const icon = envelope.status === "completed"
|
|
119
|
+
? themeFg(theme, "success", "✓")
|
|
120
|
+
: themeFg(theme, "error", "✗");
|
|
121
|
+
const label = envelope.label
|
|
122
|
+
? ` ${themeFg(theme, "muted", `(${envelope.label})`)}`
|
|
123
|
+
: "";
|
|
124
|
+
const duration = envelope.duration_ms !== undefined
|
|
125
|
+
? ` ${themeFg(theme, "muted", `· ${formatDurationMs(envelope.duration_ms)}`)}`
|
|
126
|
+
: "";
|
|
127
|
+
let text = `${icon} ${themeBold(theme, envelope.agent)} ${themeFg(theme, "muted", envelope.id)}${label} ${themeFg(theme, "muted", envelope.status)}${duration}`;
|
|
128
|
+
const preview = (envelope.status === "failed"
|
|
129
|
+
? envelope.error ?? envelope.output_preview
|
|
130
|
+
: envelope.output_preview)?.trim() ?? "";
|
|
131
|
+
const lines = preview.split("\n").filter((line) => line.trim());
|
|
132
|
+
const shown = options.expanded ? lines : lines.slice(0, 1);
|
|
133
|
+
for (const line of shown.length > 0 ? shown : ["(no output)"]) {
|
|
134
|
+
text += `\n ${themeFg(theme, "muted", `⎿ ${options.expanded ? line : truncateLine(line)}`)}`;
|
|
135
|
+
}
|
|
136
|
+
if (!options.expanded && lines.length > 1) {
|
|
137
|
+
text += `\n ${themeFg(theme, "muted", "⎿ … (expand for full output)")}`;
|
|
138
|
+
}
|
|
139
|
+
return text;
|
|
140
|
+
});
|
|
141
|
+
return blocks.join("\n");
|
|
142
|
+
}
|
|
143
|
+
function snapshotOf(run) {
|
|
144
|
+
return {
|
|
145
|
+
id: run.id,
|
|
146
|
+
agent: run.agent,
|
|
147
|
+
label: run.label,
|
|
148
|
+
prompt: run.prompt,
|
|
149
|
+
status: run.status,
|
|
150
|
+
startedAt: run.startedAt,
|
|
151
|
+
completedAt: run.completedAt,
|
|
152
|
+
output: run.output,
|
|
153
|
+
error: run.error,
|
|
154
|
+
toolCalls: run.toolCalls.map((call) => ({ ...call })),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function controllerSummary(run, status = run.status) {
|
|
158
|
+
const startedAt = Date.parse(run.startedAt);
|
|
159
|
+
const completedAt = run.completedAt ? Date.parse(run.completedAt) : undefined;
|
|
160
|
+
const currentTool = [...run.toolCalls]
|
|
161
|
+
.reverse()
|
|
162
|
+
.find((call) => call.status === "running")?.name;
|
|
163
|
+
return {
|
|
164
|
+
agent_run_id: run.id,
|
|
165
|
+
invocation_name: run.agent,
|
|
166
|
+
agent_name: run.agent,
|
|
167
|
+
label: run.label ?? run.agent,
|
|
168
|
+
prompt: run.prompt,
|
|
169
|
+
status,
|
|
170
|
+
started_at: Number.isFinite(startedAt) ? startedAt : Date.now(),
|
|
171
|
+
...(completedAt !== undefined && Number.isFinite(completedAt)
|
|
172
|
+
? { completed_at: completedAt }
|
|
173
|
+
: {}),
|
|
174
|
+
last_activity_at: completedAt !== undefined && Number.isFinite(completedAt)
|
|
175
|
+
? completedAt
|
|
176
|
+
: Number.isFinite(startedAt)
|
|
177
|
+
? startedAt
|
|
178
|
+
: Date.now(),
|
|
179
|
+
...(currentTool ? { current_tool: currentTool } : {}),
|
|
180
|
+
nested_tools: run.toolCalls.map((call) => ({
|
|
181
|
+
toolName: call.name,
|
|
182
|
+
verb: call.name,
|
|
183
|
+
detail: call.output ?? call.error ?? "",
|
|
184
|
+
...(asRecord(call.args) ? { toolInput: asRecord(call.args) } : {}),
|
|
185
|
+
})),
|
|
186
|
+
output_preview: run.output,
|
|
187
|
+
output: run.output,
|
|
188
|
+
error: run.error,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function nameList(names) {
|
|
192
|
+
return names.length > 0 ? names.join(", ") : "(none)";
|
|
193
|
+
}
|
|
194
|
+
function bulletList(items) {
|
|
195
|
+
return items.length > 0 ? items.map((item) => ` - ${item}`) : [" - none"];
|
|
196
|
+
}
|
|
197
|
+
function activeRecipeTools(state, activeTools) {
|
|
198
|
+
const active = new Set(activeTools);
|
|
199
|
+
const recipeTools = new Set(state.resolved.tools);
|
|
200
|
+
return [...recipeTools]
|
|
201
|
+
.filter((tool) => active.has(tool))
|
|
202
|
+
.sort();
|
|
203
|
+
}
|
|
204
|
+
function recipeSummary(state, activeTools) {
|
|
205
|
+
const subagents = visibleSubagents(state).map((agent) => agent.name);
|
|
206
|
+
return [
|
|
207
|
+
"Active Recipe",
|
|
208
|
+
`Name: ${state.resolved.manifest.name}@${state.resolved.manifest.version}`,
|
|
209
|
+
state.resolved.manifest.description ? `Description: ${state.resolved.manifest.description}` : undefined,
|
|
210
|
+
`Agent: ${state.resolved.agentName}`,
|
|
211
|
+
`Model: ${state.resolved.agent.model?.name ?? "(session default)"}`,
|
|
212
|
+
`Thinking level: ${state.resolved.agent.model?.thinkingLevel ?? "(session default)"}`,
|
|
213
|
+
`Subagents: ${nameList(subagents)}`,
|
|
214
|
+
"",
|
|
215
|
+
"Active recipe tools:",
|
|
216
|
+
...bulletList(activeRecipeTools(state, activeTools)),
|
|
217
|
+
"",
|
|
218
|
+
`Directory: ${state.resolved.recipeDir}`,
|
|
219
|
+
`Workspace: ${state.cwd}`,
|
|
220
|
+
].filter((line) => line !== undefined).join("\n");
|
|
221
|
+
}
|
|
222
|
+
function applyChildToolEvent(run, event) {
|
|
223
|
+
const existing = run.toolCalls.find((call) => call.id === event.id);
|
|
224
|
+
if (event.type === "start") {
|
|
225
|
+
if (existing) {
|
|
226
|
+
existing.name = event.name;
|
|
227
|
+
existing.args = event.args;
|
|
228
|
+
existing.status = "running";
|
|
229
|
+
existing.completedAt = undefined;
|
|
230
|
+
existing.output = undefined;
|
|
231
|
+
existing.error = undefined;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
run.toolCalls.push({
|
|
235
|
+
id: event.id,
|
|
236
|
+
name: event.name,
|
|
237
|
+
args: event.args,
|
|
238
|
+
status: "running",
|
|
239
|
+
startedAt: new Date().toISOString(),
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const call = existing ??
|
|
244
|
+
(() => {
|
|
245
|
+
const created = {
|
|
246
|
+
id: event.id,
|
|
247
|
+
name: event.name,
|
|
248
|
+
args: event.args,
|
|
249
|
+
status: "running",
|
|
250
|
+
startedAt: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
run.toolCalls.push(created);
|
|
253
|
+
return created;
|
|
254
|
+
})();
|
|
255
|
+
call.name = event.name;
|
|
256
|
+
call.args = event.args;
|
|
257
|
+
if (event.type === "update") {
|
|
258
|
+
const text = resultText(event.partialResult);
|
|
259
|
+
if (text)
|
|
260
|
+
call.output = text;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
call.completedAt = new Date().toISOString();
|
|
264
|
+
call.status = event.isError ? "failed" : "completed";
|
|
265
|
+
const text = resultText(event.result);
|
|
266
|
+
if (text)
|
|
267
|
+
call.output = text;
|
|
268
|
+
if (event.isError)
|
|
269
|
+
call.error = text || "Tool failed";
|
|
270
|
+
}
|
|
271
|
+
export function createRecipesExtension(opts = {}) {
|
|
272
|
+
const env = opts.env ?? process.env;
|
|
273
|
+
const createChildAgentRunner = opts.createChildAgentRunner ?? createRecipeChildAgentRunner;
|
|
274
|
+
let state = null;
|
|
275
|
+
let lastLaunchErrorKey = null;
|
|
276
|
+
let childRunIndex = 0;
|
|
277
|
+
const childRuns = new Map();
|
|
278
|
+
// Runs restored from a previous Pi process via rehydrateChildRuns(). They
|
|
279
|
+
// have no live runner: readable (status/wait) but not controllable.
|
|
280
|
+
const archivedRuns = new Map();
|
|
281
|
+
let runStore = null;
|
|
282
|
+
// Background completions pending delivery to the parent model.
|
|
283
|
+
const completions = new ChildCompletionQueue();
|
|
284
|
+
// Latest extension context, for the idle check gating completion delivery.
|
|
285
|
+
let sessionCtx = null;
|
|
286
|
+
let localAgentContext = null;
|
|
287
|
+
let sessionConfigurationError = null;
|
|
288
|
+
const visibleAgentDefinitions = new Map();
|
|
289
|
+
function storeFor(cwd) {
|
|
290
|
+
if (runStore?.cwd !== cwd) {
|
|
291
|
+
runStore = { cwd, store: new ChildAgentRunStore(cwd) };
|
|
292
|
+
}
|
|
293
|
+
return runStore.store;
|
|
294
|
+
}
|
|
295
|
+
async function persistRun(cwd, run) {
|
|
296
|
+
try {
|
|
297
|
+
await storeFor(cwd).writeStatus(snapshotOf(run));
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// persistence is best-effort; the live run stays authoritative
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function nextChildRunId() {
|
|
304
|
+
return `agent-run-${++childRunIndex}`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Restore run snapshots persisted under `.pi/agents/` by a previous Pi
|
|
308
|
+
* process, so run ids referenced in a resumed conversation stay resolvable.
|
|
309
|
+
* A run persisted as `running` died with the old process — it is flipped to
|
|
310
|
+
* `interrupted` (never silently "resumed"). Returns the number restored.
|
|
311
|
+
*/
|
|
312
|
+
async function rehydrateChildRuns(cwd) {
|
|
313
|
+
const store = storeFor(cwd);
|
|
314
|
+
const persisted = await store.readPersistedSnapshots();
|
|
315
|
+
let restored = 0;
|
|
316
|
+
for (const snapshot of persisted) {
|
|
317
|
+
const indexMatch = /^(?:agent-run|recipe-agent)-(\d+)$/.exec(snapshot.id);
|
|
318
|
+
if (indexMatch) {
|
|
319
|
+
childRunIndex = Math.max(childRunIndex, Number(indexMatch[1]));
|
|
320
|
+
}
|
|
321
|
+
if (childRuns.has(snapshot.id) || archivedRuns.has(snapshot.id)) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (snapshot.status === "running") {
|
|
325
|
+
snapshot.status = "interrupted";
|
|
326
|
+
snapshot.completedAt = snapshot.completedAt ?? new Date().toISOString();
|
|
327
|
+
snapshot.error = "Pi session restarted while the run was in flight";
|
|
328
|
+
await persistRun(cwd, snapshot);
|
|
329
|
+
}
|
|
330
|
+
archivedRuns.set(snapshot.id, snapshot);
|
|
331
|
+
restored += 1;
|
|
332
|
+
}
|
|
333
|
+
return restored;
|
|
334
|
+
}
|
|
335
|
+
function findRunSnapshot(id) {
|
|
336
|
+
return childRuns.get(id) ?? archivedRuns.get(id);
|
|
337
|
+
}
|
|
338
|
+
const localRunController = {
|
|
339
|
+
list() {
|
|
340
|
+
return [
|
|
341
|
+
...childRuns.values(),
|
|
342
|
+
...[...archivedRuns.values()].filter((snapshot) => !childRuns.has(snapshot.id)),
|
|
343
|
+
].map((run) => controllerSummary(run));
|
|
344
|
+
},
|
|
345
|
+
get(id) {
|
|
346
|
+
const run = findRunSnapshot(id);
|
|
347
|
+
return run ? controllerSummary(run) : null;
|
|
348
|
+
},
|
|
349
|
+
async start(input) {
|
|
350
|
+
if (!state || !localAgentContext) {
|
|
351
|
+
throw new Error("No recipe session is active");
|
|
352
|
+
}
|
|
353
|
+
const run = await runChildAgent(state, input.name, input.prompt, input.label, localAgentContext, input.onUpdate);
|
|
354
|
+
return controllerSummary(run);
|
|
355
|
+
},
|
|
356
|
+
async wait(id, signal) {
|
|
357
|
+
const run = findRunSnapshot(id);
|
|
358
|
+
if (!run)
|
|
359
|
+
throw new Error(`Unknown agent run: ${id}`);
|
|
360
|
+
if (run.status !== "running" || !childRuns.has(id)) {
|
|
361
|
+
return controllerSummary(run);
|
|
362
|
+
}
|
|
363
|
+
await waitForRun(childRuns.get(id), signal);
|
|
364
|
+
return controllerSummary(findRunSnapshot(id));
|
|
365
|
+
},
|
|
366
|
+
async message(id, message) {
|
|
367
|
+
if (archivedRuns.has(id) && !childRuns.has(id)) {
|
|
368
|
+
throw new Error(`Agent run ${id} belongs to a previous Pi session and cannot be controlled`);
|
|
369
|
+
}
|
|
370
|
+
const run = childRuns.get(id);
|
|
371
|
+
if (!run)
|
|
372
|
+
throw new Error(`Unknown agent run: ${id}`);
|
|
373
|
+
if (run.status === "running") {
|
|
374
|
+
await run.runner.steer(message);
|
|
375
|
+
return controllerSummary(run);
|
|
376
|
+
}
|
|
377
|
+
run.status = "running";
|
|
378
|
+
run.completedAt = undefined;
|
|
379
|
+
run.error = undefined;
|
|
380
|
+
run.output = undefined;
|
|
381
|
+
run.promise = executeChildPrompt(run, message, state?.cwd ?? process.cwd());
|
|
382
|
+
void persistRun(state?.cwd ?? process.cwd(), run);
|
|
383
|
+
return controllerSummary(run);
|
|
384
|
+
},
|
|
385
|
+
async interrupt(id) {
|
|
386
|
+
if (archivedRuns.has(id) && !childRuns.has(id)) {
|
|
387
|
+
throw new Error(`Agent run ${id} belongs to a previous Pi session and cannot be controlled`);
|
|
388
|
+
}
|
|
389
|
+
const run = childRuns.get(id);
|
|
390
|
+
if (!run)
|
|
391
|
+
throw new Error(`Unknown agent run: ${id}`);
|
|
392
|
+
if (run.status === "running") {
|
|
393
|
+
run.status = "interrupted";
|
|
394
|
+
run.completedAt = run.completedAt ?? new Date().toISOString();
|
|
395
|
+
await run.runner.cancel();
|
|
396
|
+
if (state)
|
|
397
|
+
void persistRun(state.cwd, run);
|
|
398
|
+
}
|
|
399
|
+
return controllerSummary(run);
|
|
400
|
+
},
|
|
401
|
+
async close(id) {
|
|
402
|
+
if (archivedRuns.has(id) && !childRuns.has(id)) {
|
|
403
|
+
throw new Error(`Agent run ${id} belongs to a previous Pi session and cannot be controlled`);
|
|
404
|
+
}
|
|
405
|
+
const run = childRuns.get(id);
|
|
406
|
+
if (!run)
|
|
407
|
+
throw new Error(`Unknown agent run: ${id}`);
|
|
408
|
+
if (run.status === "running") {
|
|
409
|
+
run.status = "interrupted";
|
|
410
|
+
await run.runner.cancel();
|
|
411
|
+
await run.promise;
|
|
412
|
+
}
|
|
413
|
+
await run.runner.shutdown();
|
|
414
|
+
childRuns.delete(id);
|
|
415
|
+
return controllerSummary(run, "closed");
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
async function closeAllChildRuns() {
|
|
419
|
+
await Promise.all([...childRuns.keys()].map((id) => localRunController.close(id)));
|
|
420
|
+
}
|
|
421
|
+
function archivedControlError(id) {
|
|
422
|
+
return {
|
|
423
|
+
...textResult(`Agent run ${id} belongs to a previous Pi session and cannot be controlled; only status and wait are available.`, { id }),
|
|
424
|
+
isError: true,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Launch selection is immutable for the lifetime of this extension. Resolved
|
|
428
|
+
// values are exported below for shell tools, but must not become inputs to a
|
|
429
|
+
// later session load in the same process.
|
|
430
|
+
const launchRecipeDir = stringFlag(env.PI_RECIPE_DIR);
|
|
431
|
+
const launchAgentName = stringFlag(env.PI_AGENT_NAME);
|
|
432
|
+
function recipeFlag(pi) {
|
|
433
|
+
return stringFlag(pi.getFlag("recipe")) ?? launchRecipeDir;
|
|
434
|
+
}
|
|
435
|
+
function selectedAgentName(pi) {
|
|
436
|
+
return stringFlag(pi.getFlag("agent")) ?? launchAgentName;
|
|
437
|
+
}
|
|
438
|
+
function loadState(pi, cwd) {
|
|
439
|
+
const flag = recipeFlag(pi);
|
|
440
|
+
if (!flag)
|
|
441
|
+
return null;
|
|
442
|
+
const recipeDir = resolve(cwd, flag);
|
|
443
|
+
if (!existsSync(recipeDir)) {
|
|
444
|
+
throw new RecipeLaunchError(recipeNotFoundMessage(flag, recipeDir));
|
|
445
|
+
}
|
|
446
|
+
const requestedAgentName = selectedAgentName(pi);
|
|
447
|
+
const key = [cwd, recipeDir, requestedAgentName ?? ""].join("\0");
|
|
448
|
+
if (state?.key === key)
|
|
449
|
+
return state;
|
|
450
|
+
let resolved;
|
|
451
|
+
try {
|
|
452
|
+
resolved = resolveRecipe({
|
|
453
|
+
recipeDir,
|
|
454
|
+
agentName: requestedAgentName,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
throw new RecipeLaunchError(recipeLoadErrorMessage(flag, err instanceof Error ? err.message : String(err)));
|
|
459
|
+
}
|
|
460
|
+
// Keep the recipe selected by CLI flags visible to shell commands and
|
|
461
|
+
// recipe-authored instructions. In production `env` is process.env, so
|
|
462
|
+
// built-in shell tools and child agents inherit these resolved values.
|
|
463
|
+
env.PI_RECIPE_DIR = recipeDir;
|
|
464
|
+
env.PI_AGENT_NAME = resolved.agentName;
|
|
465
|
+
state = {
|
|
466
|
+
key,
|
|
467
|
+
cwd,
|
|
468
|
+
resolved,
|
|
469
|
+
extensionsLoaded: false,
|
|
470
|
+
configured: false,
|
|
471
|
+
};
|
|
472
|
+
return state;
|
|
473
|
+
}
|
|
474
|
+
function safeLoadState(pi, cwd, ctx) {
|
|
475
|
+
try {
|
|
476
|
+
return loadState(pi, cwd);
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
if (!(err instanceof RecipeLaunchError))
|
|
480
|
+
throw err;
|
|
481
|
+
const key = [cwd, recipeFlag(pi) ?? "", err.message].join("\0");
|
|
482
|
+
if (ctx && lastLaunchErrorKey !== key) {
|
|
483
|
+
ctx.ui.notify(err.message, "warning");
|
|
484
|
+
lastLaunchErrorKey = key;
|
|
485
|
+
}
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
async function loadRecipeExtensions(pi, ctx, launchState) {
|
|
490
|
+
if (launchState.extensionsLoaded)
|
|
491
|
+
return;
|
|
492
|
+
let loadedCount = 0;
|
|
493
|
+
for (const extensionPath of launchState.resolved.extensionPaths) {
|
|
494
|
+
try {
|
|
495
|
+
const factory = await loadRecipeExtensionFactory(launchState.resolved.recipeDir, extensionPath);
|
|
496
|
+
await factory(pi);
|
|
497
|
+
loadedCount += 1;
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
501
|
+
ctx.ui.notify(`Recipe extension failed to load: ${extensionPath}\n${message}`, "warning");
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
launchState.extensionsLoaded = true;
|
|
505
|
+
if (launchState.resolved.extensionPaths.length > 0) {
|
|
506
|
+
ctx.ui.notify(`Recipe extensions: ${loadedCount}/${launchState.resolved.extensionPaths.length} loaded`, "info");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
async function configureSession(pi, ctx, launchState) {
|
|
510
|
+
if (launchState.configured)
|
|
511
|
+
return;
|
|
512
|
+
const labelParts = [
|
|
513
|
+
`${launchState.resolved.manifest.name}@${launchState.resolved.manifest.version}`,
|
|
514
|
+
`agent:${launchState.resolved.agentName}`,
|
|
515
|
+
].filter(Boolean);
|
|
516
|
+
pi.setSessionName(labelParts.join(" "));
|
|
517
|
+
const { provider, model } = modelParts(launchState.resolved.modelSpec);
|
|
518
|
+
const lookupProvider = provider === "gemini" ? "google" : provider;
|
|
519
|
+
const resolvedModel = ctx.modelRegistry.find(lookupProvider, model);
|
|
520
|
+
if (!resolvedModel) {
|
|
521
|
+
throw new Error(`Recipe model is not available: ${launchState.resolved.modelSpec}`);
|
|
522
|
+
}
|
|
523
|
+
applyRecipeAgentModelConfigToModel(resolvedModel, launchState.resolved.modelConfig);
|
|
524
|
+
const ok = await pi.setModel(resolvedModel);
|
|
525
|
+
if (!ok) {
|
|
526
|
+
throw new Error(`Recipe model has no configured API key: ${launchState.resolved.modelSpec}`);
|
|
527
|
+
}
|
|
528
|
+
if (launchState.resolved.thinkingLevel) {
|
|
529
|
+
pi.setThinkingLevel(launchState.resolved.thinkingLevel);
|
|
530
|
+
}
|
|
531
|
+
const activeTools = new Set(launchState.resolved.tools);
|
|
532
|
+
pi.setActiveTools([...activeTools]);
|
|
533
|
+
launchState.configured = true;
|
|
534
|
+
sessionConfigurationError = null;
|
|
535
|
+
}
|
|
536
|
+
async function configureMcp(launchState, ctx) {
|
|
537
|
+
const mcpSelections = scopedMcpSelections(launchState);
|
|
538
|
+
if (mcpSelections.length === 0) {
|
|
539
|
+
await clearMcpSession(env, launchState.cwd);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
configureMcpLocalConfigPath({
|
|
543
|
+
cwd: launchState.cwd,
|
|
544
|
+
recipeDir: launchState.resolved.recipeDir,
|
|
545
|
+
env,
|
|
546
|
+
});
|
|
547
|
+
const [, session] = await Promise.all([
|
|
548
|
+
materializeSessionMcpCli({
|
|
549
|
+
cwd: launchState.cwd,
|
|
550
|
+
env,
|
|
551
|
+
}),
|
|
552
|
+
materializeMcpSession({
|
|
553
|
+
cwd: launchState.cwd,
|
|
554
|
+
manifest: launchState.resolved.manifest,
|
|
555
|
+
agentMcp: mcpSelections,
|
|
556
|
+
env,
|
|
557
|
+
}),
|
|
558
|
+
]);
|
|
559
|
+
if (session.servers.length > 0) {
|
|
560
|
+
void preloadMcpCatalogs({ env }).catch((error) => {
|
|
561
|
+
console.warn(`Recipe MCP catalog preload failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
562
|
+
});
|
|
563
|
+
ctx.ui.notify(`Recipe MCP: ${session.servers.length} server(s) configured; runtime warming in background`, "info");
|
|
564
|
+
const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
|
|
565
|
+
if (detail) {
|
|
566
|
+
ctx.ui.notify([
|
|
567
|
+
"Recipe MCP: some configured servers or tools were filtered.",
|
|
568
|
+
"",
|
|
569
|
+
detail,
|
|
570
|
+
].join("\n"), "warning");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
const detail = formatMcpConfigurationDiagnostics(session.diagnostics ?? []);
|
|
575
|
+
ctx.ui.notify([
|
|
576
|
+
"Recipe MCP: no servers are available to this agent. Check package policy and .pi/mcp.local.json.",
|
|
577
|
+
...(detail ? ["", detail] : []),
|
|
578
|
+
].join("\n"), "warning");
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function runChildAgent(launchState, agentName, prompt, label, ctx, onUpdate) {
|
|
582
|
+
const id = nextChildRunId();
|
|
583
|
+
let run;
|
|
584
|
+
const notifyUpdate = () => {
|
|
585
|
+
if (!run || !onUpdate)
|
|
586
|
+
return;
|
|
587
|
+
try {
|
|
588
|
+
void Promise.resolve(onUpdate(controllerSummary(run))).catch(() => { });
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// Detached runs can outlive the parent tool call. Late UI updates are
|
|
592
|
+
// cosmetic and must not stop the child or completion delivery.
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
const runner = createChildAgentRunner({
|
|
596
|
+
recipeDir: launchState.resolved.recipeDir,
|
|
597
|
+
workspaceDir: launchState.cwd,
|
|
598
|
+
env,
|
|
599
|
+
agentName,
|
|
600
|
+
modelRegistry: ctx.modelRegistry,
|
|
601
|
+
onAssistantMessage(text, stream) {
|
|
602
|
+
if (!run)
|
|
603
|
+
return;
|
|
604
|
+
if (stream === "delta") {
|
|
605
|
+
run.output = `${run.output ?? ""}${text}`;
|
|
606
|
+
}
|
|
607
|
+
else if (!run.output?.trim()) {
|
|
608
|
+
run.output = text;
|
|
609
|
+
}
|
|
610
|
+
notifyUpdate();
|
|
611
|
+
},
|
|
612
|
+
onToolEvent(event) {
|
|
613
|
+
if (!run)
|
|
614
|
+
return;
|
|
615
|
+
applyChildToolEvent(run, event);
|
|
616
|
+
notifyUpdate();
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
run = {
|
|
620
|
+
id,
|
|
621
|
+
agent: agentName,
|
|
622
|
+
label,
|
|
623
|
+
prompt,
|
|
624
|
+
status: "running",
|
|
625
|
+
startedAt: new Date().toISOString(),
|
|
626
|
+
toolCalls: [],
|
|
627
|
+
runner,
|
|
628
|
+
promise: Promise.resolve(undefined),
|
|
629
|
+
notifyUpdate,
|
|
630
|
+
};
|
|
631
|
+
notifyUpdate();
|
|
632
|
+
void persistRun(launchState.cwd, run);
|
|
633
|
+
run.promise = executeChildPrompt(run, prompt, launchState.cwd);
|
|
634
|
+
childRuns.set(id, run);
|
|
635
|
+
return run;
|
|
636
|
+
}
|
|
637
|
+
function executeChildPrompt(run, prompt, cwd) {
|
|
638
|
+
return (async () => {
|
|
639
|
+
try {
|
|
640
|
+
await run.runner.start();
|
|
641
|
+
const result = await run.runner.prompt(prompt);
|
|
642
|
+
const finalOutput = promptResultText(result);
|
|
643
|
+
if (finalOutput && finalOutput.length >= (run.output?.length ?? 0)) {
|
|
644
|
+
run.output = finalOutput;
|
|
645
|
+
}
|
|
646
|
+
else if (!run.output?.trim()) {
|
|
647
|
+
run.output = "(no final response)";
|
|
648
|
+
}
|
|
649
|
+
if (run.status === "running")
|
|
650
|
+
run.status = "completed";
|
|
651
|
+
}
|
|
652
|
+
catch (err) {
|
|
653
|
+
if (run.status !== "interrupted")
|
|
654
|
+
run.status = "failed";
|
|
655
|
+
run.error = err instanceof Error ? err.message : String(err);
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
run.completedAt = new Date().toISOString();
|
|
659
|
+
// Queue the parent wake-up BEFORE persisting. The deliverer reads the
|
|
660
|
+
// in-memory queue (not disk), so enqueuing first makes a persisted
|
|
661
|
+
// status.json imply the completion is already queued — closing a race
|
|
662
|
+
// where the agent_end poke could observe the run's status.json but hit
|
|
663
|
+
// an empty queue, deferring delivery to the full batch window. It also
|
|
664
|
+
// still notifies if the disk write fails. Wait and terminal status
|
|
665
|
+
// reads acknowledge it back out.
|
|
666
|
+
const envelope = envelopeFromRun(run);
|
|
667
|
+
if (envelope)
|
|
668
|
+
completions.enqueue(envelope);
|
|
669
|
+
run.notifyUpdate();
|
|
670
|
+
await persistRun(cwd, run);
|
|
671
|
+
}
|
|
672
|
+
return run;
|
|
673
|
+
})();
|
|
674
|
+
}
|
|
675
|
+
async function waitForRun(run, signal) {
|
|
676
|
+
if (!signal) {
|
|
677
|
+
await run.promise;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (signal.aborted)
|
|
681
|
+
return;
|
|
682
|
+
let onAbort = () => { };
|
|
683
|
+
const aborted = new Promise((resolve) => {
|
|
684
|
+
onAbort = resolve;
|
|
685
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
686
|
+
});
|
|
687
|
+
await Promise.race([run.promise, aborted]);
|
|
688
|
+
signal.removeEventListener("abort", onAbort);
|
|
689
|
+
}
|
|
690
|
+
return (pi) => {
|
|
691
|
+
// Deliver queued background completions by waking the parent model with
|
|
692
|
+
// a triggerTurn message. Delivery only happens when the session is
|
|
693
|
+
// genuinely idle: a message queued while the parent turn is streaming
|
|
694
|
+
// (or tearing down at the agent_end boundary) would be stranded in pi's
|
|
695
|
+
// queues, and holding until idle also gives synchronous readers
|
|
696
|
+
// (wait or terminal status) time to acknowledge results
|
|
697
|
+
// the model already saw. While mid-turn, retry on a short timer armed by
|
|
698
|
+
// the agent_end poke.
|
|
699
|
+
let deliveryRetryTimer = null;
|
|
700
|
+
const deliverCompletions = () => {
|
|
701
|
+
if (deliveryRetryTimer) {
|
|
702
|
+
clearTimeout(deliveryRetryTimer);
|
|
703
|
+
deliveryRetryTimer = null;
|
|
704
|
+
}
|
|
705
|
+
if (!completions.hasPending())
|
|
706
|
+
return;
|
|
707
|
+
// The retry timer can outlive this extension instance across a reload,
|
|
708
|
+
// where pi API calls throw as stale. Never crash the timer callback;
|
|
709
|
+
// the reload rebuilds run state and drops the batch with it.
|
|
710
|
+
try {
|
|
711
|
+
if (!(sessionCtx?.isIdle?.() ?? true)) {
|
|
712
|
+
deliveryRetryTimer = setTimeout(deliverCompletions, COMPLETION_DELIVERY_RETRY_MS);
|
|
713
|
+
deliveryRetryTimer.unref?.();
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const batch = completions.consumeBatch();
|
|
717
|
+
if (batch.length === 0)
|
|
718
|
+
return;
|
|
719
|
+
pi.sendMessage({
|
|
720
|
+
customType: AGENT_COMPLETIONS_TYPE,
|
|
721
|
+
content: renderCompletionNotice(batch),
|
|
722
|
+
display: true,
|
|
723
|
+
details: { completions: batch },
|
|
724
|
+
},
|
|
725
|
+
// followUp keeps a wake race-safe: if a user turn started between
|
|
726
|
+
// the idle check and here, the notice queues behind it.
|
|
727
|
+
{ triggerTurn: true, deliverAs: "followUp" });
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
// swallow — see above
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
completions.setDeliverer(deliverCompletions);
|
|
734
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
735
|
+
sessionCtx = ctx;
|
|
736
|
+
completions.poke();
|
|
737
|
+
});
|
|
738
|
+
pi.registerMessageRenderer(AGENT_COMPLETIONS_TYPE, (message, options, theme) => {
|
|
739
|
+
const batch = message.details
|
|
740
|
+
?.completions;
|
|
741
|
+
if (!batch?.length)
|
|
742
|
+
return undefined;
|
|
743
|
+
return new Text(formatCompletionMessage(batch, options, theme), 0, 0);
|
|
744
|
+
});
|
|
745
|
+
pi.registerFlag("recipe", {
|
|
746
|
+
description: "Recipe folder, installed recipe name, or installed recipe source to use for this Pi session",
|
|
747
|
+
type: "string",
|
|
748
|
+
});
|
|
749
|
+
pi.registerFlag("agent", {
|
|
750
|
+
description: "Agent to use",
|
|
751
|
+
type: "string",
|
|
752
|
+
});
|
|
753
|
+
pi.registerCommand("recipe", {
|
|
754
|
+
description: "Inspect or reload the active recipe",
|
|
755
|
+
handler: async (args, ctx) => {
|
|
756
|
+
const action = args.trim();
|
|
757
|
+
if (action === "reload") {
|
|
758
|
+
const launchState = safeLoadState(pi, ctx.cwd, ctx);
|
|
759
|
+
if (!launchState) {
|
|
760
|
+
if (!recipeFlag(pi)) {
|
|
761
|
+
ctx.ui.notify("No recipe is active. Launch Pi with --recipe <recipe>.", "info");
|
|
762
|
+
}
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
await closeAllChildRuns();
|
|
766
|
+
state = null;
|
|
767
|
+
archivedRuns.clear();
|
|
768
|
+
completions.clear();
|
|
769
|
+
await ctx.waitForIdle();
|
|
770
|
+
await ctx.reload();
|
|
771
|
+
ctx.ui.notify(`Recipe reload requested: ${launchState.resolved.manifest.name}@${launchState.resolved.manifest.version}`, "info");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (action) {
|
|
775
|
+
ctx.ui.notify("Usage: /recipe [reload]", "info");
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const launchState = safeLoadState(pi, ctx.cwd, ctx);
|
|
779
|
+
if (!launchState) {
|
|
780
|
+
if (!recipeFlag(pi)) {
|
|
781
|
+
ctx.ui.notify("No recipe is active. Launch Pi with --recipe <recipe>.", "info");
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
ctx.ui.notify(recipeSummary(launchState, pi.getActiveTools()), "info");
|
|
786
|
+
},
|
|
787
|
+
});
|
|
788
|
+
const agentTool = createAgentTool(localRunController, visibleAgentDefinitions, {
|
|
789
|
+
acknowledgeCompletions: (ids) => completions.acknowledge(ids),
|
|
790
|
+
});
|
|
791
|
+
agentTool.label = "Agent";
|
|
792
|
+
agentTool.renderCall = (params, theme, context) => {
|
|
793
|
+
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
794
|
+
text.setText(formatAgentCall(params, theme));
|
|
795
|
+
return text;
|
|
796
|
+
};
|
|
797
|
+
pi.registerTool(agentTool);
|
|
798
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
799
|
+
sessionCtx = ctx;
|
|
800
|
+
localAgentContext = ctx;
|
|
801
|
+
const launchState = safeLoadState(pi, ctx.cwd, ctx);
|
|
802
|
+
if (!launchState)
|
|
803
|
+
return;
|
|
804
|
+
visibleAgentDefinitions.clear();
|
|
805
|
+
for (const [name, definition] of launchState.resolved.subagents) {
|
|
806
|
+
visibleAgentDefinitions.set(name, definition);
|
|
807
|
+
}
|
|
808
|
+
await loadRecipeExtensions(pi, ctx, launchState);
|
|
809
|
+
try {
|
|
810
|
+
await configureMcp(launchState, ctx);
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
ctx.ui.notify(`Recipe MCP failed to configure: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
try {
|
|
817
|
+
await configureSession(pi, ctx, launchState);
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
821
|
+
// A recipe's declared model is part of its behavior contract. Continuing
|
|
822
|
+
// with Pi's previously active model can produce plausible but invalid
|
|
823
|
+
// results, so stop the session instead of silently falling back.
|
|
824
|
+
sessionConfigurationError = message;
|
|
825
|
+
pi.setActiveTools([]);
|
|
826
|
+
ctx.ui.notify(`Recipe session cannot start: ${message}`, "warning");
|
|
827
|
+
if (ctx.mode === "json" || ctx.mode === "print") {
|
|
828
|
+
process.exitCode = 1;
|
|
829
|
+
}
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// Restore run snapshots persisted by a previous Pi process so run ids
|
|
833
|
+
// referenced in a resumed conversation stay resolvable (read-only).
|
|
834
|
+
try {
|
|
835
|
+
const restored = await rehydrateChildRuns(launchState.cwd);
|
|
836
|
+
if (restored > 0) {
|
|
837
|
+
ctx.ui.notify(`Agents: rehydrated ${restored} previous run(s) (read-only)`, "info");
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// rehydration is best-effort
|
|
842
|
+
}
|
|
843
|
+
ctx.ui.notify(`Recipe: ${launchState.resolved.manifest.name}@${launchState.resolved.manifest.version} (${basename(launchState.resolved.recipeDir)})`, "info");
|
|
844
|
+
});
|
|
845
|
+
pi.on("session_shutdown", async () => {
|
|
846
|
+
await closeAllChildRuns();
|
|
847
|
+
clearMcpCatalogPreload(env);
|
|
848
|
+
await stopMcpDaemon(env);
|
|
849
|
+
});
|
|
850
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
851
|
+
if (!sessionConfigurationError)
|
|
852
|
+
return;
|
|
853
|
+
// setModel(false) leaves Pi's previously active model selected. Abort as
|
|
854
|
+
// soon as the agent loop starts so that model can never receive a recipe
|
|
855
|
+
// prompt after recipe configuration failed.
|
|
856
|
+
ctx.abort();
|
|
857
|
+
});
|
|
858
|
+
pi.on("resources_discover", (event) => {
|
|
859
|
+
const launchState = safeLoadState(pi, event.cwd);
|
|
860
|
+
if (!launchState)
|
|
861
|
+
return {};
|
|
862
|
+
return {
|
|
863
|
+
skillPaths: launchState.resolved.skillPaths,
|
|
864
|
+
promptPaths: launchState.resolved.promptPaths,
|
|
865
|
+
};
|
|
866
|
+
});
|
|
867
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
868
|
+
const launchState = safeLoadState(pi, ctx.cwd, ctx);
|
|
869
|
+
if (!launchState)
|
|
870
|
+
return {};
|
|
871
|
+
return {
|
|
872
|
+
systemPrompt: launchState.resolved.systemPromptOverride(event.systemPrompt),
|
|
873
|
+
};
|
|
874
|
+
});
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/** @deprecated Use `createRecipesExtension`. */
|
|
878
|
+
export const createPiRecipesExtension = createRecipesExtension;
|
|
879
|
+
export default createRecipesExtension();
|
|
880
|
+
//# sourceMappingURL=pi-extension.js.map
|