@marcoscale98/pi-extensible-workflows 5.17.0-fork.1 → 6.2.1-fork.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 (128) hide show
  1. package/CHANGELOG.md +145 -0
  2. package/README.md +2 -2
  3. package/dist/src/agent-execution.d.ts +43 -10
  4. package/dist/src/agent-execution.js +442 -162
  5. package/dist/src/background-widget.d.ts +5 -1
  6. package/dist/src/background-widget.js +5 -10
  7. package/dist/src/decoders.d.ts +2 -3
  8. package/dist/src/decoders.js +59 -37
  9. package/dist/src/execution.d.ts +6 -2
  10. package/dist/src/execution.js +48 -7
  11. package/dist/src/host-delivery.js +5 -8
  12. package/dist/src/host-navigator.js +5 -2
  13. package/dist/src/host-phases.d.ts +1 -0
  14. package/dist/src/host-recovery.d.ts +11 -8
  15. package/dist/src/host-recovery.js +14 -22
  16. package/dist/src/host-runtime.js +56 -5
  17. package/dist/src/host-view.d.ts +6 -1
  18. package/dist/src/host-view.js +46 -20
  19. package/dist/src/host.d.ts +2 -2
  20. package/dist/src/host.js +246 -118
  21. package/dist/src/index.d.ts +1 -2
  22. package/dist/src/index.js +1985 -1690
  23. package/dist/src/index.js.map +3 -3
  24. package/dist/src/paths.d.ts +8 -5
  25. package/dist/src/paths.js +24 -13
  26. package/dist/src/persistence.d.ts +1 -0
  27. package/dist/src/persistence.js +1 -0
  28. package/dist/src/pi-runtime-runner.js +4 -1
  29. package/dist/src/registry.d.ts +9 -7
  30. package/dist/src/registry.js +29 -71
  31. package/dist/src/runtime/agent-runner.d.ts +2 -0
  32. package/dist/src/script-tools.d.ts +27 -0
  33. package/dist/src/script-tools.js +87 -0
  34. package/dist/src/session-lease.d.ts +1 -1
  35. package/dist/src/session-lease.js +45 -11
  36. package/dist/src/settings.d.ts +19 -3
  37. package/dist/src/settings.js +63 -50
  38. package/dist/src/store.d.ts +3 -1
  39. package/dist/src/store.js +17 -1
  40. package/dist/src/trajectory.d.ts +12 -3
  41. package/dist/src/trajectory.js +32 -11
  42. package/dist/src/types.d.ts +129 -45
  43. package/dist/src/types.js +5 -8
  44. package/dist/src/utils.d.ts +21 -1
  45. package/dist/src/utils.js +46 -6
  46. package/dist/src/validation.d.ts +14 -3
  47. package/dist/src/validation.js +143 -87
  48. package/dist/starter/index.js +8 -11
  49. package/dist/starter/index.js.map +1 -1
  50. package/dist/starter/review-loop.d.ts +0 -13
  51. package/dist/starter/review-loop.js +8 -9
  52. package/dist/subagents/index.js +904 -956
  53. package/dist/subagents/index.js.map +3 -3
  54. package/dist/subagents/src/contracts.d.ts +48 -31
  55. package/dist/subagents/src/contracts.js +20 -9
  56. package/dist/subagents/src/index.d.ts +1 -1
  57. package/dist/subagents/src/index.js +14 -8
  58. package/dist/subagents/src/manager.js +52 -73
  59. package/dist/subagents/src/navigator.js +3 -14
  60. package/dist/subagents/src/view.d.ts +1 -1
  61. package/dist/subagents/src/view.js +15 -68
  62. package/dist/subagents/src/worktree.js +1 -1
  63. package/dist/trajectory/assets/index.html +35 -13
  64. package/dist/trajectory/index.js +576 -932
  65. package/dist/trajectory/index.js.map +3 -3
  66. package/dist/trajectory/src/assets/index.html +35 -13
  67. package/dist/trajectory/src/index.js +134 -104
  68. package/dist/trajectory/src/server.js +268 -27
  69. package/dist/trajectory/src/server.js.map +3 -3
  70. package/dist/trajectory/src/startup-mutex.d.ts +17 -0
  71. package/dist/trajectory/src/startup-mutex.js +172 -0
  72. package/examples/workflow-extension-template/README.md +4 -7
  73. package/examples/workflow-extension-template/extension.test.mjs +4 -8
  74. package/examples/workflow-extension-template/index.js +0 -3
  75. package/package.json +10 -10
  76. package/skills/pi-extensible-workflows/SKILL.md +12 -8
  77. package/src/agent-execution.ts +412 -164
  78. package/src/background-widget.ts +5 -11
  79. package/src/decoders.ts +51 -37
  80. package/src/execution.ts +46 -9
  81. package/src/host-delivery.ts +5 -8
  82. package/src/host-navigator.ts +4 -2
  83. package/src/host-phases.ts +1 -1
  84. package/src/host-recovery.ts +23 -28
  85. package/src/host-runtime.ts +44 -6
  86. package/src/host-view.ts +41 -16
  87. package/src/host.ts +229 -142
  88. package/src/index.ts +1 -2
  89. package/src/paths.ts +18 -9
  90. package/src/persistence.ts +1 -0
  91. package/src/pi-runtime-runner.ts +3 -1
  92. package/src/registry.ts +28 -64
  93. package/src/runtime/agent-runner.ts +2 -0
  94. package/src/script-tools.ts +76 -0
  95. package/src/session-lease.ts +34 -6
  96. package/src/settings.ts +56 -50
  97. package/src/store.ts +18 -2
  98. package/src/trajectory.ts +32 -15
  99. package/src/types.ts +82 -34
  100. package/src/utils.ts +42 -5
  101. package/src/validation.ts +121 -80
  102. package/starter/index.ts +0 -1
  103. package/starter/review-loop.ts +8 -9
  104. package/starter/roles/developer.md +0 -1
  105. package/starter/roles/oracle.md +0 -1
  106. package/starter/roles/researcher.md +0 -1
  107. package/starter/roles/reviewer.md +0 -1
  108. package/starter/roles/scout.md +0 -1
  109. package/subagents/README.md +10 -16
  110. package/subagents/src/contracts.ts +21 -11
  111. package/subagents/src/index.ts +17 -8
  112. package/subagents/src/manager.ts +48 -128
  113. package/subagents/src/navigator.ts +4 -13
  114. package/subagents/src/view.ts +16 -63
  115. package/subagents/src/worktree.ts +1 -1
  116. package/trajectory/src/assets/index.html +35 -13
  117. package/trajectory/src/index.ts +112 -91
  118. package/trajectory/src/server.ts +46 -10
  119. package/trajectory/src/startup-mutex.ts +127 -0
  120. package/dist/src/roles.d.ts +0 -70
  121. package/dist/src/roles.js +0 -325
  122. package/dist/starter/roles/developer.md +0 -6
  123. package/dist/starter/roles/oracle.md +0 -18
  124. package/dist/starter/roles/researcher.md +0 -20
  125. package/dist/starter/roles/reviewer.md +0 -19
  126. package/dist/starter/roles/scout.md +0 -18
  127. package/examples/workflow-extension-template/roles/reviewer.md +0 -4
  128. package/src/roles.ts +0 -326
