@narumitw/pi-subagents 0.52.0 → 0.53.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.
@@ -0,0 +1,65 @@
1
+ import type { AutomationRequest, WorkflowPlan } from "./automation-contract.js";
2
+ import { parseWorkflowPlan, WORKFLOW_PLAN_VERSION } from "./automation-contract.js";
3
+ import { resolveConsultResourceLaunchPolicy } from "./consult-resources.js";
4
+ import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
5
+ import type { ChildLaunchPolicy } from "./runner.js";
6
+
7
+ export const AUTOMATION_PLANNER_TOOLS = ["read", "grep", "find", "ls"] as const;
8
+ export const AUTOMATION_PLANNER_MAX_TIMEOUT_MS = 60_000;
9
+ export const AUTOMATION_PLANNER_MAX_TURNS = 8;
10
+ export const AUTOMATION_PLANNER_MAX_TOOL_CALLS = 16;
11
+
12
+ export interface AutomationPlannerPolicy {
13
+ tools: string[];
14
+ resources: "project-context" | "none";
15
+ launchPolicy: ChildLaunchPolicy;
16
+ }
17
+
18
+ export function buildAutomationPlannerPrompt(request: AutomationRequest): string {
19
+ const prompt = [
20
+ "Compile the following explicit automation request into the smallest justified workflow proposal.",
21
+ `Return only JSON for ${WORKFLOW_PLAN_VERSION}; do not wrap it in Markdown or add prose.`,
22
+ "Do not provide hidden reasoning, chain-of-thought, or internal deliberation.",
23
+ "Use summary and risks only for concise, user-visible conclusions.",
24
+ "The executor, not this planning turn, owns identities, generations, agent selection, trust, tools, authority, workspace policy, and enforcement.",
25
+ "Your proposal cannot grant authority, tools, trust, network, secrets, descendants, or budget beyond the request.",
26
+ "Propose at most the request maxTasks and never propose workflow grandchildren.",
27
+ "Each task must include id, objective, dependsOn, inputArtifacts, producesArtifacts, sideEffectPolicy, readPaths, writePaths, ownershipKeys, requiredCapabilities, requiredTools, acceptanceCriteria, requiredEvidence, integrationOwner, and budget.",
28
+ "Use requiredVerificationRole and verifierFor only for a distinct direct verifier.",
29
+ "Declare dependencies with dependsOn, declare artifact id, kind, and version, and connect every consumed artifact through a direct dependency.",
30
+ "Use one authoritative integrationOwner for multi-task mutating work.",
31
+ "If required information is absent, list it in missingInputs instead of inventing it.",
32
+ "Request:",
33
+ JSON.stringify(request),
34
+ "Expected top-level fields: version, requestVersion, summary, missingInputs, risks, tasks.",
35
+ ].join("\n");
36
+ const bounded = truncateUtf8(prompt, DEFAULT_MAX_CONTEXT_BYTES);
37
+ if (bounded.truncated)
38
+ throw new Error("Automation planner prompt exceeds the bounded context limit");
39
+ return bounded.text;
40
+ }
41
+
42
+ export async function resolveAutomationPlannerPolicy(
43
+ projectTrusted: boolean,
44
+ cwd: string,
45
+ resolver: typeof resolveConsultResourceLaunchPolicy = resolveConsultResourceLaunchPolicy,
46
+ ): Promise<AutomationPlannerPolicy> {
47
+ const resources = projectTrusted ? "project-context" : "none";
48
+ const launchPolicy = await resolver(resources, projectTrusted, cwd);
49
+ return {
50
+ tools: [...AUTOMATION_PLANNER_TOOLS],
51
+ resources,
52
+ launchPolicy: {
53
+ ...launchPolicy,
54
+ tools: [...AUTOMATION_PLANNER_TOOLS],
55
+ disableExtensions: true,
56
+ },
57
+ };
58
+ }
59
+
60
+ export function parseAutomationPlannerOutput(output: string): WorkflowPlan {
61
+ if (typeof output !== "string" || !output.trim()) {
62
+ throw new Error("Automation planner returned no workflow plan");
63
+ }
64
+ return parseWorkflowPlan(output.trim());
65
+ }
@@ -0,0 +1,580 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as path from "node:path";
3
+ import type { AgentToolResult, AgentToolUpdateCallback } from "@earendil-works/pi-agent-core";
4
+ import {
5
+ type ExtensionAPI,
6
+ type ExtensionContext,
7
+ getAgentDir,
8
+ type ToolDefinition,
9
+ } from "@earendil-works/pi-coding-agent";
10
+ import { Text } from "@earendil-works/pi-tui";
11
+ import { type Static, Type } from "typebox";
12
+ import { discoverAgents, getBuiltInAgent, type SubagentSettings } from "./agents.js";
13
+ import {
14
+ AutomationRequestSchema,
15
+ parseAutomationRequest,
16
+ type WorkflowPlan,
17
+ } from "./automation-contract.js";
18
+ import {
19
+ AUTOMATION_PLANNER_MAX_TIMEOUT_MS,
20
+ AUTOMATION_PLANNER_MAX_TOOL_CALLS,
21
+ AUTOMATION_PLANNER_MAX_TURNS,
22
+ AUTOMATION_PLANNER_TOOLS,
23
+ buildAutomationPlannerPrompt,
24
+ parseAutomationPlannerOutput,
25
+ resolveAutomationPlannerPolicy,
26
+ } from "./automation-planner.js";
27
+ import {
28
+ assertDelegationTargetAllowed,
29
+ resolveSubagentTarget,
30
+ targetPolicyAudit,
31
+ } from "./cwd-policy.js";
32
+ import { executeSubagent } from "./execution.js";
33
+ import { renderFallbackResult, safeLine, toolHeader } from "./render-common.js";
34
+ import {
35
+ getResultFinalOutput,
36
+ isResultError,
37
+ runSingleAgent,
38
+ type SingleResult,
39
+ type SubagentDetails,
40
+ } from "./runner.js";
41
+ import { boundedPrivateText } from "./safe-text.js";
42
+ import { DEFAULT_DELEGATION_CWD_POLICY, resolveBlockingMaxParallelTasks } from "./settings.js";
43
+ import {
44
+ type CompiledWorkflowPlan,
45
+ compileWorkflowPlan,
46
+ type WorkflowPlanCompilerResult,
47
+ } from "./workflow-plan-compiler.js";
48
+ import { AutomationPlanPersistence, createWorkflowPlanRecord } from "./workflow-plan-patch.js";
49
+ import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
50
+
51
+ export const SubagentAutomationParams = Type.Object(
52
+ { request: AutomationRequestSchema },
53
+ { additionalProperties: false },
54
+ );
55
+ export type SubagentAutomationParams = Static<typeof SubagentAutomationParams>;
56
+
57
+ export interface AutomationDetails {
58
+ status:
59
+ | "planning"
60
+ | "planner-failed"
61
+ | "parent-owned"
62
+ | "needs-input"
63
+ | "compiler-rejected"
64
+ | "executed";
65
+ requestVersion: string;
66
+ planVersion?: string;
67
+ planId?: string;
68
+ workflowGeneration?: number;
69
+ revision?: number;
70
+ childCount: number;
71
+ reasonCodes: string[];
72
+ missingInputs?: string[];
73
+ planner?: {
74
+ agent: string;
75
+ tools: string[];
76
+ resources: "project-context" | "none";
77
+ timeoutMs: number;
78
+ maxTurns: number;
79
+ maxToolCalls: number;
80
+ failed?: boolean;
81
+ };
82
+ compiled?: CompiledWorkflowPlan;
83
+ execution?: SubagentDetails;
84
+ isError?: boolean;
85
+ }
86
+
87
+ export interface AutomationPlannerRequest {
88
+ prompt: string;
89
+ ctx: ExtensionContext;
90
+ signal: AbortSignal;
91
+ settings: SubagentSettings | undefined;
92
+ timeoutMs: number;
93
+ maxTurns: number;
94
+ maxToolCalls: number;
95
+ }
96
+
97
+ export interface AutomationExecutionOptions {
98
+ getSettings(): SubagentSettings | undefined;
99
+ runPlanner?: (request: AutomationPlannerRequest) => Promise<string>;
100
+ runWorkflow?: (
101
+ params: Parameters<typeof executeSubagent>[1],
102
+ signal: AbortSignal,
103
+ ctx: ExtensionContext,
104
+ ) => ReturnType<typeof executeSubagent>;
105
+ persistCompiled?: (compiled: CompiledWorkflowPlan, ctx: ExtensionContext) => Promise<void>;
106
+ }
107
+
108
+ export function registerSubagentAutomation(
109
+ pi: ExtensionAPI,
110
+ options: AutomationExecutionOptions,
111
+ ): void {
112
+ let generation = 0;
113
+ const activeControllers = new Set<AbortController>();
114
+ const activeWork = new Set<Promise<unknown>>();
115
+ const cancelAndWait = async (reason: string) => {
116
+ generation++;
117
+ for (const controller of activeControllers) {
118
+ controller.abort(new DOMException(reason, "AbortError"));
119
+ }
120
+ await Promise.allSettled([...activeWork]);
121
+ };
122
+ pi.on("session_start", () => cancelAndWait("Autonomous workflow session replaced"));
123
+ pi.on("session_shutdown", () => cancelAndWait("Autonomous workflow session shut down"));
124
+ const description = () =>
125
+ [
126
+ "Explicitly opt in to one bounded read-only planning turn that compiles a high-level objective into the smallest justified existing workflow.",
127
+ "The deterministic compiler may return parent-owned work, request missing input, or reject without launching execution workers.",
128
+ "Mutating workflows require an authoritative integration path and an independent verifier, allow at most two concurrent mutating workers, and never allow workflow grandchildren.",
129
+ "The first version routes only built-in and user-scoped agents; use caller-authored workflow mode for project-local agents.",
130
+ ].join(" ");
131
+ const definition: ToolDefinition<typeof SubagentAutomationParams, AutomationDetails> = {
132
+ name: "subagent_auto",
133
+ label: "Autonomous Subagent Workflow",
134
+ description: description(),
135
+ promptSnippet:
136
+ "Explicitly compile one high-level objective into a bounded capability-matched workflow",
137
+ promptGuidelines: [
138
+ "Use subagent_auto only when the caller explicitly opts into autonomous workflow planning.",
139
+ "Provide a complete authority ceiling and aggregate budget; parent-owned and insufficient-evidence results launch no execution workers.",
140
+ "Use caller-authored subagent workflow mode as the compatibility fallback when deterministic task control is required.",
141
+ ],
142
+ parameters: SubagentAutomationParams,
143
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
144
+ const ownerGeneration = generation;
145
+ const controller = new AbortController();
146
+ activeControllers.add(controller);
147
+ const combined = combineSignals(signal, controller.signal);
148
+ const work = executeAutomationRequest(
149
+ toolCallId,
150
+ params,
151
+ combined.signal,
152
+ onUpdate,
153
+ ctx,
154
+ options,
155
+ () => ownerGeneration === generation,
156
+ );
157
+ activeWork.add(work);
158
+ try {
159
+ return await work;
160
+ } finally {
161
+ combined.dispose();
162
+ activeControllers.delete(controller);
163
+ activeWork.delete(work);
164
+ }
165
+ },
166
+ renderCall(args, theme) {
167
+ const request = (args as { request?: { objective?: string; version?: string } }).request;
168
+ return new Text(
169
+ toolHeader(theme, "subagent_auto", request?.objective, [request?.version ?? "request"]),
170
+ 0,
171
+ 0,
172
+ );
173
+ },
174
+ renderResult(result, renderOptions, theme) {
175
+ const status = safeLine(result.details?.status, "completed", 128);
176
+ return renderFallbackResult(
177
+ result,
178
+ renderOptions,
179
+ theme,
180
+ result.details?.isError === true ||
181
+ status.endsWith("failed") ||
182
+ status.endsWith("rejected"),
183
+ );
184
+ },
185
+ };
186
+ pi.registerTool<typeof SubagentAutomationParams, AutomationDetails>(definition);
187
+ pi.on("tool_result", (event) => {
188
+ if (event.toolName !== "subagent_auto") return;
189
+ if ((event.details as AutomationDetails | undefined)?.isError) return { isError: true };
190
+ });
191
+ }
192
+
193
+ export async function executeAutomationRequest(
194
+ toolCallId: string,
195
+ params: SubagentAutomationParams,
196
+ signal: AbortSignal,
197
+ onUpdate: AgentToolUpdateCallback<AutomationDetails> | undefined,
198
+ ctx: ExtensionContext,
199
+ options: AutomationExecutionOptions,
200
+ isCurrent: () => boolean = () => true,
201
+ ): Promise<AgentToolResult<AutomationDetails> & { isError?: boolean }> {
202
+ validateAutomationToolParams(params);
203
+ const request = parseAutomationRequest(params.request);
204
+ assertCurrent(signal, isCurrent);
205
+ const settings = options.getSettings();
206
+ const plannerBudget = reservePlannerBudget(request.aggregateBudget);
207
+ const plannerDetails: NonNullable<AutomationDetails["planner"]> = {
208
+ agent: "planner",
209
+ tools: [...AUTOMATION_PLANNER_TOOLS],
210
+ resources: ctx.isProjectTrusted() ? "project-context" : "none",
211
+ ...plannerBudget,
212
+ };
213
+ const depth = Number.parseInt(process.env.PI_SUBAGENT_DEPTH ?? "0", 10) || 0;
214
+ if (depth > 0) {
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: "Automation compiler rejected workflow recursion before planning or execution.",
220
+ },
221
+ ],
222
+ details: {
223
+ status: "compiler-rejected",
224
+ requestVersion: request.version,
225
+ childCount: 0,
226
+ reasonCodes: ["workflow-recursion-disabled"],
227
+ planner: plannerDetails,
228
+ isError: true,
229
+ },
230
+ isError: true,
231
+ };
232
+ }
233
+ const executionRequest = reserveExecutionBudget(
234
+ request,
235
+ plannerBudget,
236
+ resolveBlockingMaxParallelTasks(settings),
237
+ );
238
+ if (!executionRequest) {
239
+ return nonLaunchResult(
240
+ "compiler-rejected",
241
+ request.version,
242
+ ["execution-budget-exhausted"],
243
+ plannerDetails,
244
+ new Error("Aggregate budget cannot fund both planning and execution"),
245
+ );
246
+ }
247
+ onUpdate?.({
248
+ content: [{ type: "text", text: "Planning a bounded autonomous workflow." }],
249
+ details: {
250
+ status: "planning",
251
+ requestVersion: request.version,
252
+ childCount: 0,
253
+ reasonCodes: [],
254
+ planner: plannerDetails,
255
+ },
256
+ });
257
+ let proposal: WorkflowPlan;
258
+ try {
259
+ const prompt = buildAutomationPlannerPrompt(request);
260
+ const runPlanner = options.runPlanner ?? runDefaultPlanner;
261
+ const output = await runPlanner({
262
+ prompt,
263
+ ctx,
264
+ signal,
265
+ settings,
266
+ ...plannerBudget,
267
+ });
268
+ assertCurrent(signal, isCurrent);
269
+ proposal = parseAutomationPlannerOutput(output);
270
+ } catch (error) {
271
+ if (signal.aborted || !isCurrent()) throw abortError("Automation planning was cancelled");
272
+ return nonLaunchResult(
273
+ "planner-failed",
274
+ request.version,
275
+ ["planner-failed"],
276
+ plannerDetails,
277
+ error,
278
+ );
279
+ }
280
+ const target = resolveSubagentTarget({
281
+ workspace: ctx.cwd,
282
+ requestedCwd: ctx.cwd,
283
+ currentProjectTrusted: ctx.isProjectTrusted(),
284
+ });
285
+ try {
286
+ assertDelegationTargetAllowed(
287
+ target,
288
+ settings?.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
289
+ );
290
+ } catch (error) {
291
+ return nonLaunchResult(
292
+ "compiler-rejected",
293
+ request.version,
294
+ ["target-policy-rejected"],
295
+ plannerDetails,
296
+ error,
297
+ );
298
+ }
299
+ const agents = discoverAgents(ctx.cwd, "user", settings).agents;
300
+ const compiled = compileWorkflowPlan({
301
+ request: executionRequest,
302
+ proposal,
303
+ agents,
304
+ target: targetPolicyAudit(target),
305
+ depth,
306
+ });
307
+ assertCurrent(signal, isCurrent);
308
+ if (compiled.status !== "compiled") {
309
+ return compilerNonLaunch(request.version, proposal.version, compiled, plannerDetails);
310
+ }
311
+ try {
312
+ if (options.persistCompiled) await options.persistCompiled(compiled, ctx);
313
+ else await persistCompiledWorkflow(compiled, ctx, settings);
314
+ assertCurrent(signal, isCurrent);
315
+ } catch (error) {
316
+ return nonLaunchResult(
317
+ "compiler-rejected",
318
+ request.version,
319
+ ["plan-persistence-failed"],
320
+ plannerDetails,
321
+ error,
322
+ );
323
+ }
324
+ const workflowParams = {
325
+ workflow: compiled.workflow,
326
+ agentScope: "user" as const,
327
+ totalTimeoutMs: executionRequest.aggregateBudget.timeoutMs,
328
+ };
329
+ const execute =
330
+ options.runWorkflow ??
331
+ ((workflow, workflowSignal, workflowContext) =>
332
+ executeSubagent(toolCallId, workflow, workflowSignal, undefined, workflowContext, settings));
333
+ const result = await execute(workflowParams, signal, ctx);
334
+ assertCurrent(signal, isCurrent);
335
+ const details: AutomationDetails = {
336
+ status: "executed",
337
+ requestVersion: request.version,
338
+ planVersion: proposal.version,
339
+ planId: compiled.planId,
340
+ workflowGeneration: compiled.workflowGeneration,
341
+ revision: compiled.revision,
342
+ childCount: compiled.childCount,
343
+ reasonCodes: [],
344
+ planner: plannerDetails,
345
+ compiled,
346
+ execution: result.details,
347
+ ...(result.isError ? { isError: true } : {}),
348
+ };
349
+ return {
350
+ content: result.content,
351
+ details,
352
+ ...(result.usage ? { usage: result.usage } : {}),
353
+ ...(result.isError ? { isError: true } : {}),
354
+ };
355
+ }
356
+
357
+ async function runDefaultPlanner(request: AutomationPlannerRequest): Promise<string> {
358
+ const planner = getBuiltInAgent("planner");
359
+ if (!planner) throw new Error("The built-in automation planner is unavailable");
360
+ const policy = await resolveAutomationPlannerPolicy(
361
+ request.ctx.isProjectTrusted(),
362
+ request.ctx.cwd,
363
+ );
364
+ if (request.signal.aborted) throw abortError("Automation planner was cancelled before launch");
365
+ const child = {
366
+ ...planner,
367
+ tools: [...AUTOMATION_PLANNER_TOOLS],
368
+ systemPrompt: [
369
+ planner.systemPrompt,
370
+ "This planning turn is read-only and cannot execute delegated work or create descendants.",
371
+ "Return one exact versioned JSON workflow proposal without Markdown fences.",
372
+ ].join("\n\n"),
373
+ };
374
+ const result = await runSingleAgent(
375
+ request.ctx.cwd,
376
+ [child],
377
+ child.name,
378
+ request.prompt,
379
+ request.ctx.cwd,
380
+ undefined,
381
+ request.signal,
382
+ child.thinkingLevel,
383
+ request.timeoutMs,
384
+ undefined,
385
+ (results): SubagentDetails => ({
386
+ mode: "single",
387
+ agentScope: "user",
388
+ projectAgentsDir: null,
389
+ results,
390
+ }),
391
+ undefined,
392
+ {
393
+ ...policy.launchPolicy,
394
+ tools: [...AUTOMATION_PLANNER_TOOLS],
395
+ turnLimits: {
396
+ maxTurns: request.maxTurns,
397
+ maxToolCalls: request.maxToolCalls,
398
+ },
399
+ },
400
+ );
401
+ if (isResultError(result)) throw plannerFailure(result);
402
+ return getResultFinalOutput(result);
403
+ }
404
+
405
+ async function persistCompiledWorkflow(
406
+ compiled: CompiledWorkflowPlan,
407
+ ctx: ExtensionContext,
408
+ settings: SubagentSettings | undefined,
409
+ ): Promise<void> {
410
+ const agents = discoverAgents(ctx.cwd, "user", settings).agents;
411
+ const resolved = resolveWorkflowTasks({ workflow: compiled.workflow }, agents);
412
+ const ledger = createBlockingWorkLedger({ workflow: compiled.workflow }, resolved, undefined);
413
+ if (!ledger) throw new Error("Compiled automation workflow did not create a WorkItem ledger");
414
+ const owner =
415
+ ctx.sessionManager.getSessionId?.() ??
416
+ ctx.sessionManager.getSessionFile?.() ??
417
+ `ephemeral:${ctx.cwd}`;
418
+ const stable = createHash("sha256").update(`session:${owner}`).digest("hex").slice(0, 24);
419
+ const filePath = path.join(getAgentDir(), "pi-subagents-workflows", `automation-${stable}.json`);
420
+ await new AutomationPlanPersistence(filePath).save({
421
+ record: createWorkflowPlanRecord(compiled),
422
+ ledger: ledger.snapshot(),
423
+ });
424
+ }
425
+
426
+ function validateAutomationToolParams(value: unknown): asserts value is SubagentAutomationParams {
427
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
428
+ throw new Error("subagent_auto parameters must be an object");
429
+ }
430
+ const keys = Object.keys(value as Record<string, unknown>);
431
+ if (keys.length !== 1 || keys[0] !== "request") {
432
+ throw new Error("subagent_auto accepts exactly one request field");
433
+ }
434
+ }
435
+
436
+ function compilerNonLaunch(
437
+ requestVersion: string,
438
+ planVersion: string,
439
+ compiled: Exclude<WorkflowPlanCompilerResult, CompiledWorkflowPlan>,
440
+ planner: NonNullable<AutomationDetails["planner"]>,
441
+ ): AgentToolResult<AutomationDetails> & { isError?: boolean } {
442
+ const status =
443
+ compiled.status === "parent-owned"
444
+ ? "parent-owned"
445
+ : compiled.status === "needs-input"
446
+ ? "needs-input"
447
+ : "compiler-rejected";
448
+ const isError = status === "compiler-rejected";
449
+ return {
450
+ content: [
451
+ {
452
+ type: "text",
453
+ text:
454
+ status === "parent-owned"
455
+ ? "Automation decision: keep this objective parent-owned; no execution workers were launched."
456
+ : status === "needs-input"
457
+ ? `Automation needs input: ${(compiled.missingInputs ?? []).join(", ")}`
458
+ : `Automation compiler rejected the proposal: ${compiled.reasonCodes.join(", ")}`,
459
+ },
460
+ ],
461
+ details: {
462
+ status,
463
+ requestVersion,
464
+ planVersion,
465
+ childCount: 0,
466
+ reasonCodes: [...compiled.reasonCodes],
467
+ ...(compiled.missingInputs ? { missingInputs: [...compiled.missingInputs] } : {}),
468
+ planner,
469
+ ...(isError ? { isError: true } : {}),
470
+ },
471
+ ...(isError ? { isError: true } : {}),
472
+ };
473
+ }
474
+
475
+ function nonLaunchResult(
476
+ status: "planner-failed" | "compiler-rejected",
477
+ requestVersion: string,
478
+ reasonCodes: string[],
479
+ planner: NonNullable<AutomationDetails["planner"]>,
480
+ error: unknown,
481
+ ): AgentToolResult<AutomationDetails> & { isError: true } {
482
+ const message = boundedPrivateText(
483
+ error instanceof Error ? error.message : String(error),
484
+ 2 * 1024,
485
+ );
486
+ return {
487
+ content: [{ type: "text", text: `Automation ${status}: ${message}` }],
488
+ details: {
489
+ status,
490
+ requestVersion,
491
+ childCount: 0,
492
+ reasonCodes,
493
+ planner: { ...planner, ...(status === "planner-failed" ? { failed: true } : {}) },
494
+ isError: true,
495
+ },
496
+ isError: true,
497
+ };
498
+ }
499
+
500
+ function reservePlannerBudget(budget: {
501
+ timeoutMs: number;
502
+ maxTurns: number;
503
+ maxToolCalls: number;
504
+ }) {
505
+ return {
506
+ timeoutMs: Math.max(
507
+ 1,
508
+ Math.min(AUTOMATION_PLANNER_MAX_TIMEOUT_MS, Math.floor(budget.timeoutMs / 4)),
509
+ ),
510
+ maxTurns: Math.max(1, Math.min(AUTOMATION_PLANNER_MAX_TURNS, Math.floor(budget.maxTurns / 4))),
511
+ maxToolCalls: Math.max(
512
+ 1,
513
+ Math.min(AUTOMATION_PLANNER_MAX_TOOL_CALLS, Math.floor(budget.maxToolCalls / 4)),
514
+ ),
515
+ };
516
+ }
517
+
518
+ function reserveExecutionBudget(
519
+ request: ReturnType<typeof parseAutomationRequest>,
520
+ planner: ReturnType<typeof reservePlannerBudget>,
521
+ maxWorkflowTasks: number,
522
+ ): ReturnType<typeof parseAutomationRequest> | undefined {
523
+ const remaining = {
524
+ timeoutMs: request.aggregateBudget.timeoutMs - planner.timeoutMs,
525
+ maxTurns: request.aggregateBudget.maxTurns - planner.maxTurns,
526
+ maxToolCalls: request.aggregateBudget.maxToolCalls - planner.maxToolCalls,
527
+ };
528
+ if (remaining.timeoutMs < 1 || remaining.maxTurns < 1 || remaining.maxToolCalls < 1) {
529
+ return undefined;
530
+ }
531
+ return {
532
+ ...request,
533
+ aggregateBudget: {
534
+ ...request.aggregateBudget,
535
+ ...remaining,
536
+ maxTasks: Math.min(request.aggregateBudget.maxTasks, maxWorkflowTasks),
537
+ },
538
+ };
539
+ }
540
+
541
+ function plannerFailure(result: SingleResult): Error {
542
+ return new Error(
543
+ boundedPrivateText(
544
+ result.errorMessage || result.stderr.trim() || "Automation planner failed",
545
+ 2 * 1024,
546
+ ),
547
+ );
548
+ }
549
+
550
+ function assertCurrent(signal: AbortSignal, isCurrent: () => boolean): void {
551
+ if (signal.aborted || !isCurrent()) throw abortError("Autonomous workflow owner was replaced");
552
+ }
553
+
554
+ function abortError(message: string): Error {
555
+ const error = new Error(message);
556
+ error.name = "AbortError";
557
+ return error;
558
+ }
559
+
560
+ function combineSignals(
561
+ external: AbortSignal | undefined,
562
+ owned: AbortSignal,
563
+ ): { signal: AbortSignal; dispose(): void } {
564
+ const controller = new AbortController();
565
+ const signals = [external, owned].filter((value): value is AbortSignal => value !== undefined);
566
+ const listeners = signals.map((source) => {
567
+ const listener = () => {
568
+ if (!controller.signal.aborted) controller.abort(source.reason);
569
+ };
570
+ if (source.aborted) listener();
571
+ else source.addEventListener("abort", listener, { once: true });
572
+ return { source, listener };
573
+ });
574
+ return {
575
+ signal: controller.signal,
576
+ dispose() {
577
+ for (const { source, listener } of listeners) source.removeEventListener("abort", listener);
578
+ },
579
+ };
580
+ }
@@ -161,7 +161,7 @@ export function resolveContractTools(
161
161
  contract: DelegationContract | undefined,
162
162
  ): string[] | undefined {
163
163
  const requested = contract?.requestedAuthority?.tools;
164
- if (contract?.enforcement !== "enforce" || !requested || requested.length === 0) {
164
+ if (contract?.enforcement !== "enforce" || requested === undefined) {
165
165
  return configuredTools ? unique(configuredTools) : undefined;
166
166
  }
167
167
  const availableTools = resolveAgentToolNames(configuredTools);
package/src/subagents.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  formatAgentCatalog,
23
23
  type SubagentSettings,
24
24
  } from "./agents.js";
25
+ import { registerSubagentAutomation } from "./automation.js";
25
26
  import { registerSubagentConfigCommand, registerSubagentConfigLifecycle } from "./config-ui.js";
26
27
  import { registerSubagentConsult } from "./consult.js";
27
28
  import { executeSubagent } from "./execution.js";
@@ -50,6 +51,7 @@ export default function (pi: ExtensionAPI) {
50
51
  const refreshBlockingCatalog = blockingEnabled
51
52
  ? registerBlockingSubagent(pi, () => currentSettings)
52
53
  : () => undefined;
54
+ if (blockingEnabled) registerSubagentAutomation(pi, { getSettings: () => currentSettings });
53
55
  let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
54
56
  let refreshConsultCatalog: (catalog: string) => void = () => undefined;
55
57