pi-subagents 0.65.1 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +1 -1
  3. package/agents/researcher.md +23 -13
  4. package/docs/agents.md +16 -2
  5. package/docs/configuration.md +18 -0
  6. package/docs/extension-api.md +91 -0
  7. package/docs/models.md +58 -1
  8. package/docs/observability.md +42 -2
  9. package/docs/tool-reference.md +3 -3
  10. package/docs/workflows.md +14 -7
  11. package/package.json +2 -1
  12. package/skills/pi-subagents/references/execution-controls.md +14 -1
  13. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  14. package/src/agents/advertised-agent-prompt.ts +63 -0
  15. package/src/agents/agent-management.ts +14 -1
  16. package/src/agents/agent-serializer.ts +2 -0
  17. package/src/agents/agents.ts +8 -0
  18. package/src/api/shared-types.ts +1 -1
  19. package/src/api/workflow-resources.ts +6 -0
  20. package/src/extension/index.ts +40 -2
  21. package/src/extension/public-execution.ts +0 -1
  22. package/src/extension/rpc.ts +4 -21
  23. package/src/extension/schemas.ts +8 -6
  24. package/src/extension/tool-description.ts +6 -5
  25. package/src/intercom/native-supervisor-channel.ts +82 -54
  26. package/src/runs/background/active-async-capacity.ts +18 -18
  27. package/src/runs/background/async-job-tracker.ts +35 -3
  28. package/src/runs/background/async-status-snapshot.ts +10 -12
  29. package/src/runs/background/async-status.ts +17 -9
  30. package/src/runs/background/auto-drain.ts +40 -29
  31. package/src/runs/background/chain-root-attachment.ts +8 -0
  32. package/src/runs/background/control-channel.ts +78 -44
  33. package/src/runs/background/notify.ts +86 -12
  34. package/src/runs/background/owned-process-tree.ts +6 -6
  35. package/src/runs/background/process-terminal.ts +23 -23
  36. package/src/runs/background/run-child-session.ts +60 -32
  37. package/src/runs/background/run-status.ts +75 -5
  38. package/src/runs/background/runner-aliases.ts +18 -7
  39. package/src/runs/background/runner-child-launch.ts +86 -0
  40. package/src/runs/background/stale-run-reconciler.ts +3 -1
  41. package/src/runs/background/subagent-runner.ts +412 -209
  42. package/src/runs/background/subagent-wait.ts +3 -0
  43. package/src/runs/background/wait-completions.ts +4 -0
  44. package/src/runs/foreground/async-steering-action.ts +19 -0
  45. package/src/runs/foreground/execution.ts +101 -25
  46. package/src/runs/foreground/subagent-executor.ts +474 -197
  47. package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
  48. package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
  49. package/src/runs/shared/acceptance.ts +2 -2
  50. package/src/runs/shared/agent-contract.ts +1 -1
  51. package/src/runs/shared/async-status-projection.ts +47 -47
  52. package/src/runs/shared/child-hooks.ts +151 -2
  53. package/src/runs/shared/child-launch.ts +18 -13
  54. package/src/runs/shared/child-session.ts +42 -6
  55. package/src/runs/shared/child-tool-plan.ts +2 -2
  56. package/src/runs/shared/completion-evidence.ts +2 -2
  57. package/src/runs/shared/completion-guard.ts +1 -0
  58. package/src/runs/shared/host-step-status.ts +11 -11
  59. package/src/runs/shared/llm-intent-arbiter.ts +10 -9
  60. package/src/runs/shared/model-fallback.ts +10 -6
  61. package/src/runs/shared/nested-events.ts +5 -5
  62. package/src/runs/shared/orca-progress-tabs.ts +6 -0
  63. package/src/runs/shared/parallel-handoff.ts +57 -12
  64. package/src/runs/shared/parallel-utils.ts +2 -2
  65. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  66. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  67. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  68. package/src/runs/shared/run-fanout-budget.ts +8 -8
  69. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  70. package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
  71. package/src/runs/shared/worktree-setup-command.ts +190 -0
  72. package/src/runs/shared/worktree.ts +332 -204
  73. package/src/shared/types.ts +81 -60
  74. package/src/shared/utils.ts +7 -2
  75. package/src/shared/workflow-child-permit.ts +18 -13
  76. package/src/tui/fleet.ts +11 -5
  77. package/src/tui/render.ts +23 -5
  78. package/src/workflows/chat-progress.ts +3 -3
  79. package/src/workflows/scripted-workflow.ts +38 -10
  80. package/src/workflows/workflow-checklist.ts +11 -15
  81. package/src/workflows/workflow-child-summary.ts +57 -8
  82. package/src/workflows/workflow-preflight.ts +19 -19
  83. package/src/workflows/workflow-receipt.ts +3 -3
  84. package/src/workflows/workflow-resources.ts +96 -21
  85. package/src/workflows/workflow-settlement.ts +3 -0