package/src/validation.ts CHANGED
@@ -2,17 +2,14 @@ import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
3
  import * as acorn from "acorn";
4
4
  import { Script } from "node:vm";
5
- import type { AgentDefinition, CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
5
+ import type { CheckpointInput, JsonSchema, JsonValue, PreflightCapabilities, PreflightResult, ShellOptions, StaticWorkflowCall, StaticWorkflowExecution, StaticWorkflowScope, ValidatedWorkflowLaunch, WorkflowCallKind, WorkflowErrorCode, WorkflowMetadata, WorkflowValidationContext, WorkflowValidationParameters } from "./types.js";
6
6
  import type { WorkflowRegistryApi } from "./registry.js";
7
- import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, mergeWorkflowExtensionSettings, modelAliasName, modelCapability, object, positiveInteger, resolveModelReference, resourcePatternHasMagic, unknownModel } from "./utils.js";
7
+ import { assertModelThinking, validateSchema, deepFreeze, errorText, fail, jsonObject, jsonValue, object, positiveInteger, resourcePatternHasMagic } from "./utils.js";
8
8
  import { WORKFLOW_CALL_KINDS } from "./types.js";
9
9
  export { validateSchema } from "./utils.js";
10
- import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
11
- export { loadAgentDefinitions, loadProjectAgentDefinitions, parseRoleMarkdown, workflowRoleDirectories } from "./roles.js";
12
- export type { WorkflowRoleDirectoryInput } from "./roles.js";
13
10
 
14
11
  import { validateContextFileScopes, validateSelectorList } from "./settings.js";
15
- export { DEFAULT_SETTINGS, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath } from "./settings.js";
12
+ export { DEFAULT_SETTINGS, loadCodemodeToolsSetting, loadSettings, loadSettingsOverrides, resolveAgentResourcePolicy, resolveWorkflowSettings, saveModelAliases, validateContextFileScopes, validateModelAliasAvailability, validateSelectorList, validateWorkflowExtensionSettings, workflowProjectSettingsPath, workflowSettingsPath, workflowToolExposure } from "./settings.js";
16
13
 
17
14
  export function validateCheckpoint(value: unknown): CheckpointInput {
18
15
  if (!object(value) || Object.keys(value).some((key) => !["name", "prompt", "context"].includes(key)) || typeof value.name !== "string" || value.name.trim() === "" || typeof value.prompt !== "string" || !jsonValue(value.context)) fail("INVALID_METADATA", "checkpoint requires only name, prompt, and JSON context");
@@ -21,22 +18,6 @@ export function validateCheckpoint(value: unknown): CheckpointInput {
21
18
  return { name: value.name, prompt: value.prompt, context: value.context };
22
19
  }
23
20
 
24
- function validateRolePolicies(definitions: Readonly<Record<string, AgentDefinition>>, roles: readonly string[], availableModels: ReadonlySet<string>, aliases: Readonly<Record<string, string>> = {}, knownModels = availableModels, settingsPath?: string): void {
25
- for (const role of roles) {
26
- const definition = definitions[role];
27
- if (!definition) continue;
28
- if (definition.model !== undefined) {
29
- const resolved = modelCapability(definition.model, aliases, knownModels, settingsPath);
30
- if (!availableModels.has(resolved)) {
31
- if (modelAliasName(definition.model, aliases)) unknownModel(definition.model, resolved, settingsPath);
32
- fail("UNKNOWN_MODEL", `Unknown model for role ${role}: ${resolved}`);
33
- }
34
- }
35
- // Role tools absent from the session are tolerated at launch: the runtime resolve emits a
36
- // warning and the agent runs without them. Doctor still reports unmatched patterns.
37
- }
38
- }
39
-
40
21
  function validateWorkflowMetadata(value: unknown): WorkflowMetadata {
41
22
  if (!object(value) || typeof value.name !== "string" || value.name.trim() === "") fail("INVALID_METADATA", "Workflow metadata requires a non-empty name");
42
23
  if (value.description !== undefined && (typeof value.description !== "string" || value.description.trim() === "")) fail("INVALID_METADATA", "Workflow description must be a non-empty string when provided");
@@ -219,6 +200,24 @@ function callHasTrailingComma(source: string, call: WorkflowCall): boolean {
219
200
  return current?.type.label === ")" && previous?.type.label === ",";
220
201
  }
221
202
 
203
+ /**
204
+ * The `tools.<id>` members a workflow script names statically. Any other use of `tools`, such as a
205
+ * local declaration that shadows the global or passing it around, makes the set unknowable: undefined.
206
+ */
207
+ export function scriptToolReferences(script: string): string[] | undefined {
208
+ const body = workflowBody(script);
209
+ if (!body.trim()) return [];
210
+ const names = new Set<string>();
211
+ const opaque = (node: acorn.AnyNode, parent?: acorn.AnyNode): boolean => {
212
+ if (node.type === "Identifier" && node.name === "tools") {
213
+ if (parent?.type === "MemberExpression" && parent.object === node && !parent.computed && parent.property.type === "Identifier") names.add(parent.property.name);
214
+ else if (!(parent?.type === "Property" && parent.key === node && !parent.computed && !parent.shorthand || parent?.type === "MemberExpression" && parent.property === node && !parent.computed)) return true;
215
+ }
216
+ return astChildren(node).some((child) => opaque(child, node));
217
+ };
218
+ return opaque(parseWorkflow(body)) ? undefined : [...names];
219
+ }
220
+
222
221
  export function instrumentWorkflow(script: string): string {
223
222
  const body = workflowBody(script);
224
223
  if (!body.trim()) return body;
@@ -291,8 +290,9 @@ export function workflowPrompt(template: string, values: Readonly<Record<string,
291
290
  });
292
291
  }
293
292
 
294
- const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "skills", "extensions", "contextFiles", "role", "outputSchema", "retries", "timeoutMs"]);
295
- function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Record<string, string>>, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
293
+ const AGENT_OPTION_KEYS = new Set(["label", "model", "tools", "excludeTools", "skills", "extensions", "contextFiles", "outputSchema", "retries", "timeoutMs"]);
294
+ export function isCoreAgentOption(key: string): boolean { return AGENT_OPTION_KEYS.has(key); }
295
+ function validateAgentOption(key: string, value: unknown): void {
296
296
  switch (key) {
297
297
  case "label":
298
298
  if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent label must be a non-empty string");
@@ -300,19 +300,19 @@ function validateAgentOption(key: string, value: unknown, aliases?: Readonly<Rec
300
300
  case "model":
301
301
  if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent model must be a non-empty string");
302
302
  assertModelThinking(value, "agent model");
303
- if (aliases !== undefined) resolveModelReference(value, aliases, knownModels, settingsPath);
304
303
  break;
305
304
  case "tools":
306
305
  case "skills":
307
306
  case "extensions":
308
307
  validateSelectorList(value, "agent options", key, "INVALID_METADATA", key !== "extensions");
309
308
  break;
309
+ case "excludeTools":
310
+ if (!Array.isArray(value) || value.some((tool) => typeof tool !== "string" || !tool.trim() || tool !== tool.trim() || tool.startsWith("!") || resourcePatternHasMagic(tool))) fail("INVALID_METADATA", "agent excludeTools must be an array of exact tool names");
311
+ if (value.includes("workflow_result")) fail("INVALID_METADATA", "agent excludeTools cannot remove workflow_result");
312
+ break;
310
313
  case "contextFiles":
311
314
  validateContextFileScopes(value, "agent options");
312
315
  break;
313
- case "role":
314
- if (typeof value !== "string" || !value.trim()) fail("INVALID_METADATA", "agent role must be a non-empty string");
315
- break;
316
316
  case "outputSchema":
317
317
  validateSchema(value, "agent outputSchema");
318
318
  break;
@@ -411,36 +411,101 @@ export function inspectWorkflowScript(script: string): StaticWorkflowCall[] {
411
411
  if (value.known && jsonValue(value.value)) knownOptionEntries.push([key, value.value]);
412
412
  }
413
413
  const knownOptions: Record<string, JsonValue> = Object.fromEntries(knownOptionEntries);
414
- const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")), role: staticString(propertyNode(options, "role")) };
414
+ const base = { ...placement, kind, start: call.start, end: call.end, name: null, prompt: staticString(first), model: staticString(propertyNode(options, "model")), label: staticString(propertyNode(options, "label")) };
415
415
  return { ...base, ...(retries.known && typeof retries.value === "number" ? { retries: retries.value } : {}), ...(staticOutputSchema === undefined ? {} : { outputSchema: staticOutputSchema }), ...(optionKeys.length ? { options: knownOptions, optionKeys } : {}) };
416
416
  }
417
- if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null, role: null };
418
- if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
419
- return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null, role: null };
417
+ if (kind === "checkpoint") return { ...placement, kind, start: call.start, end: call.end, name: staticString(propertyNode(first, "name")), prompt: staticString(propertyNode(first, "prompt")), model: null };
418
+ if (kind === "shell") return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
419
+ return { ...placement, kind, start: call.start, end: call.end, name: staticString(first), prompt: null, model: null };
420
420
  });
