takomi 2.1.45 → 2.5.1
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/.pi/README.md +5 -5
- package/.pi/agents/architect.md +2 -2
- package/.pi/agents/designer.md +2 -2
- package/.pi/agents/worker.md +32 -0
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +15 -103
- package/.pi/extensions/takomi-runtime/command-text.ts +5 -4
- package/.pi/extensions/takomi-runtime/commands.ts +17 -17
- package/.pi/extensions/takomi-runtime/index.ts +278 -86
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
- package/.pi/extensions/takomi-runtime/profile.ts +9 -8
- package/.pi/extensions/takomi-runtime/routing-policy.ts +84 -65
- package/.pi/extensions/takomi-runtime/shared.ts +7 -30
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +1 -1
- package/.pi/extensions/takomi-subagents/agents.ts +5 -0
- package/.pi/extensions/takomi-subagents/index.ts +4 -0
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +7 -1
- package/.pi/extensions/takomi-subagents/tool-runner.ts +40 -1
- package/.pi/settings.json +39 -36
- package/.pi/takomi/model-routing.md +6 -0
- package/.pi/takomi-profile.json +54 -50
- package/package.json +2 -2
- package/src/pi-takomi-core/orchestration.ts +39 -21
- package/src/pi-takomi-core/routing.ts +8 -8
- package/src/pi-takomi-core/types.ts +25 -5
- package/src/pi-takomi-core/workflows.ts +3 -3
|
@@ -1,326 +1,262 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
??
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const normalized = normalizedAgentKey(agent);
|
|
264
|
-
const candidates = [normalized, ...(DEFAULT_ALIAS_KEYS[normalized] ?? []), "worker"].map(normalizedAgentKey);
|
|
265
|
-
return snapshot.agentDefaults.find((entry) => candidates.includes(normalizedAgentKey(entry.agent)));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export function applyTakomiRoutingDefaults<T extends { agent: string; model?: string; fallbackModels?: string[]; thinking?: string }>(
|
|
269
|
-
task: T,
|
|
270
|
-
snapshot: TakomiModelRoutingSnapshot,
|
|
271
|
-
): T {
|
|
272
|
-
const defaults = resolveAgentRoutingDefault(snapshot, task.agent);
|
|
273
|
-
if (!snapshot.approvedModels.length && !defaults) return task;
|
|
274
|
-
const approved = snapshot.approvedModels;
|
|
275
|
-
const model = normalizeModelToApproved(task.model, approved, defaults?.model);
|
|
276
|
-
const thinking = task.thinking ?? defaults?.thinking;
|
|
277
|
-
const mergedFallbacks = unique([...(task.fallbackModels ?? []), ...(defaults?.fallbackModels ?? [])]);
|
|
278
|
-
const fallbackModels = unique(mergedFallbacks
|
|
279
|
-
.map((fallback) => normalizeModelToApproved(fallback, approved))
|
|
280
|
-
.filter((fallback): fallback is string => Boolean(fallback))
|
|
281
|
-
.filter((fallback) => !approved.length || isTakomiModelApproved(fallback, approved)));
|
|
282
|
-
|
|
283
|
-
return {
|
|
284
|
-
...task,
|
|
285
|
-
...(model ? { model } : {}),
|
|
286
|
-
...(thinking ? { thinking } : {}),
|
|
287
|
-
...(fallbackModels.length ? { fallbackModels } : {}),
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export async function loadTakomiModelRoutingSnapshot(cwd: string): Promise<TakomiModelRoutingSnapshot> {
|
|
292
|
-
const settings = await readSettings(cwd);
|
|
293
|
-
const agentDefaults = collectAgentDefaultsFromSettings(settings);
|
|
294
|
-
const settingsModels = collectModelsFromDefaults(agentDefaults);
|
|
295
|
-
const resolvedPolicy = await resolveTakomiRoutingPolicy(cwd);
|
|
296
|
-
const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
|
|
297
|
-
const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
|
|
298
|
-
const approvedModels = unique([...settingsModels, ...policyModels]);
|
|
299
|
-
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function loadTakomiModelRoutingSnapshotSync(cwd: string): TakomiModelRoutingSnapshot {
|
|
303
|
-
const settings = readSettingsSync(cwd);
|
|
304
|
-
const agentDefaults = collectAgentDefaultsFromSettings(settings);
|
|
305
|
-
const settingsModels = collectModelsFromDefaults(agentDefaults);
|
|
306
|
-
const resolvedPolicy = resolvePolicySync(cwd);
|
|
307
|
-
const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
|
|
308
|
-
const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
|
|
309
|
-
const approvedModels = unique([...settingsModels, ...policyModels]);
|
|
310
|
-
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function renderCompactTakomiModelRoutingSummary(snapshot: TakomiModelRoutingSnapshot): string {
|
|
314
|
-
if (!snapshot.approvedModels.length && !snapshot.agentDefaults.length) return "";
|
|
315
|
-
const defaultLines = snapshot.agentDefaults
|
|
316
|
-
.filter((entry) => entry.model)
|
|
317
|
-
.map((entry) => `- ${entry.agent}: ${entry.model}${entry.thinking ? ` (${entry.thinking})` : ""}${entry.fallbackModels?.length ? `; fallbacks ${entry.fallbackModels.join(", ")}` : ""}`);
|
|
318
|
-
return [
|
|
319
|
-
"Active Takomi subagent routing summary:",
|
|
320
|
-
snapshot.sourceFiles.length ? `Policy source: ${snapshot.sourceFiles.join(", ")}` : "Policy source: settings/defaults",
|
|
321
|
-
snapshot.approvedModels.length ? `Approved model IDs: ${snapshot.approvedModels.join(", ")}` : "Approved model IDs: none discovered",
|
|
322
|
-
"When calling takomi_subagent, omit model to use these defaults or use only the approved provider-qualified IDs above. Do not use openai-codex/* when an oauth-router/* equivalent is approved.",
|
|
323
|
-
defaultLines.length ? "Role defaults:" : "",
|
|
324
|
-
...defaultLines,
|
|
325
|
-
].filter(Boolean).join("\n");
|
|
326
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { resolveTakomiRoutingPolicy } from "./routing-policy";
|
|
5
|
+
|
|
6
|
+
export const TAKOMI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
7
|
+
|
|
8
|
+
type RoutingDefaultInput = {
|
|
9
|
+
model?: unknown;
|
|
10
|
+
thinking?: unknown;
|
|
11
|
+
fallbackModels?: unknown;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type RoutingSettings = {
|
|
15
|
+
takomi?: {
|
|
16
|
+
modelRoutingPolicyFile?: string;
|
|
17
|
+
routing?: {
|
|
18
|
+
defaultProvider?: string;
|
|
19
|
+
approvedModels?: string[];
|
|
20
|
+
roleDefaults?: Record<string, RoutingDefaultInput>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
subagents?: { agentOverrides?: Record<string, RoutingDefaultInput> };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TakomiRoutingSource = "explicit task" | "project role default" | "global role default" | "harness default";
|
|
27
|
+
|
|
28
|
+
export type TakomiAgentModelDefault = {
|
|
29
|
+
agent: string;
|
|
30
|
+
model?: string;
|
|
31
|
+
thinking?: string;
|
|
32
|
+
fallbackModels?: string[];
|
|
33
|
+
source?: Exclude<TakomiRoutingSource, "explicit task" | "harness default">;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type TakomiModelRoutingSnapshot = {
|
|
37
|
+
approvedModels: string[];
|
|
38
|
+
preferredModels: string[];
|
|
39
|
+
sourceFiles: string[];
|
|
40
|
+
policyFile?: string;
|
|
41
|
+
policyConflicts?: string[];
|
|
42
|
+
defaultProvider?: string;
|
|
43
|
+
agentDefaults: TakomiAgentModelDefault[];
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const GLOBAL_SETTINGS = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
47
|
+
const CANONICAL_AGENTS = new Set(["architect", "designer", "coder", "worker", "reviewer", "orchestrator"]);
|
|
48
|
+
|
|
49
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
50
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unique(values: string[]): string[] {
|
|
54
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function canonicalAgent(value: string): string | undefined {
|
|
58
|
+
const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
59
|
+
const aliases: Record<string, string> = {
|
|
60
|
+
architecture: "architect",
|
|
61
|
+
design: "designer",
|
|
62
|
+
code: "coder",
|
|
63
|
+
review: "reviewer",
|
|
64
|
+
general: "worker",
|
|
65
|
+
};
|
|
66
|
+
const canonical = aliases[normalized] ?? normalized;
|
|
67
|
+
return CANONICAL_AGENTS.has(canonical) ? canonical : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readSettings(filePath: string): Promise<RoutingSettings> {
|
|
71
|
+
try { return JSON.parse(await fs.promises.readFile(filePath, "utf8")) as RoutingSettings; } catch { return {}; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readSettingsSync(filePath: string): RoutingSettings {
|
|
75
|
+
try { return JSON.parse(fs.readFileSync(filePath, "utf8")) as RoutingSettings; } catch { return {}; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cleanDefault(agent: string, input: RoutingDefaultInput, source: TakomiAgentModelDefault["source"]): TakomiAgentModelDefault {
|
|
79
|
+
return {
|
|
80
|
+
agent,
|
|
81
|
+
model: typeof input.model === "string" && input.model.trim() ? input.model.trim() : undefined,
|
|
82
|
+
thinking: typeof input.thinking === "string" && input.thinking.trim() ? input.thinking.trim() : undefined,
|
|
83
|
+
fallbackModels: Array.isArray(input.fallbackModels)
|
|
84
|
+
? unique(input.fallbackModels.filter((item): item is string => typeof item === "string"))
|
|
85
|
+
: undefined,
|
|
86
|
+
source,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractDefaults(settings: RoutingSettings, source: TakomiAgentModelDefault["source"]): Map<string, TakomiAgentModelDefault> {
|
|
91
|
+
const result = new Map<string, TakomiAgentModelDefault>();
|
|
92
|
+
const legacy = asRecord(settings.subagents?.agentOverrides);
|
|
93
|
+
for (const [name, value] of Object.entries(legacy)) {
|
|
94
|
+
const agent = canonicalAgent(name);
|
|
95
|
+
if (agent) result.set(agent, cleanDefault(agent, asRecord(value), source));
|
|
96
|
+
}
|
|
97
|
+
const structured = asRecord(settings.takomi?.routing?.roleDefaults);
|
|
98
|
+
for (const [name, value] of Object.entries(structured)) {
|
|
99
|
+
const agent = canonicalAgent(name);
|
|
100
|
+
if (!agent) continue;
|
|
101
|
+
const previous = result.get(agent);
|
|
102
|
+
const next = cleanDefault(agent, asRecord(value), source);
|
|
103
|
+
result.set(agent, {
|
|
104
|
+
agent,
|
|
105
|
+
model: next.model ?? previous?.model,
|
|
106
|
+
thinking: next.thinking ?? previous?.thinking,
|
|
107
|
+
fallbackModels: next.fallbackModels ?? previous?.fallbackModels,
|
|
108
|
+
source,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function configuredApprovedModels(settings: RoutingSettings): { present: boolean; models: string[] } {
|
|
115
|
+
const routing = settings.takomi?.routing;
|
|
116
|
+
return {
|
|
117
|
+
present: Boolean(routing && Object.prototype.hasOwnProperty.call(routing, "approvedModels")),
|
|
118
|
+
models: Array.isArray(routing?.approvedModels) ? unique(routing.approvedModels) : [],
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function modelsFromDefaults(defaults: Iterable<TakomiAgentModelDefault>): string[] {
|
|
123
|
+
const models: string[] = [];
|
|
124
|
+
for (const entry of defaults) {
|
|
125
|
+
if (entry.model) models.push(stripThinkingSuffix(entry.model).baseModel);
|
|
126
|
+
for (const fallback of entry.fallbackModels ?? []) models.push(stripThinkingSuffix(fallback).baseModel);
|
|
127
|
+
}
|
|
128
|
+
return unique(models);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function mergeTakomiRoutingSettings(globalSettings: RoutingSettings, projectSettings: RoutingSettings, sourceFiles: string[] = []): TakomiModelRoutingSnapshot {
|
|
132
|
+
const globalDefaults = extractDefaults(globalSettings, "global role default");
|
|
133
|
+
const projectDefaults = extractDefaults(projectSettings, "project role default");
|
|
134
|
+
const mergedDefaults = new Map(globalDefaults);
|
|
135
|
+
for (const [agent, projectDefault] of projectDefaults) {
|
|
136
|
+
const base = mergedDefaults.get(agent);
|
|
137
|
+
mergedDefaults.set(agent, {
|
|
138
|
+
agent,
|
|
139
|
+
model: projectDefault.model ?? base?.model,
|
|
140
|
+
thinking: projectDefault.thinking ?? base?.thinking,
|
|
141
|
+
fallbackModels: projectDefault.fallbackModels ?? base?.fallbackModels,
|
|
142
|
+
source: "project role default",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const globalApproved = configuredApprovedModels(globalSettings);
|
|
147
|
+
const projectApproved = configuredApprovedModels(projectSettings);
|
|
148
|
+
const approvedModels = projectApproved.present
|
|
149
|
+
? projectApproved.models
|
|
150
|
+
: globalApproved.present
|
|
151
|
+
? globalApproved.models
|
|
152
|
+
: modelsFromDefaults(mergedDefaults.values());
|
|
153
|
+
const defaultProvider = projectSettings.takomi?.routing?.defaultProvider
|
|
154
|
+
?? globalSettings.takomi?.routing?.defaultProvider;
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
approvedModels,
|
|
158
|
+
preferredModels: approvedModels,
|
|
159
|
+
sourceFiles,
|
|
160
|
+
defaultProvider,
|
|
161
|
+
agentDefaults: [...mergedDefaults.values()],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function stripThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
166
|
+
const colonIdx = model.lastIndexOf(":");
|
|
167
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
168
|
+
const suffix = model.slice(colonIdx + 1).toLowerCase();
|
|
169
|
+
if (!(TAKOMI_THINKING_LEVELS as readonly string[]).includes(suffix)) return { baseModel: model, thinkingSuffix: "" };
|
|
170
|
+
return { baseModel: model.slice(0, colonIdx), thinkingSuffix: `:${suffix}` };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isTakomiModelApproved(requested: string, approved: string[]): boolean {
|
|
174
|
+
const requestedBase = stripThinkingSuffix(requested).baseModel;
|
|
175
|
+
return approved.some((candidate) => stripThinkingSuffix(candidate).baseModel === requestedBase);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resolveAgentRoutingDefault(snapshot: TakomiModelRoutingSnapshot, agent: string): TakomiAgentModelDefault | undefined {
|
|
179
|
+
const canonical = canonicalAgent(agent);
|
|
180
|
+
return canonical ? snapshot.agentDefaults.find((entry) => entry.agent === canonical) : undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function qualifyDefaultModel(model: string | undefined, defaultProvider: string | undefined): string | undefined {
|
|
184
|
+
if (!model || model.includes("/") || !defaultProvider) return model;
|
|
185
|
+
return `${defaultProvider}/${model}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function applyTakomiRoutingDefaults<T extends { agent: string; model?: string; fallbackModels?: string[]; thinking?: string }>(
|
|
189
|
+
task: T,
|
|
190
|
+
snapshot: TakomiModelRoutingSnapshot,
|
|
191
|
+
): T {
|
|
192
|
+
const defaults = resolveAgentRoutingDefault(snapshot, task.agent);
|
|
193
|
+
const explicitModel = Boolean(task.model);
|
|
194
|
+
const model = task.model ?? qualifyDefaultModel(defaults?.model, snapshot.defaultProvider);
|
|
195
|
+
const fallbackSource = task.fallbackModels !== undefined
|
|
196
|
+
? task.fallbackModels
|
|
197
|
+
: explicitModel
|
|
198
|
+
? []
|
|
199
|
+
: defaults?.fallbackModels ?? [];
|
|
200
|
+
const fallbackModels = unique(fallbackSource.map((item) => qualifyDefaultModel(item, snapshot.defaultProvider) ?? item));
|
|
201
|
+
return {
|
|
202
|
+
...task,
|
|
203
|
+
...(model ? { model } : {}),
|
|
204
|
+
...(task.thinking ?? defaults?.thinking ? { thinking: task.thinking ?? defaults?.thinking } : {}),
|
|
205
|
+
...(fallbackModels.length ? { fallbackModels } : { fallbackModels: undefined }),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function detectPolicyConflicts(text: string | undefined, snapshot: TakomiModelRoutingSnapshot): string[] {
|
|
210
|
+
if (!text) return [];
|
|
211
|
+
const conflicts: string[] = [];
|
|
212
|
+
const prohibited = [...text.matchAll(/(?:do not|never)\s+use\s+([a-z0-9-]+)/gi)].map((match) => match[1].toLowerCase());
|
|
213
|
+
for (const provider of prohibited) {
|
|
214
|
+
const configured = snapshot.approvedModels.filter((model) => stripThinkingSuffix(model).baseModel.toLowerCase().startsWith(`${provider}/`));
|
|
215
|
+
if (configured.length) conflicts.push(`Policy prohibits provider '${provider}', but executable settings approve: ${configured.join(", ")}`);
|
|
216
|
+
}
|
|
217
|
+
const preferred = text.match(/(?:preferred|default)\s+(?:provider|router)\s*:\s*([a-z0-9-]+)/i)?.[1];
|
|
218
|
+
if (preferred && snapshot.defaultProvider && preferred.toLowerCase() !== snapshot.defaultProvider.toLowerCase()) {
|
|
219
|
+
conflicts.push(`Policy names default provider '${preferred}', but executable settings use '${snapshot.defaultProvider}'.`);
|
|
220
|
+
}
|
|
221
|
+
return conflicts;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export async function loadTakomiModelRoutingSnapshot(cwd: string): Promise<TakomiModelRoutingSnapshot> {
|
|
225
|
+
const projectPath = path.resolve(cwd, ".pi", "settings.json");
|
|
226
|
+
const [globalSettings, projectSettings, policy] = await Promise.all([
|
|
227
|
+
readSettings(GLOBAL_SETTINGS),
|
|
228
|
+
readSettings(projectPath),
|
|
229
|
+
resolveTakomiRoutingPolicy(cwd),
|
|
230
|
+
]);
|
|
231
|
+
const files = [GLOBAL_SETTINGS, projectPath].filter((file) => fs.existsSync(file));
|
|
232
|
+
const snapshot = mergeTakomiRoutingSettings(globalSettings, projectSettings, files);
|
|
233
|
+
snapshot.policyFile = policy.policyPath;
|
|
234
|
+
snapshot.policyConflicts = detectPolicyConflicts(policy.text, snapshot);
|
|
235
|
+
return snapshot;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function loadTakomiModelRoutingSnapshotSync(cwd: string): TakomiModelRoutingSnapshot {
|
|
239
|
+
const projectPath = path.resolve(cwd, ".pi", "settings.json");
|
|
240
|
+
return mergeTakomiRoutingSettings(
|
|
241
|
+
readSettingsSync(GLOBAL_SETTINGS),
|
|
242
|
+
readSettingsSync(projectPath),
|
|
243
|
+
[GLOBAL_SETTINGS, projectPath].filter((file) => fs.existsSync(file)),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function renderCompactTakomiModelRoutingSummary(snapshot: TakomiModelRoutingSnapshot): string {
|
|
248
|
+
if (!snapshot.approvedModels.length && !snapshot.agentDefaults.length) return "";
|
|
249
|
+
const defaultLines = snapshot.agentDefaults.map((entry) =>
|
|
250
|
+
`- ${entry.agent}: ${entry.model ?? "Pi default"}${entry.thinking ? ` (${entry.thinking})` : ""}${entry.fallbackModels?.length ? `; fallbacks ${entry.fallbackModels.join(", ")}` : ""} [${entry.source ?? "harness default"}]`
|
|
251
|
+
);
|
|
252
|
+
return [
|
|
253
|
+
"Active Takomi subagent routing summary:",
|
|
254
|
+
`Executable settings: ${snapshot.sourceFiles.join(", ") || "harness defaults"}`,
|
|
255
|
+
snapshot.policyFile ? `Advisory routing guidance: ${snapshot.policyFile}` : "",
|
|
256
|
+
snapshot.approvedModels.length ? `Approved exact model IDs: ${snapshot.approvedModels.join(", ")}` : "Approved exact model IDs: none configured",
|
|
257
|
+
"Provider-qualified model IDs are atomic. Takomi never substitutes a different provider by model-family matching.",
|
|
258
|
+
"Resolution: explicit task model → project role default → global role default → harness/Pi default.",
|
|
259
|
+
...(snapshot.policyConflicts?.length ? ["ROUTING CONFIGURATION ERROR:", ...snapshot.policyConflicts.map((item) => `- ${item}`)] : []),
|
|
260
|
+
...defaultLines,
|
|
261
|
+
].filter(Boolean).join("\n");
|
|
262
|
+
}
|