@@ -2,10 +2,10 @@ import { randomUUID } from "node:crypto";
2
2
  import { stableJsonDigest } from "../shared/launch-contract.ts";
3
3
  import {
4
4
  createWorkflowResourcePermit,
5
- type WorkflowResourceAuthority,
5
+ type WorkflowResourceHostAuthority,
6
6
  type WorkflowResourcePermit,
7
7
  } from "../shared/workflow-child-permit.ts";
8
- import type { WorkflowResourceProvenanceV1 } from "../shared/types.ts";
8
+ import type { WorkflowResourceProvenance } from "../shared/types.ts";
9
9
 
10
10
  const RESOURCE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11
11
  const MAX_ARGS_BYTES = 16 * 1024;
@@ -14,18 +14,75 @@ const MAX_STRING_BYTES = 16 * 1024;
14
14
  export interface ResolvedWorkflowResource {
15
15
  script: string;
16
16
  permit: WorkflowResourcePermit;
17
- provenance: WorkflowResourceProvenanceV1;
17
+ provenance: WorkflowResourceProvenance;
18
18
  }
19
19
 
20
20
  export type WorkflowResourceResolution =
21
21
  | { ok: true; resource: ResolvedWorkflowResource }
22
22
  | { ok: false; error: string };
23
23
 
24
- type WorkflowResourceDefinition = {
24
+ export interface WorkflowResourceDefinition {
25
25
  name: string;
26
26
  version: number;
27
- resolve: (args: Record<string, unknown>) => { script: string; authority: WorkflowResourceAuthority } | { error: string };
28
- };
27
+ /** Trusted synchronous validation/expansion. The extension owns semantic command binding. */
28
+ resolve: (args: Readonly<Record<string, unknown>>) => { script: string; hostCommands?: readonly WorkflowResourceHostAuthority[] } | { error: string };
29
+ }
30
+
31
+ export interface WorkflowResourceRegistration {
32
+ dispose(): void;
33
+ }
34
+
35
+ export interface RegisterWorkflowResourceInput {
36
+ sessionId: string;
37
+ definition: WorkflowResourceDefinition;
38
+ }
39
+
40
+ interface WorkflowResourceRegistry {
41
+ version: 1;
42
+ bySession: Map<string, Map<string, WorkflowResourceDefinition>>;
43
+ }
44
+
45
+ function registry(): WorkflowResourceRegistry {
46
+ const key = Symbol.for("pi-subagents.workflow-resources.v1");
47
+ const globalObject = globalThis as Record<PropertyKey, unknown>;
48
+ const existing = globalObject[key];
49
+ if (existing === undefined) {
50
+ const created: WorkflowResourceRegistry = { version: 1, bySession: new Map() };
51
+ globalObject[key] = created;
52
+ return created;
53
+ }
54
+ if (!isPlainRecord(existing) || existing.version !== 1 || !(existing.bySession instanceof Map)) throw new Error("Malformed or unsupported workflow resource registry.");
55
+ return existing as unknown as WorkflowResourceRegistry;
56
+ }
57
+
58
+ /** Session ID scopes lookup, not authentication. Dispose on session_shutdown; issued permits remain valid. */
59
+ export function registerWorkflowResource(input: RegisterWorkflowResourceInput): WorkflowResourceRegistration {
60
+ if (!isPlainRecord(input) || Object.keys(input).some((key) => key !== "sessionId" && key !== "definition")) throw new Error("Workflow registration requires only sessionId and definition.");
61
+ const { sessionId, definition } = input;
62
+ if (typeof sessionId !== "string" || !sessionId || sessionId.trim() !== sessionId || sessionId.length > 256 || sessionId.includes("\0")) throw new Error("Workflow registration requires a non-empty trimmed sessionId of at most 256 characters without NUL.");
63
+ if (!isPlainRecord(definition) || Object.keys(definition).some((key) => !["name", "version", "resolve"].includes(key))) throw new Error("Workflow definition requires only name, version and resolve.");
64
+ const { name, version, resolve } = definition;
65
+ if (typeof name !== "string" || !RESOURCE_NAME_PATTERN.test(name)) throw new Error("Workflow definition requires a safe resource name.");
66
+ if (!Number.isSafeInteger(version) || version < 1) throw new Error("Workflow definition version must be a positive safe integer.");
67
+ if (typeof resolve !== "function") throw new Error("Workflow definition requires a synchronous resolve function.");
68
+ if (findWorkflowResource(name)) throw new Error(`Workflow resource '${name}' is a protected builtin.`);
69
+ const current = registry();
70
+ const bucket = current.bySession.get(sessionId) ?? new Map<string, WorkflowResourceDefinition>();
71
+ if (!(bucket instanceof Map)) throw new Error("Malformed workflow resource session registry.");
72
+ if (bucket.has(name)) throw new Error(`Workflow resource '${name}' is already registered in this session; dispose it first.`);
73
+ const snapshot = Object.freeze({ name, version, resolve });
74
+ bucket.set(name, snapshot);
75
+ current.bySession.set(sessionId, bucket);
76
+ let disposed = false;
77
+ return {
78
+ dispose() {
79
+ if (disposed) return;
80
+ disposed = true;
81
+ if (bucket.get(name) === snapshot) bucket.delete(name);
82
+ if (bucket.size === 0 && current.bySession.get(sessionId) === bucket) current.bySession.delete(sessionId);
83
+ },
84
+ };
85
+ }
29
86
 
