pi-subagents 0.32.0 → 0.33.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 +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
|
@@ -45,7 +45,7 @@ const DYNAMIC_STEP_KEYS = new Set(["expand", "parallel", "collect", "concurrency
|
|
|
45
45
|
const RUNNER_DYNAMIC_STEP_KEYS = new Set([...DYNAMIC_STEP_KEYS, "effectiveAcceptance", "sessionFiles", "thinkingOverrides"]);
|
|
46
46
|
const DYNAMIC_EXPAND_KEYS = new Set(["from", "item", "key", "maxItems", "onEmpty"]);
|
|
47
47
|
const DYNAMIC_EXPAND_FROM_KEYS = new Set(["output", "path"]);
|
|
48
|
-
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "acceptance"]);
|
|
48
|
+
const DYNAMIC_PARALLEL_KEYS = new Set(["agent", "task", "phase", "label", "outputSchema", "cwd", "output", "outputMode", "reads", "progress", "skill", "model", "toolBudget", "acceptance"]);
|
|
49
49
|
const RUNNER_DYNAMIC_PARALLEL_KEYS = new Set([
|
|
50
50
|
...DYNAMIC_PARALLEL_KEYS,
|
|
51
51
|
"outputName", "structured", "inheritProjectContext", "inheritSkills", "skills", "outputPath", "maxSubagentDepth",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ModelInfo as AvailableModelInfo } from "../../shared/model-info.ts";
|
|
2
2
|
import type { Usage } from "../../shared/types.ts";
|
|
3
|
+
import { checkModelScope, type ModelScopeConfig, type ModelScopeViolation, type ModelSource } from "./model-scope.ts";
|
|
3
4
|
|
|
4
5
|
export type { AvailableModelInfo };
|
|
5
6
|
|
|
@@ -29,6 +30,144 @@ export interface ParentModel {
|
|
|
29
30
|
id: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Normalize a model id or provider segment for fuzzy comparison: case-fold,
|
|
35
|
+
* treat dots/underscores as dashes (so `4.5` matches `4-5`), and collapse
|
|
36
|
+
* repeated separators. Pure.
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeModelSegment(segment: string): string {
|
|
39
|
+
return segment
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/[._]+/g, "-")
|
|
42
|
+
.replace(/-+/g, "-")
|
|
43
|
+
.replace(/^-|-$/g, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPlausibleDateStamp(year: string, month: string, day: string): boolean {
|
|
47
|
+
const yyyy = Number(year);
|
|
48
|
+
const mm = Number(month);
|
|
49
|
+
const dd = Number(day);
|
|
50
|
+
return yyyy >= 1900 && yyyy <= 2099 && mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Drop a trailing date stamp (`-20251001` or `-2025-10-01`) so dated and undated ids match. Pure. */
|
|
54
|
+
function stripTrailingDateStamp(segment: string): string {
|
|
55
|
+
const dashed = /^(.*)-(\d{4})-(\d{2})-(\d{2})$/.exec(segment);
|
|
56
|
+
if (dashed && isPlausibleDateStamp(dashed[2]!, dashed[3]!, dashed[4]!)) return dashed[1]!;
|
|
57
|
+
const compact = /^(.*)-(\d{4})(\d{2})(\d{2})$/.exec(segment);
|
|
58
|
+
if (compact && isPlausibleDateStamp(compact[2]!, compact[3]!, compact[4]!)) return compact[1]!;
|
|
59
|
+
return segment;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveBaseModelCandidate(
|
|
63
|
+
baseModel: string,
|
|
64
|
+
availableModels: AvailableModelInfo[],
|
|
65
|
+
preferredProvider?: string,
|
|
66
|
+
): string | undefined {
|
|
67
|
+
if (baseModel.includes("/")) {
|
|
68
|
+
const exact = availableModels.find((entry) => entry.fullId === baseModel);
|
|
69
|
+
if (exact) return exact.fullId;
|
|
70
|
+
} else {
|
|
71
|
+
const exactMatches = availableModels.filter((entry) => entry.id === baseModel);
|
|
72
|
+
if (preferredProvider) {
|
|
73
|
+
const preferredMatch = exactMatches.find((entry) => entry.provider === preferredProvider);
|
|
74
|
+
if (preferredMatch) return preferredMatch.fullId;
|
|
75
|
+
}
|
|
76
|
+
if (exactMatches.length === 1) return exactMatches[0]!.fullId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return fuzzyResolveModel(baseModel, availableModels, preferredProvider);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fuzzy-resolve a base model id (thinking suffix already stripped) against the
|
|
84
|
+
* registry, tolerating separator, case, and optional date-stamp differences so
|
|
85
|
+
* users do not have to spell provider/model exactly. A qualified `provider/id`
|
|
86
|
+
* query only matches within the named provider — this never silently switches
|
|
87
|
+
* providers for security/cost-sensitive configs. Returns the matched `fullId`,
|
|
88
|
+
* or `undefined` when there is no match or the match is ambiguous across
|
|
89
|
+
* providers (and no `preferredProvider` disambiguates). Pure.
|
|
90
|
+
*/
|
|
91
|
+
export function fuzzyResolveModel(
|
|
92
|
+
baseModel: string,
|
|
93
|
+
availableModels: AvailableModelInfo[],
|
|
94
|
+
preferredProvider?: string,
|
|
95
|
+
): string | undefined {
|
|
96
|
+
let queryProvider: string | undefined;
|
|
97
|
+
let queryIdRaw = baseModel;
|
|
98
|
+
const slashIdx = baseModel.indexOf("/");
|
|
99
|
+
if (slashIdx !== -1) {
|
|
100
|
+
queryProvider = normalizeModelSegment(baseModel.slice(0, slashIdx));
|
|
101
|
+
queryIdRaw = baseModel.slice(slashIdx + 1);
|
|
102
|
+
} else {
|
|
103
|
+
const providerSeparators = [":", "."];
|
|
104
|
+
for (const separator of providerSeparators) {
|
|
105
|
+
const separatorIdx = baseModel.indexOf(separator);
|
|
106
|
+
if (separatorIdx <= 0) continue;
|
|
107
|
+
const providerPart = normalizeModelSegment(baseModel.slice(0, separatorIdx));
|
|
108
|
+
if (!availableModels.some((entry) => normalizeModelSegment(entry.provider) === providerPart)) continue;
|
|
109
|
+
queryProvider = providerPart;
|
|
110
|
+
queryIdRaw = baseModel.slice(separatorIdx + 1);
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const queryId = normalizeModelSegment(queryIdRaw);
|
|
115
|
+
const queryIdNoDate = stripTrailingDateStamp(queryId);
|
|
116
|
+
|
|
117
|
+
const candidates = availableModels.filter((entry) => {
|
|
118
|
+
const entryId = normalizeModelSegment(entry.id);
|
|
119
|
+
if (entryId !== queryId && stripTrailingDateStamp(entryId) !== queryIdNoDate) return false;
|
|
120
|
+
if (queryProvider !== undefined && normalizeModelSegment(entry.provider) !== queryProvider) return false;
|
|
121
|
+
return true;
|
|
122
|
+
});
|
|
123
|
+
if (candidates.length === 0) return undefined;
|
|
124
|
+
if (preferredProvider) {
|
|
125
|
+
const preferredProviderNorm = normalizeModelSegment(preferredProvider);
|
|
126
|
+
const preferred = candidates.find((entry) => normalizeModelSegment(entry.provider) === preferredProviderNorm);
|
|
127
|
+
if (preferred) return preferred.fullId;
|
|
128
|
+
}
|
|
129
|
+
if (candidates.length === 1) return candidates[0]!.fullId;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a possibly-loose model id to a canonical `provider/id` (plus any
|
|
135
|
+
* thinking suffix). Exact registry matches win; fuzzy normalization
|
|
136
|
+
* (separator/case/date-stamp via {@link fuzzyResolveModel}) is a fallback so
|
|
137
|
+
* spelling differences still resolve. Never switches providers for a qualified
|
|
138
|
+
* query. Pure.
|
|
139
|
+
*/
|
|
140
|
+
export function resolveModelCandidate(
|
|
141
|
+
model: string | undefined,
|
|
142
|
+
availableModels: AvailableModelInfo[] | undefined,
|
|
143
|
+
preferredProvider?: string,
|
|
144
|
+
): string | undefined {
|
|
145
|
+
if (!model) return undefined;
|
|
146
|
+
if (!availableModels || availableModels.length === 0) return model;
|
|
147
|
+
|
|
148
|
+
const resolvedWhole = resolveBaseModelCandidate(model, availableModels, preferredProvider);
|
|
149
|
+
if (resolvedWhole) return resolvedWhole;
|
|
150
|
+
|
|
151
|
+
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
152
|
+
if (!thinkingSuffix) return model;
|
|
153
|
+
const resolvedBase = resolveBaseModelCandidate(baseModel, availableModels, preferredProvider);
|
|
154
|
+
if (resolvedBase) return `${resolvedBase}${thinkingSuffix}`;
|
|
155
|
+
return model;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ResolveSubagentModelOverrideOptions {
|
|
159
|
+
/** When set with `enforce: true`, out-of-scope models are rejected. */
|
|
160
|
+
scope?: ModelScopeConfig;
|
|
161
|
+
/** Origin of the requested model: explicit caller-supplied (hard error) vs inherited (warn). Defaults to `"inherited"`. */
|
|
162
|
+
source?: ModelSource;
|
|
163
|
+
/** Called for warn-severity violations instead of `console.warn`. */
|
|
164
|
+
onWarn?: (violation: ModelScopeViolation) => void;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function defaultScopeWarn(violation: ModelScopeViolation): void {
|
|
168
|
+
console.warn(`[pi-subagents] ${violation.message}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
32
171
|
/**
|
|
33
172
|
* Resolve the `--model` override passed to a spawned subagent.
|
|
34
173
|
*
|
|
@@ -43,38 +182,39 @@ export interface ParentModel {
|
|
|
43
182
|
* session's model.
|
|
44
183
|
*
|
|
45
184
|
* An explicitly requested model string is resolved via {@link resolveModelCandidate}.
|
|
185
|
+
* When `options.scope.enforce` is on, an out-of-scope resolved model throws for
|
|
186
|
+
* an explicit (`source: "explicit"`) request and warns for an inherited one.
|
|
46
187
|
*/
|
|
47
188
|
export function resolveSubagentModelOverride(
|
|
48
189
|
requestedModel: string | boolean | undefined,
|
|
49
190
|
parentModel: ParentModel | undefined,
|
|
50
191
|
availableModels: AvailableModelInfo[] | undefined,
|
|
51
192
|
preferredProvider?: string,
|
|
193
|
+
options?: ResolveSubagentModelOverrideOptions,
|
|
52
194
|
): string | undefined {
|
|
53
195
|
const trimmed = typeof requestedModel === "string" ? requestedModel.trim() : "";
|
|
54
196
|
const explicit = trimmed && trimmed !== INHERIT_MODEL ? trimmed : undefined;
|
|
197
|
+
let resolved: string | undefined;
|
|
55
198
|
if (explicit === undefined) {
|
|
56
|
-
|
|
199
|
+
resolved = parentModel ? `${parentModel.provider}/${parentModel.id}` : undefined;
|
|
200
|
+
} else {
|
|
201
|
+
resolved = resolveModelCandidate(explicit, availableModels, preferredProvider);
|
|
202
|
+
}
|
|
203
|
+
if (resolved && options?.scope?.enforce) {
|
|
204
|
+
const source: ModelSource = explicit === undefined ? "inherited" : (options.source ?? "inherited");
|
|
205
|
+
const violation = checkModelScope(resolved, options.scope, source);
|
|
206
|
+
if (violation) {
|
|
207
|
+
if (violation.severity === "error") throw new Error(violation.message);
|
|
208
|
+
(options.onWarn ?? defaultScopeWarn)(violation);
|
|
209
|
+
}
|
|
57
210
|
}
|
|
58
|
-
return
|
|
211
|
+
return resolved;
|
|
59
212
|
}
|
|
60
213
|
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
): string | undefined {
|
|
66
|
-
if (!model) return undefined;
|
|
67
|
-
if (model.includes("/")) return model;
|
|
68
|
-
if (!availableModels || availableModels.length === 0) return model;
|
|
69
|
-
|
|
70
|
-
const { baseModel, thinkingSuffix } = splitThinkingSuffix(model);
|
|
71
|
-
const matches = availableModels.filter((entry) => entry.id === baseModel);
|
|
72
|
-
if (preferredProvider) {
|
|
73
|
-
const preferredMatch = matches.find((entry) => entry.provider === preferredProvider);
|
|
74
|
-
if (preferredMatch) return `${preferredMatch.fullId}${thinkingSuffix}`;
|
|
75
|
-
}
|
|
76
|
-
if (matches.length !== 1) return model;
|
|
77
|
-
return `${matches[0]!.fullId}${thinkingSuffix}`;
|
|
214
|
+
export interface BuildModelCandidatesOptions {
|
|
215
|
+
/** Fallback models are inherited agent config and warn, rather than error, when out of scope. */
|
|
216
|
+
scope?: ModelScopeConfig;
|
|
217
|
+
onWarn?: (violation: ModelScopeViolation) => void;
|
|
78
218
|
}
|
|
79
219
|
|
|
80
220
|
export function buildModelCandidates(
|
|
@@ -82,13 +222,20 @@ export function buildModelCandidates(
|
|
|
82
222
|
fallbackModels: string[] | undefined,
|
|
83
223
|
availableModels: AvailableModelInfo[] | undefined,
|
|
84
224
|
preferredProvider?: string,
|
|
225
|
+
options?: BuildModelCandidatesOptions,
|
|
85
226
|
): string[] {
|
|
86
227
|
const seen = new Set<string>();
|
|
87
228
|
const candidates: string[] = [];
|
|
88
|
-
|
|
229
|
+
const rawCandidates = [primaryModel, ...(fallbackModels ?? [])];
|
|
230
|
+
for (let index = 0; index < rawCandidates.length; index++) {
|
|
231
|
+
const raw = rawCandidates[index];
|
|
89
232
|
if (!raw) continue;
|
|
90
233
|
const normalized = resolveModelCandidate(raw.trim(), availableModels, preferredProvider);
|
|
91
234
|
if (!normalized || seen.has(normalized)) continue;
|
|
235
|
+
if (index > 0 && options?.scope?.enforce) {
|
|
236
|
+
const violation = checkModelScope(normalized, options.scope, "inherited");
|
|
237
|
+
if (violation) (options.onWarn ?? defaultScopeWarn)(violation);
|
|
238
|
+
}
|
|
92
239
|
seen.add(normalized);
|
|
93
240
|
candidates.push(normalized);
|
|
94
241
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional model-scope enforcement for subagent model resolution.
|
|
3
|
+
*
|
|
4
|
+
* When `subagents.modelScope.enforce` is enabled in settings, a resolved model
|
|
5
|
+
* that does not match any `allow` pattern is rejected. The severity depends on
|
|
6
|
+
* where the model came from: an explicit caller-supplied model (`--model`,
|
|
7
|
+
* tool-call `model`, or a TUI clarify pick) is a hard error, while a model
|
|
8
|
+
* inherited from agent frontmatter / `defaultModel` / the parent session only
|
|
9
|
+
* emits a warning so existing configurations keep working.
|
|
10
|
+
*
|
|
11
|
+
* The decision logic ({@link checkModelScope}) is a pure function of its
|
|
12
|
+
* inputs so it can be unit-tested without touching the filesystem or config.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { splitKnownThinkingSuffix } from "../../shared/model-info.ts";
|
|
16
|
+
|
|
17
|
+
export interface ModelScopeConfig {
|
|
18
|
+
enforce?: boolean;
|
|
19
|
+
/** Glob-style allow patterns (only `*` is special), matched against `provider/id`. */
|
|
20
|
+
allow?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Where a resolved model originated, deciding enforcement severity. */
|
|
24
|
+
export type ModelSource = "explicit" | "inherited";
|
|
25
|
+
|
|
26
|
+
export interface ModelScopeViolation {
|
|
27
|
+
/** Resolved model id (without thinking suffix) that fell outside the scope. */
|
|
28
|
+
model: string;
|
|
29
|
+
severity: "warn" | "error";
|
|
30
|
+
message: string;
|
|
31
|
+
allowedPatterns: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function stripThinkingSuffix(model: string): string {
|
|
35
|
+
return splitKnownThinkingSuffix(model).baseModel;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Escape RegExp specials except `*`, then turn `*` into `.*`. */
|
|
39
|
+
function globToRegExp(pattern: string): RegExp {
|
|
40
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
41
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Test whether a resolved model matches a single allow pattern. Both sides are
|
|
46
|
+
* compared case-insensitively against the full `provider/id` (thinking suffix
|
|
47
|
+
* stripped from the model).
|
|
48
|
+
*/
|
|
49
|
+
export function matchesScopePattern(model: string, pattern: string): boolean {
|
|
50
|
+
return globToRegExp(pattern).test(stripThinkingSuffix(model));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure scope decision. Returns a {@link ModelScopeViolation} when the model is
|
|
55
|
+
* out of scope and enforcement is on, otherwise `undefined`. Enforcement with
|
|
56
|
+
* no `allow` list is a no-op (the settings parser rejects that combination, but
|
|
57
|
+
* this stays defensive for callers that build configs programmatically).
|
|
58
|
+
*/
|
|
59
|
+
export function checkModelScope(
|
|
60
|
+
model: string | undefined,
|
|
61
|
+
scope: ModelScopeConfig | undefined,
|
|
62
|
+
source: ModelSource,
|
|
63
|
+
): ModelScopeViolation | undefined {
|
|
64
|
+
if (!model || !scope?.enforce) return undefined;
|
|
65
|
+
const allow = scope.allow;
|
|
66
|
+
if (!allow || allow.length === 0) return undefined;
|
|
67
|
+
if (allow.some((pattern) => matchesScopePattern(model, pattern))) return undefined;
|
|
68
|
+
|
|
69
|
+
const baseModel = stripThinkingSuffix(model);
|
|
70
|
+
const severity: ModelScopeViolation["severity"] = source === "explicit" ? "error" : "warn";
|
|
71
|
+
return {
|
|
72
|
+
model: baseModel,
|
|
73
|
+
severity,
|
|
74
|
+
allowedPatterns: allow,
|
|
75
|
+
message:
|
|
76
|
+
`Model '${baseModel}' is outside the configured subagent model scope. ` +
|
|
77
|
+
`Allowed patterns: ${allow.join(", ")}.`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate and normalize a raw `subagents.modelScope` value from settings.
|
|
83
|
+
* Throws a descriptive error for malformed configs (matching the surrounding
|
|
84
|
+
* settings-parsing style). Returns `undefined` when the field is absent.
|
|
85
|
+
*/
|
|
86
|
+
export function parseModelScopeConfig(
|
|
87
|
+
value: unknown,
|
|
88
|
+
meta: { filePath: string },
|
|
89
|
+
): ModelScopeConfig | undefined {
|
|
90
|
+
if (value === undefined) return undefined;
|
|
91
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
92
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope'; expected an object.`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const input = value as Record<string, unknown>;
|
|
96
|
+
const config: ModelScopeConfig = {};
|
|
97
|
+
|
|
98
|
+
if ("enforce" in input) {
|
|
99
|
+
if (typeof input.enforce !== "boolean") {
|
|
100
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.enforce'; expected a boolean.`);
|
|
101
|
+
}
|
|
102
|
+
config.enforce = input.enforce;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if ("allow" in input) {
|
|
106
|
+
if (!Array.isArray(input.allow)) {
|
|
107
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
|
|
108
|
+
}
|
|
109
|
+
const allow: string[] = [];
|
|
110
|
+
for (const entry of input.allow) {
|
|
111
|
+
if (typeof entry !== "string") {
|
|
112
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected an array of strings.`);
|
|
113
|
+
}
|
|
114
|
+
const trimmed = entry.trim();
|
|
115
|
+
if (trimmed) allow.push(trimmed);
|
|
116
|
+
}
|
|
117
|
+
if (allow.length === 0) {
|
|
118
|
+
throw new Error(`Subagent settings in '${meta.filePath}' have invalid 'modelScope.allow'; expected a non-empty array of patterns.`);
|
|
119
|
+
}
|
|
120
|
+
config.allow = allow;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (config.enforce === true && (!config.allow || config.allow.length === 0)) {
|
|
124
|
+
throw new Error(`Subagent settings in '${meta.filePath}' set modelScope.enforce without a non-empty 'allow' list; supply allowed model patterns or disable enforcement.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return Object.keys(config).length > 0 ? config : undefined;
|
|
128
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type AsyncJobState,
|
|
9
9
|
type AsyncStatus,
|
|
10
10
|
type NestedRouteInfo,
|
|
11
|
+
type TurnBudgetState,
|
|
11
12
|
type NestedRunSummary,
|
|
12
13
|
type NestedRunState,
|
|
13
14
|
type NestedStepSummary,
|
|
@@ -195,6 +196,24 @@ function sanitizeCost(value: unknown): NestedRunSummary["totalCost"] | undefined
|
|
|
195
196
|
: undefined;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function sanitizeTurnBudget(value: unknown): TurnBudgetState | undefined {
|
|
200
|
+
if (!value || typeof value !== "object") return undefined;
|
|
201
|
+
const raw = value as Record<string, unknown>;
|
|
202
|
+
const maxTurns = clampNumber(raw.maxTurns);
|
|
203
|
+
const graceTurns = clampNumber(raw.graceTurns);
|
|
204
|
+
const turnCount = clampNumber(raw.turnCount);
|
|
205
|
+
const outcome = raw.outcome === "within-budget" || raw.outcome === "wrap-up-requested" || raw.outcome === "exceeded" ? raw.outcome : undefined;
|
|
206
|
+
if (maxTurns === undefined || graceTurns === undefined || turnCount === undefined || !outcome) return undefined;
|
|
207
|
+
return {
|
|
208
|
+
maxTurns,
|
|
209
|
+
graceTurns,
|
|
210
|
+
turnCount,
|
|
211
|
+
outcome,
|
|
212
|
+
...(clampNumber(raw.wrapUpRequestedAtTurn) !== undefined ? { wrapUpRequestedAtTurn: clampNumber(raw.wrapUpRequestedAtTurn) } : {}),
|
|
213
|
+
...(clampNumber(raw.exceededAtTurn) !== undefined ? { exceededAtTurn: clampNumber(raw.exceededAtTurn) } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
198
217
|
function sanitizeState(value: unknown, fallback: NestedRunState): NestedRunState {
|
|
199
218
|
return value === "queued" || value === "running" || value === "complete" || value === "failed" || value === "paused"
|
|
200
219
|
? value
|
|
@@ -224,6 +243,9 @@ function sanitizeStep(input: unknown, depth: number): NestedStepSummary | undefi
|
|
|
224
243
|
...(clampNumber(raw.endedAt) !== undefined ? { endedAt: clampNumber(raw.endedAt) } : {}),
|
|
225
244
|
...(stringValue(raw.error, 1024) ? { error: stringValue(raw.error, 1024) } : {}),
|
|
226
245
|
...(raw.timedOut === true ? { timedOut: true } : {}),
|
|
246
|
+
...(sanitizeTurnBudget(raw.turnBudget) ? { turnBudget: sanitizeTurnBudget(raw.turnBudget) } : {}),
|
|
247
|
+
...(raw.turnBudgetExceeded === true ? { turnBudgetExceeded: true } : {}),
|
|
248
|
+
...(raw.wrapUpRequested === true ? { wrapUpRequested: true } : {}),
|
|
227
249
|
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) => sanitizeSummary(child, depth + 1)).filter((child): child is NestedRunSummary => Boolean(child)).slice(0, MAX_CHILDREN) } : {}),
|
|
228
250
|
};
|
|
229
251
|
}
|
|
@@ -276,6 +298,9 @@ export function sanitizeSummary(input: unknown, depth = 0): NestedRunSummary | u
|
|
|
276
298
|
...(clampNumber(raw.timeoutMs) !== undefined ? { timeoutMs: clampNumber(raw.timeoutMs) } : {}),
|
|
277
299
|
...(clampNumber(raw.deadlineAt) !== undefined ? { deadlineAt: clampNumber(raw.deadlineAt) } : {}),
|
|
278
300
|
...(raw.timedOut === true ? { timedOut: true } : {}),
|
|
301
|
+
...(sanitizeTurnBudget(raw.turnBudget) ? { turnBudget: sanitizeTurnBudget(raw.turnBudget) } : {}),
|
|
302
|
+
...(raw.turnBudgetExceeded === true ? { turnBudgetExceeded: true } : {}),
|
|
303
|
+
...(raw.wrapUpRequested === true ? { wrapUpRequested: true } : {}),
|
|
279
304
|
...(stringValue(raw.error, 1024) ? { error: stringValue(raw.error, 1024) } : {}),
|
|
280
305
|
...(steps && steps.length > 0 ? { steps } : {}),
|
|
281
306
|
...(depth < MAX_DEPTH && Array.isArray(raw.children) ? { children: raw.children.map((child) => sanitizeSummary(child, depth + 1)).filter((child): child is NestedRunSummary => Boolean(child)).slice(0, MAX_CHILDREN) } : {}),
|
|
@@ -834,6 +859,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
834
859
|
...(status.timeoutMs !== undefined ? { timeoutMs: status.timeoutMs } : {}),
|
|
835
860
|
...(status.deadlineAt !== undefined ? { deadlineAt: status.deadlineAt } : {}),
|
|
836
861
|
...(status.timedOut !== undefined ? { timedOut: status.timedOut } : {}),
|
|
862
|
+
...(status.turnBudget ? { turnBudget: status.turnBudget } : {}),
|
|
863
|
+
...(status.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: status.turnBudgetExceeded } : {}),
|
|
864
|
+
...(status.wrapUpRequested !== undefined ? { wrapUpRequested: status.wrapUpRequested } : {}),
|
|
837
865
|
...(status.error ? { error: status.error } : {}),
|
|
838
866
|
...(status.startedAt !== undefined ? { startedAt: status.startedAt } : { startedAt: fallback.ts }),
|
|
839
867
|
...(status.endedAt !== undefined ? { endedAt: status.endedAt } : {}),
|
|
@@ -854,6 +882,9 @@ export function nestedSummaryFromAsyncStatus(status: AsyncStatus, asyncDir: stri
|
|
|
854
882
|
...(step.endedAt !== undefined ? { endedAt: step.endedAt } : {}),
|
|
855
883
|
...(step.error ? { error: step.error } : {}),
|
|
856
884
|
...(step.timedOut !== undefined ? { timedOut: step.timedOut } : {}),
|
|
885
|
+
...(step.turnBudget ? { turnBudget: step.turnBudget } : {}),
|
|
886
|
+
...(step.turnBudgetExceeded !== undefined ? { turnBudgetExceeded: step.turnBudgetExceeded } : {}),
|
|
887
|
+
...(step.wrapUpRequested !== undefined ? { wrapUpRequested: step.wrapUpRequested } : {}),
|
|
857
888
|
})).slice(0, MAX_STEPS) } : {}),
|
|
858
889
|
};
|
|
859
890
|
}
|
|
@@ -38,6 +38,7 @@ export interface RunnerSubagentStep {
|
|
|
38
38
|
};
|
|
39
39
|
structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
|
|
40
40
|
effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
|
|
41
|
+
toolBudget?: import("../../shared/types.ts").ResolvedToolBudget;
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export interface ParallelStepGroup {
|
|
@@ -5,7 +5,8 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
|
|
6
6
|
import { resolveMcpDirectToolNames } from "./mcp-direct-tool-allowlist.ts";
|
|
7
7
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
|
|
8
|
-
import type
|
|
8
|
+
import { TEMP_ROOT_DIR, type JsonSchemaObject, type ResolvedToolBudget } from "../../shared/types.ts";
|
|
9
|
+
import { TOOL_BUDGET_ENV, encodeToolBudgetEnv } from "./tool-budget.ts";
|
|
9
10
|
|
|
10
11
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
11
12
|
const TASK_ARG_LIMIT = 8000;
|
|
@@ -13,6 +14,8 @@ const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(impor
|
|
|
13
14
|
const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts");
|
|
14
15
|
export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
|
|
15
16
|
export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
|
|
17
|
+
export const SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV = "PI_SUBAGENT_ORCHESTRATOR_SESSION_ID";
|
|
18
|
+
export const SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV = "PI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
|
|
16
19
|
export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
|
|
17
20
|
export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
|
|
18
21
|
export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
|
|
@@ -26,6 +29,7 @@ export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
|
|
|
26
29
|
export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
|
|
27
30
|
export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
|
|
28
31
|
export const SUBAGENT_PARENT_SESSION_ENV = "PI_SUBAGENT_PARENT_SESSION";
|
|
32
|
+
export const SUBAGENT_STEER_INBOX_ENV = "PI_SUBAGENT_STEER_INBOX";
|
|
29
33
|
|
|
30
34
|
interface BuildPiArgsInput {
|
|
31
35
|
parentSessionId?: string;
|
|
@@ -60,11 +64,13 @@ interface BuildPiArgsInput {
|
|
|
60
64
|
parentDepth?: number;
|
|
61
65
|
parentPath?: NestedPathEntry[];
|
|
62
66
|
parentCapabilityToken?: string;
|
|
67
|
+
steerInboxDir?: string;
|
|
63
68
|
structuredOutput?: {
|
|
64
69
|
schema: JsonSchemaObject;
|
|
65
70
|
schemaPath: string;
|
|
66
71
|
outputPath: string;
|
|
67
72
|
};
|
|
73
|
+
toolBudget?: ResolvedToolBudget;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
interface BuildPiArgsResult {
|
|
@@ -73,6 +79,14 @@ interface BuildPiArgsResult {
|
|
|
73
79
|
tempDir?: string;
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
function sanitizeSupervisorChannelSegment(value: string): string {
|
|
83
|
+
return value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function supervisorChannelDir(runId: string, agent: string, childIndex: number): string {
|
|
87
|
+
return path.join(TEMP_ROOT_DIR, "supervisor-channels", `${sanitizeSupervisorChannelSegment(runId)}-${sanitizeSupervisorChannelSegment(agent)}-${childIndex}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined, replaceExisting = false): string | undefined {
|
|
77
91
|
if (!model || !thinking) return model;
|
|
78
92
|
const colonIdx = model.lastIndexOf(":");
|
|
@@ -206,6 +220,16 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
206
220
|
if (input.orchestratorIntercomTarget) {
|
|
207
221
|
env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
|
|
208
222
|
}
|
|
223
|
+
if (input.parentSessionId) {
|
|
224
|
+
env[SUBAGENT_ORCHESTRATOR_SESSION_ID_ENV] = input.parentSessionId;
|
|
225
|
+
}
|
|
226
|
+
if (input.orchestratorIntercomTarget && input.parentSessionId && input.runId && input.childAgentName) {
|
|
227
|
+
const childIndex = input.childIndex ?? 0;
|
|
228
|
+
const channelDir = supervisorChannelDir(input.runId, input.childAgentName, childIndex);
|
|
229
|
+
fs.mkdirSync(path.join(channelDir, "requests"), { recursive: true });
|
|
230
|
+
fs.mkdirSync(path.join(channelDir, "replies"), { recursive: true });
|
|
231
|
+
env[SUBAGENT_SUPERVISOR_CHANNEL_DIR_ENV] = channelDir;
|
|
232
|
+
}
|
|
209
233
|
if (input.runId) {
|
|
210
234
|
env[SUBAGENT_RUN_ID_ENV] = input.runId;
|
|
211
235
|
}
|
|
@@ -224,6 +248,11 @@ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
|
|
|
224
248
|
env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
|
|
225
249
|
env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
|
|
226
250
|
}
|
|
251
|
+
if (input.steerInboxDir) {
|
|
252
|
+
env[SUBAGENT_STEER_INBOX_ENV] = input.steerInboxDir;
|
|
253
|
+
}
|
|
254
|
+
const encodedToolBudget = encodeToolBudgetEnv(input.toolBudget);
|
|
255
|
+
if (encodedToolBudget) env[TOOL_BUDGET_ENV] = encodedToolBudget;
|
|
227
256
|
|
|
228
257
|
env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId ?? process.env[SUBAGENT_PARENT_SESSION_ENV] ?? "";
|
|
229
258
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import {
|
|
4
|
+
import { registerNativeSupervisorClient } from "../../intercom/native-supervisor-channel.ts";
|
|
5
|
+
import { consumeSteerRequestsFromDir, writeSteerRequestToDir, type SteerRequest } from "../background/control-channel.ts";
|
|
6
|
+
import { SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_STEER_INBOX_ENV } from "./pi-args.ts";
|
|
5
7
|
import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV, validateStructuredOutputValue } from "./structured-output.ts";
|
|
6
|
-
import
|
|
8
|
+
import { TOOL_BUDGET_ENV, decodeToolBudgetEnv, shouldBlockToolForBudget, toolBudgetBlockedMessage, toolBudgetSoftNudge } from "./tool-budget.ts";
|
|
9
|
+
import type { JsonSchemaObject, ResolvedToolBudget } from "../../shared/types.ts";
|
|
7
10
|
|
|
8
11
|
const SUBAGENT_INHERIT_PROJECT_CONTEXT_ENV = "PI_SUBAGENT_INHERIT_PROJECT_CONTEXT";
|
|
9
12
|
const SUBAGENT_INHERIT_SKILLS_ENV = "PI_SUBAGENT_INHERIT_SKILLS";
|
|
@@ -155,7 +158,110 @@ export function stripParentOnlySubagentMessages(messages: unknown[]): unknown[]
|
|
|
155
158
|
return changed ? filtered : messages;
|
|
156
159
|
}
|
|
157
160
|
|
|
161
|
+
export function formatSteerMessage(request: SteerRequest): string {
|
|
162
|
+
return [
|
|
163
|
+
"Mid-run steering from the parent orchestrator:",
|
|
164
|
+
"",
|
|
165
|
+
request.message,
|
|
166
|
+
"",
|
|
167
|
+
"Incorporate this guidance at the next safe point. Do not restart the task unless the guidance explicitly asks you to.",
|
|
168
|
+
].join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function registerToolBudget(pi: ExtensionAPI, budget: ResolvedToolBudget | undefined): void {
|
|
172
|
+
if (!budget) return;
|
|
173
|
+
let toolCount = 0;
|
|
174
|
+
let softNudged = false;
|
|
175
|
+
const sendUserMessage = (pi as { sendUserMessage?: (content: string, options: { deliverAs: "steer" }) => unknown }).sendUserMessage;
|
|
176
|
+
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: { toolName?: string }) => unknown) => void;
|
|
177
|
+
onRuntimeEvent("tool_call", (event) => {
|
|
178
|
+
const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
|
|
179
|
+
toolCount++;
|
|
180
|
+
if (budget.soft !== undefined && toolCount >= budget.soft && !softNudged) {
|
|
181
|
+
softNudged = true;
|
|
182
|
+
try {
|
|
183
|
+
sendUserMessage?.(toolBudgetSoftNudge(budget, toolCount), { deliverAs: "steer" });
|
|
184
|
+
} catch {
|
|
185
|
+
// Budget nudges are advisory; blocking below remains authoritative.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!shouldBlockToolForBudget(budget, toolName, toolCount)) return undefined;
|
|
189
|
+
return { block: true, reason: toolBudgetBlockedMessage(budget, toolName, toolCount) };
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function registerSteeringInbox(pi: ExtensionAPI): void {
|
|
194
|
+
const steerInbox = process.env[SUBAGENT_STEER_INBOX_ENV]?.trim();
|
|
195
|
+
if (!steerInbox) return;
|
|
196
|
+
const sendUserMessage = (pi as { sendUserMessage?: (content: string, options: { deliverAs: "steer" }) => unknown }).sendUserMessage;
|
|
197
|
+
if (typeof sendUserMessage !== "function") return;
|
|
198
|
+
|
|
199
|
+
let canSteer = false;
|
|
200
|
+
let disposed = false;
|
|
201
|
+
let flushing = false;
|
|
202
|
+
let started = false;
|
|
203
|
+
let watcher: fs.FSWatcher | undefined;
|
|
204
|
+
let interval: NodeJS.Timeout | undefined;
|
|
205
|
+
const flush = (): void => {
|
|
206
|
+
if (disposed || flushing || !canSteer) return;
|
|
207
|
+
flushing = true;
|
|
208
|
+
try {
|
|
209
|
+
const requests = consumeSteerRequestsFromDir(steerInbox);
|
|
210
|
+
for (let index = 0; index < requests.length; index++) {
|
|
211
|
+
const request = requests[index]!;
|
|
212
|
+
try {
|
|
213
|
+
sendUserMessage(formatSteerMessage(request), { deliverAs: "steer" });
|
|
214
|
+
} catch {
|
|
215
|
+
for (const pending of requests.slice(index)) writeSteerRequestToDir(steerInbox, pending);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
flushing = false;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const start = (): void => {
|
|
224
|
+
if (started || disposed) return;
|
|
225
|
+
try {
|
|
226
|
+
fs.mkdirSync(steerInbox, { recursive: true });
|
|
227
|
+
} catch {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
started = true;
|
|
231
|
+
try {
|
|
232
|
+
watcher = fs.watch(steerInbox, () => flush());
|
|
233
|
+
watcher.on("error", () => {});
|
|
234
|
+
} catch {
|
|
235
|
+
watcher = undefined;
|
|
236
|
+
}
|
|
237
|
+
interval = setInterval(flush, 250);
|
|
238
|
+
interval.unref?.();
|
|
239
|
+
};
|
|
240
|
+
const activate = (): undefined => {
|
|
241
|
+
start();
|
|
242
|
+
canSteer = true;
|
|
243
|
+
flush();
|
|
244
|
+
return undefined;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onRuntimeEvent = pi.on as unknown as (event: string, handler: (event: unknown) => unknown) => void;
|
|
248
|
+
onRuntimeEvent("session_start", () => start());
|
|
249
|
+
for (const eventName of ["message_start", "message_update", "message_end", "tool_execution_start", "tool_execution_end", "turn_end"] as const) {
|
|
250
|
+
onRuntimeEvent(eventName, activate);
|
|
251
|
+
}
|
|
252
|
+
onRuntimeEvent("session_shutdown", () => {
|
|
253
|
+
disposed = true;
|
|
254
|
+
try {
|
|
255
|
+
watcher?.close();
|
|
256
|
+
} catch {}
|
|
257
|
+
if (interval) clearInterval(interval);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
158
261
|
export default function registerSubagentPromptRuntime(pi: ExtensionAPI): void {
|
|
262
|
+
registerSteeringInbox(pi);
|
|
263
|
+
registerToolBudget(pi, decodeToolBudgetEnv(process.env[TOOL_BUDGET_ENV]));
|
|
264
|
+
registerNativeSupervisorClient(pi);
|
|
159
265
|
const structuredOutputPath = process.env[STRUCTURED_OUTPUT_CAPTURE_ENV];
|
|
160
266
|
const structuredSchemaPath = process.env[STRUCTURED_OUTPUT_SCHEMA_ENV];
|
|
161
267
|
if (structuredOutputPath && structuredSchemaPath) {
|