421
421
  }
422
422
 
423
- function validateStaticAgentOptions(node: acorn.AnyNode | undefined, aliases: Readonly<Record<string, string>> = {}, knownModels?: ReadonlySet<string>, settingsPath?: string): void {
424
- if (node?.type !== "ObjectExpression") return;
425
- for (const key of AGENT_OPTION_KEYS) {
426
- const value = staticValue(propertyNode(node, key));
427
- if (value.known) validateAgentOption(key, value.value, aliases, knownModels, settingsPath);
423
+ /** Every option is a preparation input, because any hook may read it; one dynamic value leaves the whole call to runtime. */
424
+ function staticPreparationOptions(node: acorn.AnyNode | undefined, omitted?: string): Record<string, JsonValue> | undefined {
425
+ if (node?.type !== "ObjectExpression") return undefined;
426
+ const options = new Map<string, JsonValue>();
427
+ for (const property of node.properties) {
428
+ if (property.type === "SpreadElement" || property.computed) return undefined;
429
+ const key = propertyKeyName(property);
430
+ if (key === undefined) return undefined;
431
+ if (key === omitted) continue;
432
+ const value = staticValue(property.value);
433
+ // NOTE: an output schema only shapes the result, so a computed one (a const declared elsewhere) does not hide the call.
434
+ if (key === "outputSchema" && !value.known) continue;
435
+ if (!value.known || !jsonValue(value.value)) return undefined;
436
+ options.set(key, value.value);
428
437
  }
438
+ return Object.fromEntries(options);
439
+ }
440
+ /**
441
+ * A handle prepares its first turn with the create options merged with that send's options, so create options are complete
442
+ * only when every use of the handle provably passes no send options: `agent.create(...).send(prompt)` directly, or a
443
+ * `const` handle whose every reference is `handle.send(prompt)`. Computed access, destructuring, aliases or any other
444
+ * use leave the handle to runtime.
445
+ */
446
+ function handlesWithCompleteOptions(program: acorn.Program): acorn.CallExpression[] {
447
+ const parents = new Map<acorn.AnyNode, acorn.AnyNode>();
448
+ const references = new Map<string, acorn.AnyNode[]>();
449
+ const visit = (node: acorn.AnyNode): void => {
450
+ if (node.type === "Identifier") references.set(node.name, [...(references.get(node.name) ?? []), node]);
451
+ for (const child of astChildren(node)) { parents.set(child, node); visit(child); }
452
+ };
453
+ visit(program);
454
+ const plainSend = (member: acorn.AnyNode | undefined): boolean => {
455
+ const call = member ? parents.get(member) : undefined;
456
+ return member?.type === "MemberExpression" && !member.computed && member.property.type === "Identifier" && member.property.name === "send"
457
+ && call?.type === "CallExpression" && call.callee === member && call.arguments.length <= 1 && call.arguments.every((argument) => argument.type !== "SpreadElement");
458
+ };
459
+ return agentCreateCalls(program).filter((create) => {
460
+ const parent = parents.get(create);
461
+ if (parent?.type === "MemberExpression" && parent.object === create) return plainSend(parent);
462
+ const declaration = parent ? parents.get(parent) : undefined;
463
+ if (parent?.type !== "VariableDeclarator" || parent.init !== create || parent.id.type !== "Identifier" || declaration?.type !== "VariableDeclaration" || declaration.kind !== "const") return false;
464
+ const name = parent.id.name;
465
+ return (references.get(name) ?? []).every((reference) => reference === parent.id || (parents.get(reference)?.type === "MemberExpression" && (parents.get(reference) as acorn.MemberExpression).object === reference && plainSend(parents.get(reference))));
466
+ });
429
467
  }
430
- function hasDynamicAgentRole(node: acorn.AnyNode | undefined): boolean {
431
- if (!node) return false;
432
- if (node.type !== "ObjectExpression") return true;
433
- for (let index = node.properties.length - 1; index >= 0; index -= 1) {
434
- const property = node.properties[index];
435
- if (!property || property.type === "SpreadElement" || property.computed) return true;
436
- const key = propertyKeyName(property);
437
- if (key === "role") {
438
- const roleValue = staticValue(property.value);
439
- if (roleValue.known && typeof roleValue.value === "string") return false;
440
- return true;
468
+ /**
469
+ * Distinct, completely static option objects of agent(...) and agent.create(...) calls, so launch can inspect them
470
+ * before any effect. Calls with any dynamic option stay runtime-checked, and so do handles whose sends may add options.
471
+ */
472
+ export function staticAgentPreparationOptions(script: string, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>> = {}): Array<Readonly<Record<string, JsonValue>>> {
473
+ const program = parseWorkflow(script);
474
+ const sources = [program, ...registeredFunctionPrograms(program, registered)];
475
+ const distinct = new Map<string, Readonly<Record<string, JsonValue>>>();
476
+ for (const source of sources) for (const options of programAgentOptions(source)) distinct.set(JSON.stringify(options), deepFreeze(options));
477
+ return [...distinct.values()];
478
+ }
479
+ /** Programs of the registered functions the script calls by name, so their agents are inspected before launch like inline ones. */
480
+ function registeredFunctionPrograms(program: acorn.Program, registered: Readonly<Record<string, { run: (...args: never[]) => unknown }>>): acorn.Program[] {
481
+ const called = new Set<string>();
482
+ const visit = (node: acorn.AnyNode): void => {
483
+ if (node.type === "CallExpression" && node.callee.type === "Identifier" && Object.prototype.hasOwnProperty.call(registered, node.callee.name)) called.add(node.callee.name);
484
+ for (const child of astChildren(node)) visit(child);
485
+ };
486
+ visit(program);
487
+ const programs: acorn.Program[] = [];
488
+ for (const name of called) {
489
+ const source = registered[name]?.run.toString() ?? "";
490
+ // A method prints as `async run(...) {}`, which only parses inside an object literal.
491
+ for (const wrapped of [`(${source})`, `({${source}})`]) {
492
+ try { programs.push(acorn.parse(wrapped, { ecmaVersion: "latest", sourceType: "module" })); break; } catch { /* try the next form; an unparsable function stays runtime-checked */ }
441
493
  }
442
494
  }
443
- return false;
495
+ return programs;
496
+ }
497
+ function programAgentOptions(program: acorn.Program): Array<Readonly<Record<string, JsonValue>>> {
498
+ const calls = workflowCalls(program).filter((call) => call.callee.name === "agent").map((call) => call.arguments.some((argument) => argument.type === "SpreadElement") ? undefined : call.arguments.length < 2 ? {} : staticPreparationOptions(callArgument(call, 1)));
499
+ const handles = handlesWithCompleteOptions(program).map((call) => staticPreparationOptions(callArgument(call, 0), "name"));
500
+ return [...calls, ...handles].filter((options): options is Record<string, JsonValue> => options !== undefined);
501
+ }
502
+
503
+ function validateStaticAgentOptions(node: acorn.AnyNode | undefined): void {
504
+ if (node?.type !== "ObjectExpression") return;
505
+ for (const key of AGENT_OPTION_KEYS) {
506
+ const value = staticValue(propertyNode(node, key));
507
+ if (value.known) validateAgentOption(key, value.value);
508
+ }
444
509
  }
445
510
  function validateStaticShellOptions(call: WorkflowCall): void {
446
511
  if (call.arguments.some((argument) => argument.type === "SpreadElement")) return;
@@ -477,7 +542,7 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
477
542
  const phases = calls.filter((call) => call.callee.name === "phase").map((call) => literalString(call.arguments[0])).filter((phase): phase is string => phase !== undefined);
478
543
  for (const call of calls) {
479
544
  const operation = call.callee.name;
480
- if (operation === "agent") validateStaticAgentOptions(call.arguments[1], capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
545
+ if (operation === "agent") validateStaticAgentOptions(call.arguments[1]);
481
546
  if (operation === "withWorktree") validateStaticWithWorktree(call, compatibility);
482
547
  if (operation === "shell") validateStaticShellOptions(call);
483
548
  if ((operation === "parallel" || operation === "pipeline") && call.arguments.some((argument) => argument.type === "SpreadElement")) continue;
@@ -488,11 +553,10 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
488
553
  const handleOptions = agentCreateCalls(program).map((call) => {
489
554
  const options = call.arguments.length === 1 ? callArgument(call, 0) : undefined;
490
555
  if (options?.type !== "ObjectExpression" || !literalString(propertyNode(options, "name"))?.trim()) fail("INVALID_METADATA", "agent.create requires an options object with a stable explicit name");
491
- validateStaticAgentOptions(options, capabilities.modelAliases ?? {}, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath);
556
+ validateStaticAgentOptions(options);
492
557
  return options;
493
558
  });
494
559
  const agentOptions = [...calls.filter((call) => call.callee.name === "agent").map((call) => callArgument(call, 1)), ...handleOptions];
495
- const dynamicAgentRoles = agentOptions.some((options) => hasDynamicAgentRole(options));
496
560
  const staticSchemas: JsonSchema[] = [];
497
561
  for (const options of agentOptions) {
498
562
  const value = staticValue(propertyNode(options, "outputSchema"));
@@ -502,25 +566,16 @@ export function preflight(script: string, capabilities: PreflightCapabilities, s
502
566
  staticSchemas.push(schema);
503
567
  }
504
568
  checkedSchemas.push(...staticSchemas);
505
- const modelRefs = agentOptions.flatMap((options) => { const requested = literalString(propertyNode(options, "model")); return requested === undefined ? [] : [{ requested, resolved: modelCapability(requested, capabilities.modelAliases, capabilities.knownModels ?? capabilities.models, capabilities.settingsPath) }]; });
506
- const models = modelRefs.map(({ resolved }) => resolved);
569
+ const models: string[] = [];
507
570
  const tools = agentOptions.flatMap((options) => {
508
571
  const value = propertyNode(options, "tools");
509
572
  return value?.type === "ArrayExpression" ? value.elements.flatMap((element) => { const tool = element && element.type !== "SpreadElement" ? literalString(element) : undefined; return tool === undefined ? [] : [tool]; }) : [];
510
573
  });
511
- const agentTypes = agentOptions.flatMap((options) => { const value = staticString(propertyNode(options, "role")); return value === null ? [] : [value]; });
512
574
  for (const pattern of tools) {
513
575
  const body = pattern.startsWith("!") ? pattern.slice(1) : pattern;
514
576
  if (!pattern.startsWith("!") && !resourcePatternHasMagic(pattern) && !capabilities.tools.has(body)) fail("UNKNOWN_TOOL", `Unknown tool: ${body}`);
515
577
  }
516
- const missingModel = capabilities.skipModelAvailability ? undefined : modelRefs.find(({ resolved }) => !capabilities.models.has(resolved));
517
- if (missingModel) {
518
- if (modelAliasName(missingModel.requested, capabilities.modelAliases ?? {})) unknownModel(missingModel.requested, missingModel.resolved, capabilities.settingsPath);
519
- fail("UNKNOWN_MODEL", `Unknown model: ${missingModel.resolved}`);
520
- }
521
- const missingType = agentTypes.find((type) => !capabilities.agentTypes.has(type));
522
- if (missingType) fail("UNKNOWN_AGENT_TYPE", `Unknown agent type: ${missingType}`);
523
- return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools, agentTypes }), schemas: deepFreeze(checkedSchemas), dynamicAgentRoles });
578
+ return Object.freeze({ metadata: deepFreeze(checkedMetadata), referenced: deepFreeze({ phases, models, tools }), schemas: deepFreeze(checkedSchemas) });
524
579
  }
525
580
 
526
581
  export function validateWorkflowLaunch(params: WorkflowValidationParameters, context: WorkflowValidationContext, registry?: WorkflowRegistryApi): ValidatedWorkflowLaunch {
@@ -544,23 +599,9 @@ export function validateWorkflowLaunchWithRegistry(params: WorkflowValidationPar
544
599
  const script = typeof params.script === "string" && params.script.trim() ? params.script : fileScript ?? "";
545
600
  if (!script) fail("INVALID_SYNTAX", "Provide script or scriptPath");
546
601
  const metadata = validateWorkflowMetadata({ name: explicitName, ...(typeof params.description === "string" ? { description: params.description } : {}) });
547
- const globalAgentDefinitions = loadAgentDefinitions(context.cwd, context.agentDir, false, registry && typeof registry.roleDirectoryRegistrations === "function" ? registry.roleDirectoryRegistrations() : registry && typeof registry.roleDirectories === "function" ? registry.roleDirectories() : undefined);
548
- const projectAgentDefinitions = context.projectTrusted ? loadProjectAgentDefinitions(context.cwd) : {};
549
- const agentDefinitions = deepFreeze({ ...globalAgentDefinitions, ...projectAgentDefinitions });
550
- const aliases = context.modelAliases ?? {};
551
- const knownModels = context.knownModels ?? context.availableModels;
552
- const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, agentTypes: new Set(Object.keys(agentDefinitions)), modelAliases: aliases, knownModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
553
- const roleNames = checked.dynamicAgentRoles ? Object.keys(agentDefinitions) : checked.referenced.agentTypes;
554
- validateRolePolicies(agentDefinitions, roleNames, context.availableModels, aliases, knownModels, context.settingsPath);
555
- if (registry?.validateExtensionSettings) {
556
- const validate = registry.validateExtensionSettings.bind(registry);
557
- const validatorContext = (source: "effective" | "role", role?: string) => ({ source, cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}), ...(role === undefined ? {} : { role }) });
558
- validate(context.extensionSettings, validatorContext("effective"));
559
- for (const role of roleNames) {
560
- validate(mergeWorkflowExtensionSettings(context.extensionSettings, agentDefinitions[role]?.extensionSettings), validatorContext("role", role));
561
- }
562
- }
563
- return { script, checked, agentDefinitions, projectAgentDefinitions, roleNames };
602
+ const checked = preflight(script, { models: context.availableModels, tools: context.rootTools, modelAliases: context.modelAliases ?? {}, knownModels: context.knownModels ?? context.availableModels, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) }, [], metadata);
603
+ registry?.validateExtensionSettings(context.extensionSettings, { source: "effective", cwd: context.cwd, projectTrusted: context.projectTrusted, ...(context.settingsPath ? { settingsPath: context.settingsPath } : {}) });
604
+ return { script, checked };
564
605
  }
565
606
 
566
607
  export { createLaunchSnapshot, loadLaunchSnapshot } from "./utils.js";
package/starter/index.ts CHANGED
@@ -27,7 +27,6 @@ const extension: WorkflowExtension = {
27
27
  resolve: ({ rootModel }) => `${rootModel.provider}/${rootModel.model}`,
28
28
  },
29
29
  },
30
- roleDirectories: [new URL("./roles/", import.meta.url)],
31
30
  };