30
87
  function isPlainRecord(value: unknown): value is Record<string, unknown> {
31
88
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
@@ -68,19 +125,19 @@ function validatePlainJson(value: unknown, path: string, depth = 0): void {
68
125
  }
69
126
  }
70
127
 
71
- function normalizeArgs(value: unknown): { args?: Record<string, unknown>; error?: string } {
128
+ function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
72
129
  if (value === undefined) return { args: {} };
73
130
  if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
74
131
  try {
75
132
  validatePlainJson(value, "workflow args");
76
133
  if (jsonByteLength(value) > MAX_ARGS_BYTES) return { error: `workflow args exceed ${MAX_ARGS_BYTES} bytes.` };
77
- return { args: { ...value } };
134
+ return { args: JSON.parse(JSON.stringify(value)) };
78
135
  } catch (error) {
79
136
  return { error: error instanceof Error ? error.message : String(error) };
80
137
  }
81
138
  }
82
139
 
83
- function resolveRunCi(args: Record<string, unknown>): { script: string; authority: WorkflowResourceAuthority } | { error: string } {
140
+ function resolveRunCi(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
84
141
  const allowed = new Set(["command", "timeoutMs"]);
85
142
  const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
86
143
  if (unsupported.length > 0) return { error: `workflow 'run-ci' args contain unsupported fields: ${unsupported.join(", ")}.` };
@@ -91,18 +148,17 @@ function resolveRunCi(args: Record<string, unknown>): { script: string; authorit
91
148
  const params = { kind: "command", command, timeoutMs, role: "ci" };
92
149
  return {
93
150
  script: `return await runs.host("ci", ${JSON.stringify(params)});`,
94
- authority: { host: { keys: ["ci"], commands: [command] } },
151
+ hostCommands: [{ key: "ci", command }],
95
152
  };
96
153
  }
97
154
 
98
- function resolveReview(args: Record<string, unknown>): { script: string; authority: WorkflowResourceAuthority } | { error: string } {
155
+ function resolveReview(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
99
156
  const unsupported = Object.keys(args).filter((key) => key !== "task");
100
157
  if (unsupported.length > 0) return { error: `workflow 'review' args contain unsupported fields: ${unsupported.join(", ")}.` };
101
158
  const task = args.task;
102
159
  if (typeof task !== "string" || !task.trim()) return { error: "workflow 'review' requires a non-empty string args.task." };
103
160
  return {
104
161
  script: `return (await runs.run("review", { agent: "reviewer", task: ${JSON.stringify(task.trim())} })).output;`,
105
- authority: {},
106
162
  };
107
163
  }
108
164
 
@@ -120,25 +176,44 @@ function listWorkflowResourceNames(): string[] {
120
176
  }
121
177
 
122
178
  /** Resolve only extension-owned resources so policy can distinguish them from raw scripts; caller-provided script text is never consulted. */
123
- export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown): WorkflowResourceResolution {
179
+ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
180
+ try {
181
+ const result = resolveResource(nameValue, argsValue, sessionId);
182
+ return result.ok ? result : { ok: false, error: result.error.slice(0, 4096) };
183
+ } catch (error) {
184
+ return { ok: false, error: error instanceof Error ? error.message.slice(0, 4096) : "Workflow resource resolution failed." };
185
+ }
186
+ }
187
+
188
+ function resolveResource(nameValue: unknown, argsValue?: unknown, sessionId?: string): WorkflowResourceResolution {
124
189
  if (typeof nameValue !== "string" || !nameValue.trim()) return { ok: false, error: "workflow must be a non-empty resource name." };
125
190
  const name = nameValue.trim();
126
191
  if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
127
- const resource = findWorkflowResource(name);
192
+ const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : undefined);
128
193
  if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
129
194
  const normalizedArgs = normalizeArgs(argsValue);
130
- if (normalizedArgs.error) return { ok: false, error: normalizedArgs.error };
131
- const resolved = resource.resolve(normalizedArgs.args!);
132
- if ("error" in resolved) return { ok: false, error: resolved.error };
195
+ if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
196
+ const resolved = resource.resolve(normalizedArgs.args);
197
+ if (resolved && typeof (resolved as unknown as { then?: unknown }).then === "function") {
198
+ void Promise.resolve(resolved).catch(() => {});
199
+ throw new Error("Workflow resource resolve must be synchronous.");
200
+ }
201
+ if (!isPlainRecord(resolved)) throw new Error("Workflow resource resolve must return an expansion or error object.");
202
+ if ("error" in resolved) {
203
+ if (Object.keys(resolved).some((key) => key !== "error") || typeof resolved.error !== "string" || !resolved.error.trim()) throw new Error("Workflow resource returned an invalid error.");
204
+ return { ok: false, error: resolved.error };
205
+ }
206
+ const { script, hostCommands } = resolved;
207
+ if (Object.keys(resolved).some((key) => key !== "script" && key !== "hostCommands") || typeof script !== "string" || !script.trim()) throw new Error("Workflow resource returned an invalid expansion.");
133
208
  const resourceId = randomUUID();
134
209
  const permit = createWorkflowResourcePermit({
135
210
  resourceName: resource.name,
136
211
  resourceVersion: resource.version,
137
212
  resourceId,
138
- scriptDigest: stableJsonDigest(resolved.script),
139
- authority: resolved.authority,
213
+ scriptDigest: stableJsonDigest(script),
214
+ authority: { host: hostCommands },
140
215
  });
141
- const provenance: WorkflowResourceProvenanceV1 = Object.freeze({
216
+ const provenance: WorkflowResourceProvenance = Object.freeze({
142
217
  kind: "workflow",
143
218
  name: resource.name,
144
219
  version: resource.version,
@@ -146,5 +221,5 @@ export function resolveWorkflowResource(nameValue: unknown, argsValue?: unknown)
146
221
  expansion: "resolved",
147
222
  id: resourceId,
148
223
  });
149
- return { ok: true, resource: { script: resolved.script, permit, provenance } };
224
+ return { ok: true, resource: { script, permit, provenance } };
150
225
  }
@@ -209,6 +209,8 @@ export function planWorkflowSettlement(input: {
209
209
  if (!resolution) resolution = classifyWorkflowSettlement(status);
210
210
  }
211
211
  const recovery = workflowRecoveryActions(receipt);
212
+ delete status.workflowReceiptPath;
213
+ if (receipt && input.receiptPath) status.workflowReceiptPath = input.receiptPath;
212
214
  if (receipt) {
213
215
  receipt = {
214
216
  ...receipt,
@@ -233,6 +235,7 @@ export function planWorkflowSettlement(input: {
233
235
  timestamp: now,
234
236
  };
235
237
  const terminal = status.state === "complete" || status.state === "failed" || status.state === "partial" || status.state === "stopped";
238
+ if (!status.workflowReceiptPath) delete publicResult.workflowReceipt;
236
239
  const completionEvent = terminal ? {
237
240
  type: "subagent.workflow.completed",
238
241
  state: status.state,