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.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/agents/researcher.md +23 -13
- package/docs/agents.md +16 -2
- package/docs/configuration.md +18 -0
- package/docs/extension-api.md +91 -0
- package/docs/models.md +58 -1
- package/docs/observability.md +42 -2
- package/docs/tool-reference.md +3 -3
- package/docs/workflows.md +14 -7
- package/package.json +2 -1
- package/skills/pi-subagents/references/execution-controls.md +14 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/src/agents/advertised-agent-prompt.ts +63 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +8 -0
- package/src/api/shared-types.ts +1 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/index.ts +40 -2
- package/src/extension/public-execution.ts +0 -1
- package/src/extension/rpc.ts +4 -21
- package/src/extension/schemas.ts +8 -6
- package/src/extension/tool-description.ts +6 -5
- package/src/intercom/native-supervisor-channel.ts +82 -54
- package/src/runs/background/active-async-capacity.ts +18 -18
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +40 -29
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/notify.ts +86 -12
- package/src/runs/background/owned-process-tree.ts +6 -6
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +60 -32
- package/src/runs/background/run-status.ts +75 -5
- package/src/runs/background/runner-aliases.ts +18 -7
- package/src/runs/background/runner-child-launch.ts +86 -0
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/subagent-runner.ts +412 -209
- package/src/runs/background/subagent-wait.ts +3 -0
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/async-steering-action.ts +19 -0
- package/src/runs/foreground/execution.ts +101 -25
- package/src/runs/foreground/subagent-executor.ts +474 -197
- package/src/runs/foreground/workflow-detach-reconcile.ts +8 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +56 -2
- package/src/runs/shared/acceptance.ts +2 -2
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +47 -47
- package/src/runs/shared/child-hooks.ts +151 -2
- package/src/runs/shared/child-launch.ts +18 -13
- package/src/runs/shared/child-session.ts +42 -6
- package/src/runs/shared/child-tool-plan.ts +2 -2
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +1 -0
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +10 -9
- package/src/runs/shared/model-fallback.ts +10 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +6 -0
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +13 -3
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +332 -204
- package/src/shared/types.ts +81 -60
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +23 -5
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +38 -10
- package/src/workflows/workflow-checklist.ts +11 -15
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- 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
|
|
5
|
+
type WorkflowResourceHostAuthority,
|
|
6
6
|
type WorkflowResourcePermit,
|
|
7
7
|
} from "../shared/workflow-child-permit.ts";
|
|
8
|
-
import type {
|
|
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:
|
|
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
|
-
|
|
24
|
+
export interface WorkflowResourceDefinition {
|
|
25
25
|
name: string;
|
|
26
26
|
version: number;
|
|
27
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
151
|
+
hostCommands: [{ key: "ci", command }],
|
|
95
152
|
};
|
|
96
153
|
}
|
|
97
154
|
|
|
98
|
-
function resolveReview(args: Record<string, unknown
|
|
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
|
|
131
|
-
const resolved = resource.resolve(normalizedArgs.args
|
|
132
|
-
if (
|
|
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(
|
|
139
|
-
authority:
|
|
213
|
+
scriptDigest: stableJsonDigest(script),
|
|
214
|
+
authority: { host: hostCommands },
|
|
140
215
|
});
|
|
141
|
-
const provenance:
|
|
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
|
|
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,
|