32
31
 
33
32
  export default function (): void {
@@ -17,27 +17,28 @@ export const reviewLoop = defineWorkflowFunction({
17
17
  );
18
18
  let devResult: JsonValue = null;
19
19
  let review: Static<typeof reviewSchema> = { pass: false, findings: [] };
20
+ // One developer keeps its transcript across iterations; each reviewer starts fresh.
21
+ const developer = agent.create({ name: "developer", label: "Developer", role: "developer" });
20
22
 
21
23
  for (let iterations = 1; iterations <= maxIterations; iterations += 1) {
22
24
  const devPrompt =
23
25
  iterations === 1
24
26
  ? prompt("Implement this task:\n\n{task}", { task })
25
27
  : prompt(
26
- `Address the previous review findings and complete the task.
27
- <original_task>{task}</original_task>
28
+ `Address the latest review findings and complete the original task.
28
29
  <last_review>{review}</last_review>`,
29
- { task, review: review.findings },
30
+ { review: review.findings },
30
31
  );
31
- devResult = await agent(devPrompt, { role: "developer" });
32
+ devResult = await developer.send(devPrompt);
32
33
  review = await agent(
33
34
  prompt(
34
- `Review the implementation against the task. Set pass=true only when the task is complete and correct. The developer may have addressed a previous review run of yours. So its summary is related to the last round of review if present.
35
+ `Review the actual current implementation against the entire original task. Inspect the code and run relevant checks yourself. Do not rely on the developer summary or on previous findings; they may be incomplete or wrong. Set pass=true only when the whole task is complete and correct.
35
36
  <original_task>{task}</original_task>
36
- <last_review>{review}</last_review>
37
+ <previous_review_findings>{review}</previous_review_findings>
37
38
  <dev_summary>{devResult}</dev_summary>`,
38
39
  { task, devResult, review: review.findings },
39
40
  ),
40
- { role: "reviewer", outputSchema: reviewSchema },
41
+ { label: `Reviewer ${String(iterations)}`, role: "reviewer", outputSchema: reviewSchema },
41
42
  );
42
43
 
43
44
  if (review.pass) return { pass: true, iterations, devResult, review };
@@ -69,5 +70,3 @@ export const reviewLoop = defineWorkflowFunction({
69
70
  ),
70
71
  });
71
72
 
72
- export const reviewLoopInputSchema = reviewLoop.input;
73
- export const reviewLoopOutputSchema = reviewLoop.output;
@@ -1,5 +1,4 @@
1
1
  ---
2
- model: developer-model
3
2
  description: Developer focused agent
4
3
  ---
5
4
 
@@ -1,5 +1,4 @@
1
1
  ---
2
- model: oracle-model
3
2
  tools: ["!*", "read", "grep", "find", "ls", "bash"]
4
3
  description: Oracle. Use for a second opinion on a plan, design, or decision before committing to it
5
4
  ---
@@ -1,5 +1,4 @@
1
1
  ---
2
- model: researcher-model
3
2
  tools: ["!edit", "!write", "!bash"]
4
3
  description: Researcher. Use for deep research across the codebase, docs, and the web when web tools are available
5
4
  ---
@@ -1,5 +1,4 @@
1
1
  ---
2
- model: reviewer-model
3
2
  tools: ["!*", "read", "grep", "find", "ls"]
4
3
  description: Reviewer. Use when we need to review decisions or code changes
5
4
  ---
@@ -1,5 +1,4 @@
1
1
  ---
2
- model: scout-model
3
2
  tools: ["!*", "read", "grep", "find", "ls"]
4
3
  description: Scout. Use for fast read-only codebase recon before planning or implementing
5
4
  ---
@@ -18,7 +18,7 @@ Use the built-in subagent tools for focused, independent tasks. Use `pi-extensib
18
18
 
19
19
  - Five focused tools: run, inspect, steer, stop, and retry.
20
20
  - Background fan-out with one durable ID per run, or foreground execution with an inline terminal result.
21
- - Reuses workflow roles, model aliases, settings including extension-defined namespaces, and agent options: `label`, `model`, `tools`, `skills`, `extensions`, `contextFiles`, `worktree`, `outputSchema`, `retries` (0 through 255), and `timeoutMs`.
21
+ - Reuses generic workflow preparation hooks, model aliases, settings including extension-defined namespaces, and agent options: `label`, `model`, `tools`, `skills`, `extensions`, `contextFiles`, `worktree`, `outputSchema`, `retries` (0 through 255), and `timeoutMs`.
22
22
  - Repeatable inspection of progress, token accounting, tool calls, results, failures, and worktrees.
23
23
 
24
24
  Built-in subagent tools are available when the core extension is installed. Use the core package's Pi extension manifest to enable them.
@@ -44,23 +44,23 @@ subagents_run({ prompt: "Summarize README.md.", mode: "foreground" })
44
44
 
45
45
  ## Interactive inspection
46
46
 
47
- In Pi's TUI, `/subagents` opens the `/workflow` dashboard layout for the durable standalone runs of the current session: the run list sits beside the selected run's details at 80 columns or wider, and narrower terminals drill down from the list to the details. The details show the run ID and the same activity, stall warning, state, model, role, tools, attempts, duration, cumulative token accounting, cost, and error fields as a `/workflow` agent; a running run shows its live activity, which `subagents_inspect` leaves out. Requests without a role show `Role: (none)`. While a run is active the view refreshes the active runs and the selection every second; runs launched while it is open appear after an action or when it reopens. `a` or `enter` opens registered standalone agent actions, editor controls for the prompt, the live system prompt, and the result, `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and the `/workflow` run actions that apply: `Delete`, `Delete all completed`, `Delete all failed`, `Copy run path`, and `Copy agent ID`. Stop and every deletion ask for confirmation, and a run whose worktree still awaits cleanup cannot be deleted. Outside the TUI, `/subagents` opens a picker with the same run summary and bulk deletions as the `/workflow` picker. Background runs appear below the editor in the workflow widget's frame, one row per run with its model, tokens, cost, elapsed time, and quiet or stalled warning, and each leaves a receipt in the transcript when it settles. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, retry, or delete a run.
47
+ In Pi's TUI, `/subagents` opens the `/workflow` dashboard layout for the durable standalone runs of the current session: the run list sits beside the selected run's details at 80 columns or wider, and narrower terminals drill down from the list to the details. The details show the run ID and the same activity, stall warning, state, model, tools, attempts, duration, cumulative token accounting, cost, and error fields as a `/workflow` agent; a running run shows its live activity, which `subagents_inspect` leaves out. While a run is active the view refreshes the active runs and the selection every second; runs launched while it is open appear after an action or when it reopens. `a` or `enter` opens registered standalone agent actions, editor controls for the prompt, the live system prompt, and the result, `Steer` and `Stop` while running, `Retry` for failed or stopped runs, and the `/workflow` run actions that apply: `Delete`, `Delete all completed`, `Delete all failed`, `Copy run path`, and `Copy agent ID`. Stop and every deletion ask for confirmation, and a run whose worktree still awaits cleanup cannot be deleted. Outside the TUI, `/subagents` opens a picker with the same run summary and bulk deletions as the `/workflow` picker. Background runs appear below the editor in the workflow widget's frame, one row per run with its model, tokens, cost, elapsed time, and quiet or stalled warning, and each leaves a receipt in the transcript when it settles. Inspection does not launch a new LLM call; lifecycle actions may steer, stop, retry, or delete a run.
48
48
 
49
49
  ## Tools
50
50
 
51
- Every tool schema is a closed object. Unknown properties are rejected. The model-visible standalone surface contains exactly these five names:
51
+ `subagents_run` accepts additional JSON agent options for loaded preparation hooks; unknown options are ignored when no plugin handles them. The other control-tool schemas are closed objects. The model-visible standalone surface contains exactly these five names:
52
52
 
53
53
  | Tool | Input schema |
54
54
  | --- | --- |
55
- | `subagents_run` | `{ prompt: string, mode?: "background" \| "foreground", label?: string, model?: string, tools?: string[], skills?: string[], extensions?: string[], contextFiles?: string[], role?: string, worktree?: string, outputSchema?: object, retries?: integer 0..255, timeoutMs?: positive integer \| null }` |
56
- | `subagents_inspect` | `{ id?: string }` |
55
+ | `subagents_run` | `{ prompt: string, mode?: "background" \| "foreground", label?: string, model?: string, tools?: string[], skills?: string[], extensions?: string[], contextFiles?: string[], worktree?: string, outputSchema?: object, retries?: integer 0..255, timeoutMs?: positive integer \| null }` |
56
+ | `subagents_inspect` | `{ id?: string, scope?: "session" \| "all" }` |
57
57
  | `subagents_steer` | `{ id: string, message: string }` |
58
58
  | `subagents_stop` | `{ id: string }` |
59
59
  | `subagents_retry` | `{ id: string }` |
60
60
 
61
- `prompt` is the only required `subagents_run` property. `mode` defaults to "background". Resource candidates start enabled, so plain positive top-level selector lists are additive and top-level `[]` adds no matches. Use `["!*", "read", "grep"]` to restrict a selector to those resources, or `["!*"]` to select none. A `role` string selects an existing workflow role. Model, tools, skills, extensions, and contextFiles are top-level `AgentOptions` overrides applied after the role file. Concrete models are `provider/model:thinking`. Capability selectors use ordered minimatch rules and are applied after global, trusted-project, and role selectors. Use `tools: ["*"]` to re-enable all tools after a role restriction.
61
+ `prompt` is the only required `subagents_run` property. `mode` defaults to "background". Resource candidates start enabled, so plain positive top-level selector lists are additive and top-level `[]` adds no matches. Use `["!*", "read", "grep"]` to restrict a selector to those resources, or `["!*"]` to select none. Unknown JSON options are forwarded to enabled plugins. `role` has no core meaning. Concrete models are `provider/model:thinking`. Capability selectors cannot exceed root and parent ceilings. `excludeTools` takes exact tool names removed after preparation hooks, as Pi's `excludeTools` does; patterns and `workflow_result` are rejected with `INVALID_METADATA`, names also remove same-named custom tools, are frozen with the configuration that retries reuse, and names matching no root or custom tool only warn.
62
62
 
63
- `subagents_inspect({})` returns all accessible run summaries ordered by start time. `subagents_inspect({ id })` returns the detailed lifecycle record, including state, start and finish timestamps, and the live snapshot under `progress`: `state`, cumulative `accounting`, `toolCalls`, `activity`, and `lastEventAt`. The snapshot state never includes the effective system prompt, and inspection has no `usage` field; token totals are derived from accounting. Materialized worktree path and branch are included when available. For completed runs it also includes `value`; for failed runs it includes `error`. A running run has no terminal value yet. Unknown IDs fail with `RUN_NOT_FOUND`.
63
+ `subagents_inspect({})` returns the current Pi session's run summaries ordered by start time; `subagents_inspect({ scope: "all" })` lists every stored run. `subagents_inspect({ id })` returns the detailed lifecycle record, including state, start and finish timestamps, and the live snapshot under `progress`: `state`, cumulative `accounting`, `toolCalls`, `activity`, and `lastEventAt`. The snapshot state never includes the effective system prompt, and inspection has no `usage` field; token totals are derived from accounting. Materialized worktree path and branch are included when available. For completed runs it also includes `value`; for failed runs it includes `error`. A running run has no terminal value yet. Unknown IDs fail with `RUN_NOT_FOUND`.
64
64
 
65
65
  ## Launching and concurrency
66
66
 
@@ -82,7 +82,7 @@ Each call owns an independent run, so calls can execute concurrently and settle
82
82
 
83
83
  ## IDs, inspection, and terminal values
84
84
 
85
- Run records are stored below the agent directory's private `subagents/<id>/` directory, normally `~/.pi/agent/subagents/<id>/`. The record includes the normalized request, its frozen external settings and role definitions for deterministic retries, and status. A shared storage owner marker uses the process ID, process start, session ID, and token; every running record carries the same manager identity so a live manager does not reconcile another manager's active run.
85
+ Run records are stored below the agent directory's private `subagents/<id>/` directory, normally `~/.pi/agent/subagents/<id>/`. The record includes the normalized request, its frozen concrete prepared configuration for deterministic retries, and status. A shared storage owner marker uses the process ID, process start, session ID, and token; every running record carries the same manager identity so a live manager does not reconcile another manager's active run.
86
86
 
87
87
  Inspection is repeatable. Use the list form for ordered summaries and the ID form for one detailed status plus its terminal value or failure information. Progress is retained in memory and is persisted when the executor marks a progress update for persistence. The result and failure files remain available after manager restart.
88
88
 
@@ -102,13 +102,7 @@ Set `worktree` on `subagents_run` to create a named isolated Git worktree for th
102
102
 
103
103
  ## Agent options, roles, and settings
104
104
 
105
- `subagents_run` accepts the same execution options as a workflow `agent(...)`: model, thinking level, tool, skill, and extension selectors, named roles or inline role overrides, worktrees, structured output, retries, and timeout. The extension also reuses workflow model aliases, settings, resource selector policy, and role discovery:
106
-
107
- - global roles: `<agentDir>/pi-extensible-workflows/roles/<name>.md`, normally `~/.pi/agent/pi-extensible-workflows/roles/`;
108
- - trusted project roles: `<cwd>/.pi/pi-extensible-workflows/roles/<name>.md`;
109
- - global and trusted project settings: the normal workflow settings path under the agent directory and `<cwd>/.pi/pi-extensible-workflows/settings.json`.
110
-
111
- The current Pi model, thinking level, active tools, project trust, session ID, role definitions, aliases, and resource policy are captured from the extension context for each run. Internal workflow and subagent control tools are not exposed to the child agent.
105
+ `subagents_run` accepts the same execution options as workflow agents, plus JSON extension options. Enable the optional `@piewf/pi-ext-roles` extension in the parent Pi session when role behavior is required; core does not discover role files or read shared role settings. [Generic preparation hooks](https://vekexasia.github.io/pi-extensible-workflows/extensions.html#preparation) receive authorized inventories, effective cwd/agentDir/trust, root defaults, immutable original options, and cancellation. Prepared configurations are persisted before execution and reused on retry without resolving external policies again. Lost capabilities or trust fail closed. Internal workflow and subagent control tools remain excluded from the child agent.
112
106
 
113
107
  ## Programmatic host integration
114
108
 
@@ -142,4 +136,4 @@ The v5.4 release removes the seven-tool surface's redundant `subagents_list`, `s
142
136
  | `subagents_stop({ id })` | unchanged |
143
137
  | `subagents_retry({ id })` | unchanged; the original launch mode is preserved |
144
138
 
145
- The standalone manager deliberately keeps durable run IDs, repeatable inspection, bounded steering, named worktree cleanup, and workflow role/settings reuse.
139
+ The standalone manager deliberately keeps durable run IDs, repeatable inspection, bounded steering, named worktree cleanup, and generic workflow preparation and settings reuse.
@@ -29,18 +29,19 @@ export const SUBAGENTS_RUN_PARAMETERS = Type.Object({
29
29
  label: Type.Optional(Type.String({ description: "Optional display label for the subagent" })),
30
30
  model: Type.Optional(Type.String({ description: "Optional model as provider/model:thinking or alias[:thinking]" })),
31
31
  tools: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered tool selectors; candidates start enabled and !* restricts the set" })),
32
+ excludeTools: Type.Optional(Type.Array(Type.String(), { description: "Optional exact tool names removed after preparation; no patterns, cannot remove workflow_result" })),
32
33
  skills: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered skill selectors; candidates start enabled and !* restricts the set" })),
33
34
  extensions: Type.Optional(Type.Array(Type.String(), { description: "Optional ordered extension selectors; candidates start enabled and !* restricts the set" })),
34
35
  contextFiles: Type.Optional(Type.Array(Type.String(), { description: "Optional context-file scopes: global, project, cwd" })),
35
- role: Type.Optional(Type.String({ description: "Workflow role name" })),
36
36
  worktree: Type.Optional(Type.String({ description: "Optional named worktree; requires a clean working tree" })),
37
37
  outputSchema: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional JSON schema for the result" })),
38
38
  retries: Type.Optional(Type.Integer({ minimum: 0, maximum: SUBAGENT_MAX_RETRIES, description: "Optional retry count; at most 255 retries" })),
39
39
  timeoutMs: Type.Optional(Type.Union([Type.Integer({ minimum: 1, description: "Optional execution timeout in milliseconds" }), Type.Null()])),
40
- }, { additionalProperties: false });
40
+ }, { additionalProperties: true });
41
41
 
