pi-subagents 0.53.0 → 0.55.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 +66 -5
- package/README.md +1 -1
- package/agents/reviewer.md +12 -2
- package/docs/agents.md +4 -4
- package/docs/configuration.md +6 -6
- package/docs/extension-api.md +14 -4
- package/docs/models.md +41 -6
- package/docs/observability.md +14 -3
- package/docs/tool-reference.md +21 -6
- package/docs/workflows.md +7 -1
- package/index.ts +10 -1
- package/package.json +1 -1
- package/prompts/council.md +19 -7
- package/prompts/parallel-review.md +5 -1
- package/prompts/review-loop.md +9 -5
- package/skills/council-mode/SKILL.md +50 -26
- package/skills/pi-subagents/SKILL.md +5 -1
- package/skills/pi-subagents/references/constraints-and-recipes.md +16 -1
- package/skills/pi-subagents/references/execution-controls.md +13 -4
- package/skills/pi-subagents/references/multi-lane-orchestration.md +3 -3
- package/skills/pi-subagents/references/prompting-and-roles.md +14 -7
- package/src/agents/agent-management.ts +115 -14
- package/src/agents/agent-serializer.ts +2 -2
- package/src/agents/agents.ts +195 -47
- package/src/api/external-job-provider.ts +10 -1
- package/src/api/preflight.ts +33 -19
- package/src/api/project-panes.ts +2 -0
- package/src/extension/doctor.ts +10 -0
- package/src/extension/fanout-child.ts +3 -2
- package/src/extension/index.ts +24 -4
- package/src/extension/public-execution.ts +12 -9
- package/src/extension/rpc.ts +77 -3
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -6
- package/src/extension/tool-result.ts +19 -0
- package/src/inspectors/herdr/client.ts +3 -3
- package/src/inspectors/herdr/focus.ts +55 -0
- package/src/inspectors/herdr/project-panes.ts +228 -44
- package/src/integrations/herdr-status.ts +26 -4
- package/src/runs/background/async-execution.ts +112 -13
- package/src/runs/background/async-job-tracker.ts +33 -14
- package/src/runs/background/async-resume.ts +20 -2
- package/src/runs/background/async-retention.ts +1 -1
- package/src/runs/background/async-status.ts +10 -0
- package/src/runs/background/chain-root-attachment.ts +5 -0
- package/src/runs/background/control-channel.ts +98 -10
- package/src/runs/background/notify.ts +62 -1
- package/src/runs/background/result-watcher.ts +4 -3
- package/src/runs/background/run-status.ts +15 -1
- package/src/runs/background/stale-run-reconciler.ts +3 -0
- package/src/runs/background/subagent-runner.ts +251 -38
- package/src/runs/background/wait-completions.ts +2 -0
- package/src/runs/background/wait-tool.ts +4 -3
- package/src/runs/foreground/async-stop-action.ts +23 -2
- package/src/runs/foreground/execution.ts +110 -8
- package/src/runs/foreground/subagent-executor.ts +361 -45
- package/src/runs/foreground/workflow-detach-reconcile.ts +3 -0
- package/src/runs/shared/acceptance.ts +1 -0
- package/src/runs/shared/child-identity.ts +36 -0
- package/src/runs/shared/completion-guard.ts +50 -1
- package/src/runs/shared/external-job-bridge.ts +53 -37
- package/src/runs/shared/external-job-runner.ts +126 -23
- package/src/runs/shared/model-fallback.ts +46 -15
- package/src/runs/shared/model-scope.ts +106 -39
- package/src/runs/shared/orca-progress-tabs.ts +71 -9
- package/src/runs/shared/parallel-utils.ts +12 -0
- package/src/runs/shared/pi-args.ts +31 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +39 -10
- package/src/runs/shared/tool-availability.ts +1 -3
- package/src/shared/launch-contract.ts +6 -5
- package/src/shared/thinking-ceiling.ts +52 -0
- package/src/shared/types.ts +72 -5
- package/src/tui/fleet-status.ts +66 -13
- package/src/tui/render.ts +5 -4
- package/src/watchdog/permission-arbiter.ts +59 -51
- package/src/workflows/scripted-workflow.ts +107 -10
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
3
|
import { filterFallbackCandidates, parseModelKey, recordModelFailure } from "./model-exclusions.ts";
|
|
4
|
-
import { checkModelScope, type
|
|
4
|
+
import { checkModelScope, type ModelScopeCheckRule, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
5
5
|
|
|
6
6
|
export type { AvailableModelInfo };
|
|
7
7
|
|
|
@@ -22,6 +22,16 @@ export function splitThinkingSuffix(model: string): { baseModel: string; thinkin
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function formatSubagentModelVerificationError(expectedModel: string, observedModel: string, availableModels: AvailableModelInfo[] | undefined): string | undefined {
|
|
26
|
+
if (!availableModels || availableModels.length === 0) return undefined;
|
|
27
|
+
const expectedBase = splitThinkingSuffix(expectedModel).baseModel;
|
|
28
|
+
const observedBase = splitThinkingSuffix(observedModel).baseModel;
|
|
29
|
+
if (expectedBase === observedBase) return undefined;
|
|
30
|
+
const expectedEntry = availableModels.find((entry) => entry.fullId === expectedBase);
|
|
31
|
+
if (expectedEntry && expectedEntry.id === observedBase) return undefined;
|
|
32
|
+
return `model_verification_failed: child reported a different model than the launch candidate. Expected '${expectedModel}' but observed '${observedModel}'.`;
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/** Sentinel model value requesting that a subagent inherit the parent session's model. */
|
|
26
36
|
export const INHERIT_MODEL = "inherit";
|
|
27
37
|
|
|
@@ -236,7 +246,7 @@ function resolveRequiredSubagentModelCandidate(
|
|
|
236
246
|
|
|
237
247
|
export interface ResolveSubagentModelOverrideOptions {
|
|
238
248
|
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
239
|
-
scope?:
|
|
249
|
+
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
240
250
|
/** Origin of the requested model: explicit caller-supplied (hard error) vs inherited (warn). Defaults to `"inherited"`. */
|
|
241
251
|
source?: ModelSource;
|
|
242
252
|
/** Called for warn-severity violations instead of `console.warn`. */
|
|
@@ -247,6 +257,32 @@ function defaultScopeWarn(violation: ModelScopeViolation): void {
|
|
|
247
257
|
console.warn(`[pi-subagents] ${violation.message}`);
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
function configuredScopes(scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined): ModelScopeCheckRule[] {
|
|
261
|
+
return scope ? (Array.isArray(scope) ? scope : [scope]) : [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function throwForUnresolvedEnforcedInheritScope(scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined, includeMixed = false): void {
|
|
265
|
+
const unresolvedInheritScope = configuredScopes(scope)
|
|
266
|
+
.find((entry) => entry.enforce === true && (includeMixed ? entry.allow?.includes(INHERIT_MODEL) : entry.allow?.length === 1 && entry.allow[0] === INHERIT_MODEL));
|
|
267
|
+
if (!unresolvedInheritScope) return;
|
|
268
|
+
const origin = unresolvedInheritScope.origin ?? "modelScope";
|
|
269
|
+
throw new Error(`Cannot enforce subagent model scope (${origin}): 'inherit' requires a current parent session model.`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function enforceModelScopes(
|
|
273
|
+
model: string,
|
|
274
|
+
scope: ModelScopeCheckRule | ModelScopeCheckRule[] | undefined,
|
|
275
|
+
source: ModelSource,
|
|
276
|
+
onWarn: ((violation: ModelScopeViolation) => void) | undefined,
|
|
277
|
+
): void {
|
|
278
|
+
const violations = configuredScopes(scope)
|
|
279
|
+
.map((entry) => checkModelScope(model, entry, source))
|
|
280
|
+
.filter((violation): violation is ModelScopeViolation => violation !== undefined);
|
|
281
|
+
const error = violations.find((violation) => violation.severity === "error");
|
|
282
|
+
if (error) throw new Error(error.message);
|
|
283
|
+
for (const violation of violations) (onWarn ?? defaultScopeWarn)(violation);
|
|
284
|
+
}
|
|
285
|
+
|
|
250
286
|
/**
|
|
251
287
|
* Resolve the `--model` override passed to a spawned subagent.
|
|
252
288
|
*
|
|
@@ -274,19 +310,16 @@ export function resolveSubagentModelOverride(
|
|
|
274
310
|
): string | undefined {
|
|
275
311
|
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
276
312
|
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
313
|
+
if (!parentModel) throwForUnresolvedEnforcedInheritScope(options?.scope, explicit === undefined || options?.source === "inherited");
|
|
277
314
|
let resolved: string | undefined;
|
|
278
315
|
if (explicit === undefined) {
|
|
279
316
|
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
280
317
|
} else {
|
|
281
318
|
resolved = resolveRequiredSubagentModelCandidate(explicit, availableModels, preferredProvider);
|
|
282
319
|
}
|
|
283
|
-
if (resolved && options?.scope
|
|
320
|
+
if (resolved && options?.scope) {
|
|
284
321
|
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
285
|
-
|
|
286
|
-
if (violation) {
|
|
287
|
-
if (violation.severity === "error") throw new Error(violation.message);
|
|
288
|
-
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
289
|
-
}
|
|
322
|
+
enforceModelScopes(resolved, options.scope, source, options.onWarn);
|
|
290
323
|
}
|
|
291
324
|
return resolved;
|
|
292
325
|
}
|
|
@@ -318,7 +351,7 @@ export function resolveEffectiveSubagentModel(
|
|
|
318
351
|
|
|
319
352
|
export interface BuildModelCandidatesOptions {
|
|
320
353
|
/** Fallback models warn by default and throw when strict scope enforcement is enabled. */
|
|
321
|
-
scope?:
|
|
354
|
+
scope?: ModelScopeCheckRule | ModelScopeCheckRule[];
|
|
322
355
|
onWarn?: (violation: ModelScopeViolation) => void;
|
|
323
356
|
/** The primary model came from the running parent session, not configuration. */
|
|
324
357
|
primaryModelFromParent?: boolean;
|
|
@@ -341,6 +374,7 @@ export function buildModelCandidates(
|
|
|
341
374
|
preferredProvider?: string,
|
|
342
375
|
options?: BuildModelCandidatesOptions,
|
|
343
376
|
): string[] {
|
|
377
|
+
if (!primaryModel) throwForUnresolvedEnforcedInheritScope(options?.scope, true);
|
|
344
378
|
const seen = new Set<string>();
|
|
345
379
|
const candidates: string[] = [];
|
|
346
380
|
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
@@ -358,12 +392,9 @@ export function buildModelCandidates(
|
|
|
358
392
|
continue;
|
|
359
393
|
}
|
|
360
394
|
if (seen.has(normalized)) continue;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (violation.severity === "error") throw new Error(violation.message);
|
|
365
|
-
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
366
|
-
}
|
|
395
|
+
const scopes = configuredScopes(options?.scope);
|
|
396
|
+
if (index > 0 || scopes.some((scope) => scope.enforce === true && scope.strict === true)) {
|
|
397
|
+
enforceModelScopes(normalized, scopes, "inherited", options?.onWarn);
|
|
367
398
|
}
|
|
368
399
|
seen.add(normalized);
|
|
369
400
|
candidates.push(normalized);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
17
17
|
|
|
18
|
-
export interface
|
|
18
|
+
export interface ModelScopeRule {
|
|
19
19
|
enforce?: boolean;
|
|
20
20
|
/** Reject inherited and fallback models outside the allowlist instead of warning. */
|
|
21
21
|
strict?: boolean;
|
|
@@ -23,6 +23,19 @@ export interface ModelScopeConfig {
|
|
|
23
23
|
allow?: string[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export interface ModelScopeConfig extends ModelScopeRule {
|
|
27
|
+
/** Additional restrictions keyed by canonical agent name. */
|
|
28
|
+
agents?: Record<string, ModelScopeRule>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModelScopeCheckRule extends ModelScopeRule {
|
|
32
|
+
origin?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResolvedModelScope extends ModelScopeCheckRule {
|
|
36
|
+
origin: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
/** Where a resolved model originated, deciding enforcement severity. */
|
|
27
40
|
export type ModelSource = "explicit" | "inherited";
|
|
28
41
|
|
|
@@ -32,6 +45,7 @@ export interface ModelScopeViolation {
|
|
|
32
45
|
severity: "warn" | "error";
|
|
33
46
|
message: string;
|
|
34
47
|
allowedPatterns: string[];
|
|
48
|
+
origin: string;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
51
|
function stripThinkingSuffix(model: string): string {
|
|
@@ -61,7 +75,7 @@ export function matchesScopePattern(model: string, pattern: string): boolean {
|
|
|
61
75
|
*/
|
|
62
76
|
export function checkModelScope(
|
|
63
77
|
model: string | undefined,
|
|
64
|
-
scope:
|
|
78
|
+
scope: ModelScopeCheckRule | undefined,
|
|
65
79
|
source: ModelSource,
|
|
66
80
|
): ModelScopeViolation | undefined {
|
|
67
81
|
if (!model || !scope?.enforce) return undefined;
|
|
@@ -71,16 +85,89 @@ export function checkModelScope(
|
|
|
71
85
|
|
|
72
86
|
const baseModel = stripThinkingSuffix(model);
|
|
73
87
|
const severity: ModelScopeViolation["severity"] = source === "explicit" || scope.strict === true ? "error" : "warn";
|
|
88
|
+
const origin = scope.origin ?? "modelScope";
|
|
74
89
|
return {
|
|
75
90
|
model: baseModel,
|
|
76
91
|
severity,
|
|
77
92
|
allowedPatterns: allow,
|
|
93
|
+
origin,
|
|
78
94
|
message:
|
|
79
|
-
`Model '${baseModel}' is outside the configured subagent model scope. ` +
|
|
95
|
+
`Model '${baseModel}' is outside the configured subagent model scope (${origin}). ` +
|
|
80
96
|
`Allowed patterns: ${allow.join(", ")}.`,
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
99
|
|
|
100
|
+
function expandInheritPattern(pattern: string, parentModel: { provider: string; id: string } | undefined): string {
|
|
101
|
+
return pattern === "inherit" && parentModel ? `${parentModel.provider}/${parentModel.id}` : pattern;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertRecord(value: unknown, field: string, meta: { filePath: string }, expected = "an object"): Record<string, unknown> {
|
|
105
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
106
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}'; expected ${expected}.`);
|
|
107
|
+
}
|
|
108
|
+
return value as Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseAllowList(value: unknown, field: string, meta: { filePath: string }): string[] {
|
|
112
|
+
if (!Array.isArray(value)) {
|
|
113
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}'; expected an array of strings.`);
|
|
114
|
+
}
|
|
115
|
+
const allow: string[] = [];
|
|
116
|
+
for (const entry of value) {
|
|
117
|
+
if (typeof entry !== "string") {
|
|
118
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}'; expected an array of strings.`);
|
|
119
|
+
}
|
|
120
|
+
const trimmed = entry.trim();
|
|
121
|
+
if (trimmed) allow.push(trimmed);
|
|
122
|
+
}
|
|
123
|
+
if (allow.length === 0) {
|
|
124
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}'; expected a non-empty array of patterns.`);
|
|
125
|
+
}
|
|
126
|
+
return allow;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseScopeRule(input: Record<string, unknown>, field: string, meta: { filePath: string }): ModelScopeRule {
|
|
130
|
+
const rule: ModelScopeRule = {};
|
|
131
|
+
if ("enforce" in input) {
|
|
132
|
+
if (typeof input.enforce !== "boolean") throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}.enforce'; expected a boolean.`);
|
|
133
|
+
rule.enforce = input.enforce;
|
|
134
|
+
}
|
|
135
|
+
if ("strict" in input) {
|
|
136
|
+
if (typeof input.strict !== "boolean") throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}.strict'; expected a boolean.`);
|
|
137
|
+
rule.strict = input.strict;
|
|
138
|
+
}
|
|
139
|
+
if ("allow" in input) rule.allow = parseAllowList(input.allow, `${field}.allow`, meta);
|
|
140
|
+
return rule;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Resolve the global and matching agent policies into independent launch-time checks. */
|
|
144
|
+
export function resolveModelScopesForAgent(
|
|
145
|
+
config: ModelScopeConfig | undefined,
|
|
146
|
+
agentName: string,
|
|
147
|
+
parentModel: { provider: string; id: string } | undefined,
|
|
148
|
+
): ResolvedModelScope[] {
|
|
149
|
+
if (!config) return [];
|
|
150
|
+
const scopes: ResolvedModelScope[] = [];
|
|
151
|
+
if (config.allow) {
|
|
152
|
+
scopes.push({
|
|
153
|
+
...(config.enforce !== undefined ? { enforce: config.enforce } : {}),
|
|
154
|
+
...(config.strict !== undefined ? { strict: config.strict } : {}),
|
|
155
|
+
allow: config.allow.map((pattern) => expandInheritPattern(pattern, parentModel)),
|
|
156
|
+
origin: "modelScope",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const agentScope = config.agents?.[agentName];
|
|
160
|
+
if (agentScope?.allow) {
|
|
161
|
+
scopes.push({
|
|
162
|
+
enforce: agentScope.enforce ?? config.enforce,
|
|
163
|
+
strict: agentScope.strict ?? config.strict,
|
|
164
|
+
allow: agentScope.allow.map((pattern) => expandInheritPattern(pattern, parentModel)),
|
|
165
|
+
origin: `modelScope.agents.${agentName}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return scopes;
|
|
169
|
+
}
|
|
170
|
+
|
|
84
171
|
/**
|
|
85
172
|
* Validate and normalize a raw `subagents.modelScope` value from settings.
|
|
86
173
|
* Throws a descriptive error for malformed configs (matching the surrounding
|
|
@@ -91,46 +178,26 @@ export function parseModelScopeConfig(
|
|
|
91
178
|
meta: { filePath: string },
|
|
92
179
|
): ModelScopeConfig | undefined {
|
|
93
180
|
if (value === undefined) return undefined;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if ("strict" in input) {
|
|
109
|
-
if (typeof input.strict !== "boolean") {
|
|
110
|
-
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.strict'; expected a boolean.`);
|
|
111
|
-
}
|
|
112
|
-
config.strict = input.strict;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if ("allow" in input) {
|
|
116
|
-
if (!Array.isArray(input.allow)) {
|
|
117
|
-
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
|
|
118
|
-
}
|
|
119
|
-
const allow: string[] = [];
|
|
120
|
-
for (const entry of input.allow) {
|
|
121
|
-
if (typeof entry !== "string") {
|
|
122
|
-
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
|
|
181
|
+
const input = assertRecord(value, "modelScope", meta);
|
|
182
|
+
const config: ModelScopeConfig = parseScopeRule(input, "modelScope", meta);
|
|
183
|
+
|
|
184
|
+
if ("agents" in input) {
|
|
185
|
+
const agents: Record<string, ModelScopeRule> = {};
|
|
186
|
+
for (const [rawName, rawScope] of Object.entries(assertRecord(input.agents, "modelScope.agents", meta, "an object keyed by agent name"))) {
|
|
187
|
+
const name = rawName.trim();
|
|
188
|
+
const field = `modelScope.agents.${name}`;
|
|
189
|
+
if (!name) throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.agents' key; expected a non-empty agent name.`);
|
|
190
|
+
const agentInput = assertRecord(rawScope, field, meta);
|
|
191
|
+
if ("agents" in agentInput) {
|
|
192
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid '${field}.agents'; nested agent scopes are not supported.`);
|
|
123
193
|
}
|
|
124
|
-
|
|
125
|
-
if (trimmed) allow.push(trimmed);
|
|
126
|
-
}
|
|
127
|
-
if (allow.length === 0) {
|
|
128
|
-
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected a non-empty array of patterns.`);
|
|
194
|
+
agents[name] = parseScopeRule(agentInput, field, meta);
|
|
129
195
|
}
|
|
130
|
-
config.
|
|
196
|
+
config.agents = agents;
|
|
131
197
|
}
|
|
132
198
|
|
|
133
|
-
|
|
199
|
+
const hasAnyAllow = Boolean(config.allow?.length) || Object.values(config.agents ?? {}).some((scope) => Boolean(scope.allow?.length));
|
|
200
|
+
if (config.enforce === true && !hasAnyAllow) {
|
|
134
201
|
throw new Error(`Subagent settings in '${meta.filePath}' set modelScope.enforce without a non-empty 'allow' list; supply allowed model patterns or disable enforcement.`);
|
|
135
202
|
}
|
|
136
203
|
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
import type { Message } from "@earendil-works/pi-ai";
|
|
6
6
|
import { resolveNodeExecutable } from "../../shared/node-executable.ts";
|
|
7
|
+
import { getProjectSubagentsDir } from "../../shared/artifacts.ts";
|
|
7
8
|
import { TEMP_ROOT_DIR, type OrcaProgressTabsConfig } from "../../shared/types.ts";
|
|
8
9
|
import { extractTextFromContent, extractToolArgsPreview, getAgentDir } from "../../shared/utils.ts";
|
|
9
10
|
|
|
@@ -23,20 +24,22 @@ const ORCA_CREATE_WATCHDOG_SCRIPT = [
|
|
|
23
24
|
"const {spawn}=require('node:child_process');",
|
|
24
25
|
"const fs=require('node:fs');",
|
|
25
26
|
"const timeout=Number(process.argv[1]),grace=Number(process.argv[2]),waitTimeout=Number(process.argv[3]);",
|
|
26
|
-
"const previous=process.argv[4],done=process.argv[5],
|
|
27
|
+
"const previous=process.argv[4],done=process.argv[5],manifest=process.argv[6],command=process.argv[7],args=process.argv.slice(8);",
|
|
27
28
|
"function mark(){try{fs.writeFileSync(done.replace(/\\.pending$/,'.ready'),'')}catch{}}",
|
|
28
29
|
"function exists(file){try{return fs.existsSync(file)}catch{return false}}",
|
|
29
30
|
"function keepQueued(){try{const now=new Date();fs.utimesSync(done,now,now)}catch{}}",
|
|
30
31
|
"function predecessorReady(){if(previous==='-')return true;if(exists(previous.replace(/\\.pending$/,'.ready')))return true;try{return Date.now()-fs.statSync(previous).mtimeMs>=waitTimeout}catch{return true}}",
|
|
32
|
+
"function updateManifest(state,stdout=''){if(manifest==='-')return;try{const payload=JSON.parse(fs.readFileSync(manifest,'utf8'));payload.state=state;payload.updatedAt=new Date().toISOString();const raw=stdout.trim().split(/\\r?\\n/).filter(Boolean).at(-1);if(raw){try{payload.orca=JSON.parse(raw)}catch{payload.orcaRaw=raw.slice(0,4096)}}fs.writeFileSync(manifest,JSON.stringify(payload,null,2)+'\\n')}catch{}}",
|
|
31
33
|
"function start(){",
|
|
32
34
|
" try{",
|
|
33
|
-
" const child=spawn(command,args,{stdio:'ignore',windowsHide:true});",
|
|
34
|
-
" let hardKill;",
|
|
35
|
+
" const child=spawn(command,args,{stdio:['ignore','pipe','ignore'],windowsHide:true});",
|
|
36
|
+
" let hardKill,stdout='';",
|
|
37
|
+
" if(child.stdout)child.stdout.on('data',chunk=>{stdout+=String(chunk);if(stdout.length>65536)stdout=stdout.slice(-65536)});",
|
|
35
38
|
" const timer=setTimeout(()=>{child.kill('SIGTERM');hardKill=setTimeout(()=>child.kill('SIGKILL'),grace)},timeout);",
|
|
36
39
|
" const clear=()=>{clearTimeout(timer);if(hardKill)clearTimeout(hardKill);mark()};",
|
|
37
|
-
" child.once('error',()=>{clear();process.exitCode=1});",
|
|
38
|
-
" child.once('close',code=>{clear();process.exitCode=code===0?0:1});",
|
|
39
|
-
" }catch{mark();process.exitCode=1}",
|
|
40
|
+
" child.once('error',()=>{updateManifest('failed');clear();process.exitCode=1});",
|
|
41
|
+
" child.once('close',code=>{updateManifest(code===0?'open':'failed',stdout);clear();process.exitCode=code===0?0:1});",
|
|
42
|
+
" }catch{updateManifest('failed');mark();process.exitCode=1}",
|
|
40
43
|
"}",
|
|
41
44
|
"if(predecessorReady())start();",
|
|
42
45
|
"else{(function waitPrev(){keepQueued();if(predecessorReady())return start();setTimeout(waitPrev,20)})();}",
|
|
@@ -51,6 +54,7 @@ const ORCA_CLEANUP_WATCHDOG_SCRIPT = [
|
|
|
51
54
|
|
|
52
55
|
export interface OrcaProgressTab {
|
|
53
56
|
append(text: string): void;
|
|
57
|
+
section(input: { agent: string; index: number; count: number }): void;
|
|
54
58
|
event(event: { type?: string; message?: Message; toolName?: string; args?: unknown }): void;
|
|
55
59
|
finish(status: "completed" | "failed" | "stopped", sessionFile?: string): void;
|
|
56
60
|
}
|
|
@@ -143,6 +147,41 @@ function safeSegment(value: unknown, fallback = "subagent"): string {
|
|
|
143
147
|
return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || fallback;
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
interface OrcaObserverManifest {
|
|
151
|
+
schemaVersion: 1;
|
|
152
|
+
kind: "orca-observer-view";
|
|
153
|
+
observer: "orca";
|
|
154
|
+
role: "run";
|
|
155
|
+
runId: string;
|
|
156
|
+
title: string;
|
|
157
|
+
worktree: string;
|
|
158
|
+
state: "opening" | "open" | "failed";
|
|
159
|
+
createdAt: string;
|
|
160
|
+
updatedAt?: string;
|
|
161
|
+
logPath: string;
|
|
162
|
+
orca?: unknown;
|
|
163
|
+
orcaRaw?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function observerManifestPath(cwd: string, stem: string): string | undefined {
|
|
167
|
+
try {
|
|
168
|
+
const dir = path.join(getProjectSubagentsDir(resolveSequenceScope(cwd)), "views", "orca");
|
|
169
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
170
|
+
return path.join(dir, `${stem}.json`);
|
|
171
|
+
} catch {
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeObserverManifest(manifestPath: string | undefined, manifest: OrcaObserverManifest): void {
|
|
177
|
+
if (!manifestPath) return;
|
|
178
|
+
try {
|
|
179
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
180
|
+
} catch {
|
|
181
|
+
// The Orca observer stays display-only when project-local manifests cannot be written.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
function sleepSync(ms: number): void {
|
|
147
186
|
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
148
187
|
Atomics.wait(signal, 0, 0, ms);
|
|
@@ -280,6 +319,7 @@ export function createOrcaProgressTab(input: {
|
|
|
280
319
|
runId: string;
|
|
281
320
|
agent: string;
|
|
282
321
|
index: number;
|
|
322
|
+
stepCount?: number;
|
|
283
323
|
config?: OrcaProgressTabsConfig;
|
|
284
324
|
env?: NodeJS.ProcessEnv;
|
|
285
325
|
command?: string;
|
|
@@ -305,14 +345,28 @@ export function createOrcaProgressTab(input: {
|
|
|
305
345
|
if (reservation === undefined) return undefined;
|
|
306
346
|
const tabSequence = reservation.sequence;
|
|
307
347
|
const stem = `${runId}-${index}-${randomUUID()}`;
|
|
348
|
+
const stepCount = typeof input.stepCount === "number" && Number.isInteger(input.stepCount) && input.stepCount > 0 ? input.stepCount : 1;
|
|
308
349
|
const logPath = path.join(root, `${stem}.log`);
|
|
309
350
|
const donePath = path.join(root, `${stem}.done`);
|
|
310
|
-
const title = `
|
|
351
|
+
const title = `subagents · ${agent} · ${tabSequence}`;
|
|
311
352
|
const markCreateReady = () => {
|
|
312
353
|
try { fs.writeFileSync(reservation.createDone.replace(/\.pending$/, ".ready"), ""); } catch { /* unblock later tabs */ }
|
|
313
354
|
};
|
|
355
|
+
const manifestPath = observerManifestPath(cwd, stem);
|
|
356
|
+
writeObserverManifest(manifestPath, {
|
|
357
|
+
schemaVersion: 1,
|
|
358
|
+
kind: "orca-observer-view",
|
|
359
|
+
observer: "orca",
|
|
360
|
+
role: "run",
|
|
361
|
+
runId,
|
|
362
|
+
title,
|
|
363
|
+
worktree: path.resolve(cwd),
|
|
364
|
+
state: "opening",
|
|
365
|
+
createdAt: new Date().toISOString(),
|
|
366
|
+
logPath,
|
|
367
|
+
});
|
|
314
368
|
try {
|
|
315
|
-
fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} ·
|
|
369
|
+
fs.writeFileSync(logPath, `pi-subagents / ${agent}\nrun ${runId} · ${stepCount === 1 ? "1 child" : `${stepCount} children`}\n${"─".repeat(48)}\n`, { encoding: "utf-8", mode: 0o600 });
|
|
316
370
|
fs.rmSync(donePath, { force: true });
|
|
317
371
|
} catch {
|
|
318
372
|
markCreateReady();
|
|
@@ -361,6 +415,7 @@ export function createOrcaProgressTab(input: {
|
|
|
361
415
|
String(ORCA_CREATE_WAIT_TIMEOUT_MS),
|
|
362
416
|
reservation.previousCreateDone ?? "-",
|
|
363
417
|
reservation.createDone,
|
|
418
|
+
manifestPath ?? "-",
|
|
364
419
|
command,
|
|
365
420
|
"terminal", "create",
|
|
366
421
|
"--worktree", `path:${path.resolve(cwd)}`,
|
|
@@ -398,6 +453,13 @@ export function createOrcaProgressTab(input: {
|
|
|
398
453
|
if (finished) return;
|
|
399
454
|
writeProgress(text);
|
|
400
455
|
},
|
|
456
|
+
section(section) {
|
|
457
|
+
if (finished) return;
|
|
458
|
+
const label = safeSegment(section.agent, "subagent");
|
|
459
|
+
const count = Number.isInteger(section.count) && section.count > 0 ? section.count : 1;
|
|
460
|
+
const index = Number.isInteger(section.index) && section.index >= 0 ? section.index : 0;
|
|
461
|
+
writeProgress(`\n${"─".repeat(16)} child ${index + 1}/${count} · ${label} ${"─".repeat(16)}\n`);
|
|
462
|
+
},
|
|
401
463
|
event(event) {
|
|
402
464
|
if (!available || finished) return;
|
|
403
465
|
if (event.type === "tool_execution_start" && event.toolName) {
|
|
@@ -421,7 +483,7 @@ export function createOrcaProgressTab(input: {
|
|
|
421
483
|
try { verifiedSessionFile = fs.realpathSync(sessionFile); } catch { /* the session is no longer available */ }
|
|
422
484
|
}
|
|
423
485
|
const terminalMessage = verifiedSessionFile
|
|
424
|
-
? `completed. To remove the Pi session
|
|
486
|
+
? `completed. To remove the Pi session for this run, run rm -- ${shellQuote(verifiedSessionFile)}`
|
|
425
487
|
: status;
|
|
426
488
|
const truncation = truncated ? `\n[progress mirror truncated at ${MAX_MIRROR_BYTES} bytes]\n` : "";
|
|
427
489
|
let footer = `${truncation}\n${"─".repeat(48)}\n${terminalMessage}\n`;
|
|
@@ -8,6 +8,13 @@ export interface RunnerSubagentStep {
|
|
|
8
8
|
agent: string;
|
|
9
9
|
task: string;
|
|
10
10
|
runner?: ResolvedRunnerConfig;
|
|
11
|
+
externalJobFollowUp?: {
|
|
12
|
+
sourceRunId: string;
|
|
13
|
+
sourceStepIndex: number;
|
|
14
|
+
parentProviderJobId: string;
|
|
15
|
+
requestId: string;
|
|
16
|
+
requestDigest: string;
|
|
17
|
+
};
|
|
11
18
|
/** Resolved launch context for this child. */
|
|
12
19
|
context?: "fresh" | "fork";
|
|
13
20
|
importAsyncRoot?: {
|
|
@@ -23,7 +30,11 @@ export interface RunnerSubagentStep {
|
|
|
23
30
|
cwd?: string;
|
|
24
31
|
model?: string;
|
|
25
32
|
thinking?: string;
|
|
33
|
+
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
26
34
|
modelCandidates?: string[];
|
|
35
|
+
/** The primary model is inherited from the parent session and should not be verified against the child-reported active registry model. */
|
|
36
|
+
skipPrimaryModelVerification?: boolean;
|
|
37
|
+
modelVerificationRegistry?: Array<{ provider: string; id: string; fullId: string }>;
|
|
27
38
|
tools?: string[];
|
|
28
39
|
extensions?: string[];
|
|
29
40
|
subagentOnlyExtensions?: string[];
|
|
@@ -93,6 +104,7 @@ export interface DynamicRunnerGroup {
|
|
|
93
104
|
gateOn?: import("../../shared/types.ts").ChainGateLayer;
|
|
94
105
|
capabilityCeiling?: import("./capability-ceiling.ts").ResolvedSubagentCapabilityCeiling;
|
|
95
106
|
capabilityAudit?: import("./capability-ceiling.ts").SubagentCapabilityAudit;
|
|
107
|
+
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type RunFanoutBudgetDescriptor,
|
|
27
27
|
} from "../../shared/types.ts";
|
|
28
28
|
import { THINKING_LEVELS } from "../../shared/model-info.ts";
|
|
29
|
+
import { decodeThinkingCeiling, intersectThinkingCeilings, SUBAGENT_THINKING_CEILING_ENV } from "../../shared/thinking-ceiling.ts";
|
|
29
30
|
import { encodeRunFanoutBudgetDescriptor, RUN_FANOUT_BUDGET_ENV } from "./run-fanout-budget.ts";
|
|
30
31
|
import {
|
|
31
32
|
TOOL_BUDGET_ENV,
|
|
@@ -186,11 +187,13 @@ export interface BuildPiArgsInput {
|
|
|
186
187
|
taskDelivery?: SubagentTaskDelivery;
|
|
187
188
|
waitToolEnabled?: boolean;
|
|
188
189
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
190
|
+
thinkingCeiling?: import("../../shared/model-info.ts").ThinkingLevel;
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
export interface BuildPiArgsResult {
|
|
192
194
|
args: string[];
|
|
193
195
|
env: Record<string, string | undefined>;
|
|
196
|
+
warnings: string[];
|
|
194
197
|
tempDir?: string;
|
|
195
198
|
toolDiagnosticPath?: string;
|
|
196
199
|
runtimeAcknowledgedExtensionsPath?: string;
|
|
@@ -251,6 +254,7 @@ export interface ResolvePiLaunchToolPlanInput {
|
|
|
251
254
|
capabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
252
255
|
inheritedCapabilityCeiling?: ResolvedSubagentCapabilityCeiling;
|
|
253
256
|
agentName?: string;
|
|
257
|
+
permissionRules?: PermissionRules;
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
export interface PiLaunchToolPlan {
|
|
@@ -271,6 +275,8 @@ export interface PiLaunchToolPlan {
|
|
|
271
275
|
extensionArgs: string[];
|
|
272
276
|
disableAmbientExtensions: boolean;
|
|
273
277
|
capabilityAudit?: SubagentCapabilityAudit;
|
|
278
|
+
/** Non-fatal launch warnings; they do not change behavior. */
|
|
279
|
+
warnings: string[];
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
function extensionIdentifier(value: string): string {
|
|
@@ -288,6 +294,10 @@ function boundedExtensionIdentifiers(values: string[]): {
|
|
|
288
294
|
};
|
|
289
295
|
}
|
|
290
296
|
|
|
297
|
+
function hasPermissionRules(rules: PermissionRules | undefined): boolean {
|
|
298
|
+
return rules !== undefined && Object.keys(rules).length > 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
291
301
|
export function projectLaunchResolvedChildExtensions(
|
|
292
302
|
toolPlan: Pick<
|
|
293
303
|
PiLaunchToolPlan,
|
|
@@ -451,7 +461,9 @@ export function resolvePiLaunchToolPlan(
|
|
|
451
461
|
: [];
|
|
452
462
|
const permSystemExt = capabilityCeiling?.denyExtensions
|
|
453
463
|
? undefined
|
|
454
|
-
:
|
|
464
|
+
: hasPermissionRules(input.permissionRules)
|
|
465
|
+
? resolvePermissionSystemExtension()
|
|
466
|
+
: undefined;
|
|
455
467
|
const runtimeExtensions = [
|
|
456
468
|
PROMPT_RUNTIME_EXTENSION_PATH,
|
|
457
469
|
...(fanoutAuthorized ? [FANOUT_CHILD_EXTENSION_PATH] : []),
|
|
@@ -460,6 +472,16 @@ export function resolvePiLaunchToolPlan(
|
|
|
460
472
|
const disableAmbientExtensions =
|
|
461
473
|
capabilityCeiling?.denyExtensions === true ||
|
|
462
474
|
input.extensions !== undefined;
|
|
475
|
+
const warnings: string[] = [];
|
|
476
|
+
// An explicit empty list disables ambient extensions, including model providers.
|
|
477
|
+
if (capabilityCeiling?.denyExtensions !== true && Array.isArray(input.extensions) && input.extensions.length === 0) {
|
|
478
|
+
const agentLabel = input.agentName ? ` for agent '${input.agentName}'` : "";
|
|
479
|
+
warnings.push(
|
|
480
|
+
`extensions: [] override${agentLabel} disables ALL ambient extensions for this child (not just "adds nothing"), `
|
|
481
|
+
+ "including any model-provider extension needed to resolve a provider-qualified model. "
|
|
482
|
+
+ "List the extensions this child actually needs instead of an empty array.",
|
|
483
|
+
);
|
|
484
|
+
}
|
|
463
485
|
const configuredExtensions = capabilityCeiling?.denyExtensions
|
|
464
486
|
? []
|
|
465
487
|
: [
|
|
@@ -540,6 +562,7 @@ export function resolvePiLaunchToolPlan(
|
|
|
540
562
|
configuredExtensions,
|
|
541
563
|
extensionArgs,
|
|
542
564
|
disableAmbientExtensions,
|
|
565
|
+
warnings,
|
|
543
566
|
...(capabilityAudit ? { capabilityAudit } : {}),
|
|
544
567
|
};
|
|
545
568
|
}
|
|
@@ -587,6 +610,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
587
610
|
process.env[SUBAGENT_CAPABILITY_CEILING_ENV],
|
|
588
611
|
),
|
|
589
612
|
agentName: input.childAgentName,
|
|
613
|
+
permissionRules: input.permissionRules,
|
|
590
614
|
});
|
|
591
615
|
if (toolPlan.explicitToolAllowlist) {
|
|
592
616
|
args.push(
|
|
@@ -802,6 +826,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
802
826
|
const encodedCapabilityCeiling = encodeSubagentCapabilityCeiling(
|
|
803
827
|
toolPlan.capabilityCeiling,
|
|
804
828
|
);
|
|
829
|
+
const thinkingCeiling = intersectThinkingCeilings(
|
|
830
|
+
input.thinkingCeiling,
|
|
831
|
+
decodeThinkingCeiling(process.env[SUBAGENT_THINKING_CEILING_ENV]),
|
|
832
|
+
);
|
|
833
|
+
if (thinkingCeiling) env[SUBAGENT_THINKING_CEILING_ENV] = thinkingCeiling;
|
|
805
834
|
if (encodedCapabilityCeiling)
|
|
806
835
|
env[SUBAGENT_CAPABILITY_CEILING_ENV] = encodedCapabilityCeiling;
|
|
807
836
|
if (encodedPermissionRules)
|
|
@@ -829,6 +858,7 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
829
858
|
return {
|
|
830
859
|
args,
|
|
831
860
|
env,
|
|
861
|
+
warnings: toolPlan.warnings,
|
|
832
862
|
tempDir,
|
|
833
863
|
toolDiagnosticPath,
|
|
834
864
|
runtimeAcknowledgedExtensionsPath,
|