pi-ui-extend 1.0.39 → 1.0.41
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/README.md +1 -1
- package/dist/app/commands/command-registry.js +2 -2
- package/dist/app/commands/command-session-actions.d.ts +0 -1
- package/dist/app/commands/command-session-actions.js +22 -13
- package/dist/app/icons.d.ts +14 -0
- package/dist/app/icons.js +33 -0
- package/dist/app/rendering/conversation-tool-renderer.js +2 -2
- package/dist/app/rendering/dcp-stats.d.ts +6 -1
- package/dist/app/rendering/dcp-stats.js +214 -46
- package/dist/app/rendering/editor-panels.js +8 -5
- package/dist/app/session/lazy-session-manager.js +12 -1
- package/dist/app/session/tabs-controller.d.ts +2 -5
- package/dist/app/session/tabs-controller.js +12 -21
- package/dist/app/subagents/subagents-model.d.ts +14 -1
- package/dist/app/subagents/subagents-model.js +34 -15
- package/dist/app/types.d.ts +2 -0
- package/dist/bundled-extensions/session-title/config.js +1 -1
- package/dist/markdown-format.js +27 -9
- package/dist/schemas/pi-tools-suite-schema.d.ts +29 -16
- package/dist/schemas/pi-tools-suite-schema.js +46 -31
- package/external/pi-tools-suite/README.md +392 -52
- package/external/pi-tools-suite/docs/browser-qa-subagent.md +31 -21
- package/external/pi-tools-suite/docs/context-gateway-p00-adr.md +216 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-gate-review.md +122 -0
- package/external/pi-tools-suite/docs/context-gateway-p01n-measurement.md +133 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-ra-evidence.md +111 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rb-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rc-evidence.md +69 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rd-evidence.md +100 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-re-evidence.md +74 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rf-evidence.md +153 -0
- package/external/pi-tools-suite/docs/context-gateway-p01r-rg-evidence.md +235 -0
- package/external/pi-tools-suite/docs/evals.md +684 -0
- package/external/pi-tools-suite/docs/subagent-model-pools.md +109 -0
- package/external/pi-tools-suite/package.json +10 -3
- package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/scripts/browser-qa-runner.mjs +82 -1
- package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/SKILL.md → agents/browser-qa.md} +261 -12
- package/external/pi-tools-suite/src/async-subagents/agents/implement.md +20 -0
- package/external/pi-tools-suite/src/async-subagents/agents/oracle.md +16 -0
- package/external/pi-tools-suite/src/async-subagents/agents/research.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/agents/verify.md +18 -0
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +27 -243
- package/external/pi-tools-suite/src/async-subagents/commands.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +41 -0
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +13 -93
- package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +494 -0
- package/external/pi-tools-suite/src/async-subagents/core/browser-qa.ts +9 -0
- package/external/pi-tools-suite/src/async-subagents/core/config.ts +200 -143
- package/external/pi-tools-suite/src/async-subagents/core/model-fallback.ts +1 -1
- package/external/pi-tools-suite/src/async-subagents/core/model-selection.ts +54 -0
- package/external/pi-tools-suite/src/async-subagents/core/prompt.ts +7 -6
- package/external/pi-tools-suite/src/async-subagents/core/routing.ts +52 -45
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +12 -4
- package/external/pi-tools-suite/src/async-subagents/index.ts +11 -1
- package/external/pi-tools-suite/src/async-subagents/lib.ts +6 -2
- package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +46 -18
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
- package/external/pi-tools-suite/src/async-subagents/types.ts +2 -0
- package/external/pi-tools-suite/src/coding-discipline/index.ts +41 -142
- package/external/pi-tools-suite/src/config.ts +1 -22
- package/external/pi-tools-suite/src/context-gateway/accounting.ts +151 -0
- package/external/pi-tools-suite/src/context-gateway/config.ts +111 -0
- package/external/pi-tools-suite/src/context-gateway/index.ts +160 -0
- package/external/pi-tools-suite/src/context-gateway/metadata-normalization.ts +88 -0
- package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +89 -0
- package/external/pi-tools-suite/src/context-gateway/telemetry.ts +429 -0
- package/external/pi-tools-suite/src/context-gateway/test-output-parser.ts +326 -0
- package/external/pi-tools-suite/src/context-gateway/types.ts +152 -0
- package/external/pi-tools-suite/src/dcp/auto-compress-budget.ts +106 -0
- package/external/pi-tools-suite/src/dcp/auto-compress.ts +810 -106
- package/external/pi-tools-suite/src/dcp/commands.ts +64 -139
- package/external/pi-tools-suite/src/dcp/compress-tool.ts +369 -35
- package/external/pi-tools-suite/src/dcp/compression-blocks.ts +510 -64
- package/external/pi-tools-suite/src/dcp/compression-preview.ts +113 -0
- package/external/pi-tools-suite/src/dcp/compression-progress.ts +70 -0
- package/external/pi-tools-suite/src/dcp/config.ts +36 -61
- package/external/pi-tools-suite/src/dcp/conversation-index.ts +421 -0
- package/external/pi-tools-suite/src/dcp/debug-log.ts +7 -5
- package/external/pi-tools-suite/src/dcp/index.ts +617 -203
- package/external/pi-tools-suite/src/dcp/journal.ts +566 -0
- package/external/pi-tools-suite/src/dcp/progress-controller.ts +244 -0
- package/external/pi-tools-suite/src/dcp/prompts.ts +10 -7
- package/external/pi-tools-suite/src/dcp/provider-tool-results.ts +189 -0
- package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +298 -78
- package/external/pi-tools-suite/src/dcp/pruner-compression-blocks.ts +173 -281
- package/external/pi-tools-suite/src/dcp/pruner-emergency.ts +2 -4
- package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +17 -5
- package/external/pi-tools-suite/src/dcp/pruner-metadata.ts +11 -1
- package/external/pi-tools-suite/src/dcp/pruner-nudge.ts +30 -82
- package/external/pi-tools-suite/src/dcp/pruner-tools.ts +22 -133
- package/external/pi-tools-suite/src/dcp/pruner.ts +18 -33
- package/external/pi-tools-suite/src/dcp/recovery.ts +129 -0
- package/external/pi-tools-suite/src/dcp/shadow-plan.ts +127 -0
- package/external/pi-tools-suite/src/dcp/state-transaction.ts +102 -0
- package/external/pi-tools-suite/src/dcp/state.ts +158 -580
- package/external/pi-tools-suite/src/dcp/ui.ts +1 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +55 -220
- package/external/pi-tools-suite/src/index.ts +9 -0
- package/external/pi-tools-suite/src/model-tools/index.ts +76 -42
- package/external/pi-tools-suite/src/repo-discovery/index.ts +84 -18
- package/external/pi-tools-suite/src/repo-discovery/native-compact.ts +458 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +189 -43
- package/external/pi-tools-suite/src/tool-descriptions.ts +43 -38
- package/external/pi-tools-suite/src/truncation-metadata-normalizer/index.ts +17 -0
- package/package.json +6 -6
- package/schemas/pi-tools-suite.json +159 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/auth-scaffold-spec.md +0 -78
- package/external/pi-tools-suite/src/async-subagents/private-skills/browser-qa/references/qa-design.md +0 -223
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +0 -195
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-auth.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills/browser-qa/references → agents/browser-qa/examples}/qa-flow.example.jsonc +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.LICENSE +0 -0
- /package/external/pi-tools-suite/src/async-subagents/{private-skills → agents}/browser-qa/vendor/fflate.mjs +0 -0
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { applyEdits, modify, parse as parseJsonc } from "jsonc-parser";
|
|
6
|
+
import { readAgentDefinitionsFromDir, readProjectAgentDefinitions, type AgentDefinition } from "./agents-dir.js";
|
|
6
7
|
import { ensurePiToolsSuiteUserConfig, getPiToolsSuiteUserConfigPath } from "../../config.js";
|
|
7
8
|
import type { AgentTask, RetryConfig } from "./types.js";
|
|
8
9
|
|
|
@@ -15,14 +16,24 @@ export interface ModelByParentEntry {
|
|
|
15
16
|
|
|
16
17
|
export interface SubagentTypeConfig {
|
|
17
18
|
description?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Agent icon name resolved by UIs (pix TUI icon themes, Pix Desktop lucide
|
|
21
|
+
* icons). Opaque pass-through here: unknown names render as the neutral
|
|
22
|
+
* default agent icon.
|
|
23
|
+
*/
|
|
24
|
+
icon?: string;
|
|
25
|
+
/** Ranked candidates. The preset filters availability, never changes this order. */
|
|
26
|
+
models?: string[];
|
|
27
|
+
/** Legacy primary candidate; new profiles use models. */
|
|
18
28
|
model?: string;
|
|
19
|
-
/**
|
|
29
|
+
/** Legacy candidates after model; new profiles use one ordered models list. */
|
|
20
30
|
fallbackModels?: string[];
|
|
21
31
|
/**
|
|
22
32
|
* Parent-model-aware model selection. Keys are glob model refs (e.g. "zai/*")
|
|
23
33
|
* matched against the current parent model; the first matching key wins.
|
|
24
34
|
* Values may be a model ref string or { model, fallbackModels? }.
|
|
25
|
-
*
|
|
35
|
+
* Ordinary roles: explicit task/forced model and preset models take priority.
|
|
36
|
+
* Oracle alone keeps parent-aware selection ahead of presets.
|
|
26
37
|
*/
|
|
27
38
|
modelByParent?: Record<string, ModelByParentEntry>;
|
|
28
39
|
thinking?: string;
|
|
@@ -70,6 +81,9 @@ export interface SubagentVisionConfig {
|
|
|
70
81
|
|
|
71
82
|
export interface SubagentPreset {
|
|
72
83
|
description?: string;
|
|
84
|
+
/** Available model pool. Agent candidate order wins; [] allows no models. */
|
|
85
|
+
models?: string[];
|
|
86
|
+
/** Legacy default model; prefer models for new presets. */
|
|
73
87
|
model?: string;
|
|
74
88
|
/** Ordered global model fallbacks used when this preset's selected model hits quota/rate limits. */
|
|
75
89
|
fallbackModels?: string[];
|
|
@@ -92,9 +106,10 @@ export interface SubagentPresetTypeOverride {
|
|
|
92
106
|
}
|
|
93
107
|
|
|
94
108
|
export interface SubagentConfig {
|
|
109
|
+
/** Ambiguous-task router hint and legacy resolver default; never a spawn error fallback. */
|
|
95
110
|
defaultType?: string;
|
|
96
111
|
types: Record<string, SubagentTypeConfig>;
|
|
97
|
-
/** LLM
|
|
112
|
+
/** Fallback LLM role selection for omitted types; explicit valid types bypass it. */
|
|
98
113
|
routing?: SubagentRoutingConfig;
|
|
99
114
|
/** Vision capability overrides for parent-model guidance. */
|
|
100
115
|
vision?: SubagentVisionConfig;
|
|
@@ -133,6 +148,13 @@ export interface ResolvedAgentTaskConfig {
|
|
|
133
148
|
timeoutMs?: number;
|
|
134
149
|
}
|
|
135
150
|
|
|
151
|
+
export class SubagentModelSelectionError extends Error {
|
|
152
|
+
constructor(taskId: string, message: string) {
|
|
153
|
+
super(`Task ${taskId}: ${message} No agents were launched. Configure a compatible model pool/candidate list or provide an explicit model override.`);
|
|
154
|
+
this.name = "SubagentModelSelectionError";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
136
158
|
export interface ResolveAgentTaskOptions {
|
|
137
159
|
/** Default model for spawned sub-agents when task/profile do not specify one. */
|
|
138
160
|
model?: string;
|
|
@@ -166,7 +188,7 @@ export const DEFAULT_MAX_CONCURRENT = 5;
|
|
|
166
188
|
/** Default lightweight LLM router used when subagentType is omitted. */
|
|
167
189
|
export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
|
|
168
190
|
enabled: true,
|
|
169
|
-
model: "zai/glm-
|
|
191
|
+
model: "zai/glm-5-turbo",
|
|
170
192
|
fallbackModels: ["openai-codex/gpt-5.6-luna"],
|
|
171
193
|
maxTaskChars: 1200,
|
|
172
194
|
maxTokens: 512,
|
|
@@ -175,98 +197,13 @@ export const DEFAULT_ROUTING_CONFIG: ResolvedSubagentRoutingConfig = {
|
|
|
175
197
|
debug: false,
|
|
176
198
|
};
|
|
177
199
|
|
|
200
|
+
const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
201
|
+
|
|
178
202
|
const BUILTIN_CONFIG: SubagentConfig = {
|
|
203
|
+
defaultType: "research",
|
|
179
204
|
maxConcurrent: DEFAULT_MAX_CONCURRENT,
|
|
180
205
|
routing: { ...DEFAULT_ROUTING_CONFIG },
|
|
181
|
-
types:
|
|
182
|
-
quick: {
|
|
183
|
-
description: "Use for tiny cheap tasks: answer a simple question, inspect one known file, or verify one fact. Not for broad repo search.",
|
|
184
|
-
model: "openai-codex/gpt-5.6-luna",
|
|
185
|
-
fallbackModels: ["zai/glm-4.5-air"],
|
|
186
|
-
thinking: "off",
|
|
187
|
-
},
|
|
188
|
-
scan: {
|
|
189
|
-
description: "Use for finding files, symbols, text, or inventory across a repo. Return paths/facts; do not judge code quality.",
|
|
190
|
-
model: "openai-codex/gpt-5.6-luna",
|
|
191
|
-
fallbackModels: ["zai/glm-4.5-air"],
|
|
192
|
-
thinking: "off",
|
|
193
|
-
},
|
|
194
|
-
research: {
|
|
195
|
-
description: "Use for multi-file codebase research: read several files and explain how something works. No edits.",
|
|
196
|
-
model: "openai-codex/gpt-5.6-terra",
|
|
197
|
-
fallbackModels: ["zai/glm-5-turbo"],
|
|
198
|
-
thinking: "low",
|
|
199
|
-
},
|
|
200
|
-
docs: {
|
|
201
|
-
description: "Use for documentation work: README/API docs review, docs gaps, changelog, migration notes, examples.",
|
|
202
|
-
model: "openai-codex/gpt-5.6-luna",
|
|
203
|
-
fallbackModels: ["zai/glm-4.5-air"],
|
|
204
|
-
thinking: "low",
|
|
205
|
-
},
|
|
206
|
-
frontend: {
|
|
207
|
-
description: "Use for frontend UI/UX visual work: styling, layout, typography, animation, responsive states, component polish, and accessibility. Avoid backend/business logic unless needed for UI behavior.",
|
|
208
|
-
thinking: "medium",
|
|
209
|
-
promptAppend: [
|
|
210
|
-
"Act as a frontend UI/UX engineer for visual and product-facing work.",
|
|
211
|
-
"Prioritize layout, typography, spacing, color, motion, responsive states, accessibility, and consistency with the existing design system.",
|
|
212
|
-
"Before editing, inspect nearby components/styles and infer the project's design language. Avoid backend/business-logic changes unless required for UI behavior.",
|
|
213
|
-
"When no mockup exists, choose a clear aesthetic direction and explain it briefly. Verify with targeted build/lint/tests or screenshot-relevant checks when possible.",
|
|
214
|
-
].join("\n"),
|
|
215
|
-
},
|
|
216
|
-
"browser-qa": {
|
|
217
|
-
description: "Use for browser-based visual QA: reproduce UI bugs and verify fixes with deterministic assertions, screenshots, video, and traces.",
|
|
218
|
-
model: "zai/glm-5.3-flash",
|
|
219
|
-
fallbackModels: ["openai-codex/gpt-5.6-luna"],
|
|
220
|
-
thinking: "low",
|
|
221
|
-
timeoutMs: 300_000,
|
|
222
|
-
tools: ["read", "grep", "bash"],
|
|
223
|
-
isolatedSkills: [getBrowserQaSkillPath()],
|
|
224
|
-
},
|
|
225
|
-
implement: {
|
|
226
|
-
description: "Use when the sub-agent should make or plan code changes for a feature, bug fix, or refactor.",
|
|
227
|
-
modelByParent: {
|
|
228
|
-
"openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-terra", fallbackModels: ["zai/glm-5.3"] },
|
|
229
|
-
"openai-codex/gpt-5.6-sol*": { model: "openai-codex/gpt-5.6-terra", fallbackModels: ["zai/glm-5.3"] },
|
|
230
|
-
},
|
|
231
|
-
thinking: "high",
|
|
232
|
-
},
|
|
233
|
-
tests: {
|
|
234
|
-
description: "Use for tests: locate coverage, find gaps, run/check targeted test commands, diagnose failing tests.",
|
|
235
|
-
model: "openai-codex/gpt-5.6-terra",
|
|
236
|
-
fallbackModels: ["zai/glm-5-turbo"],
|
|
237
|
-
thinking: "medium",
|
|
238
|
-
},
|
|
239
|
-
review: {
|
|
240
|
-
description: "Use for review/audit of existing code or changes: correctness, security, performance, maintainability, API risks, quality. Do not implement new code.",
|
|
241
|
-
modelByParent: {
|
|
242
|
-
"openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
243
|
-
"openai-codex/gpt-5.6-terra*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
244
|
-
},
|
|
245
|
-
thinking: "high",
|
|
246
|
-
},
|
|
247
|
-
deep: {
|
|
248
|
-
description: "Use for broad hard reasoning: architecture, root-cause analysis, cross-module impact, complex debugging or tradeoffs.",
|
|
249
|
-
modelByParent: {
|
|
250
|
-
"openai-codex/gpt-5.6-luna*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
251
|
-
"openai-codex/gpt-5.6-terra*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
252
|
-
},
|
|
253
|
-
thinking: "high",
|
|
254
|
-
},
|
|
255
|
-
oracle: {
|
|
256
|
-
description: "Oracle: cross-provider flagship second opinion for hard or high-stakes uncertainty. Use sparingly to pressure-test architecture, plans, root-cause hypotheses, risk/security calls, or final recommendations when independent disagreement is valuable. Read-only; advise, do not edit.",
|
|
257
|
-
model: "openai-codex/gpt-5.6-sol",
|
|
258
|
-
fallbackModels: ["zai/glm-5.3"],
|
|
259
|
-
modelByParent: {
|
|
260
|
-
"zai/*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
261
|
-
"openai-codex/*": { model: "zai/glm-5.3", fallbackModels: ["openai-codex/gpt-5.6-sol"] },
|
|
262
|
-
"antigravity/*": { model: "zai/glm-5.3", fallbackModels: ["openai-codex/gpt-5.6-sol"] },
|
|
263
|
-
"anthropic/*": { model: "openai-codex/gpt-5.6-sol", fallbackModels: ["zai/glm-5.3"] },
|
|
264
|
-
},
|
|
265
|
-
thinking: "max",
|
|
266
|
-
tools: ["read", "grep", "bash"],
|
|
267
|
-
promptAppend: "You are an oracle: a flagship model from a different provider giving a second opinion to the parent agent. Give a concise, decisive recommendation with key tradeoffs and risks. Disagree when warranted; do not rubber-stamp. Do not edit unless explicitly asked.",
|
|
268
|
-
},
|
|
269
|
-
},
|
|
206
|
+
types: normalizeAgentDefinitions(readAgentDefinitionsFromDir(BUILTIN_AGENTS_DIR)),
|
|
270
207
|
};
|
|
271
208
|
|
|
272
209
|
export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process.env): SubagentConfig {
|
|
@@ -275,6 +212,10 @@ export function loadSubagentConfig(cwd: string, env: NodeJS.ProcessEnv = process
|
|
|
275
212
|
for (const file of piToolsSuiteConfigFiles(cwd, env)) {
|
|
276
213
|
mergeConfig(config, readPiToolsSuiteSubagentConfig(file));
|
|
277
214
|
}
|
|
215
|
+
// Project-local agent definitions (.pi/agents/*.md) win name collisions
|
|
216
|
+
// over user/project JSONC config; loaded fresh on every call so edits and
|
|
217
|
+
// /reload are respected without a restart.
|
|
218
|
+
mergeConfig(config, projectAgentTypes(cwd));
|
|
278
219
|
}
|
|
279
220
|
for (const file of configFiles(cwd, env)) {
|
|
280
221
|
mergeConfig(config, readConfigFile(file));
|
|
@@ -307,8 +248,8 @@ export function getSubagentConfigSamplePath(): string {
|
|
|
307
248
|
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "async-subagents.sample.jsonc");
|
|
308
249
|
}
|
|
309
250
|
|
|
310
|
-
export function
|
|
311
|
-
return
|
|
251
|
+
export function getBuiltinSubagentDefinitionsDir(): string {
|
|
252
|
+
return BUILTIN_AGENTS_DIR;
|
|
312
253
|
}
|
|
313
254
|
|
|
314
255
|
export function getSubagentConfigInitTargetPath(cwd: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
@@ -329,6 +270,22 @@ function piToolsSuiteConfigFiles(cwd: string, env: NodeJS.ProcessEnv): string[]
|
|
|
329
270
|
].filter((file): file is string => typeof file === "string" && fs.existsSync(file));
|
|
330
271
|
}
|
|
331
272
|
|
|
273
|
+
/** Normalize `.pi/agents/*.md` definitions through the shared type-profile path. */
|
|
274
|
+
function projectAgentTypes(cwd: string): Partial<SubagentConfig> {
|
|
275
|
+
const definitions = readProjectAgentDefinitions(cwd);
|
|
276
|
+
const types = normalizeAgentDefinitions(definitions);
|
|
277
|
+
if (Object.keys(types).length === 0) return {};
|
|
278
|
+
return { types };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeAgentDefinitions(definitions: Record<string, AgentDefinition>): Record<string, SubagentTypeConfig> {
|
|
282
|
+
const types: Record<string, SubagentTypeConfig> = {};
|
|
283
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
284
|
+
types[name] = normalizeSubagentTypeProfile(definition.raw, name, definition.file);
|
|
285
|
+
}
|
|
286
|
+
return types;
|
|
287
|
+
}
|
|
288
|
+
|
|
332
289
|
function findProjectPiToolsSuiteConfig(startDir: string): string | undefined {
|
|
333
290
|
let dir = path.resolve(startDir);
|
|
334
291
|
const root = path.parse(dir).root;
|
|
@@ -373,12 +330,19 @@ export function resolveAgentTaskConfig(
|
|
|
373
330
|
const selectedType = selectSubagentType(task, config);
|
|
374
331
|
const profile = selectedType ? config.types[selectedType] : undefined;
|
|
375
332
|
const preset = globalOptions.preset;
|
|
376
|
-
|
|
333
|
+
// A new pool preset has no per-role policy. Ignore any inherited legacy
|
|
334
|
+
// matrix/defaults so changing the pool cannot resurrect an expensive model.
|
|
335
|
+
const legacyPreset = preset?.models === undefined ? preset : undefined;
|
|
377
336
|
const explicitType = trimString(task.subagentType);
|
|
337
|
+
const requestedType = explicitType || trimString(config.defaultType);
|
|
338
|
+
// Legacy preset matrices still apply to an explicitly configured type with
|
|
339
|
+
// the same name. There are no implicit aliases between type names.
|
|
340
|
+
const presetType = (requestedType ? legacyPreset?.types?.[requestedType] : undefined)
|
|
341
|
+
?? (selectedType ? legacyPreset?.types?.[selectedType] : undefined);
|
|
378
342
|
const taskExtraArgs = arrayOfStrings(task.extraArgs) ?? [];
|
|
379
343
|
const profileExtraArgs = arrayOfStrings(profile?.extraArgs) ?? [];
|
|
380
344
|
const presetTypeExtraArgs = arrayOfStrings(presetType?.extraArgs) ?? [];
|
|
381
|
-
const presetExtraArgs = arrayOfStrings(
|
|
345
|
+
const presetExtraArgs = arrayOfStrings(legacyPreset?.extraArgs) ?? [];
|
|
382
346
|
const globalExtraArgs = arrayOfStrings(globalOptions.extraArgs) ?? [];
|
|
383
347
|
const promptAppend = joinTextBlocks(profile?.promptAppend, task.promptAppend);
|
|
384
348
|
const forcedModel = trimString(globalOptions.forcedModel);
|
|
@@ -387,33 +351,60 @@ export function resolveAgentTaskConfig(
|
|
|
387
351
|
const parentMatchModel = trimString(parentMatch?.model);
|
|
388
352
|
const presetTypeModel = trimString(presetType?.model);
|
|
389
353
|
const globalModel = trimString(globalOptions.model);
|
|
390
|
-
const presetModel = trimString(
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
const usedParentMatch = Boolean(parentMatchModel) &&
|
|
394
|
-
|
|
354
|
+
const presetModel = trimString(legacyPreset?.model);
|
|
355
|
+
const profileModels = profile?.models ?? modelList(profile?.model, profile?.fallbackModels) ?? [];
|
|
356
|
+
const profileModel = profileModels[0];
|
|
357
|
+
const usedParentMatch = Boolean(parentMatchModel) && !forcedModel && !taskModel
|
|
358
|
+
&& (selectedType === "oracle" || !(presetTypeModel || globalModel || presetModel));
|
|
359
|
+
const primaryModel = forcedModel || taskModel || (usedParentMatch ? parentMatchModel : undefined)
|
|
360
|
+
|| presetTypeModel || globalModel || presetModel || profileModel;
|
|
361
|
+
const configuredFallbacks = forcedModel || taskModel
|
|
395
362
|
? []
|
|
396
|
-
: usedParentMatch && parentMatch?.fallbackModels
|
|
363
|
+
: usedParentMatch && parentMatch?.fallbackModels !== undefined
|
|
397
364
|
? parentMatch.fallbackModels
|
|
398
|
-
: resolveFallbackModels({ model, presetType, preset, profile });
|
|
365
|
+
: resolveFallbackModels({ model: primaryModel, presetType, preset: legacyPreset, profileModels, profile });
|
|
399
366
|
const extraArgs = forcedModel
|
|
400
367
|
? stripModelArgs([...profileExtraArgs, ...presetTypeExtraArgs, ...taskExtraArgs, ...presetExtraArgs, ...globalExtraArgs])
|
|
401
368
|
: [...profileExtraArgs, ...presetTypeExtraArgs, ...taskExtraArgs, ...presetExtraArgs, ...globalExtraArgs];
|
|
402
|
-
const
|
|
369
|
+
const cliModel = modelFromArgs(extraArgs);
|
|
370
|
+
const explicitModel = forcedModel || cliModel || taskModel;
|
|
371
|
+
let candidates = modelList(explicitModel || primaryModel, explicitModel ? [] : configuredFallbacks) ?? [];
|
|
372
|
+
if (!explicitModel) {
|
|
373
|
+
if (selectedType === "oracle" && globalOptions.parentModel && !usedParentMatch
|
|
374
|
+
&& !presetTypeModel && !presetModel && !globalModel && !profile?.model) {
|
|
375
|
+
const parentProvider = globalOptions.parentModel.split("/")[0];
|
|
376
|
+
candidates = [
|
|
377
|
+
...candidates.filter((ref) => ref.split("/")[0] !== parentProvider),
|
|
378
|
+
...candidates.filter((ref) => ref.split("/")[0] === parentProvider),
|
|
379
|
+
];
|
|
380
|
+
}
|
|
381
|
+
if (preset?.models !== undefined) {
|
|
382
|
+
const available = new Set(preset.models);
|
|
383
|
+
candidates = candidates.filter((ref) => available.has(ref));
|
|
384
|
+
if (candidates.length === 0) {
|
|
385
|
+
throw new SubagentModelSelectionError(task.id, "No ranked candidate is in the active preset's models pool.");
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (candidates.length === 0) {
|
|
390
|
+
throw new SubagentModelSelectionError(task.id, "No model candidates are configured; set models on the agent profile.");
|
|
391
|
+
}
|
|
392
|
+
const [model, ...fallbackModels] = candidates;
|
|
393
|
+
const timeoutMs = task.timeoutMs ?? globalOptions.timeoutMs ?? presetType?.timeoutMs ?? legacyPreset?.timeoutMs ?? profile?.timeoutMs ?? config.timeoutMs;
|
|
403
394
|
|
|
404
395
|
return {
|
|
405
396
|
profile,
|
|
406
397
|
extraArgs,
|
|
407
|
-
isolatedSkills:
|
|
398
|
+
isolatedSkills: arrayOfStrings(profile?.isolatedSkills) ?? [],
|
|
408
399
|
fallbackModels,
|
|
409
400
|
retry: resolveRetryConfig(config.retry, profile?.retry),
|
|
410
401
|
maxResultBytes: profile?.maxResultBytes ?? config.maxResultBytes,
|
|
411
402
|
timeoutMs,
|
|
412
403
|
task: {
|
|
413
404
|
...task,
|
|
414
|
-
subagentType:
|
|
405
|
+
subagentType: selectedType,
|
|
415
406
|
model,
|
|
416
|
-
thinking: trimString(globalOptions.thinking) || trimString(task.thinking) || trimString(presetType?.thinking) || trimString(globalOptions.defaultThinking) || trimString(
|
|
407
|
+
thinking: trimString(globalOptions.thinking) || trimString(task.thinking) || trimString(presetType?.thinking) || trimString(globalOptions.defaultThinking) || trimString(legacyPreset?.thinking) || trimString(profile?.thinking),
|
|
417
408
|
promptAppend,
|
|
418
409
|
promptOverride: trimString(task.promptOverride) || trimString(profile?.promptOverride),
|
|
419
410
|
tools: task.tools && task.tools.length > 0 ? task.tools : arrayOfStrings(profile?.tools),
|
|
@@ -422,18 +413,14 @@ export function resolveAgentTaskConfig(
|
|
|
422
413
|
};
|
|
423
414
|
}
|
|
424
415
|
|
|
425
|
-
function resolveIsolatedSkills(selectedType: string | undefined, profile: SubagentTypeConfig | undefined): string[] {
|
|
426
|
-
const configured = arrayOfStrings(profile?.isolatedSkills) ?? [];
|
|
427
|
-
if (selectedType !== "browser-qa") return configured;
|
|
428
|
-
return [...new Set([getBrowserQaSkillPath(), ...configured])];
|
|
429
|
-
}
|
|
430
|
-
|
|
431
416
|
export function resolveSubagentRoutingConfig(config: SubagentConfig): ResolvedSubagentRoutingConfig {
|
|
432
417
|
return { ...DEFAULT_ROUTING_CONFIG, ...(config.routing ?? {}) };
|
|
433
418
|
}
|
|
434
419
|
|
|
435
420
|
export function defaultSubagentType(config: SubagentConfig): string | undefined {
|
|
436
|
-
|
|
421
|
+
const configured = trimString(config.defaultType);
|
|
422
|
+
if (configured) return Object.prototype.hasOwnProperty.call(config.types, configured) ? configured : undefined;
|
|
423
|
+
return Object.keys(config.types).find((name) => trimString(name));
|
|
437
424
|
}
|
|
438
425
|
|
|
439
426
|
/** Merge global and per-type retry partials into a fully resolved RetryConfig. Per-type wins. */
|
|
@@ -530,6 +517,7 @@ function normalizeConfig(value: Record<string, unknown>, file: string): Partial<
|
|
|
530
517
|
if (!isRecord(rawPreset)) throw new Error(`Subagent preset "${name}" must be an object: ${file}`);
|
|
531
518
|
presets[name] = {
|
|
532
519
|
description: trimString(rawPreset.description),
|
|
520
|
+
models: normalizeModels(rawPreset.models, `preset "${name}"`, file),
|
|
533
521
|
model: trimString(rawPreset.model),
|
|
534
522
|
fallbackModels: modelList(rawPreset.fallbackModels, rawPreset.fallbackModel),
|
|
535
523
|
thinking: trimString(rawPreset.thinking),
|
|
@@ -546,26 +534,42 @@ function normalizeConfig(value: Record<string, unknown>, file: string): Partial<
|
|
|
546
534
|
const types: Record<string, SubagentTypeConfig> = {};
|
|
547
535
|
for (const [name, rawProfile] of Object.entries(value.types)) {
|
|
548
536
|
if (!isRecord(rawProfile)) throw new Error(`Subagent type "${name}" must be an object: ${file}`);
|
|
549
|
-
types[name] =
|
|
550
|
-
description: trimString(rawProfile.description),
|
|
551
|
-
model: trimString(rawProfile.model),
|
|
552
|
-
fallbackModels: modelList(rawProfile.fallbackModels, rawProfile.fallbackModel),
|
|
553
|
-
modelByParent: normalizeModelByParent(rawProfile.modelByParent, name, file),
|
|
554
|
-
thinking: trimString(rawProfile.thinking),
|
|
555
|
-
tools: arrayOfStrings(rawProfile.tools),
|
|
556
|
-
isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
|
|
557
|
-
extraArgs: arrayOfStrings(rawProfile.extraArgs),
|
|
558
|
-
promptAppend: textBlock(rawProfile.promptAppend),
|
|
559
|
-
promptOverride: textBlock(rawProfile.promptOverride),
|
|
560
|
-
retry: isRecord(rawProfile.retry) ? normalizeRetryConfig(rawProfile.retry) : undefined,
|
|
561
|
-
maxResultBytes: finiteNumber(rawProfile.maxResultBytes) !== undefined ? Math.max(0, Math.round(finiteNumber(rawProfile.maxResultBytes)!)) : undefined,
|
|
562
|
-
timeoutMs: positiveMilliseconds(rawProfile.timeoutMs),
|
|
563
|
-
};
|
|
537
|
+
types[name] = normalizeSubagentTypeProfile(rawProfile, name, file);
|
|
564
538
|
}
|
|
565
539
|
output.types = types;
|
|
566
540
|
return output;
|
|
567
541
|
}
|
|
568
542
|
|
|
543
|
+
/**
|
|
544
|
+
* Normalize one raw sub-agent type profile (`SubagentTypeConfig` shape) from
|
|
545
|
+
* any source (JSONC config files or project agent definitions). Shared so all
|
|
546
|
+
* sources apply identical field validation and trimming.
|
|
547
|
+
*/
|
|
548
|
+
export function normalizeSubagentTypeProfile(
|
|
549
|
+
rawProfile: Record<string, unknown>,
|
|
550
|
+
name: string,
|
|
551
|
+
file: string,
|
|
552
|
+
): SubagentTypeConfig {
|
|
553
|
+
const models = normalizeModels(rawProfile.models, `type "${name}"`, file);
|
|
554
|
+
return {
|
|
555
|
+
description: trimString(rawProfile.description),
|
|
556
|
+
icon: trimString(rawProfile.icon),
|
|
557
|
+
models,
|
|
558
|
+
model: models === undefined ? trimString(rawProfile.model) : undefined,
|
|
559
|
+
fallbackModels: models === undefined ? modelList(rawProfile.fallbackModels, rawProfile.fallbackModel) : undefined,
|
|
560
|
+
modelByParent: models === undefined ? normalizeModelByParent(rawProfile.modelByParent, name, file) : undefined,
|
|
561
|
+
thinking: trimString(rawProfile.thinking),
|
|
562
|
+
tools: arrayOfStrings(rawProfile.tools),
|
|
563
|
+
isolatedSkills: arrayOfStrings(rawProfile.isolatedSkills),
|
|
564
|
+
extraArgs: arrayOfStrings(rawProfile.extraArgs),
|
|
565
|
+
promptAppend: textBlock(rawProfile.promptAppend),
|
|
566
|
+
promptOverride: textBlock(rawProfile.promptOverride),
|
|
567
|
+
retry: isRecord(rawProfile.retry) ? normalizeRetryConfig(rawProfile.retry) : undefined,
|
|
568
|
+
maxResultBytes: finiteNumber(rawProfile.maxResultBytes) !== undefined ? Math.max(0, Math.round(finiteNumber(rawProfile.maxResultBytes)!)) : undefined,
|
|
569
|
+
timeoutMs: positiveMilliseconds(rawProfile.timeoutMs),
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
569
573
|
function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): void {
|
|
570
574
|
if (source.defaultType) target.defaultType = source.defaultType;
|
|
571
575
|
if (source.routing) target.routing = { ...(target.routing ?? {}), ...source.routing };
|
|
@@ -575,14 +579,45 @@ function mergeConfig(target: SubagentConfig, source: Partial<SubagentConfig>): v
|
|
|
575
579
|
if (source.timeoutMs !== undefined) target.timeoutMs = source.timeoutMs;
|
|
576
580
|
if (source.retry) target.retry = { ...(target.retry ?? {}), ...source.retry };
|
|
577
581
|
for (const [name, profile] of Object.entries(source.types ?? {})) {
|
|
578
|
-
target.types[name] =
|
|
582
|
+
target.types[name] = mergeTypeProfile(target.types[name] ?? {}, profile);
|
|
579
583
|
}
|
|
580
584
|
for (const [name, preset] of Object.entries(source.presets ?? {})) {
|
|
581
585
|
target.presets = target.presets ?? {};
|
|
582
|
-
|
|
586
|
+
const previous = target.presets[name] ?? {};
|
|
587
|
+
const merged = { ...previous, ...compactPreset(preset) };
|
|
588
|
+
// The explicit selector form in the later config wins in both directions.
|
|
589
|
+
if (preset.models !== undefined) {
|
|
590
|
+
delete merged.model;
|
|
591
|
+
delete merged.fallbackModels;
|
|
592
|
+
delete merged.types;
|
|
593
|
+
delete merged.thinking;
|
|
594
|
+
delete merged.extraArgs;
|
|
595
|
+
delete merged.timeoutMs;
|
|
596
|
+
} else if (preset.model || preset.fallbackModels !== undefined || preset.types) {
|
|
597
|
+
delete merged.models;
|
|
598
|
+
}
|
|
599
|
+
target.presets[name] = merged;
|
|
583
600
|
}
|
|
584
601
|
}
|
|
585
602
|
|
|
603
|
+
/** Preserve legacy field overrides without allowing inherited primary models
|
|
604
|
+
* to win over a newly supplied candidate list (or the reverse). */
|
|
605
|
+
function mergeTypeProfile(base: SubagentTypeConfig, source: SubagentTypeConfig): SubagentTypeConfig {
|
|
606
|
+
const merged = { ...compactProfile(base), ...compactProfile(source) };
|
|
607
|
+
if (source.models !== undefined) {
|
|
608
|
+
merged.models = [...source.models];
|
|
609
|
+
delete merged.model;
|
|
610
|
+
delete merged.fallbackModels;
|
|
611
|
+
delete merged.modelByParent;
|
|
612
|
+
} else if (source.model || source.fallbackModels !== undefined) {
|
|
613
|
+
const previous = base.models ?? modelList(base.model, base.fallbackModels) ?? [];
|
|
614
|
+
delete merged.models;
|
|
615
|
+
merged.model = source.model ?? previous[0];
|
|
616
|
+
merged.fallbackModels = source.fallbackModels ?? base.fallbackModels ?? previous.slice(1);
|
|
617
|
+
}
|
|
618
|
+
return merged;
|
|
619
|
+
}
|
|
620
|
+
|
|
586
621
|
function normalizeRoutingConfig(value: Record<string, unknown>): SubagentRoutingConfig {
|
|
587
622
|
const routing: SubagentRoutingConfig = {};
|
|
588
623
|
if (typeof value.enabled === "boolean") routing.enabled = value.enabled;
|
|
@@ -625,8 +660,10 @@ function modelPatternRegExp(pattern: string): RegExp {
|
|
|
625
660
|
function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
|
|
626
661
|
const compact: SubagentTypeConfig = {};
|
|
627
662
|
if (profile.description) compact.description = profile.description;
|
|
663
|
+
if (profile.icon) compact.icon = profile.icon;
|
|
664
|
+
if (profile.models !== undefined) compact.models = profile.models;
|
|
628
665
|
if (profile.model) compact.model = profile.model;
|
|
629
|
-
if (profile.fallbackModels
|
|
666
|
+
if (profile.fallbackModels) compact.fallbackModels = profile.fallbackModels;
|
|
630
667
|
if (profile.modelByParent) compact.modelByParent = profile.modelByParent;
|
|
631
668
|
if (profile.thinking) compact.thinking = profile.thinking;
|
|
632
669
|
if (profile.tools && profile.tools.length > 0) compact.tools = profile.tools;
|
|
@@ -643,8 +680,9 @@ function compactProfile(profile: SubagentTypeConfig): SubagentTypeConfig {
|
|
|
643
680
|
function compactPreset(preset: SubagentPreset): SubagentPreset {
|
|
644
681
|
const compact: SubagentPreset = {};
|
|
645
682
|
if (preset.description) compact.description = preset.description;
|
|
683
|
+
if (preset.models !== undefined) compact.models = preset.models;
|
|
646
684
|
if (preset.model) compact.model = preset.model;
|
|
647
|
-
if (preset.fallbackModels
|
|
685
|
+
if (preset.fallbackModels) compact.fallbackModels = preset.fallbackModels;
|
|
648
686
|
if (preset.thinking) compact.thinking = preset.thinking;
|
|
649
687
|
if (preset.extraArgs && preset.extraArgs.length > 0) compact.extraArgs = preset.extraArgs;
|
|
650
688
|
if (preset.timeoutMs !== undefined) compact.timeoutMs = preset.timeoutMs;
|
|
@@ -671,7 +709,7 @@ function normalizePresetTypeOverrides(value: unknown, file: string, presetName:
|
|
|
671
709
|
const extraArgs = arrayOfStrings(rawOverride.extraArgs);
|
|
672
710
|
const timeoutMs = positiveMilliseconds(rawOverride.timeoutMs);
|
|
673
711
|
if (model) override.model = model;
|
|
674
|
-
if (fallbackModels
|
|
712
|
+
if (fallbackModels) override.fallbackModels = fallbackModels;
|
|
675
713
|
if (thinking) override.thinking = thinking;
|
|
676
714
|
if (extraArgs && extraArgs.length > 0) override.extraArgs = extraArgs;
|
|
677
715
|
if (timeoutMs !== undefined) override.timeoutMs = timeoutMs;
|
|
@@ -680,6 +718,14 @@ function normalizePresetTypeOverrides(value: unknown, file: string, presetName:
|
|
|
680
718
|
return Object.keys(types).length > 0 ? types : undefined;
|
|
681
719
|
}
|
|
682
720
|
|
|
721
|
+
function normalizeModels(value: unknown, owner: string, file: string): string[] | undefined {
|
|
722
|
+
if (value === undefined) return undefined;
|
|
723
|
+
if (!Array.isArray(value) || value.some((ref) => typeof ref !== "string" || !/^[^\s/*]+\/[^\s*]+$/.test(ref.trim()))) {
|
|
724
|
+
throw new Error(`Subagent ${owner} models must be an array of provider/model references: ${file}`);
|
|
725
|
+
}
|
|
726
|
+
return [...new Set(value.map((ref: string) => ref.trim()))];
|
|
727
|
+
}
|
|
728
|
+
|
|
683
729
|
function normalizeModelByParent(value: unknown, typeName: string, file: string): Record<string, ModelByParentEntry> | undefined {
|
|
684
730
|
if (value === undefined || value === null) return undefined;
|
|
685
731
|
if (!isRecord(value)) throw new Error(`Subagent type "${typeName}" modelByParent must be an object: ${file}`);
|
|
@@ -716,13 +762,12 @@ function resolveFallbackModels(options: {
|
|
|
716
762
|
model?: string;
|
|
717
763
|
presetType?: SubagentPresetTypeOverride;
|
|
718
764
|
preset?: SubagentPreset;
|
|
765
|
+
profileModels: string[];
|
|
719
766
|
profile?: SubagentTypeConfig;
|
|
720
767
|
}): string[] {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
...(options.profile?.fallbackModels ?? []),
|
|
725
|
-
];
|
|
768
|
+
// A selected budget's fallback list is authoritative, including [] (none).
|
|
769
|
+
const fallbacks = options.presetType?.fallbackModels
|
|
770
|
+
?? options.preset?.fallbackModels ?? options.profile?.fallbackModels ?? options.profileModels.slice(1);
|
|
726
771
|
const seen = new Set<string>();
|
|
727
772
|
if (options.model) seen.add(options.model);
|
|
728
773
|
const result: string[] = [];
|
|
@@ -736,10 +781,12 @@ function resolveFallbackModels(options: {
|
|
|
736
781
|
}
|
|
737
782
|
|
|
738
783
|
function applyEnvModelOverrides(config: SubagentConfig, env: NodeJS.ProcessEnv): void {
|
|
739
|
-
for (const
|
|
784
|
+
for (const name of Object.keys(config.types)) {
|
|
740
785
|
const key = typeEnvKey(name);
|
|
741
786
|
const model = trimString(env[`ASYNC_SUBAGENTS_${key}_MODEL`] || env[`PI_SUBAGENTS_${key}_MODEL`]);
|
|
742
|
-
if (model)
|
|
787
|
+
if (model) {
|
|
788
|
+
config.types[name] = mergeTypeProfile(config.types[name] ?? {}, { model });
|
|
789
|
+
}
|
|
743
790
|
}
|
|
744
791
|
}
|
|
745
792
|
|
|
@@ -849,7 +896,7 @@ function modelList(...values: unknown[]): string[] | undefined {
|
|
|
849
896
|
models.push(model);
|
|
850
897
|
}
|
|
851
898
|
}
|
|
852
|
-
return models.length > 0 ? models : undefined;
|
|
899
|
+
return models.length > 0 || values.some(Array.isArray) ? models : undefined;
|
|
853
900
|
}
|
|
854
901
|
|
|
855
902
|
function patternList(...values: unknown[]): string[] | undefined {
|
|
@@ -917,6 +964,16 @@ function stripModelArgs(args: string[]): string[] {
|
|
|
917
964
|
return output;
|
|
918
965
|
}
|
|
919
966
|
|
|
967
|
+
/** CLI overrides are explicit, too; retain the final flag's actual model. */
|
|
968
|
+
function modelFromArgs(args: string[]): string | undefined {
|
|
969
|
+
let model: string | undefined;
|
|
970
|
+
for (let i = 0; i < args.length; i++) {
|
|
971
|
+
if (args[i] === "--model" || args[i] === "-m") model = trimString(args[++i]);
|
|
972
|
+
else if (args[i].startsWith("--model=")) model = trimString(args[i].slice("--model=".length));
|
|
973
|
+
}
|
|
974
|
+
return model;
|
|
975
|
+
}
|
|
976
|
+
|
|
920
977
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
921
978
|
return typeof value === "object" && value !== null;
|
|
922
979
|
}
|
|
@@ -104,7 +104,7 @@ function firstAvailableFallback(failedModel: string, fallbackModels: string[]):
|
|
|
104
104
|
return candidates.find((candidate) => modelProvider(candidate) !== failedProvider && !isSessionModelUnavailable(candidate));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function isSessionModelUnavailable(model: string): boolean {
|
|
107
|
+
export function isSessionModelUnavailable(model: string): boolean {
|
|
108
108
|
return exhaustedModels.has(model) || exhaustedProviders.has(modelProvider(model));
|
|
109
109
|
}
|
|
110
110
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
2
|
+
import { isBlindModelRef, SubagentModelSelectionError, type ResolvedAgentTaskConfig, type SubagentConfig } from "./config.js";
|
|
3
|
+
import { isSessionModelUnavailable } from "./model-fallback.js";
|
|
4
|
+
|
|
5
|
+
export interface SubagentModelRegistry {
|
|
6
|
+
find(provider: string, modelId: string): Model<Api> | undefined;
|
|
7
|
+
/** SDK snapshot excludes providers which are registered but not configured. */
|
|
8
|
+
getAvailable?(): Model<Api>[];
|
|
9
|
+
getApiKeyAndHeaders?(model: Model<Api>): Promise<{ ok?: boolean }>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Rank was already determined by the profile/preset intersection. No LLM call. */
|
|
13
|
+
export async function selectAvailableAgentModels(
|
|
14
|
+
resolved: ResolvedAgentTaskConfig,
|
|
15
|
+
config: SubagentConfig,
|
|
16
|
+
registry?: SubagentModelRegistry,
|
|
17
|
+
signal?: AbortSignal,
|
|
18
|
+
): Promise<ResolvedAgentTaskConfig> {
|
|
19
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
20
|
+
const needsImages = Boolean(resolved.task.imagePaths?.length) || resolved.task.subagentType === "browser-qa";
|
|
21
|
+
if (!registry && !needsImages) return resolved; // Narrow offline/test callers.
|
|
22
|
+
const candidates = [resolved.task.model, ...resolved.fallbackModels].filter((ref): ref is string => Boolean(ref));
|
|
23
|
+
let registeredAvailable: Set<string> | undefined;
|
|
24
|
+
try {
|
|
25
|
+
if (registry?.getAvailable) {
|
|
26
|
+
registeredAvailable = new Set(registry.getAvailable().map((model) => `${model.provider}/${model.id}`));
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
throw new SubagentModelSelectionError(resolved.task.id, "Model runtime availability could not be read.");
|
|
30
|
+
}
|
|
31
|
+
const available: string[] = [];
|
|
32
|
+
for (const ref of candidates) {
|
|
33
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
34
|
+
if (registeredAvailable && !registeredAvailable.has(ref)) continue;
|
|
35
|
+
if (isSessionModelUnavailable(ref)) continue;
|
|
36
|
+
const slash = ref.indexOf("/");
|
|
37
|
+
if (slash <= 0) continue;
|
|
38
|
+
try {
|
|
39
|
+
const model = registry?.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
40
|
+
if (!model) continue;
|
|
41
|
+
if (needsImages && (isBlindModelRef(ref, config) || !model.input?.includes("image"))) continue;
|
|
42
|
+
if (registry?.getApiKeyAndHeaders && (await registry.getApiKeyAndHeaders(model)).ok === false) continue;
|
|
43
|
+
} catch {
|
|
44
|
+
// Auth errors may contain sensitive provider details; never echo them.
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
available.push(ref);
|
|
48
|
+
}
|
|
49
|
+
if (signal?.aborted) throw new Error("Aborted");
|
|
50
|
+
const [model, ...fallbackModels] = available;
|
|
51
|
+
if (!model) throw new SubagentModelSelectionError(resolved.task.id,
|
|
52
|
+
`No configured candidate is available${needsImages ? " with confirmed image support" : ""} in the model runtime.`);
|
|
53
|
+
return { ...resolved, task: { ...resolved.task, model }, fallbackModels };
|
|
54
|
+
}
|
|
@@ -23,18 +23,19 @@ Scope and constraints:
|
|
|
23
23
|
- Do not spawn other agents or background jobs unless explicitly allowed.
|
|
24
24
|
${scopeLine}${imageLine}- Follow the parent task's constraints and repository instructions.
|
|
25
25
|
- Keep output compact by default.
|
|
26
|
+
- Stay within the assigned scope and preserve unrelated work. Do not re-plan the project.
|
|
27
|
+
- Report concrete blockers and gathered evidence rather than widening the task or escalating models yourself.
|
|
28
|
+
- Keep noisy intermediate output in artifacts; return only decision-relevant evidence to the parent.
|
|
26
29
|
- If the task explicitly asks for verbatim/raw file contents, command output, logs, or exact text, output only that content exactly and do not summarize it.
|
|
27
30
|
- If requested raw content is very large, include the requested relevant portion and clearly say what was omitted.
|
|
28
31
|
|
|
29
32
|
Output format:
|
|
30
33
|
- If the task explicitly requests a raw/verbatim/exact output format, follow that request instead and do not add the standard summary sections below.
|
|
31
34
|
- Otherwise use:
|
|
32
|
-
1.
|
|
33
|
-
2. Evidence
|
|
34
|
-
3.
|
|
35
|
-
|
|
36
|
-
5. Tests/commands run, with short results
|
|
37
|
-
6. Risks / open questions
|
|
35
|
+
1. Result: answer or completion status, briefly.
|
|
36
|
+
2. Evidence: relevant file:line references, changed files, check results and artifact paths.
|
|
37
|
+
3. Limits: blockers, unverified assumptions, risks or necessary next action; omit when empty.
|
|
38
|
+
- Do not repeat the task or paste full logs/diffs unless requested. Preserve required QA artifact links.
|
|
38
39
|
`;
|
|
39
40
|
|
|
40
41
|
return task.promptAppend
|