42
42
  export const SUBAGENTS_INSPECT_PARAMETERS = Type.Object({
43
43
  id: Type.Optional(Type.String({ description: "Subagent ID; omit to list ordered run summaries" })),
44
+ scope: Type.Optional(Type.Union([Type.Literal("session"), Type.Literal("all")], { description: "List scope when id is omitted: session (default) lists runs from the current Pi session; all lists every stored run" })),
44
45
  }, { additionalProperties: false });
45
46
 
46
47
  export const SUBAGENTS_ID_PARAMETERS = Type.Object({
@@ -60,15 +61,25 @@ export const SUBAGENTS_RETRY_PARAMETERS = Type.Object({
60
61
  id: Type.String({ description: "Failed or stopped subagent ID to retry" }),
61
62
  }, { additionalProperties: false });
62
63
 
63
- export const SUBAGENTS_TOOL_SCHEMAS = {
64
- subagents_run: SUBAGENTS_RUN_PARAMETERS,
65
- subagents_inspect: SUBAGENTS_INSPECT_PARAMETERS,
66
- subagents_steer: SUBAGENTS_STEER_PARAMETERS,
67
- subagents_stop: SUBAGENTS_STOP_PARAMETERS,
68
- subagents_retry: SUBAGENTS_RETRY_PARAMETERS,
69
- } as const;
64
+ // Structured results codemode scripts receive instead of the JSON text; nested records stay loose so they cannot drift from persistence.
65
+ export const SUBAGENT_STATUS_OUTPUT = Type.Object({
66
+ id: Type.String(),
67
+ state: Type.String(),
68
+ sessionId: Type.Optional(Type.String()),
69
+ startedAt: Type.Optional(Type.Number()),
70
+ finishedAt: Type.Optional(Type.Number()),
71
+ attempts: Type.Optional(Type.Number()),
72
+ attemptDetails: Type.Optional(Type.Array(Type.Unknown())),
73
+ worktree: Type.Optional(Type.Object({ path: Type.String(), branch: Type.String() })),
74
+ error: Type.Optional(Type.Object({ code: Type.String(), message: Type.String() })),
75
+ progress: Type.Optional(Type.Unknown()),
76
+ value: Type.Optional(Type.Unknown()),
77
+ });
78
+ // One run when id is given, otherwise the ordered run summaries; the summaries stay short in the codemode description.
79
+ export const SUBAGENTS_INSPECT_OUTPUT = Type.Union([SUBAGENT_STATUS_OUTPUT, Type.Array(Type.Object({ id: Type.String(), state: Type.String() }))]);
80
+ export const SUBAGENTS_STEER_OUTPUT = Type.Object({ id: Type.String(), accepted: Type.Boolean() });
70
81
 
71
- export type SubagentRunRequest = Static<typeof SUBAGENTS_RUN_PARAMETERS>;
82
+ export type SubagentRunRequest = Static<typeof SUBAGENTS_RUN_PARAMETERS> & { readonly [key: string]: unknown };
72
83
  export function normalizeSubagentRunRequest(value: unknown): SubagentRunRequest {
73
84
  if (!Value.Check(SUBAGENTS_RUN_PARAMETERS, value)) throw new WorkflowError("INVALID_METADATA", "Invalid subagents_run parameters");
74
85
  if (typeof value.worktree === "string" && !value.worktree.trim()) throw new WorkflowError("INVALID_METADATA", "worktree name must be a non-empty string");
@@ -103,7 +114,6 @@ export interface SubagentStatus {
103
114
  export interface SubagentNotification {
104
115
  readonly id: string;
105
116
  readonly label?: string;
106
- readonly role?: string;
107
117
  readonly state: "completed" | "failed";
108
118
  readonly error?: { readonly code: string; readonly message: string };
109
119
  }