pi-subagents 0.56.0 → 0.58.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 +92 -0
- package/agents/claude-code-writer.md +15 -0
- package/agents/claude-code.md +15 -0
- package/agents/codex-exec-writer.md +15 -0
- package/agents/codex-exec.md +15 -0
- package/agents/cursor-agent-writer.md +14 -0
- package/agents/cursor-agent.md +14 -0
- package/docs/agents.md +124 -21
- package/docs/configuration.md +37 -0
- package/docs/extension-api.md +41 -2
- package/docs/models.md +3 -3
- package/docs/observability.md +8 -7
- package/docs/tool-reference.md +14 -3
- package/docs/workflows.md +44 -0
- package/package.json +1 -1
- package/skills/pi-subagents/SKILL.md +2 -0
- package/skills/pi-subagents/references/execution-controls.md +2 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -0
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/agent-management.ts +36 -5
- package/src/agents/agent-refinements.ts +4 -4
- package/src/agents/agent-serializer.ts +5 -0
- package/src/agents/agents.ts +257 -51
- package/src/agents/builtin-names.ts +6 -0
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +18 -4
- package/src/api/agents.ts +10 -5
- package/src/api/preflight.ts +28 -3
- package/src/extension/config.ts +70 -0
- package/src/extension/doctor.ts +3 -3
- package/src/extension/index.ts +33 -8
- package/src/extension/public-execution.ts +29 -13
- package/src/extension/rpc.ts +55 -19
- package/src/extension/schemas.ts +6 -5
- package/src/extension/tool-description.ts +18 -12
- package/src/inspectors/herdr/actions.ts +2 -1
- package/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/src/integrations/herdr-status.ts +51 -3
- package/src/runs/background/active-async-capacity.ts +77 -10
- package/src/runs/background/async-execution.ts +127 -19
- package/src/runs/background/async-job-tracker.ts +5 -0
- package/src/runs/background/async-resume.ts +6 -2
- package/src/runs/background/async-retention.ts +20 -3
- package/src/runs/background/async-status.ts +7 -0
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/chain-root-attachment.ts +15 -1
- package/src/runs/background/fleet-view.ts +16 -10
- package/src/runs/background/inspect-rpc.ts +8 -8
- package/src/runs/background/notify.ts +26 -3
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +27 -14
- package/src/runs/background/result-watcher.ts +36 -15
- package/src/runs/background/run-status.ts +33 -6
- package/src/runs/background/scheduled-runs.ts +7 -1
- package/src/runs/background/subagent-runner.ts +239 -56
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/foreground/execution.ts +92 -11
- package/src/runs/foreground/foreground-control.ts +6 -0
- package/src/runs/foreground/foreground-history.ts +22 -1
- package/src/runs/foreground/subagent-executor.ts +322 -102
- package/src/runs/foreground/workflow-detach-reconcile.ts +144 -18
- package/src/runs/shared/child-protocol.ts +21 -7
- package/src/runs/shared/claude-code-adapter.ts +129 -0
- package/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/src/runs/shared/completion-guard.ts +4 -3
- package/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/external-cli-contract.ts +167 -0
- package/src/runs/shared/external-cli-preflight.ts +122 -0
- package/src/runs/shared/external-cli-runner.ts +348 -55
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +386 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +155 -42
- package/src/runs/shared/model-exclusions.ts +69 -7
- package/src/runs/shared/model-fallback.ts +39 -5
- package/src/runs/shared/mutation-evidence.ts +7 -2
- package/src/runs/shared/nested-events.ts +3 -1
- package/src/runs/shared/nested-render.ts +2 -2
- package/src/runs/shared/parallel-utils.ts +8 -1
- package/src/runs/shared/pi-args.ts +61 -6
- package/src/runs/shared/process-signal.ts +13 -0
- package/src/runs/shared/run-history.ts +21 -1
- package/src/runs/shared/single-output.ts +17 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +85 -9
- package/src/shared/fork-context.ts +21 -0
- package/src/shared/formatters.ts +13 -1
- package/src/shared/launch-contract.ts +4 -0
- package/src/shared/pruned-fork.ts +450 -0
- package/src/shared/session-file-trust.ts +19 -0
- package/src/shared/session-tokens.ts +14 -3
- package/src/shared/settings.ts +10 -2
- package/src/shared/shortcuts.ts +17 -0
- package/src/shared/types.ts +160 -10
- package/src/shared/utils.ts +6 -29
- package/src/shared/workflow-child-permit.ts +116 -0
- package/src/slash/delegation-adapters.ts +0 -1
- package/src/slash/slash-commands.ts +8 -6
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +27 -10
- package/src/tui/fleet-transcript.ts +11 -5
- package/src/tui/fleet.ts +28 -13
- package/src/tui/render.ts +55 -21
- package/src/workflows/scripted-workflow.ts +299 -31
- package/src/workflows/workflow-child-summary.ts +117 -0
- package/src/workflows/workflow-receipt.ts +155 -5
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerRuntimeAgent, type RuntimeAgentDefinition, type RuntimeAgentRegistration } from "./runtime-agent-registry.ts";
|
|
3
|
+
|
|
4
|
+
export const RUNTIME_AGENT_REGISTER_EVENT = "pi-subagents:runtime-agent-register:v1";
|
|
5
|
+
export const RUNTIME_AGENT_REGISTER_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export type RuntimeAgentRegistrationResult =
|
|
8
|
+
| { ok: true; registration: RuntimeAgentRegistration }
|
|
9
|
+
| { ok: false; error: Error };
|
|
10
|
+
|
|
11
|
+
export interface RuntimeAgentRegistrationRequest {
|
|
12
|
+
version: 1;
|
|
13
|
+
name: string;
|
|
14
|
+
definition: RuntimeAgentDefinition;
|
|
15
|
+
result?: RuntimeAgentRegistrationResult;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RegisterRuntimeAgentViaEventsInput {
|
|
19
|
+
pi: Pick<ExtensionAPI, "events">;
|
|
20
|
+
name: string;
|
|
21
|
+
definition: RuntimeAgentDefinition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function errorFrom(value: unknown): Error {
|
|
25
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Register through the installed pi-subagents owner in this Pi process. */
|
|
29
|
+
export function registerAgentViaEvents(input: RegisterRuntimeAgentViaEventsInput): RuntimeAgentRegistration {
|
|
30
|
+
const request: RuntimeAgentRegistrationRequest = {
|
|
31
|
+
version: RUNTIME_AGENT_REGISTER_VERSION,
|
|
32
|
+
name: input.name,
|
|
33
|
+
definition: input.definition,
|
|
34
|
+
};
|
|
35
|
+
input.pi.events.emit(RUNTIME_AGENT_REGISTER_EVENT, request);
|
|
36
|
+
const result = request.result as unknown;
|
|
37
|
+
if (result === undefined) {
|
|
38
|
+
throw new Error("pi-subagents is not installed, not ready, or does not support runtime agent event registration.");
|
|
39
|
+
}
|
|
40
|
+
if (result && typeof result === "object" && !Array.isArray(result)) {
|
|
41
|
+
const candidate = result as Record<string, unknown>;
|
|
42
|
+
if (candidate.ok === true && candidate.registration && typeof candidate.registration === "object" && typeof (candidate.registration as { dispose?: unknown }).dispose === "function") {
|
|
43
|
+
return candidate.registration as RuntimeAgentRegistration;
|
|
44
|
+
}
|
|
45
|
+
if (candidate.ok === false && candidate.error instanceof Error) throw candidate.error;
|
|
46
|
+
}
|
|
47
|
+
throw new Error("pi-subagents returned a malformed runtime agent registration result.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Install the process-local registration listener for the owning pi-subagents runtime. */
|
|
51
|
+
export function registerRuntimeAgentEventListener(pi: ExtensionAPI): () => void {
|
|
52
|
+
return pi.events.on(RUNTIME_AGENT_REGISTER_EVENT, (rawRequest) => {
|
|
53
|
+
if (!rawRequest || typeof rawRequest !== "object" || Array.isArray(rawRequest)) return;
|
|
54
|
+
const request = rawRequest as Record<string, unknown>;
|
|
55
|
+
if (request.result !== undefined) return;
|
|
56
|
+
try {
|
|
57
|
+
if (request.version !== RUNTIME_AGENT_REGISTER_VERSION) {
|
|
58
|
+
throw new Error(`Unsupported runtime agent registration event version '${String(request.version)}'.`);
|
|
59
|
+
}
|
|
60
|
+
const registration = registerRuntimeAgent({
|
|
61
|
+
pi,
|
|
62
|
+
name: request.name as string,
|
|
63
|
+
definition: request.definition as RuntimeAgentDefinition,
|
|
64
|
+
});
|
|
65
|
+
request.result = { ok: true, registration } satisfies RuntimeAgentRegistrationResult;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
request.result = { ok: false, error: errorFrom(error) } satisfies RuntimeAgentRegistrationResult;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
|
|
3
|
+
import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
|
|
3
4
|
import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
|
|
4
5
|
import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
|
|
5
6
|
import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
|
|
@@ -26,6 +27,7 @@ export interface RuntimeAgentDefinition {
|
|
|
26
27
|
thinking?: string | false;
|
|
27
28
|
systemPromptMode?: "append" | "replace";
|
|
28
29
|
inheritProjectContext?: boolean;
|
|
30
|
+
inheritGlobalContext?: boolean;
|
|
29
31
|
inheritSkills?: boolean;
|
|
30
32
|
defaultContext?: AgentDefaultContext;
|
|
31
33
|
defaultAsync?: boolean;
|
|
@@ -39,6 +41,7 @@ export interface RuntimeAgentDefinition {
|
|
|
39
41
|
skillPath?: readonly string[];
|
|
40
42
|
extensions?: readonly string[];
|
|
41
43
|
subagentOnlyExtensions?: readonly string[];
|
|
44
|
+
mutationTools?: readonly string[];
|
|
42
45
|
output?: string;
|
|
43
46
|
outputMode?: OutputMode;
|
|
44
47
|
defaultReads?: readonly string[];
|
|
@@ -169,12 +172,15 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
|
|
|
169
172
|
if (runner.type !== "external-cli") throw new Error("Runtime agent definition runner.type must be 'pi', 'external-cli', or 'external-job'.");
|
|
170
173
|
if (typeof runner.command !== "string" || !runner.command.trim()) throw new Error("Runtime agent definition external-cli runner requires a non-empty command string.");
|
|
171
174
|
if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) throw new Error("Runtime agent definition external-cli runner args must be an array of strings.");
|
|
175
|
+
if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Runtime agent definition external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
|
|
176
|
+
if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Runtime agent definition ${runner.adapter} adapter owns its argv; runner args are not supported.`);
|
|
172
177
|
if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") throw new Error("Runtime agent definition external-cli runner promptDelivery must be 'stdin'.");
|
|
173
|
-
const
|
|
178
|
+
const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, "Runtime agent definition external-cli runner capabilities");
|
|
179
|
+
const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
|
|
174
180
|
const unknown = Object.keys(runner).filter((key) => !supported.has(key));
|
|
175
181
|
if (unknown.length > 0) throw new Error(`Runtime agent definition external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
|
|
176
182
|
const args = runner.args as string[] | undefined;
|
|
177
|
-
return { type: "external-cli", command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}) };
|
|
183
|
+
return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
|
|
178
184
|
}
|
|
179
185
|
|
|
180
186
|
function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
|
|
@@ -200,9 +206,9 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
200
206
|
const definition = value as Record<string, unknown>;
|
|
201
207
|
const supported = new Set([
|
|
202
208
|
"description", "systemPrompt", "aliases", "tools", "mcpDirectTools", "model", "fallbackModels", "thinking",
|
|
203
|
-
"systemPromptMode", "inheritProjectContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
|
|
209
|
+
"systemPromptMode", "inheritProjectContext", "inheritGlobalContext", "inheritSkills", "defaultContext", "defaultAsync", "defaultTimeoutMs",
|
|
204
210
|
"defaultToolTimeoutMs", "defaultTurnBudget", "defaultAcceptance", "acceptanceRole", "runner", "skills", "skillPath",
|
|
205
|
-
"extensions", "subagentOnlyExtensions", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
|
|
211
|
+
"extensions", "subagentOnlyExtensions", "mutationTools", "output", "outputMode", "defaultReads", "defaultProgress", "interactive",
|
|
206
212
|
"maxSubagentDepth", "completionGuard", "toolBudget", "permissions",
|
|
207
213
|
]);
|
|
208
214
|
const unknown = Object.keys(definition).filter((key) => !supported.has(key));
|
|
@@ -223,6 +229,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
223
229
|
const model = validateOptionalString(definition.model, "Runtime agent definition model");
|
|
224
230
|
const fallbackModels = validateStringList(definition.fallbackModels, "Runtime agent definition fallbackModels");
|
|
225
231
|
const inheritProjectContext = validateBoolean(definition.inheritProjectContext, "Runtime agent definition inheritProjectContext");
|
|
232
|
+
const inheritGlobalContext = validateBoolean(definition.inheritGlobalContext, "Runtime agent definition inheritGlobalContext");
|
|
226
233
|
const inheritSkills = validateBoolean(definition.inheritSkills, "Runtime agent definition inheritSkills");
|
|
227
234
|
const defaultAsync = validateBoolean(definition.defaultAsync, "Runtime agent definition defaultAsync");
|
|
228
235
|
const defaultTimeoutMs = validatePositiveInteger(definition.defaultTimeoutMs, "Runtime agent definition defaultTimeoutMs");
|
|
@@ -234,6 +241,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
234
241
|
const skillPath = validateStringList(definition.skillPath, "Runtime agent definition skillPath");
|
|
235
242
|
const extensions = validateStringList(definition.extensions, "Runtime agent definition extensions");
|
|
236
243
|
const subagentOnlyExtensions = validateStringList(definition.subagentOnlyExtensions, "Runtime agent definition subagentOnlyExtensions");
|
|
244
|
+
const mutationTools = validateStringList(definition.mutationTools, "Runtime agent definition mutationTools");
|
|
237
245
|
const output = validateOptionalString(definition.output, "Runtime agent definition output");
|
|
238
246
|
const defaultReads = validateStringList(definition.defaultReads, "Runtime agent definition defaultReads");
|
|
239
247
|
const defaultProgress = validateBoolean(definition.defaultProgress, "Runtime agent definition defaultProgress");
|
|
@@ -253,6 +261,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
253
261
|
...(thinking !== undefined ? { thinking: thinking as string | false } : {}),
|
|
254
262
|
...(systemPromptMode !== undefined ? { systemPromptMode: systemPromptMode as "append" | "replace" } : {}),
|
|
255
263
|
...(inheritProjectContext !== undefined ? { inheritProjectContext } : {}),
|
|
264
|
+
...(inheritGlobalContext !== undefined ? { inheritGlobalContext } : {}),
|
|
256
265
|
...(inheritSkills !== undefined ? { inheritSkills } : {}),
|
|
257
266
|
...(defaultContext !== undefined ? { defaultContext: defaultContext as AgentDefaultContext } : {}),
|
|
258
267
|
...(defaultAsync !== undefined ? { defaultAsync } : {}),
|
|
@@ -266,6 +275,7 @@ function validateDefinition(value: unknown): RuntimeAgentDefinition {
|
|
|
266
275
|
...(skillPath ? { skillPath } : {}),
|
|
267
276
|
...(extensions ? { extensions } : {}),
|
|
268
277
|
...(subagentOnlyExtensions ? { subagentOnlyExtensions } : {}),
|
|
278
|
+
...(mutationTools ? { mutationTools } : {}),
|
|
269
279
|
...(output ? { output } : {}),
|
|
270
280
|
...(outputMode !== undefined ? { outputMode: outputMode as OutputMode } : {}),
|
|
271
281
|
...(defaultReads ? { defaultReads } : {}),
|
|
@@ -330,6 +340,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
|
|
|
330
340
|
...(definition.thinking !== undefined ? { thinking: definition.thinking } : {}),
|
|
331
341
|
systemPromptMode: definition.systemPromptMode ?? defaultSystemPromptMode(name),
|
|
332
342
|
inheritProjectContext: definition.inheritProjectContext ?? defaultInheritProjectContext(name),
|
|
343
|
+
inheritGlobalContext: definition.inheritGlobalContext ?? false,
|
|
333
344
|
inheritSkills: definition.inheritSkills ?? defaultInheritSkills(),
|
|
334
345
|
...(definition.defaultContext !== undefined ? { defaultContext: definition.defaultContext } : {}),
|
|
335
346
|
...(definition.defaultAsync !== undefined ? { defaultAsync: definition.defaultAsync } : {}),
|
|
@@ -345,6 +356,7 @@ function toAgentConfig(name: string, definition: RuntimeAgentDefinition): AgentC
|
|
|
345
356
|
...(definition.skillPath !== undefined ? { skillPath: [...definition.skillPath] } : {}),
|
|
346
357
|
...(definition.extensions !== undefined ? { extensions: [...definition.extensions] } : {}),
|
|
347
358
|
...(definition.subagentOnlyExtensions !== undefined ? { subagentOnlyExtensions: [...definition.subagentOnlyExtensions] } : {}),
|
|
359
|
+
...(definition.mutationTools !== undefined ? { mutationTools: [...definition.mutationTools] } : {}),
|
|
348
360
|
...(definition.output !== undefined ? { output: definition.output } : {}),
|
|
349
361
|
...(definition.outputMode !== undefined ? { outputMode: definition.outputMode } : {}),
|
|
350
362
|
...(definition.defaultReads !== undefined ? { defaultReads: [...definition.defaultReads] } : {}),
|
|
@@ -364,6 +376,8 @@ export function registerRuntimeAgent(input: RegisterRuntimeAgentInput): RuntimeA
|
|
|
364
376
|
const name = validateString(input.name, "Runtime agent name", MAX_AGENT_NAME_LENGTH);
|
|
365
377
|
const definition = validateDefinition(input.definition);
|
|
366
378
|
const agent = toAgentConfig(name, definition);
|
|
379
|
+
const profileError = validateCodeOwnedProfileRunner(agent);
|
|
380
|
+
if (profileError) throw new Error(profileError);
|
|
367
381
|
assertNoBuiltinCollision(agent);
|
|
368
382
|
const current = registry();
|
|
369
383
|
const records = current.byPi.get(pi) ?? [];
|
package/src/api/agents.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { RegisterRuntimeAgentInput, RuntimeAgentDefinition, RuntimeAgentRegistration } from "../agents/runtime-agent-registry.ts";
|
|
2
|
+
export { registerRuntimeAgent as registerAgent } from "../agents/runtime-agent-registry.ts";
|
|
3
|
+
export {
|
|
4
|
+
RUNTIME_AGENT_REGISTER_EVENT,
|
|
5
|
+
RUNTIME_AGENT_REGISTER_VERSION,
|
|
6
|
+
registerAgentViaEvents,
|
|
7
|
+
type RegisterRuntimeAgentViaEventsInput,
|
|
8
|
+
type RuntimeAgentRegistrationRequest,
|
|
9
|
+
type RuntimeAgentRegistrationResult,
|
|
10
|
+
} from "../agents/runtime-agent-events.ts";
|
|
2
11
|
|
|
3
12
|
export type { RegisterRuntimeAgentInput, RuntimeAgentDefinition, RuntimeAgentRegistration };
|
|
4
|
-
|
|
5
|
-
export function registerAgent(input: RegisterRuntimeAgentInput): RuntimeAgentRegistration {
|
|
6
|
-
return registerRuntimeAgent(input);
|
|
7
|
-
}
|
package/src/api/preflight.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
4
|
+
import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
|
|
5
5
|
import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
|
|
6
6
|
import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
|
|
7
7
|
import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
|
|
@@ -42,8 +42,10 @@ export type SubagentLaunchContractReasonCode =
|
|
|
42
42
|
| "thinking_ceiling"
|
|
43
43
|
| "invalid_extension_bindings";
|
|
44
44
|
|
|
45
|
+
export type SubagentLaunchContractDiagnosticCode = SubagentLaunchContractReasonCode | "host_required" | "snapshot_warning" | "workspace_scope_authority";
|
|
46
|
+
|
|
45
47
|
export interface SubagentLaunchContractDiagnostic {
|
|
46
|
-
code:
|
|
48
|
+
code: SubagentLaunchContractDiagnosticCode;
|
|
47
49
|
severity: "error" | "warning" | "host-required";
|
|
48
50
|
message: string;
|
|
49
51
|
}
|
|
@@ -157,6 +159,7 @@ export interface SubagentLaunchContract {
|
|
|
157
159
|
thinkingCeiling?: ThinkingLevel;
|
|
158
160
|
systemPromptMode: AgentConfig["systemPromptMode"];
|
|
159
161
|
inheritProjectContext: boolean;
|
|
162
|
+
inheritGlobalContext: boolean;
|
|
160
163
|
inheritSkills: boolean;
|
|
161
164
|
skills: SubagentLaunchContractSkills;
|
|
162
165
|
tools: SubagentLaunchContractTools;
|
|
@@ -204,6 +207,24 @@ function resolveLaunchContractContext(input: SubagentLaunchContractInput, agent:
|
|
|
204
207
|
});
|
|
205
208
|
}
|
|
206
209
|
|
|
210
|
+
function taskWorkspaceScopeAuthorityDiagnostic(task: string | undefined): SubagentLaunchContractDiagnostic | undefined {
|
|
211
|
+
if (!task) return undefined;
|
|
212
|
+
const text = task.replace(/\s+/g, " ").trim();
|
|
213
|
+
if (!text) return undefined;
|
|
214
|
+
const createsWorkspacePackage = /\b(?:add|create|introduce|make|set up)\b.{0,80}\b(?:new\s+)?(?:workspace\s+)?package\b/i.test(text)
|
|
215
|
+
|| /\b(?:new\s+)?package\b.{0,80}\b(?:workspace|monorepo)\b/i.test(text);
|
|
216
|
+
if (!createsWorkspacePackage) return undefined;
|
|
217
|
+
const packageOnlyAuthority = /\b(?:only|solely)\b.{0,40}\b(?:edit|change|modify|touch|write(?:\s+to)?)\b.{0,80}\b(?:package(?:\s+directory)?|packages\/[\w.-]+)\b/i.test(text)
|
|
218
|
+
|| /\b(?:do not|don't|must not)\b.{0,40}\b(?:edit|change|modify|touch|write(?:\s+to)?)\b.{0,80}\b(?:root|workspace|lockfile|metadata)\b/i.test(text)
|
|
219
|
+
|| /\bwithout\b.{0,40}\b(?:root|workspace|lockfile|metadata)\b.{0,40}\b(?:edit|change|modification|write)s?\b/i.test(text);
|
|
220
|
+
if (!packageOnlyAuthority) return undefined;
|
|
221
|
+
return {
|
|
222
|
+
code: "workspace_scope_authority",
|
|
223
|
+
severity: "warning",
|
|
224
|
+
message: "Task asks for a workspace package change while limiting authority to package-scope edits. New workspace packages often need root workspace metadata or lockfile changes, so confirm that authority before launch.",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
207
228
|
function candidateList(inputAgent: string, selected: AgentConfig | undefined, cwd: string): SubagentLaunchContractAgentCandidate[] {
|
|
208
229
|
const all = discoverAgentsAll(cwd);
|
|
209
230
|
return [...all.builtin, ...all.package, ...all.user, ...all.project]
|
|
@@ -221,6 +242,8 @@ function candidateList(inputAgent: string, selected: AgentConfig | undefined, cw
|
|
|
221
242
|
|
|
222
243
|
export async function resolveSubagentLaunchContract(input: SubagentLaunchContractInput): Promise<SubagentLaunchContractResult> {
|
|
223
244
|
const diagnostics: SubagentLaunchContractDiagnostic[] = [];
|
|
245
|
+
const authorityDiagnostic = taskWorkspaceScopeAuthorityDiagnostic(input.task);
|
|
246
|
+
if (authorityDiagnostic) diagnostics.push(authorityDiagnostic);
|
|
224
247
|
const effectiveCwd = path.resolve(input.cwd);
|
|
225
248
|
try {
|
|
226
249
|
if (!fs.statSync(effectiveCwd).isDirectory()) {
|
|
@@ -251,7 +274,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
251
274
|
return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
|
|
252
275
|
}
|
|
253
276
|
if (!resolvedAgent.agent) {
|
|
254
|
-
return { ok: false, code: "missing_agent", message:
|
|
277
|
+
return { ok: false, code: "missing_agent", message: formatUnknownAgentError(input.agent, unknownAgentDiagnosticContext(discovered)), diagnostics };
|
|
255
278
|
}
|
|
256
279
|
const agent = resolvedAgent.agent;
|
|
257
280
|
let extensionBindings: ExtensionBindings | undefined;
|
|
@@ -400,6 +423,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
400
423
|
...(thinkingCeiling ? { thinkingCeiling } : {}),
|
|
401
424
|
systemPromptMode: agent.systemPromptMode,
|
|
402
425
|
inheritProjectContext: agent.inheritProjectContext,
|
|
426
|
+
inheritGlobalContext: agent.inheritGlobalContext,
|
|
403
427
|
inheritSkills: agent.inheritSkills,
|
|
404
428
|
skills: {
|
|
405
429
|
requested: requestedSkills,
|
|
@@ -455,6 +479,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
|
|
|
455
479
|
systemPrompt: effectiveSystemPrompt,
|
|
456
480
|
systemPromptMode: agent.systemPromptMode,
|
|
457
481
|
inheritProjectContext: agent.inheritProjectContext,
|
|
482
|
+
inheritGlobalContext: agent.inheritGlobalContext,
|
|
458
483
|
inheritSkills: agent.inheritSkills,
|
|
459
484
|
skills: requestedSkills,
|
|
460
485
|
tools: toolPlan.effectiveToolAllowlist,
|
package/src/extension/config.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
|
|
|
6
6
|
import { validateMissionStoreConfig } from "../missions/store.ts";
|
|
7
7
|
import { validateAuthorityPolicy } from "../policy/authority.ts";
|
|
8
8
|
import { getAgentDir } from "../shared/utils.ts";
|
|
9
|
+
import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
|
|
9
10
|
import { validatePermissionConfig } from "../runs/shared/permissions.ts";
|
|
11
|
+
import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
|
|
10
12
|
|
|
11
13
|
const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
|
|
12
14
|
const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
|
|
@@ -16,6 +18,23 @@ const BASE_KEY_IDS = new Set([
|
|
|
16
18
|
...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
|
|
17
19
|
]);
|
|
18
20
|
|
|
21
|
+
class PrunedForkConfigError extends Error {}
|
|
22
|
+
|
|
23
|
+
function validateForkContextConfig(value: unknown): void {
|
|
24
|
+
if (value === undefined) return;
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
|
|
26
|
+
const config = value as Record<string, unknown>;
|
|
27
|
+
if (config.mode !== undefined && config.mode !== "full" && config.mode !== "pruned") {
|
|
28
|
+
throw new Error('config.forkContext.mode must be "full" or "pruned"');
|
|
29
|
+
}
|
|
30
|
+
if (config.model !== undefined && (typeof config.model !== "string" || !config.model.trim())) {
|
|
31
|
+
throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
|
|
32
|
+
}
|
|
33
|
+
if (config.mode === "pruned" && config.model === undefined) {
|
|
34
|
+
throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
function isValidKeyId(value: string): boolean {
|
|
20
39
|
if (value !== value.trim()) return false;
|
|
21
40
|
const parts = value.toLowerCase().split("+");
|
|
@@ -62,6 +81,31 @@ function validateArtifactConfig(value: unknown): void {
|
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
function validateCapacityConfig(value: unknown): void {
|
|
85
|
+
if (value === undefined) return;
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
|
|
87
|
+
const abandonedSlotReleaseAfterMs = (value as Record<string, unknown>).abandonedSlotReleaseAfterMs;
|
|
88
|
+
if (abandonedSlotReleaseAfterMs !== undefined
|
|
89
|
+
&& abandonedSlotReleaseAfterMs !== false
|
|
90
|
+
&& (typeof abandonedSlotReleaseAfterMs !== "number"
|
|
91
|
+
|| !Number.isInteger(abandonedSlotReleaseAfterMs)
|
|
92
|
+
|| abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS
|
|
93
|
+
|| abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
|
|
94
|
+
throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validate the user-controlled TTL policy before it reaches the exclusion store. */
|
|
99
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
100
|
+
function validateModelExclusionsConfig(value: unknown): void {
|
|
101
|
+
if (value === undefined) return;
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
|
|
103
|
+
const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
|
|
104
|
+
if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
|
|
105
|
+
throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
65
109
|
function validateOrcaProgressTabsConfig(value: unknown): void {
|
|
66
110
|
if (value === undefined) return;
|
|
67
111
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
|
|
@@ -97,6 +141,7 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
97
141
|
if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
|
|
98
142
|
throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
|
|
99
143
|
}
|
|
144
|
+
validateForkContextConfig(config.forkContext);
|
|
100
145
|
if (config.foregroundDetachShortcut !== undefined
|
|
101
146
|
&& (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
|
|
102
147
|
throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
|
|
@@ -119,6 +164,8 @@ function validateConfig(config: Record<string, unknown>): void {
|
|
|
119
164
|
validateScheduledRunsConfig(config.scheduledRuns);
|
|
120
165
|
validateFleetKeybindingsConfig(config.fleetKeybindings);
|
|
121
166
|
validateArtifactConfig(config.artifactConfig);
|
|
167
|
+
validateCapacityConfig(config.capacity);
|
|
168
|
+
validateModelExclusionsConfig(config.modelExclusions);
|
|
122
169
|
validateMainWindowRendererConfig(config.mainWindowRenderer);
|
|
123
170
|
validateOrcaProgressTabsConfig(config.orcaProgressTabs);
|
|
124
171
|
}
|
|
@@ -150,6 +197,28 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
|
|
|
150
197
|
return next;
|
|
151
198
|
}
|
|
152
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Resolve the default TTL that the process-wide exclusion store should use.
|
|
202
|
+
*
|
|
203
|
+
* @param config Extension configuration after validation.
|
|
204
|
+
* @returns The configured TTL in milliseconds, or the built-in 24-hour default.
|
|
205
|
+
*/
|
|
206
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
207
|
+
export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
|
|
208
|
+
return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Apply the configured exclusion policy to the process-wide model store.
|
|
213
|
+
*
|
|
214
|
+
* @param config Extension configuration after validation.
|
|
215
|
+
* @returns Nothing.
|
|
216
|
+
*/
|
|
217
|
+
// TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
|
|
218
|
+
export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
|
|
219
|
+
setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
|
|
220
|
+
}
|
|
221
|
+
|
|
153
222
|
export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
|
|
154
223
|
return config.asyncByDefault !== false;
|
|
155
224
|
}
|
|
@@ -159,6 +228,7 @@ export function loadConfig(): ExtensionConfig {
|
|
|
159
228
|
try {
|
|
160
229
|
return readConfigForUpdate(configPath);
|
|
161
230
|
} catch (error) {
|
|
231
|
+
if (error instanceof PrunedForkConfigError) throw error;
|
|
162
232
|
console.error(`Failed to load subagent config from '${configPath}':`, error);
|
|
163
233
|
}
|
|
164
234
|
return {};
|
package/src/extension/doctor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
|
|
4
4
|
import { isAsyncAvailable } from "../runs/background/async-execution.ts";
|
|
5
5
|
import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
|
|
6
|
-
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
6
|
+
import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
7
7
|
import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
|
|
8
8
|
import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
|
|
9
9
|
import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
|
|
@@ -197,13 +197,13 @@ function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
|
|
|
197
197
|
const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
|
|
198
198
|
const sessionId = input.currentSessionId ?? input.state.currentSessionId;
|
|
199
199
|
const snapshot = sessionId
|
|
200
|
-
? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
|
|
200
|
+
? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.config.capacity?.abandonedSlotReleaseAfterMs) })
|
|
201
201
|
: { used: 0, limit: limit ?? 0 };
|
|
202
202
|
input.state.activeAsyncCapacity = snapshot;
|
|
203
203
|
return [
|
|
204
204
|
`- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
|
|
205
205
|
"- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
|
|
206
|
-
"- release: terminal
|
|
206
|
+
"- release: terminal state plus matching observed process-terminal proof, or abandoned-timeout for failed runs with a dead runner PID and stale activity when enabled; false keeps unknown-proof slots",
|
|
207
207
|
];
|
|
208
208
|
}
|
|
209
209
|
|
package/src/extension/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Toggle: async parameter (default: true; set asyncByDefault:false in config.json to opt out)
|
|
10
10
|
*
|
|
11
11
|
* Config file: ~/.pi/agent/extensions/subagent/config.json
|
|
12
|
-
* { "asyncByDefault": true, "defaultSubagentContext": "fork", "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
12
|
+
* { "asyncByDefault": true, "defaultSubagentContext": "fork", "forkContext": { "mode": "pruned", "model": "provider/model" }, "forceTopLevelAsync": true, "maxSubagentDepth": 1, "intercomBridge": { "mode": "always", "instructionFile": "./intercom-bridge.md" }, "worktreeSetupHook": "./scripts/setup-worktree.mjs" }
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
@@ -21,9 +21,11 @@ import { keyText, type ExtensionAPI, type ExtensionContext, type ToolDefinition
|
|
|
21
21
|
import { Box, Container, Spacer, Text, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
22
22
|
import { discoverAgents, discoverAgentsAll, type AgentConfig, type AgentScope } from "../agents/agents.ts";
|
|
23
23
|
import { clearRuntimeAgentsForPi, listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
|
|
24
|
+
import { registerRuntimeAgentEventListener } from "../agents/runtime-agent-events.ts";
|
|
24
25
|
import { ensureAccessibleDir } from "../shared/accessible-dir.ts";
|
|
25
26
|
import { cleanupAllArtifactDirs, cleanupOldArtifacts, getArtifactsDir } from "../shared/artifacts.ts";
|
|
26
27
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
28
|
+
import { getAgentDir } from "../shared/utils.ts";
|
|
27
29
|
import { currentCompletionOwnerId } from "../shared/completion-owner.ts";
|
|
28
30
|
import { cleanupOldChainDirs } from "../shared/settings.ts";
|
|
29
31
|
import { clearLegacyResultAnimationTimer, renderSubagentResult, renderSubagentSummary } from "../tui/render.ts";
|
|
@@ -32,10 +34,11 @@ import { SubagentFleetStatus, resolveFleetViewPlacement } from "../tui/fleet-sta
|
|
|
32
34
|
import { createSubagentParamsSchema } from "./schemas.ts";
|
|
33
35
|
import { createSubagentExecutor, type SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
34
36
|
import { createAsyncJobTracker } from "../runs/background/async-job-tracker.ts";
|
|
35
|
-
import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
37
|
+
import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
|
|
36
38
|
import { cleanupResultIndexes, missionObserverResultCandidateFiles } from "../runs/background/result-files.ts";
|
|
37
39
|
import { ASYNC_RETENTION_DELAY_MS, cleanupAsyncRetention } from "../runs/background/async-retention.ts";
|
|
38
40
|
import { createResultWatcher } from "../runs/background/result-watcher.ts";
|
|
41
|
+
import { createResultDeliveryOwnership } from "../runs/background/result-delivery-ownership.ts";
|
|
39
42
|
import { createScheduledRunManager } from "../runs/background/scheduled-runs.ts";
|
|
40
43
|
import { registerSlashCommands } from "../slash/slash-commands.ts";
|
|
41
44
|
import { registerPromptTemplateDelegationBridge } from "../slash/prompt-template-bridge.ts";
|
|
@@ -56,7 +59,7 @@ import { formatSteeringNotice, handleSubagentSteeringNotice, SUBAGENT_STEERING_M
|
|
|
56
59
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_PARENT_SESSION_ENV } from "../runs/shared/pi-args.ts";
|
|
57
60
|
import { resolveCurrentSubagentCapabilityCeiling } from "../runs/shared/capability-ceiling.ts";
|
|
58
61
|
import { formatDuration, shortenPath } from "../shared/formatters.ts";
|
|
59
|
-
import { loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
62
|
+
import { applyModelExclusionsConfig, loadConfig, resolveAsyncByDefault, resolveScheduledStoreRoot } from "./config.ts";
|
|
60
63
|
import { buildSubagentToolDescription, buildSubagentToolPromptMetadata } from "./tool-description.ts";
|
|
61
64
|
import { finalizeToolResult } from "./tool-result.ts";
|
|
62
65
|
import { collectGoalContinuationNotices } from "../missions/goal-driver.ts";
|
|
@@ -382,9 +385,13 @@ export function projectActiveHerdrRuns(state: SubagentState): HerdrStatusRun[] {
|
|
|
382
385
|
.filter((job) => active(job.status))
|
|
383
386
|
.map((job) => {
|
|
384
387
|
const children = job.mode === "workflow" ? foregroundChildrenByWorkflow.get(job.asyncId) : undefined;
|
|
388
|
+
const currentStep = job.steps?.find((step) => step.status === "running")
|
|
389
|
+
?? (job.currentStep !== undefined ? job.steps?.[job.currentStep] : undefined)
|
|
390
|
+
?? job.steps?.find((step) => step.status === "pending");
|
|
385
391
|
return {
|
|
386
392
|
id: job.asyncId,
|
|
387
393
|
agents: children?.length ? children.map((child) => child.agent) : job.agents,
|
|
394
|
+
...(currentStep?.label ? { taskLabel: currentStep.label } : {}),
|
|
388
395
|
needsAttention: job.activityState === "needs_attention" || children?.some((child) => child.needsAttention),
|
|
389
396
|
};
|
|
390
397
|
});
|
|
@@ -401,6 +408,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
401
408
|
cleanupOldChainDirs();
|
|
402
409
|
|
|
403
410
|
const config = loadConfig();
|
|
411
|
+
// Apply the process-wide exclusion TTL before any child launch can record a model failure.
|
|
412
|
+
applyModelExclusionsConfig(config);
|
|
404
413
|
const waitToolConfig = resolveWaitToolConfig(config.waitTool);
|
|
405
414
|
const asyncByDefault = resolveAsyncByDefault(config);
|
|
406
415
|
const fleetViewEnabled = config.fleetView !== false;
|
|
@@ -459,7 +468,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
459
468
|
const supervisorChannel = createNativeSupervisorChannel(pi, state);
|
|
460
469
|
const waitSubscriptionManager = createWaitSubscriptionManager(pi, state);
|
|
461
470
|
const mainWatchdog = registerMainWatchdog(pi);
|
|
462
|
-
const
|
|
471
|
+
const resultDeliveryOwnership = createResultDeliveryOwnership(state);
|
|
472
|
+
const completionNotifier = registerSubagentNotify(pi, state, { batchConfig: config.completionBatch, ownership: resultDeliveryOwnership });
|
|
463
473
|
const fleetStatus = fleetViewEnabled
|
|
464
474
|
? new SubagentFleetStatus(state, async (itemKey) => {
|
|
465
475
|
const ctx = state.lastUiContext;
|
|
@@ -521,6 +531,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
521
531
|
10 * 60 * 1000,
|
|
522
532
|
{
|
|
523
533
|
notifier: completionNotifier,
|
|
534
|
+
ownership: resultDeliveryOwnership,
|
|
524
535
|
observeCompletion: (result) => scheduledRunManager.handleAsyncCompletion(result),
|
|
525
536
|
observedCompletionRunIds: () => scheduledRunManager.observedCompletionRunIds(),
|
|
526
537
|
hasDeliveryDemand: hasResultDeliveryDemand,
|
|
@@ -528,7 +539,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
528
539
|
resultScanLogging: config.resultScanLogging,
|
|
529
540
|
},
|
|
530
541
|
);
|
|
531
|
-
const { startResultWatcher, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
542
|
+
const { startResultWatcher, transitionResultDelivery, primeExistingResults, stopResultWatcher } = resultWatcher;
|
|
532
543
|
refreshResultDelivery = resultWatcher.refreshResultDelivery;
|
|
533
544
|
const asyncRetentionAbort = new AbortController();
|
|
534
545
|
const asyncRetentionTimer = setTimeout(async () => {
|
|
@@ -695,6 +706,12 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
695
706
|
0,
|
|
696
707
|
0,
|
|
697
708
|
);
|
|
709
|
+
if (args.workflowScriptPath)
|
|
710
|
+
return new Text(
|
|
711
|
+
`${title}${gap}${theme.fg("accent", args.workflowScriptPath)}${args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : ""}`,
|
|
712
|
+
0,
|
|
713
|
+
0,
|
|
714
|
+
);
|
|
698
715
|
const asyncLabel = args.async === true ? `${gap}${theme.fg("warning", "[async]")}` : "";
|
|
699
716
|
return new Text(
|
|
700
717
|
`${title}${gap}${theme.fg("accent", args.agent || "?")}${asyncLabel}`,
|
|
@@ -778,6 +795,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
778
795
|
fleetStatus?.refresh();
|
|
779
796
|
};
|
|
780
797
|
const eventUnsubscribes = [
|
|
798
|
+
registerRuntimeAgentEventListener(pi),
|
|
781
799
|
pi.events.on(SUBAGENT_ASYNC_STARTED_EVENT, asyncStartedHandler),
|
|
782
800
|
pi.events.on(SUBAGENT_ASYNC_COMPLETE_EVENT, asyncCompleteHandler),
|
|
783
801
|
pi.events.on(SUBAGENT_PROCESS_TERMINAL_EVENT, () => {
|
|
@@ -794,7 +812,9 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
794
812
|
if (event.toolName !== "subagent") return;
|
|
795
813
|
if (!ctx.hasUI) return;
|
|
796
814
|
state.lastUiContext = ctx;
|
|
815
|
+
const activeJobCount = state.asyncJobs.size;
|
|
797
816
|
restoreActiveJobs(ctx);
|
|
817
|
+
if (state.asyncJobs.size > activeJobCount) herdrStatusBridge.syncRuns();
|
|
798
818
|
fleetStatus?.setContext(ctx);
|
|
799
819
|
fleetStatus?.refresh();
|
|
800
820
|
if (state.asyncJobs.size > 0) {
|
|
@@ -836,16 +856,20 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
836
856
|
state.activeAsyncCapacity = getActiveAsyncCapacitySnapshot(
|
|
837
857
|
state.currentSessionId,
|
|
838
858
|
resolveMaxActiveAsyncRunsPerSession(config.maxActiveAsyncRunsPerSession),
|
|
839
|
-
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []) },
|
|
859
|
+
{ liveWorkflowRunIds: new Set(state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(config.capacity?.abandonedSlotReleaseAfterMs) },
|
|
840
860
|
);
|
|
841
861
|
};
|
|
842
862
|
|
|
843
|
-
const resetSessionState = (ctx: ExtensionContext, recovering: boolean) => {
|
|
863
|
+
const resetSessionState = (ctx: ExtensionContext, recovering: boolean, previousSessionFile?: string) => {
|
|
844
864
|
state.widgetsSuspended = false;
|
|
845
865
|
state.baseCwd = ctx.cwd;
|
|
846
866
|
goalTurnId = 0;
|
|
867
|
+
const previousRuntimeSessionId = state.currentSessionId;
|
|
868
|
+
resultDeliveryOwnership.claimPredecessor(previousSessionFile, previousRuntimeSessionId);
|
|
847
869
|
state.currentSessionId = resolveCurrentSessionId(ctx.sessionManager);
|
|
870
|
+
transitionResultDelivery();
|
|
848
871
|
state.parentSessionFile = ctx.sessionManager.getSessionFile();
|
|
872
|
+
state.trustedSessionFileRoot = state.parentSessionFile ? path.join(getAgentDir(), "sessions") : undefined;
|
|
849
873
|
state.trustedSessionRoots = [...new Set([
|
|
850
874
|
...(config.defaultSessionDir ? [path.resolve(expandTilde(config.defaultSessionDir))] : []),
|
|
851
875
|
...(state.parentSessionFile ? [getSubagentSessionRoot(state.parentSessionFile)] : []),
|
|
@@ -920,6 +944,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
920
944
|
clearTimeout(asyncRetentionTimer);
|
|
921
945
|
asyncRetentionAbort.abort();
|
|
922
946
|
stopResultWatcher();
|
|
947
|
+
resultDeliveryOwnership.clear();
|
|
923
948
|
completionNotifier.dispose();
|
|
924
949
|
mainWatchdog.dispose();
|
|
925
950
|
scheduledRunManager.stop();
|
|
@@ -1020,7 +1045,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
1020
1045
|
pi.on("session_start", (event, ctx) => {
|
|
1021
1046
|
installRuntime(ctx);
|
|
1022
1047
|
const recovering = event.reason === "startup" || event.reason === "reload" || event.reason === "resume";
|
|
1023
|
-
resetSessionState(ctx, recovering);
|
|
1048
|
+
resetSessionState(ctx, recovering, event.previousSessionFile);
|
|
1024
1049
|
herdrStatusBridge.sessionStarted({
|
|
1025
1050
|
hasUI: ctx.hasUI === true,
|
|
1026
1051
|
runs: activeHerdrRuns(),
|