@osolmaz/pi-workflows 0.12.0 → 0.12.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.
Files changed (119) hide show
  1. package/README.md +4 -3
  2. package/dist/builtins/autoimplement.workflow.d.ts +485 -126
  3. package/dist/builtins/autoimplement.workflow.js +17 -105
  4. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  5. package/dist/builtins/catalog.js +4 -4
  6. package/dist/builtins/index.d.ts +2 -1
  7. package/dist/builtins/index.js +2 -1
  8. package/dist/builtins/index.js.map +1 -1
  9. package/dist/builtins/monitor.workflow.d.ts +2 -4
  10. package/dist/builtins/monitor.workflow.js +26 -128
  11. package/dist/builtins/monitor.workflow.js.map +1 -1
  12. package/dist/builtins/pi-agent-group.d.ts +72 -0
  13. package/dist/builtins/pi-agent-group.js +1087 -0
  14. package/dist/builtins/pi-agent-group.js.map +1 -0
  15. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  16. package/dist/builtins/plan-approval.workflow.js +92 -14
  17. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  18. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  19. package/dist/builtins/plan-change.workflow.js +256 -0
  20. package/dist/builtins/plan-change.workflow.js.map +1 -0
  21. package/dist/builtins/plan-presentation.js +2 -2
  22. package/dist/builtins/plan-presentation.js.map +1 -1
  23. package/dist/builtins/sanity-check.workflow.d.ts +5 -3
  24. package/dist/builtins/sanity-check.workflow.js +105 -21
  25. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  26. package/dist/extension/decision-channels.d.ts +2 -2
  27. package/dist/extension/decision-channels.js +22 -28
  28. package/dist/extension/decision-channels.js.map +1 -1
  29. package/dist/extension/index.js +62 -33
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/session-events.d.ts +2 -2
  32. package/dist/extension/widget.js +23 -3
  33. package/dist/extension/widget.js.map +1 -1
  34. package/dist/render/graph-render.js +1 -2
  35. package/dist/render/graph-render.js.map +1 -1
  36. package/dist/viewer/render.js +7 -6
  37. package/dist/viewer/render.js.map +1 -1
  38. package/dist/workflows/catalog.js +7 -2
  39. package/dist/workflows/catalog.js.map +1 -1
  40. package/dist/workflows/composition.js +8 -0
  41. package/dist/workflows/composition.js.map +1 -1
  42. package/dist/workflows/decision-presentation.d.ts +1 -1
  43. package/dist/workflows/decision-presentation.js +51 -38
  44. package/dist/workflows/decision-presentation.js.map +1 -1
  45. package/dist/workflows/engine.d.ts +2 -2
  46. package/dist/workflows/engine.js +14 -13
  47. package/dist/workflows/engine.js.map +1 -1
  48. package/dist/workflows/errors.d.ts +13 -0
  49. package/dist/workflows/errors.js +15 -0
  50. package/dist/workflows/errors.js.map +1 -1
  51. package/dist/workflows/human-decision.d.ts +16 -4
  52. package/dist/workflows/human-decision.js +175 -72
  53. package/dist/workflows/human-decision.js.map +1 -1
  54. package/dist/workflows/index.d.ts +2 -2
  55. package/dist/workflows/index.js +1 -1
  56. package/dist/workflows/index.js.map +1 -1
  57. package/dist/workflows/progress.d.ts +1 -0
  58. package/dist/workflows/progress.js +15 -3
  59. package/dist/workflows/progress.js.map +1 -1
  60. package/dist/workflows/schema.js +10 -0
  61. package/dist/workflows/schema.js.map +1 -1
  62. package/dist/workflows/store.js +5 -0
  63. package/dist/workflows/store.js.map +1 -1
  64. package/dist/workflows/types.d.ts +33 -45
  65. package/docs/HUMAN_DECISIONS.md +25 -35
  66. package/docs/HUMAN_DECISION_PRESENTATIONS.md +14 -24
  67. package/docs/MONITOR.md +5 -11
  68. package/docs/WORKFLOW_COMPOSITION.md +8 -7
  69. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  70. package/docs/plans/2026-08-21-sanity-check-plan.md +202 -94
  71. package/docs/run-bundles.md +6 -6
  72. package/docs/workflows.md +26 -6
  73. package/examples/workflows/approved-plan.workflow.ts +19 -46
  74. package/herdr-plugin.toml +1 -1
  75. package/package.json +7 -7
  76. package/schemas/human-decision-accepted-v1.schema.json +15 -3
  77. package/schemas/human-decision-continuation-v1.schema.json +10 -1
  78. package/schemas/human-decision-delivery-v1.schema.json +8 -0
  79. package/schemas/human-decision-receipt-v1.schema.json +8 -0
  80. package/schemas/human-decision-request-v1.schema.json +24 -4
  81. package/skills/autoimplement/SKILL.md +27 -0
  82. package/skills/monitor/SKILL.md +31 -3
  83. package/skills/pi-workflows/SKILL.md +2 -1
  84. package/skills/sanity-check/SKILL.md +44 -0
  85. package/src/builtins/autoimplement.workflow.ts +19 -118
  86. package/src/builtins/catalog.ts +4 -4
  87. package/src/builtins/index.ts +11 -0
  88. package/src/builtins/monitor.workflow.ts +27 -150
  89. package/src/builtins/pi-agent-group.ts +1407 -0
  90. package/src/builtins/plan-approval.workflow.ts +157 -24
  91. package/src/builtins/plan-change.workflow.ts +321 -0
  92. package/src/builtins/plan-presentation.ts +2 -2
  93. package/src/builtins/sanity-check.workflow.ts +186 -41
  94. package/src/extension/decision-channels.ts +29 -59
  95. package/src/extension/index.ts +79 -41
  96. package/src/extension/session-events.ts +2 -2
  97. package/src/extension/widget.ts +24 -5
  98. package/src/render/graph-render.ts +1 -2
  99. package/src/viewer/render.ts +7 -6
  100. package/src/workflows/catalog.ts +7 -2
  101. package/src/workflows/composition.ts +9 -0
  102. package/src/workflows/decision-presentation.ts +56 -43
  103. package/src/workflows/engine.ts +17 -15
  104. package/src/workflows/errors.ts +24 -0
  105. package/src/workflows/human-decision.ts +218 -101
  106. package/src/workflows/index.ts +5 -11
  107. package/src/workflows/progress.ts +18 -3
  108. package/src/workflows/schema.ts +17 -0
  109. package/src/workflows/store.ts +5 -0
  110. package/src/workflows/types.ts +39 -56
  111. package/dist/builtins/sanity-check-session.d.ts +0 -17
  112. package/dist/builtins/sanity-check-session.js +0 -168
  113. package/dist/builtins/sanity-check-session.js.map +0 -1
  114. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  115. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  116. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  117. package/schemas/human-decision-request-v2.schema.json +0 -69
  118. package/schemas/human-decision-resolution-v2.schema.json +0 -27
  119. package/src/builtins/sanity-check-session.ts +0 -205
