pi-agent-flow 1.1.0 → 1.2.2
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 +158 -44
- package/agents/audit.md +19 -24
- package/agents/build.md +28 -49
- package/agents/craft.md +16 -24
- package/agents/debug.md +14 -23
- package/agents/ideas.md +16 -17
- package/agents/scout.md +15 -20
- package/package.json +4 -15
- package/{agents.ts → src/agents.ts} +10 -4
- package/src/ambient.d.ts +85 -0
- package/{batch.ts → src/batch.ts} +687 -121
- package/src/cli-args.ts +283 -0
- package/src/config.ts +310 -0
- package/{flow.ts → src/flow.ts} +93 -21
- package/{hooks.ts → src/hooks.ts} +6 -6
- package/{index.ts → src/index.ts} +522 -103
- package/{render-utils.ts → src/render-utils.ts} +2 -2
- package/{render.ts → src/render.ts} +54 -10
- package/src/runner-events.ts +692 -0
- package/src/structured-output.ts +97 -0
- package/{types.ts → src/types.ts} +100 -0
- package/config.ts +0 -102
- package/runner-cli.js +0 -254
- package/runner-events.js +0 -559
- /package/{web-tool.ts → src/web-tool.ts} +0 -0
package/src/cli-args.ts
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for inheriting selected parent CLI flags in child flow processes.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
|
|
9
|
+
function looksLikeExplicitRelativePath(value: string): boolean {
|
|
10
|
+
return (
|
|
11
|
+
value.startsWith("./") ||
|
|
12
|
+
value.startsWith("../") ||
|
|
13
|
+
value.startsWith(".\\") ||
|
|
14
|
+
value.startsWith("..\\")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ResolvePathOptions {
|
|
19
|
+
allowPackageSource?: boolean;
|
|
20
|
+
alwaysResolveRelative?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolvePathArg(value: string, options: ResolvePathOptions = {}): string {
|
|
24
|
+
const { allowPackageSource = false, alwaysResolveRelative = false } = options;
|
|
25
|
+
if (!value) return value;
|
|
26
|
+
if (allowPackageSource && (value.startsWith("npm:") || value.startsWith("git:"))) return value;
|
|
27
|
+
if (value.startsWith("~/")) return path.join(os.homedir(), value.slice(2));
|
|
28
|
+
if (path.isAbsolute(value)) return value;
|
|
29
|
+
|
|
30
|
+
const resolved = path.resolve(process.cwd(), value);
|
|
31
|
+
if (
|
|
32
|
+
alwaysResolveRelative ||
|
|
33
|
+
looksLikeExplicitRelativePath(value) ||
|
|
34
|
+
path.extname(value) !== "" ||
|
|
35
|
+
fs.existsSync(resolved)
|
|
36
|
+
) {
|
|
37
|
+
return resolved;
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParsedFlowCliArgs {
|
|
43
|
+
extensionArgs: string[];
|
|
44
|
+
alwaysProxy: string[];
|
|
45
|
+
fallbackModel?: string;
|
|
46
|
+
fallbackThinking?: string;
|
|
47
|
+
fallbackTools?: string;
|
|
48
|
+
fallbackNoTools: boolean;
|
|
49
|
+
flowModelConfig?: string;
|
|
50
|
+
tieredModels: {
|
|
51
|
+
lite?: string;
|
|
52
|
+
flash?: string;
|
|
53
|
+
full?: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse process.argv into groups used for child flow invocations.
|
|
59
|
+
*
|
|
60
|
+
* - extensionArgs: forwarded with path resolution
|
|
61
|
+
* - alwaysProxy: forwarded verbatim to every child
|
|
62
|
+
* - fallbackModel/thinking/tools: used only when the flow file does not set them
|
|
63
|
+
*/
|
|
64
|
+
export function parseFlowCliArgs(argv: string[]): ParsedFlowCliArgs {
|
|
65
|
+
const extensionArgs: string[] = [];
|
|
66
|
+
const alwaysProxy: string[] = [];
|
|
67
|
+
let fallbackModel: string | undefined;
|
|
68
|
+
let fallbackThinking: string | undefined;
|
|
69
|
+
let fallbackTools: string | undefined;
|
|
70
|
+
let fallbackNoTools = false;
|
|
71
|
+
let flowModelConfig: string | undefined;
|
|
72
|
+
let tieredLiteModel: string | undefined;
|
|
73
|
+
let tieredFlashModel: string | undefined;
|
|
74
|
+
let tieredFullModel: string | undefined;
|
|
75
|
+
|
|
76
|
+
let i = 2; // skip executable + script name
|
|
77
|
+
while (i < argv.length) {
|
|
78
|
+
const raw = argv[i];
|
|
79
|
+
if (!raw.startsWith("-")) {
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const eqIdx = raw.indexOf("=");
|
|
85
|
+
const flagName = eqIdx !== -1 ? raw.slice(0, eqIdx) : raw;
|
|
86
|
+
const inlineValue = eqIdx !== -1 ? raw.slice(eqIdx + 1) : undefined;
|
|
87
|
+
|
|
88
|
+
const nextToken = argv[i + 1];
|
|
89
|
+
const nextIsValue = nextToken !== undefined && !nextToken.startsWith("-");
|
|
90
|
+
|
|
91
|
+
const getValue = (): [string | undefined, number] => {
|
|
92
|
+
if (inlineValue !== undefined) return [inlineValue, 1];
|
|
93
|
+
if (nextIsValue) return [nextToken, 2];
|
|
94
|
+
return [undefined, 1];
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (flagName === "--flow-model-config") {
|
|
98
|
+
const [value, skip] = getValue();
|
|
99
|
+
if (value !== undefined) flowModelConfig = value;
|
|
100
|
+
i += skip;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
[
|
|
106
|
+
"--mode",
|
|
107
|
+
"--session",
|
|
108
|
+
"--append-system-prompt",
|
|
109
|
+
"--export",
|
|
110
|
+
"--flow-max-depth",
|
|
111
|
+
].includes(flagName)
|
|
112
|
+
) {
|
|
113
|
+
const [, skip] = getValue();
|
|
114
|
+
i += skip;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (["--flow-prevent-cycles", "--list-models"].includes(flagName)) {
|
|
119
|
+
const [, skip] = getValue();
|
|
120
|
+
i += skip;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (
|
|
125
|
+
[
|
|
126
|
+
"--print",
|
|
127
|
+
"-p",
|
|
128
|
+
"--no-session",
|
|
129
|
+
"--continue",
|
|
130
|
+
"-c",
|
|
131
|
+
"--resume",
|
|
132
|
+
"-r",
|
|
133
|
+
"--offline",
|
|
134
|
+
"--help",
|
|
135
|
+
"-h",
|
|
136
|
+
"--version",
|
|
137
|
+
"-v",
|
|
138
|
+
"--no-flow-prevent-cycles",
|
|
139
|
+
].includes(flagName)
|
|
140
|
+
) {
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (flagName === "--no-extensions" || flagName === "-ne") {
|
|
146
|
+
extensionArgs.push(flagName);
|
|
147
|
+
i++;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (flagName === "--extension" || flagName === "-e") {
|
|
152
|
+
const [value, skip] = getValue();
|
|
153
|
+
if (value !== undefined) {
|
|
154
|
+
extensionArgs.push(flagName, resolvePathArg(value, { allowPackageSource: true }));
|
|
155
|
+
}
|
|
156
|
+
i += skip;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (["--skill", "--prompt-template", "--theme"].includes(flagName)) {
|
|
161
|
+
const [value, skip] = getValue();
|
|
162
|
+
if (value !== undefined) alwaysProxy.push(flagName, resolvePathArg(value));
|
|
163
|
+
i += skip;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (flagName === "--session-dir") {
|
|
168
|
+
const [value, skip] = getValue();
|
|
169
|
+
if (value !== undefined) {
|
|
170
|
+
alwaysProxy.push(flagName, resolvePathArg(value, { alwaysResolveRelative: true }));
|
|
171
|
+
}
|
|
172
|
+
i += skip;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
[
|
|
178
|
+
"--provider",
|
|
179
|
+
"--api-key",
|
|
180
|
+
"--system-prompt",
|
|
181
|
+
"--models",
|
|
182
|
+
].includes(flagName)
|
|
183
|
+
) {
|
|
184
|
+
const [value, skip] = getValue();
|
|
185
|
+
if (value !== undefined) alwaysProxy.push(flagName, value);
|
|
186
|
+
i += skip;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
[
|
|
192
|
+
"--no-skills",
|
|
193
|
+
"-ns",
|
|
194
|
+
"--no-prompt-templates",
|
|
195
|
+
"-np",
|
|
196
|
+
"--no-themes",
|
|
197
|
+
"--verbose",
|
|
198
|
+
].includes(flagName)
|
|
199
|
+
) {
|
|
200
|
+
alwaysProxy.push(flagName);
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (flagName === "--model") {
|
|
206
|
+
const [value, skip] = getValue();
|
|
207
|
+
if (value !== undefined) fallbackModel = value;
|
|
208
|
+
i += skip;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (flagName === "--thinking") {
|
|
213
|
+
const [value, skip] = getValue();
|
|
214
|
+
if (value !== undefined) fallbackThinking = value;
|
|
215
|
+
i += skip;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (flagName === "--tools") {
|
|
220
|
+
const [value, skip] = getValue();
|
|
221
|
+
if (value !== undefined) fallbackTools = value;
|
|
222
|
+
i += skip;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (flagName === "--no-tools") {
|
|
227
|
+
fallbackNoTools = true;
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (flagName === "--flow-lite-model") {
|
|
233
|
+
const [value, skip] = getValue();
|
|
234
|
+
if (value !== undefined) tieredLiteModel = value;
|
|
235
|
+
i += skip;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (flagName === "--flow-flash-model") {
|
|
240
|
+
const [value, skip] = getValue();
|
|
241
|
+
if (value !== undefined) tieredFlashModel = value;
|
|
242
|
+
i += skip;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (flagName === "--flow-full-model") {
|
|
247
|
+
const [value, skip] = getValue();
|
|
248
|
+
if (value !== undefined) tieredFullModel = value;
|
|
249
|
+
i += skip;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (inlineValue !== undefined) {
|
|
254
|
+
alwaysProxy.push(flagName, inlineValue);
|
|
255
|
+
i++;
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (nextIsValue) {
|
|
260
|
+
alwaysProxy.push(flagName, nextToken);
|
|
261
|
+
i += 2;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
alwaysProxy.push(flagName);
|
|
266
|
+
i++;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
extensionArgs,
|
|
271
|
+
alwaysProxy,
|
|
272
|
+
fallbackModel,
|
|
273
|
+
fallbackThinking,
|
|
274
|
+
fallbackTools,
|
|
275
|
+
fallbackNoTools,
|
|
276
|
+
flowModelConfig,
|
|
277
|
+
tieredModels: {
|
|
278
|
+
lite: tieredLiteModel,
|
|
279
|
+
flash: tieredFlashModel,
|
|
280
|
+
full: tieredFullModel,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load flow model strategy configuration from Pi settings files.
|
|
3
|
+
*
|
|
4
|
+
* Reads global (~/.pi/agent/settings.json) and project (.pi/settings.json)
|
|
5
|
+
* settings, with project overriding global for flowModelConfigs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
|
|
12
|
+
export type FlowTier = "lite" | "flash" | "full";
|
|
13
|
+
|
|
14
|
+
export interface FlowModelTierConfig {
|
|
15
|
+
primary?: string;
|
|
16
|
+
failover?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FlowModelStrategy = Partial<Record<FlowTier, FlowModelTierConfig>>;
|
|
20
|
+
export type FlowModelConfigs = Record<string, FlowModelStrategy>;
|
|
21
|
+
|
|
22
|
+
export interface LoadedFlowModelConfigs {
|
|
23
|
+
selectedName: string;
|
|
24
|
+
configs: FlowModelConfigs;
|
|
25
|
+
strategy: FlowModelStrategy;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FlowSettings {
|
|
29
|
+
toolOptimize?: boolean;
|
|
30
|
+
/** Whether to inject structured JSON output instructions into flow prompts. Default: true. */
|
|
31
|
+
structuredOutput?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const BUILTIN_FLOW_MODEL_CONFIGS: FlowModelConfigs = {
|
|
35
|
+
default: {},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const FLOW_TIERS: FlowTier[] = ["lite", "flash", "full"];
|
|
39
|
+
|
|
40
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readSettingsJson(filePath: string): Record<string, unknown> | null {
|
|
45
|
+
try {
|
|
46
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
47
|
+
return JSON.parse(content) as Record<string, unknown>;
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getGlobalSettingsPath(): string {
|
|
54
|
+
const agentDir = process.env["PI_CODING_AGENT_DIR"]?.trim() || path.join(os.homedir(), ".pi", "agent");
|
|
55
|
+
return path.join(agentDir, "settings.json");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getProjectSettingsPath(cwd: string): string {
|
|
59
|
+
return path.join(cwd, ".pi", "settings.json");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function extractSelectedFlowModelConfigName(settings: Record<string, unknown> | null): string | undefined {
|
|
63
|
+
if (!isPlainObject(settings)) return undefined;
|
|
64
|
+
const raw = settings.flowModelConfig;
|
|
65
|
+
if (typeof raw !== "string") return undefined;
|
|
66
|
+
const normalized = raw.trim();
|
|
67
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeFailoverList(
|
|
71
|
+
value: unknown,
|
|
72
|
+
sourceLabel: string,
|
|
73
|
+
strategyName: string,
|
|
74
|
+
tier: FlowTier,
|
|
75
|
+
): string[] | undefined {
|
|
76
|
+
if (value === undefined) return undefined;
|
|
77
|
+
if (!Array.isArray(value)) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[pi-agent-flow] Ignoring invalid ${sourceLabel}.flowModelConfigs.${strategyName}.${tier}.failover. Expected an array of strings.`,
|
|
80
|
+
);
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result: string[] = [];
|
|
85
|
+
for (const item of value) {
|
|
86
|
+
if (typeof item !== "string") {
|
|
87
|
+
console.warn(
|
|
88
|
+
`[pi-agent-flow] Ignoring invalid failover entry in ${sourceLabel}.flowModelConfigs.${strategyName}.${tier}. Expected a string.`,
|
|
89
|
+
);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const normalized = item.trim();
|
|
93
|
+
if (!normalized) continue;
|
|
94
|
+
result.push(normalized);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function extractFlowModelConfigs(settings: Record<string, unknown> | null, sourceLabel: string): FlowModelConfigs {
|
|
100
|
+
if (!isPlainObject(settings)) return {};
|
|
101
|
+
const rawConfigs = settings.flowModelConfigs;
|
|
102
|
+
if (rawConfigs === undefined) return {};
|
|
103
|
+
if (!isPlainObject(rawConfigs)) {
|
|
104
|
+
console.warn(
|
|
105
|
+
`[pi-agent-flow] Ignoring invalid ${sourceLabel}.flowModelConfigs. Expected an object map of strategy names.`,
|
|
106
|
+
);
|
|
107
|
+
return {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const result: FlowModelConfigs = {};
|
|
111
|
+
for (const [rawName, rawStrategy] of Object.entries(rawConfigs)) {
|
|
112
|
+
const name = rawName.trim();
|
|
113
|
+
if (!name) {
|
|
114
|
+
console.warn(
|
|
115
|
+
`[pi-agent-flow] Ignoring empty strategy name in ${sourceLabel}.flowModelConfigs.`,
|
|
116
|
+
);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!isPlainObject(rawStrategy)) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`[pi-agent-flow] Ignoring invalid ${sourceLabel}.flowModelConfigs.${name}. Expected an object with lite/flash/full tiers.`,
|
|
122
|
+
);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const strategy: FlowModelStrategy = {};
|
|
127
|
+
for (const tier of FLOW_TIERS) {
|
|
128
|
+
const rawTier = rawStrategy[tier];
|
|
129
|
+
if (rawTier === undefined) continue;
|
|
130
|
+
if (!isPlainObject(rawTier)) {
|
|
131
|
+
console.warn(
|
|
132
|
+
`[pi-agent-flow] Ignoring invalid ${sourceLabel}.flowModelConfigs.${name}.${tier}. Expected { primary?: string, failover?: string[] }.`,
|
|
133
|
+
);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const tierConfig: FlowModelTierConfig = {};
|
|
138
|
+
if (typeof rawTier.primary === "string") {
|
|
139
|
+
const primary = rawTier.primary.trim();
|
|
140
|
+
if (primary) tierConfig.primary = primary;
|
|
141
|
+
} else if (rawTier.primary !== undefined) {
|
|
142
|
+
console.warn(
|
|
143
|
+
`[pi-agent-flow] Ignoring invalid ${sourceLabel}.flowModelConfigs.${name}.${tier}.primary. Expected a string.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const failover = normalizeFailoverList(rawTier.failover, sourceLabel, name, tier);
|
|
148
|
+
if (failover !== undefined) {
|
|
149
|
+
tierConfig.failover = failover;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (tierConfig.primary !== undefined || tierConfig.failover !== undefined) {
|
|
153
|
+
strategy[tier] = tierConfig;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
result[name] = strategy;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function extractFlowSettings(settings: Record<string, unknown> | null): FlowSettings {
|
|
164
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
const flowSettings = settings.flowSettings;
|
|
168
|
+
if (!flowSettings || typeof flowSettings !== "object" || Array.isArray(flowSettings)) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
const obj = flowSettings as Record<string, unknown>;
|
|
172
|
+
const result: FlowSettings = {};
|
|
173
|
+
if (typeof obj.toolOptimize === "boolean") {
|
|
174
|
+
result.toolOptimize = obj.toolOptimize;
|
|
175
|
+
}
|
|
176
|
+
if (typeof obj.structuredOutput === "boolean") {
|
|
177
|
+
result.structuredOutput = obj.structuredOutput;
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function mergeFlowModelTierConfigs(
|
|
183
|
+
base: FlowModelTierConfig | undefined,
|
|
184
|
+
override: FlowModelTierConfig | undefined,
|
|
185
|
+
): FlowModelTierConfig | undefined {
|
|
186
|
+
if (!base && !override) return undefined;
|
|
187
|
+
return {
|
|
188
|
+
...(base?.primary !== undefined ? { primary: base.primary } : {}),
|
|
189
|
+
...(base?.failover !== undefined ? { failover: base.failover } : {}),
|
|
190
|
+
...(override?.primary !== undefined ? { primary: override.primary } : {}),
|
|
191
|
+
...(override?.failover !== undefined ? { failover: override.failover } : {}),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function mergeFlowModelStrategies(
|
|
196
|
+
base: FlowModelStrategy | undefined,
|
|
197
|
+
override: FlowModelStrategy | undefined,
|
|
198
|
+
): FlowModelStrategy {
|
|
199
|
+
const result: FlowModelStrategy = {};
|
|
200
|
+
for (const tier of FLOW_TIERS) {
|
|
201
|
+
const merged = mergeFlowModelTierConfigs(base?.[tier], override?.[tier]);
|
|
202
|
+
if (merged) result[tier] = merged;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function mergeFlowModelConfigs(...configs: FlowModelConfigs[]): FlowModelConfigs {
|
|
208
|
+
const result: FlowModelConfigs = {};
|
|
209
|
+
for (const configSet of configs) {
|
|
210
|
+
for (const [name, strategy] of Object.entries(configSet)) {
|
|
211
|
+
result[name] = mergeFlowModelStrategies(result[name], strategy);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Load flowSettings from global and project settings.json.
|
|
219
|
+
* Project overrides global (shallow merge per key).
|
|
220
|
+
*/
|
|
221
|
+
export function loadFlowSettings(cwd: string): FlowSettings {
|
|
222
|
+
const globalSettings = readSettingsJson(getGlobalSettingsPath());
|
|
223
|
+
const globalFlowSettings = extractFlowSettings(globalSettings);
|
|
224
|
+
|
|
225
|
+
const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
|
|
226
|
+
const projectFlowSettings = extractFlowSettings(projectSettings);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
...globalFlowSettings,
|
|
230
|
+
...projectFlowSettings,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function selectFlowModelStrategy(
|
|
235
|
+
configs: FlowModelConfigs,
|
|
236
|
+
requestedName?: string,
|
|
237
|
+
): LoadedFlowModelConfigs {
|
|
238
|
+
const normalizedRequested = requestedName?.trim() || "default";
|
|
239
|
+
const strategy = configs[normalizedRequested];
|
|
240
|
+
if (strategy) {
|
|
241
|
+
return { selectedName: normalizedRequested, configs, strategy };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (normalizedRequested !== "default") {
|
|
245
|
+
console.warn(
|
|
246
|
+
`[pi-agent-flow] Flow model config "${normalizedRequested}" not found. Falling back to "default".`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
selectedName: "default",
|
|
251
|
+
configs,
|
|
252
|
+
strategy: configs.default ?? {},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Load flow model configs from global and project settings.json.
|
|
258
|
+
* Project overrides global (shallow merge per strategy/tier).
|
|
259
|
+
*/
|
|
260
|
+
export function loadFlowModelConfigs(cwd: string): LoadedFlowModelConfigs {
|
|
261
|
+
const globalSettings = readSettingsJson(getGlobalSettingsPath());
|
|
262
|
+
const globalConfigs = extractFlowModelConfigs(globalSettings, "global");
|
|
263
|
+
|
|
264
|
+
const projectSettings = readSettingsJson(getProjectSettingsPath(cwd));
|
|
265
|
+
const projectConfigs = extractFlowModelConfigs(projectSettings, "project");
|
|
266
|
+
|
|
267
|
+
const configs = mergeFlowModelConfigs(BUILTIN_FLOW_MODEL_CONFIGS, globalConfigs, projectConfigs);
|
|
268
|
+
const requestedName =
|
|
269
|
+
extractSelectedFlowModelConfigName(projectSettings) ??
|
|
270
|
+
extractSelectedFlowModelConfigName(globalSettings) ??
|
|
271
|
+
"default";
|
|
272
|
+
|
|
273
|
+
return selectFlowModelStrategy(configs, requestedName);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function resolveFlowModelCandidates(opts: {
|
|
277
|
+
tier: FlowTier;
|
|
278
|
+
flowModel?: string;
|
|
279
|
+
cliTierOverride?: string;
|
|
280
|
+
strategy: FlowModelStrategy;
|
|
281
|
+
fallbackModel?: string;
|
|
282
|
+
}): { primary: string | undefined; candidates: string[] } {
|
|
283
|
+
const unique = new Set<string>();
|
|
284
|
+
const candidates: string[] = [];
|
|
285
|
+
|
|
286
|
+
const add = (value: string | undefined) => {
|
|
287
|
+
if (!value) return;
|
|
288
|
+
const normalized = value.trim();
|
|
289
|
+
if (!normalized || unique.has(normalized)) return;
|
|
290
|
+
unique.add(normalized);
|
|
291
|
+
candidates.push(normalized);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (opts.flowModel) {
|
|
295
|
+
add(opts.flowModel);
|
|
296
|
+
return { primary: candidates[0], candidates };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (opts.cliTierOverride) {
|
|
300
|
+
add(opts.cliTierOverride);
|
|
301
|
+
return { primary: candidates[0], candidates };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const tierConfig = opts.strategy[opts.tier];
|
|
305
|
+
add(tierConfig?.primary);
|
|
306
|
+
for (const model of tierConfig?.failover ?? []) add(model);
|
|
307
|
+
add(opts.fallbackModel);
|
|
308
|
+
|
|
309
|
+
return { primary: candidates[0], candidates };
|
|
310
|
+
}
|