@@ -0,0 +1,1087 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, DefaultPackageManager, DefaultResourceLoader, ExtensionRunner, getAgentDir, ModelRegistry, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
5
+ const REQUEST_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
6
+ const MAX_AGENTS = 8;
7
+ const MAX_CONCURRENCY = 8;
8
+ const MAX_PROMPT_CHARS = 96_000;
9
+ const DEFAULT_FINAL_CHARS = 256_000;
10
+ const MAX_FINAL_CHARS = 1_000_000;
11
+ const DEFAULT_TIMEOUT_MS = 15 * 60_000;
12
+ const MAX_TIMEOUT_MS = 60 * 60_000;
13
+ const MAX_ERROR_CHARS = 2_000;
14
+ const MAX_PHASE_UPDATES = 64;
15
+ const MIN_PHASE_INTERVAL_MS = 250;
16
+ const BUILTIN_TOOLS = new Set(["read", "grep", "find", "ls"]);
17
+ const RESERVED_EXTENSION_NAMES = new Set([
18
+ "workflow",
19
+ "piw",
20
+ "controller",
21
+ "workflow-update",
22
+ "workflow-answer",
23
+ "workflow-submit",
24
+ "workflow-pause",
25
+ "workflow-resume",
26
+ "workflow-cancel",
27
+ ]);
28
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
29
+ export class PiAgentGroupError extends Error {
30
+ agentId;
31
+ code;
32
+ constructor(agentId, code, message) {
33
+ super(`Pi agent ${agentId} ${code}: ${bounded(message)}`);
34
+ this.agentId = agentId;
35
+ this.code = code;
36
+ this.name = "PiAgentGroupError";
37
+ }
38
+ }
39
+ export async function runPiAgentGroup(input, options) {
40
+ const { requests, maxFinalChars } = validateGroup(input, options);
41
+ if (requests.length === 0)
42
+ return [];
43
+ if (options.signal.aborted)
44
+ throw cancellationError("group", options.signal.reason);
45
+ const groupPlan = options.sessionFactory === undefined
46
+ ? await createSdkGroupPlan(requests, options.behaviorExtensionPaths ?? [], options.signal)
47
+ : undefined;
48
+ if (options.signal.aborted)
49
+ throw cancellationError("group", options.signal.reason);
50
+ const sessionFactory = options.sessionFactory ??
51
+ (async (request, context) => await createSdkSession(request, groupPlan, context.signal));
52
+ const internalAbort = new AbortController();
53
+ const signal = AbortSignal.any([options.signal, internalAbort.signal]);
54
+ const results = Array.from({ length: requests.length });
55
+ const started = new Set();
56
+ let nextIndex = 0;
57
+ let primary;
58
+ const worker = async () => {
59
+ while (!signal.aborted) {
60
+ const index = nextIndex;
61
+ if (index >= requests.length)
62
+ return;
63
+ nextIndex += 1;
64
+ started.add(index);
65
+ try {
66
+ results[index] = await runOneAgent(requests[index], {
67
+ ...options,
68
+ maxFinalChars,
69
+ signal,
70
+ sessionFactory,
71
+ });
72
+ }
73
+ catch (error) {
74
+ if (primary === undefined && !options.signal.aborted) {
75
+ primary = { index, error };
76
+ if (options.failFast !== false)
77
+ internalAbort.abort(error);
78
+ }
79
+ if (options.failFast !== false)
80
+ return;
81
+ }
82
+ }
83
+ };
84
+ const workerCount = Math.min(options.maxConcurrency, requests.length);
85
+ await Promise.all(Array.from({ length: workerCount }, worker));
86
+ if (options.signal.aborted)
87
+ throw cancellationError("group", options.signal.reason);
88
+ if (primary !== undefined) {
89
+ await publishQueuedCancellations(requests, started, options);
90
+ throw primary.error;
91
+ }
92
+ return results;
93
+ }
94
+ export async function createEphemeralCredentialStore(signal) {
95
+ signal.throwIfAborted();
96
+ const authPath = path.join(getAgentDir(), "auth.json");
97
+ let source = {};
98
+ try {
99
+ source = JSON.parse(await fs.readFile(authPath, "utf8"));
100
+ }
101
+ catch (error) {
102
+ if (!isMissingFile(error)) {
103
+ throw new Error("Could not load Pi credentials for isolated agents");
104
+ }
105
+ }
106
+ const entries = parseCredentialEntries(source);
107
+ const pending = new Map();
108
+ const enqueue = (providerId, operation) => {
109
+ const work = (pending.get(providerId) ?? Promise.resolve(undefined))
110
+ .catch(() => undefined)
111
+ .then(operation);
112
+ pending.set(providerId, work);
113
+ const release = () => {
114
+ if (pending.get(providerId) === work)
115
+ pending.delete(providerId);
116
+ };
117
+ void work.then(release, release);
118
+ return work;
119
+ };
120
+ return {
121
+ async read(providerId) {
122
+ signal.throwIfAborted();
123
+ return cloneCredential(entries.get(providerId));
124
+ },
125
+ async list() {
126
+ signal.throwIfAborted();
127
+ return [...entries].map(([providerId, credential]) => ({
128
+ providerId,
129
+ type: credential.type,
130
+ }));
131
+ },
132
+ async modify(providerId, update) {
133
+ return await enqueue(providerId, async () => {
134
+ signal.throwIfAborted();
135
+ const current = entries.get(providerId);
136
+ const next = await update(cloneCredential(current));
137
+ signal.throwIfAborted();
138
+ if (next !== undefined)
139
+ entries.set(providerId, cloneCredential(next));
140
+ return cloneCredential(entries.get(providerId));
141
+ });
142
+ },
143
+ async delete(providerId) {
144
+ await enqueue(providerId, async () => {
145
+ signal.throwIfAborted();
146
+ entries.delete(providerId);
147
+ return undefined;
148
+ });
149
+ },
150
+ };
151
+ }
152
+ function parseCredentialEntries(source) {
153
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
154
+ throw new Error("Could not load Pi credentials for isolated agents");
155
+ }
156
+ const entries = new Map();
157
+ for (const [providerId, value] of Object.entries(source)) {
158
+ if (!isStoredCredential(value)) {
159
+ throw new Error("Could not load Pi credentials for isolated agents");
160
+ }
161
+ entries.set(providerId, cloneCredential(value));
162
+ }
163
+ return entries;
164
+ }
165
+ function isStoredCredential(value) {
166
+ if (value === null || typeof value !== "object" || Array.isArray(value))
167
+ return false;
168
+ const credential = value;
169
+ if (credential.type === "api_key") {
170
+ return credential.key === undefined || typeof credential.key === "string";
171
+ }
172
+ return (credential.type === "oauth" &&
173
+ typeof credential.refresh === "string" &&
174
+ typeof credential.access === "string" &&
175
+ typeof credential.expires === "number");
176
+ }
177
+ function cloneCredential(value) {
178
+ return value === undefined ? undefined : structuredClone(value);
179
+ }
180
+ function isMissingFile(error) {
181
+ return (error !== null &&
182
+ typeof error === "object" &&
183
+ "code" in error &&
184
+ error.code === "ENOENT");
185
+ }
186
+ export async function createEphemeralModelStore(signal) {
187
+ signal.throwIfAborted();
188
+ const storePath = path.join(getAgentDir(), "models-store.json");
189
+ let source = {};
190
+ try {
191
+ source = JSON.parse(await fs.readFile(storePath, "utf8"));
192
+ }
193
+ catch (error) {
194
+ if (!isMissingFile(error)) {
195
+ throw new Error("Could not load Pi model catalog for isolated agents");
196
+ }
197
+ }
198
+ const entries = parseModelStoreEntries(source);
199
+ return {
200
+ async read(providerId) {
201
+ signal.throwIfAborted();
202
+ const entry = entries.get(providerId);
203
+ return entry === undefined ? undefined : structuredClone(entry);
204
+ },
205
+ async write(providerId, entry) {
206
+ signal.throwIfAborted();
207
+ entries.set(providerId, structuredClone(entry));
208
+ },
209
+ async delete(providerId) {
210
+ signal.throwIfAborted();
211
+ entries.delete(providerId);
212
+ },
213
+ };
214
+ }
215
+ function parseModelStoreEntries(source) {
216
+ if (source === null || typeof source !== "object" || Array.isArray(source)) {
217
+ throw new Error("Could not load Pi model catalog for isolated agents");
218
+ }
219
+ const entries = new Map();
220
+ for (const [providerId, value] of Object.entries(source)) {
221
+ if (!isModelStoreEntry(value)) {
222
+ throw new Error("Could not load Pi model catalog for isolated agents");
223
+ }
224
+ entries.set(providerId, structuredClone(value));
225
+ }
226
+ return entries;
227
+ }
228
+ function isModelStoreEntry(value) {
229
+ if (value === null || typeof value !== "object" || Array.isArray(value))
230
+ return false;
231
+ const entry = value;
232
+ return (Array.isArray(entry.models) &&
233
+ (entry.lastModified === undefined || typeof entry.lastModified === "number") &&
234
+ (entry.checkedAt === undefined || typeof entry.checkedAt === "number") &&
235
+ (entry.etag === undefined || typeof entry.etag === "string"));
236
+ }
237
+ async function createSdkGroupPlan(requests, behaviorExtensionPaths, signal) {
238
+ signal.throwIfAborted();
239
+ const cwd = requests[0].cwd;
240
+ if (requests.some((request) => request.cwd !== cwd)) {
241
+ throw new Error("Pi agent group requests must use one working directory");
242
+ }
243
+ const agentDir = getAgentDir();
244
+ const settingsManager = SettingsManager.create(cwd, agentDir);
245
+ const dispatch = resolveGroupDispatch(requests, settingsManager);
246
+ const extensionPaths = await resolveChildExtensionPaths(cwd, agentDir, settingsManager, dispatch.provider, behaviorExtensionPaths, signal);
247
+ const credentials = await createEphemeralCredentialStore(signal);
248
+ const credentialSnapshot = new Map();
249
+ for (const { providerId } of await credentials.list()) {
250
+ const credential = await credentials.read(providerId);
251
+ if (credential !== undefined)
252
+ credentialSnapshot.set(providerId, cloneCredential(credential));
253
+ }
254
+ const models = await createEphemeralModelStore(signal);
255
+ const modelSnapshot = await models.read(dispatch.provider);
256
+ if (modelSnapshot === undefined) {
257
+ throw new PiAgentGroupError("group", "has no model catalog", `${dispatch.provider}/${dispatch.modelId}`);
258
+ }
259
+ signal.throwIfAborted();
260
+ return {
261
+ agentDir,
262
+ cwd,
263
+ dispatch,
264
+ extensionPaths,
265
+ credentialSnapshot,
266
+ modelSnapshot: structuredClone(modelSnapshot),
267
+ };
268
+ }
269
+ function resolveGroupDispatch(requests, settingsManager) {
270
+ const overrides = requests.map((request) => ({
271
+ model: request.model,
272
+ thinkingLevel: request.thinkingLevel,
273
+ }));
274
+ const hasAnyOverride = overrides.some(({ model, thinkingLevel }) => model !== undefined || thinkingLevel !== undefined);
275
+ if (hasAnyOverride) {
276
+ for (const [index, override] of overrides.entries()) {
277
+ if (override.model === undefined || override.thinkingLevel === undefined) {
278
+ throw new Error(`Pi agent ${requests[index].id} must override model and thinkingLevel together`);
279
+ }
280
+ }
281
+ const first = overrides[0];
282
+ if (overrides.some((override) => override.model.provider !== first.model.provider ||
283
+ override.model.id !== first.model.id ||
284
+ override.thinkingLevel !== first.thinkingLevel)) {
285
+ throw new Error("Pi agent group requests must use one exact model dispatch");
286
+ }
287
+ return {
288
+ provider: first.model.provider,
289
+ modelId: first.model.id,
290
+ thinkingLevel: first.thinkingLevel,
291
+ };
292
+ }
293
+ const provider = settingsManager.getDefaultProvider();
294
+ const modelId = settingsManager.getDefaultModel();
295
+ const thinkingLevel = settingsManager.getDefaultThinkingLevel();
296
+ if (provider === undefined || modelId === undefined || thinkingLevel === undefined) {
297
+ throw new Error("Pi agent group requires configured provider, model, and thinking level");
298
+ }
299
+ return { provider, modelId, thinkingLevel };
300
+ }
301
+ async function resolveChildExtensionPaths(cwd, agentDir, settingsManager, provider, behaviorExtensionPaths, signal) {
302
+ signal.throwIfAborted();
303
+ const packageManager = new DefaultPackageManager({ cwd, agentDir, settingsManager });
304
+ const resolved = await packageManager.resolve(async () => "skip");
305
+ const candidates = new Map();
306
+ for (const resource of resolved.extensions) {
307
+ if (!resource.enabled || resource.metadata.scope !== "user")
308
+ continue;
309
+ const candidate = await extensionCandidate(resource, signal);
310
+ if (candidate !== undefined)
311
+ candidates.set(candidate.path, candidate);
312
+ }
313
+ const behaviorPaths = await canonicalizeBehaviorPaths(behaviorExtensionPaths, signal);
314
+ for (const candidate of behaviorPaths)
315
+ candidates.set(candidate.path, candidate);
316
+ if (candidates.size === 0) {
317
+ throw new PiAgentGroupError("group", "has no provider extension", provider);
318
+ }
319
+ const candidateList = [...candidates.values()].toSorted((left, right) => left.path.localeCompare(right.path));
320
+ const loader = new DefaultResourceLoader({
321
+ cwd,
322
+ agentDir,
323
+ settingsManager: SettingsManager.inMemory({}, { projectTrusted: false }),
324
+ additionalExtensionPaths: candidateList.map((candidate) => candidate.path),
325
+ noExtensions: true,
326
+ noSkills: true,
327
+ noPromptTemplates: true,
328
+ noThemes: true,
329
+ noContextFiles: true,
330
+ systemPromptOverride: () => undefined,
331
+ appendSystemPromptOverride: () => [],
332
+ });
333
+ let result;
334
+ let admittedPaths;
335
+ let failure;
336
+ try {
337
+ await loader.reload();
338
+ result = loader.getExtensions();
339
+ if (result.errors.length > 0) {
340
+ throw new PiAgentGroupError("group", "could not load provider extensions", "load failed");
341
+ }
342
+ const byPath = new Map(await Promise.all(result.extensions.map(async (extension) => [await canonicalPath(extension.resolvedPath, signal), extension])));
343
+ const owners = new Set();
344
+ for (const registration of result.runtime.pendingNativeProviderRegistrations) {
345
+ if (registration.provider.id === provider) {
346
+ owners.add(await canonicalPath(registration.extensionPath, signal));
347
+ }
348
+ }
349
+ for (const registration of result.runtime.pendingProviderRegistrations) {
350
+ if (registration.name === provider) {
351
+ owners.add(await canonicalPath(registration.extensionPath, signal));
352
+ }
353
+ }
354
+ if (owners.size === 0) {
355
+ throw new PiAgentGroupError("group", "has no provider extension", provider);
356
+ }
357
+ if (owners.size > 1) {
358
+ throw new PiAgentGroupError("group", "has competing provider extensions", provider);
359
+ }
360
+ const providerPath = [...owners][0];
361
+ if (!candidates.has(providerPath) || !byPath.has(providerPath)) {
362
+ throw new PiAgentGroupError("group", "has invalid provider extension", provider);
363
+ }
364
+ const admitted = new Set([providerPath, ...behaviorPaths.map((candidate) => candidate.path)]);
365
+ for (const admittedPath of admitted) {
366
+ const extension = byPath.get(admittedPath);
367
+ if (extension === undefined) {
368
+ throw new PiAgentGroupError("group", "could not load admitted extension", "load failed");
369
+ }
370
+ validateAdmittedExtension(extension, candidates.get(admittedPath)?.source ?? "extension");
371
+ }
372
+ admittedPaths = Object.freeze([...admitted].toSorted());
373
+ }
374
+ catch (error) {
375
+ failure = error;
376
+ }
377
+ if (result !== undefined) {
378
+ try {
379
+ await shutdownLoadedExtensions(result, cwd, signal);
380
+ }
381
+ catch {
382
+ failure ??= new PiAgentGroupError("group", "could not settle extension preflight", "cleanup failed");
383
+ }
384
+ finally {
385
+ result.runtime.invalidate("Pi agent extension preflight finished");
386
+ }
387
+ }
388
+ if (failure !== undefined)
389
+ throw failure;
390
+ return admittedPaths;
391
+ }
392
+ async function shutdownLoadedExtensions(result, cwd, signal) {
393
+ const modelRuntime = await ModelRuntime.create({
394
+ allowModelNetwork: false,
395
+ refreshOnCreate: false,
396
+ signal,
397
+ credentials: credentialStoreFromSnapshot(new Map(), signal),
398
+ modelsStore: emptyModelStore(signal),
399
+ });
400
+ const runner = new ExtensionRunner(result.extensions, result.runtime, cwd, SessionManager.inMemory(cwd), new ModelRegistry(modelRuntime));
401
+ runner.setUIContext(undefined, "print");
402
+ let failed = false;
403
+ const unsubscribe = runner.onError(() => {
404
+ failed = true;
405
+ });
406
+ try {
407
+ await runner.emit({ type: "session_shutdown", reason: "quit" });
408
+ }
409
+ finally {
410
+ unsubscribe();
411
+ }
412
+ if (failed)
413
+ throw new Error("extension preflight cleanup failed");
414
+ }
415
+ function emptyModelStore(signal) {
416
+ return {
417
+ async read() {
418
+ signal.throwIfAborted();
419
+ return undefined;
420
+ },
421
+ async write() {
422
+ signal.throwIfAborted();
423
+ },
424
+ async delete() {
425
+ signal.throwIfAborted();
426
+ },
427
+ };
428
+ }
429
+ async function extensionCandidate(resource, signal) {
430
+ const resolvedPath = await canonicalPath(resource.path, signal);
431
+ if (await isPiWorkflowsExtension(resolvedPath, resource, signal))
432
+ return undefined;
433
+ return { path: resolvedPath, source: boundedSource(resource.metadata.source) };
434
+ }
435
+ async function canonicalizeBehaviorPaths(values, signal) {
436
+ const result = new Map();
437
+ for (const value of values) {
438
+ operationalText(value, "Pi agent behavior extension path", 4_000);
439
+ const resolvedPath = await canonicalPath(value, signal);
440
+ if (await isPiWorkflowsExtension(resolvedPath, undefined, signal)) {
441
+ throw new Error("Pi Workflows cannot be admitted as a child extension");
442
+ }
443
+ result.set(resolvedPath, { path: resolvedPath, source: "explicit behavior extension" });
444
+ }
445
+ return [...result.values()].toSorted((left, right) => left.path.localeCompare(right.path));
446
+ }
447
+ async function canonicalPath(value, signal) {
448
+ signal.throwIfAborted();
449
+ try {
450
+ return await fs.realpath(path.resolve(value));
451
+ }
452
+ catch {
453
+ throw new Error("Could not resolve Pi child extension path");
454
+ }
455
+ }
456
+ async function isPiWorkflowsExtension(extensionPath, resource, signal) {
457
+ if (isUnder(extensionPath, path.join(PACKAGE_ROOT, "src", "extension")) ||
458
+ isUnder(extensionPath, path.join(PACKAGE_ROOT, "dist", "extension"))) {
459
+ return true;
460
+ }
461
+ if (resource?.metadata.source.includes("@osolmaz/pi-workflows"))
462
+ return true;
463
+ let directory = resource?.metadata.baseDir ?? path.dirname(extensionPath);
464
+ for (let depth = 0; depth < 8; depth += 1) {
465
+ signal.throwIfAborted();
466
+ const manifestPath = path.join(directory, "package.json");
467
+ try {
468
+ const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8"));
469
+ if (isPiWorkflowsManifest(manifest, directory, extensionPath))
470
+ return true;
471
+ return false;
472
+ }
473
+ catch (error) {
474
+ if (!isMissingFile(error))
475
+ return false;
476
+ }
477
+ const parent = path.dirname(directory);
478
+ if (parent === directory)
479
+ return false;
480
+ directory = parent;
481
+ }
482
+ return false;
483
+ }
484
+ function isPiWorkflowsManifest(value, packageDirectory, extensionPath) {
485
+ if (!isRecord(value))
486
+ return false;
487
+ let ownsPiWorkflows = value.name === "@osolmaz/pi-workflows";
488
+ for (const field of ["dependencies", "peerDependencies", "optionalDependencies"]) {
489
+ if (isRecord(value[field]) && "@osolmaz/pi-workflows" in value[field]) {
490
+ ownsPiWorkflows = true;
491
+ }
492
+ }
493
+ if (!ownsPiWorkflows || !isRecord(value.pi) || !Array.isArray(value.pi.extensions)) {
494
+ return false;
495
+ }
496
+ return value.pi.extensions.some((entry) => typeof entry === "string" &&
497
+ path.resolve(packageDirectory, entry.replace(/\*+$/, "")) === extensionPath);
498
+ }
499
+ function isUnder(value, parent) {
500
+ const relative = path.relative(parent, value);
501
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
502
+ }
503
+ function validateAdmittedExtension(extension, source) {
504
+ for (const name of extension.tools.keys()) {
505
+ if (BUILTIN_TOOLS.has(name)) {
506
+ throw new PiAgentGroupError("group", "provider extension replaces a built-in tool", source);
507
+ }
508
+ if (isReservedExtensionName(name)) {
509
+ throw new PiAgentGroupError("group", "provider extension exposes workflow control", source);
510
+ }
511
+ }
512
+ for (const name of extension.commands.keys()) {
513
+ if (isReservedExtensionName(name)) {
514
+ throw new PiAgentGroupError("group", "provider extension exposes workflow control", source);
515
+ }
516
+ }
517
+ if (extension.handlers.has("resources_discover")) {
518
+ throw new PiAgentGroupError("group", "provider extension discovers child resources", source);
519
+ }
520
+ }
521
+ function isReservedExtensionName(value) {
522
+ const name = value.toLowerCase();
523
+ return (RESERVED_EXTENSION_NAMES.has(name) ||
524
+ name.startsWith("workflow-") ||
525
+ name.startsWith("workflow:"));
526
+ }
527
+ function boundedSource(value) {
528
+ const source = value.trim();
529
+ return source.length <= 200 ? source : `${source.slice(0, 200)}…`;
530
+ }
531
+ function credentialStoreFromSnapshot(snapshot, signal) {
532
+ const entries = new Map([...snapshot].map(([providerId, credential]) => [providerId, cloneCredential(credential)]));
533
+ const pending = new Map();
534
+ const enqueue = (providerId, operation) => {
535
+ const work = (pending.get(providerId) ?? Promise.resolve(undefined))
536
+ .catch(() => undefined)
537
+ .then(operation);
538
+ pending.set(providerId, work);
539
+ const release = () => {
540
+ if (pending.get(providerId) === work)
541
+ pending.delete(providerId);
542
+ };
543
+ void work.then(release, release);
544
+ return work;
545
+ };
546
+ return {
547
+ async read(providerId) {
548
+ signal.throwIfAborted();
549
+ return cloneCredential(entries.get(providerId));
550
+ },
551
+ async list() {
552
+ signal.throwIfAborted();
553
+ return [...entries].map(([providerId, credential]) => ({
554
+ providerId,
555
+ type: credential.type,
556
+ }));
557
+ },
558
+ async modify(providerId, update) {
559
+ return await enqueue(providerId, async () => {
560
+ signal.throwIfAborted();
561
+ const next = await update(cloneCredential(entries.get(providerId)));
562
+ signal.throwIfAborted();
563
+ if (next !== undefined)
564
+ entries.set(providerId, cloneCredential(next));
565
+ return cloneCredential(entries.get(providerId));
566
+ });
567
+ },
568
+ async delete(providerId) {
569
+ await enqueue(providerId, async () => {
570
+ signal.throwIfAborted();
571
+ entries.delete(providerId);
572
+ return undefined;
573
+ });
574
+ },
575
+ };
576
+ }
577
+ function modelStoreFromSnapshot(provider, snapshot, signal) {
578
+ const entries = new Map([[provider, structuredClone(snapshot)]]);
579
+ return {
580
+ async read(providerId) {
581
+ signal.throwIfAborted();
582
+ const entry = entries.get(providerId);
583
+ return entry === undefined ? undefined : structuredClone(entry);
584
+ },
585
+ async write(providerId, entry) {
586
+ signal.throwIfAborted();
587
+ entries.set(providerId, structuredClone(entry));
588
+ },
589
+ async delete(providerId) {
590
+ signal.throwIfAborted();
591
+ entries.delete(providerId);
592
+ },
593
+ };
594
+ }
595
+ async function runOneAgent(request, options) {
596
+ const now = options.now ?? Date.now;
597
+ const startedAt = now();
598
+ const lifecycle = lifecyclePublisher(request, startedAt, options);
599
+ const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
600
+ let session;
601
+ let unsubscribe;
602
+ let finalMessage;
603
+ let preflightAccepted;
604
+ let abortKind;
605
+ let abortFailure;
606
+ let abortWork = Promise.resolve();
607
+ let result;
608
+ let failure;
609
+ const abortSession = (kind) => {
610
+ abortKind ??= kind;
611
+ if (session !== undefined) {
612
+ abortWork = abortWork
613
+ .then(async () => await session?.abort())
614
+ .catch((error) => {
615
+ abortFailure = error;
616
+ });
617
+ }
618
+ };
619
+ const onAbort = () => abortSession("cancelled");
620
+ const timeout = setTimeout(() => abortSession("timeout"), timeoutMs);
621
+ options.signal.addEventListener("abort", onAbort, { once: true });
622
+ lifecycle.emit("running", "starting");
623
+ try {
624
+ if (options.signal.aborted)
625
+ throw cancellationError(request.id, options.signal.reason);
626
+ session = await options.sessionFactory(request, { signal: options.signal });
627
+ const model = modelName(session.model);
628
+ lifecycle.setDispatch(model, session.thinkingLevel);
629
+ lifecycle.emit("running", "starting", true);
630
+ unsubscribe = session.subscribe((event) => {
631
+ const phase = eventPhase(event, request.tools);
632
+ if (phase !== undefined)
633
+ lifecycle.emit("running", phase);
634
+ const message = assistantMessage(event);
635
+ if (message !== undefined)
636
+ finalMessage = message;
637
+ });
638
+ if (abortKind !== undefined || options.signal.aborted) {
639
+ abortSession(abortKind ?? "cancelled");
640
+ await abortWork;
641
+ if (abortKind === "timeout") {
642
+ throw new PiAgentGroupError(request.id, "timed out", `after ${timeoutMs}ms${abortSuffix(abortFailure)}`);
643
+ }
644
+ throw new PiAgentGroupError(request.id, "cancelled", `${cancellationReason(options.signal.reason)}${abortSuffix(abortFailure)}`);
645
+ }
646
+ await session.prompt(request.prompt, {
647
+ preflightResult: (accepted) => {
648
+ preflightAccepted = accepted;
649
+ },
650
+ });
651
+ await abortWork;
652
+ if (abortKind === "timeout") {
653
+ throw new PiAgentGroupError(request.id, "timed out", `after ${timeoutMs}ms${abortSuffix(abortFailure)}`);
654
+ }
655
+ if (abortKind === "cancelled" || options.signal.aborted) {
656
+ throw new PiAgentGroupError(request.id, "cancelled", `${cancellationReason(options.signal.reason)}${abortSuffix(abortFailure)}`);
657
+ }
658
+ if (preflightAccepted === false) {
659
+ throw new PiAgentGroupError(request.id, "rejected prompt", "prompt preflight failed");
660
+ }
661
+ lifecycle.emit("running", "finalizing", true);
662
+ const text = finalAssistantText(request.id, finalMessage, options.maxFinalChars);
663
+ result = {
664
+ id: request.id,
665
+ text,
666
+ model,
667
+ thinkingLevel: session.thinkingLevel,
668
+ durationMs: Math.max(0, now() - startedAt),
669
+ };
670
+ }
671
+ catch (error) {
672
+ failure = normalizeAgentError(request.id, error, abortKind, options.signal);
673
+ }
674
+ finally {
675
+ clearTimeout(timeout);
676
+ options.signal.removeEventListener("abort", onAbort);
677
+ unsubscribe?.();
678
+ if (session !== undefined && abortKind !== undefined)
679
+ await abortWork;
680
+ try {
681
+ await session?.dispose();
682
+ }
683
+ catch (error) {
684
+ failure ??= new PiAgentGroupError(request.id, "cleanup failed", errorMessage(error));
685
+ }
686
+ const state = terminalState(failure);
687
+ lifecycle.emit(state, state, true);
688
+ await lifecycle.flush();
689
+ }
690
+ if (failure !== undefined)
691
+ throw failure;
692
+ return result;
693
+ }
694
+ function lifecyclePublisher(request, startedAt, options) {
695
+ const now = options.now ?? Date.now;
696
+ let model;
697
+ let thinkingLevel;
698
+ let previous = "";
699
+ let previousAt = -Infinity;
700
+ let phaseUpdates = 0;
701
+ let work = Promise.resolve();
702
+ const emit = (state, phase, force = false) => {
703
+ const at = now();
704
+ const key = `${state}:${phase}:${model ?? ""}`;
705
+ if (!force && (key === previous || at - previousAt < MIN_PHASE_INTERVAL_MS))
706
+ return;
707
+ if (!force && phaseUpdates >= MAX_PHASE_UPDATES)
708
+ return;
709
+ previous = key;
710
+ previousAt = at;
711
+ phaseUpdates += 1;
712
+ if (options.onLifecycle === undefined)
713
+ return;
714
+ const event = {
715
+ id: request.id,
716
+ role: request.role,
717
+ state,
718
+ phase,
719
+ elapsedMs: Math.max(0, at - startedAt),
720
+ ...(model !== undefined ? { model } : {}),
721
+ ...(thinkingLevel !== undefined ? { thinkingLevel } : {}),
722
+ };
723
+ work = work.then(async () => await options.onLifecycle?.(event)).catch(() => undefined);
724
+ };
725
+ return {
726
+ emit,
727
+ setDispatch(value, thinking) {
728
+ model = value;
729
+ thinkingLevel = thinking;
730
+ },
731
+ async flush() {
732
+ await work;
733
+ },
734
+ };
735
+ }
736
+ async function createSdkSession(request, plan, signal) {
737
+ if (signal.aborted)
738
+ throw cancellationError(request.id, signal.reason);
739
+ const model = plan.modelSnapshot.models.find((candidate) => candidate.provider === plan.dispatch.provider && candidate.id === plan.dispatch.modelId);
740
+ if (model === undefined) {
741
+ throw new PiAgentGroupError(request.id, "has no model", `${plan.dispatch.provider}/${plan.dispatch.modelId}`);
742
+ }
743
+ let latestExtensions;
744
+ const runtime = await createAgentSessionRuntime(async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
745
+ const settingsManager = SettingsManager.inMemory({
746
+ defaultProvider: plan.dispatch.provider,
747
+ defaultModel: plan.dispatch.modelId,
748
+ defaultThinkingLevel: plan.dispatch.thinkingLevel,
749
+ extensions: [],
750
+ skills: [],
751
+ prompts: [],
752
+ themes: [],
753
+ }, { projectTrusted: false });
754
+ const modelRuntime = await ModelRuntime.create({
755
+ allowModelNetwork: false,
756
+ signal,
757
+ credentials: credentialStoreFromSnapshot(plan.credentialSnapshot, signal),
758
+ modelsStore: modelStoreFromSnapshot(plan.dispatch.provider, plan.modelSnapshot, signal),
759
+ });
760
+ try {
761
+ const services = await createAgentSessionServices({
762
+ cwd,
763
+ agentDir,
764
+ settingsManager,
765
+ modelRuntime,
766
+ modelRuntimeSignal: signal,
767
+ resourceLoaderOptions: {
768
+ additionalExtensionPaths: [...plan.extensionPaths],
769
+ noExtensions: true,
770
+ noSkills: true,
771
+ noPromptTemplates: true,
772
+ noThemes: true,
773
+ noContextFiles: true,
774
+ systemPromptOverride: () => undefined,
775
+ appendSystemPromptOverride: () => [],
776
+ },
777
+ });
778
+ latestExtensions = services.resourceLoader.getExtensions();
779
+ assertLoadedProfile(request.id, latestExtensions, plan.extensionPaths);
780
+ assertProviderRegistration(request.id, services, plan.dispatch.provider);
781
+ const sessionResult = await createAgentSessionFromServices({
782
+ services,
783
+ sessionManager,
784
+ ...(sessionStartEvent !== undefined ? { sessionStartEvent } : {}),
785
+ tools: request.tools,
786
+ model,
787
+ thinkingLevel: plan.dispatch.thinkingLevel,
788
+ });
789
+ return {
790
+ ...sessionResult,
791
+ services,
792
+ diagnostics: services.diagnostics,
793
+ };
794
+ }
795
+ catch (error) {
796
+ if (latestExtensions !== undefined) {
797
+ try {
798
+ await shutdownLoadedExtensions(latestExtensions, cwd, new AbortController().signal);
799
+ }
800
+ catch {
801
+ // Preserve the child setup error as the primary failure.
802
+ }
803
+ finally {
804
+ latestExtensions.runtime.invalidate("Pi agent child session creation failed");
805
+ }
806
+ }
807
+ throw error;
808
+ }
809
+ }, {
810
+ cwd: request.cwd,
811
+ agentDir: plan.agentDir,
812
+ sessionManager: SessionManager.inMemory(request.cwd),
813
+ sessionStartEvent: { type: "session_start", reason: "startup" },
814
+ });
815
+ let extensionFailure = false;
816
+ let unsubscribeExtensionErrors;
817
+ try {
818
+ const session = runtime.session;
819
+ unsubscribeExtensionErrors = session.extensionRunner.onError(() => {
820
+ extensionFailure = true;
821
+ });
822
+ await session.bindExtensions({ mode: "print" });
823
+ assertExactSession(request, session, plan.dispatch);
824
+ const auth = await runtime.services.modelRuntime.getAuth(model, { signal });
825
+ if (auth === undefined) {
826
+ throw new PiAgentGroupError(request.id, "has no provider authentication", plan.dispatch.provider);
827
+ }
828
+ const activeTools = session.getActiveToolNames().toSorted();
829
+ const requestedTools = request.tools.toSorted();
830
+ if (activeTools.join("\0") !== requestedTools.join("\0")) {
831
+ throw new PiAgentGroupError(request.id, "has unexpected tools", activeTools.length === 0 ? "no tools are active" : activeTools.join(", "));
832
+ }
833
+ const toolInfo = new Map(session.getAllTools().map((tool) => [tool.name, tool]));
834
+ for (const tool of requestedTools) {
835
+ if (toolInfo.get(tool)?.sourceInfo.source !== "builtin") {
836
+ throw new PiAgentGroupError(request.id, "has replaced built-in tool", tool);
837
+ }
838
+ }
839
+ let disposed = false;
840
+ return {
841
+ prompt: async (text, promptOptions) => await session.prompt(text, {
842
+ ...promptOptions,
843
+ expandPromptTemplates: false,
844
+ source: "interactive",
845
+ }),
846
+ subscribe: (listener) => session.subscribe((event) => listener(event)),
847
+ abort: async () => await session.abort(),
848
+ dispose: async () => {
849
+ if (disposed)
850
+ return;
851
+ disposed = true;
852
+ let disposalFailure;
853
+ try {
854
+ await runtime.dispose();
855
+ }
856
+ catch (error) {
857
+ disposalFailure = error;
858
+ }
859
+ finally {
860
+ unsubscribeExtensionErrors?.();
861
+ }
862
+ if (disposalFailure !== undefined)
863
+ throw disposalFailure;
864
+ if (extensionFailure) {
865
+ throw new PiAgentGroupError(request.id, "could not settle child extensions", "cleanup failed");
866
+ }
867
+ },
868
+ get model() {
869
+ return session.model;
870
+ },
871
+ get thinkingLevel() {
872
+ return session.thinkingLevel;
873
+ },
874
+ };
875
+ }
876
+ catch (error) {
877
+ await runtime.dispose().catch(() => undefined);
878
+ unsubscribeExtensionErrors?.();
879
+ throw error;
880
+ }
881
+ }
882
+ function assertProviderRegistration(id, services, provider) {
883
+ if (services.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
884
+ throw new PiAgentGroupError(id, "could not register provider extension", "registration failed");
885
+ }
886
+ if (!services.modelRuntime.getRegisteredProviderIds().includes(provider)) {
887
+ throw new PiAgentGroupError(id, "could not register provider extension", provider);
888
+ }
889
+ }
890
+ function assertLoadedProfile(id, result, extensionPaths) {
891
+ if (result.errors.length > 0) {
892
+ throw new PiAgentGroupError(id, "could not load provider extension", "load failed");
893
+ }
894
+ const loaded = result.extensions
895
+ .map((extension) => path.resolve(extension.resolvedPath))
896
+ .toSorted();
897
+ const expected = extensionPaths.map((extensionPath) => path.resolve(extensionPath)).toSorted();
898
+ if (loaded.join("\0") !== expected.join("\0")) {
899
+ throw new PiAgentGroupError(id, "loaded unexpected extensions", `${loaded.length} loaded`);
900
+ }
901
+ for (const extension of result.extensions)
902
+ validateAdmittedExtension(extension, "extension");
903
+ }
904
+ function assertExactSession(request, session, dispatch) {
905
+ if (session.model?.provider !== dispatch.provider ||
906
+ session.model.id !== dispatch.modelId ||
907
+ session.thinkingLevel !== dispatch.thinkingLevel) {
908
+ throw new PiAgentGroupError(request.id, "selected a different model dispatch", session.model === undefined
909
+ ? "no model selected"
910
+ : `${session.model.provider}/${session.model.id} ${session.thinkingLevel}`);
911
+ }
912
+ }
913
+ function validateGroup(input, options) {
914
+ if (!Array.isArray(input) || input.length > MAX_AGENTS) {
915
+ throw new Error(`Pi agent group must contain at most ${MAX_AGENTS} requests`);
916
+ }
917
+ positiveInteger(options.maxConcurrency, "Pi agent maxConcurrency", MAX_CONCURRENCY);
918
+ const maxFinalChars = options.maxFinalChars ?? DEFAULT_FINAL_CHARS;
919
+ positiveInteger(maxFinalChars, "Pi agent maxFinalChars", MAX_FINAL_CHARS);
920
+ if (options.behaviorExtensionPaths !== undefined &&
921
+ (!Array.isArray(options.behaviorExtensionPaths) || options.behaviorExtensionPaths.length > 16)) {
922
+ throw new Error("Pi agent behaviorExtensionPaths must contain at most 16 paths");
923
+ }
924
+ const behaviorPaths = new Set();
925
+ for (const extensionPath of options.behaviorExtensionPaths ?? []) {
926
+ operationalText(extensionPath, "Pi agent behavior extension path", 4_000);
927
+ if (behaviorPaths.has(extensionPath)) {
928
+ throw new Error("Duplicate Pi agent behavior extension path");
929
+ }
930
+ behaviorPaths.add(extensionPath);
931
+ }
932
+ const ids = new Set();
933
+ const requests = input.map((request) => {
934
+ validateRequest(request);
935
+ if (ids.has(request.id))
936
+ throw new Error(`Duplicate Pi agent id: ${request.id}`);
937
+ ids.add(request.id);
938
+ return request;
939
+ });
940
+ return { requests, maxFinalChars };
941
+ }
942
+ function validateRequest(request) {
943
+ if (!REQUEST_ID.test(request.id))
944
+ throw new Error(`Invalid Pi agent id: ${request.id}`);
945
+ operationalText(request.role, "Pi agent role", 200);
946
+ nonEmpty(request.prompt, "Pi agent prompt", MAX_PROMPT_CHARS);
947
+ if (request.prompt.trimStart().startsWith("/")) {
948
+ throw new Error(`Pi agent ${request.id} prompt must not invoke an extension command`);
949
+ }
950
+ nonEmpty(request.cwd, "Pi agent cwd", 4_000);
951
+ if (!Array.isArray(request.tools) || request.tools.length === 0) {
952
+ throw new Error(`Pi agent ${request.id} requires at least one tool`);
953
+ }
954
+ const tools = new Set();
955
+ for (const tool of request.tools) {
956
+ if (!["read", "grep", "find", "ls"].includes(tool)) {
957
+ throw new Error(`Pi agent ${request.id} has unsupported tool: ${String(tool)}`);
958
+ }
959
+ if (tools.has(tool))
960
+ throw new Error(`Pi agent ${request.id} has duplicate tool: ${tool}`);
961
+ tools.add(tool);
962
+ }
963
+ positiveInteger(request.timeoutMs ?? DEFAULT_TIMEOUT_MS, `Pi agent ${request.id} timeoutMs`, MAX_TIMEOUT_MS);
964
+ if ((request.model === undefined) !== (request.thinkingLevel === undefined)) {
965
+ throw new Error(`Pi agent ${request.id} must override model and thinkingLevel together`);
966
+ }
967
+ if (request.model !== undefined) {
968
+ operationalText(request.model.provider, `Pi agent ${request.id} model provider`, 200);
969
+ operationalText(request.model.id, `Pi agent ${request.id} model id`, 500);
970
+ }
971
+ if (request.thinkingLevel !== undefined &&
972
+ !["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(request.thinkingLevel)) {
973
+ throw new Error(`Pi agent ${request.id} has unsupported thinking level`);
974
+ }
975
+ }
976
+ function eventPhase(event, tools) {
977
+ if (event.type === "message_update" && isRecord(event.assistantMessageEvent)) {
978
+ return event.assistantMessageEvent.type === "thinking_delta" ? "thinking" : undefined;
979
+ }
980
+ if (event.type !== "tool_execution_start" || typeof event.toolName !== "string")
981
+ return undefined;
982
+ return tools.includes(event.toolName) ? `tool: ${event.toolName}` : undefined;
983
+ }
984
+ function assistantMessage(event) {
985
+ if (event.type !== "message_end" || !isRecord(event.message))
986
+ return undefined;
987
+ return event.message.role === "assistant" ? event.message : undefined;
988
+ }
989
+ function finalAssistantText(id, message, maxChars) {
990
+ if (message === undefined)
991
+ throw new PiAgentGroupError(id, "returned no final output", "missing assistant message");
992
+ if (message.stopReason === "error" || message.stopReason === "aborted") {
993
+ throw new PiAgentGroupError(id, `stopped with ${String(message.stopReason)}`, typeof message.errorMessage === "string" ? message.errorMessage : "provider stopped");
994
+ }
995
+ const text = messageText(message.content).trim();
996
+ if (!text)
997
+ throw new PiAgentGroupError(id, "returned no final output", "assistant text is empty");
998
+ if (text.length > maxChars) {
999
+ throw new PiAgentGroupError(id, "final output is too large", `${text.length} characters`);
1000
+ }
1001
+ return text;
1002
+ }
1003
+ function messageText(content) {
1004
+ if (typeof content === "string")
1005
+ return content;
1006
+ if (!Array.isArray(content))
1007
+ return "";
1008
+ return content
1009
+ .map((part) => isRecord(part) && part.type === "text" && typeof part.text === "string" ? part.text : "")
1010
+ .filter(Boolean)
1011
+ .join("\n");
1012
+ }
1013
+ async function publishQueuedCancellations(requests, started, options) {
1014
+ if (options.onLifecycle === undefined)
1015
+ return;
1016
+ await Promise.all(requests.map(async (request, index) => {
1017
+ if (started.has(index))
1018
+ return;
1019
+ await options.onLifecycle?.({
1020
+ id: request.id,
1021
+ role: request.role,
1022
+ state: "cancelled",
1023
+ phase: "cancelled",
1024
+ elapsedMs: 0,
1025
+ });
1026
+ })).catch(() => undefined);
1027
+ }
1028
+ function normalizeAgentError(id, error, abortKind, signal) {
1029
+ if (error instanceof PiAgentGroupError)
1030
+ return error;
1031
+ if (abortKind === "timeout")
1032
+ return new PiAgentGroupError(id, "timed out", errorMessage(error));
1033
+ if (abortKind === "cancelled" || signal.aborted)
1034
+ return cancellationError(id, signal.reason);
1035
+ return new PiAgentGroupError(id, "failed", errorMessage(error));
1036
+ }
1037
+ function terminalState(error) {
1038
+ if (error === undefined)
1039
+ return "completed";
1040
+ return error instanceof PiAgentGroupError && error.code === "cancelled" ? "cancelled" : "failed";
1041
+ }
1042
+ function cancellationError(id, reason) {
1043
+ return new PiAgentGroupError(id, "cancelled", cancellationReason(reason));
1044
+ }
1045
+ function cancellationReason(reason) {
1046
+ return reason === undefined ? "operation cancelled" : errorMessage(reason);
1047
+ }
1048
+ function abortSuffix(error) {
1049
+ return error === undefined ? "" : `; abort failed: ${bounded(errorMessage(error))}`;
1050
+ }
1051
+ function modelName(model) {
1052
+ if (model === undefined)
1053
+ throw new Error("No usable Pi model is configured");
1054
+ const value = `${model.provider}/${model.id}`;
1055
+ operationalText(value, "Pi model identity", 700);
1056
+ return value;
1057
+ }
1058
+ function positiveInteger(value, label, max) {
1059
+ if (!Number.isSafeInteger(value) || value < 1 || value > max) {
1060
+ throw new Error(`${label} must be an integer from 1 through ${max}`);
1061
+ }
1062
+ }
1063
+ function operationalText(value, label, max) {
1064
+ nonEmpty(value, label, max);
1065
+ if ([...value].some((character) => {
1066
+ const code = character.codePointAt(0);
1067
+ return code < 32 || (code >= 127 && code <= 159);
1068
+ })) {
1069
+ throw new Error(`${label} must not contain control characters`);
1070
+ }
1071
+ }
1072
+ function nonEmpty(value, label, max) {
1073
+ if (typeof value !== "string" || value.trim().length === 0 || value.length > max) {
1074
+ throw new Error(`${label} must be a non-empty string with at most ${max} characters`);
1075
+ }
1076
+ }
1077
+ function bounded(value) {
1078
+ const text = value.trim() || "unknown failure";
1079
+ return text.length <= MAX_ERROR_CHARS ? text : `${text.slice(0, MAX_ERROR_CHARS)}…`;
1080
+ }
1081
+ function errorMessage(error) {
1082
+ return error instanceof Error ? error.message : String(error);
1083
+ }
1084
+ function isRecord(value) {
1085
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1086
+ }
1087
+ //# sourceMappingURL=pi-agent-group.js.map