takomi 2.1.26 → 2.1.27
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/agents/architect.md +0 -1
- package/.pi/agents/coder.md +0 -1
- package/.pi/agents/designer.md +0 -1
- package/.pi/agents/orchestrator.md +0 -1
- package/.pi/agents/reviewer.md +0 -1
- package/.pi/extensions/takomi-context-manager/index.ts +6 -3
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +28 -13
- package/.pi/extensions/takomi-runtime/commands.ts +24 -7
- package/.pi/extensions/takomi-runtime/index.ts +63 -5
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +296 -0
- package/.pi/extensions/takomi-runtime/routing-policy.ts +67 -17
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +12 -2
- package/.pi/extensions/takomi-subagents/tool-runner.ts +4 -2
- package/.pi/settings.json +18 -20
- package/package.json +1 -1
package/.pi/agents/architect.md
CHANGED
package/.pi/agents/coder.md
CHANGED
package/.pi/agents/designer.md
CHANGED
package/.pi/agents/reviewer.md
CHANGED
|
@@ -11,6 +11,7 @@ import { registerDiagnostics } from "./diagnostics-tools";
|
|
|
11
11
|
import { installPrerequisiteGates } from "./prerequisite-gates";
|
|
12
12
|
import { installModelPolicyGate } from "./model-policy-gate";
|
|
13
13
|
import { detectDuplicateTakomiExtensions } from "./extension-conflicts";
|
|
14
|
+
import { loadTakomiModelRoutingSnapshot, renderCompactTakomiModelRoutingSummary } from "../takomi-runtime/model-routing-defaults";
|
|
14
15
|
import type { ContextManagerConfig } from "./types";
|
|
15
16
|
|
|
16
17
|
export default function takomiContextManager(pi: ExtensionAPI) {
|
|
@@ -33,6 +34,8 @@ export default function takomiContextManager(pi: ExtensionAPI) {
|
|
|
33
34
|
|
|
34
35
|
const candidates = findCandidates(event.prompt, state.skills, config);
|
|
35
36
|
const rewrite = rewritePrompt(event.systemPrompt, state.skills, candidates, config);
|
|
37
|
+
const routingSummary = renderCompactTakomiModelRoutingSummary(await loadTakomiModelRoutingSnapshot(ctx.cwd));
|
|
38
|
+
const rewrittenPrompt = routingSummary ? `${rewrite.prompt}\n\n${routingSummary}` : rewrite.prompt;
|
|
36
39
|
state.report = {
|
|
37
40
|
...state.report,
|
|
38
41
|
timestamp: new Date().toISOString(),
|
|
@@ -43,14 +46,14 @@ export default function takomiContextManager(pi: ExtensionAPI) {
|
|
|
43
46
|
duplicateExtensionWarnings,
|
|
44
47
|
promptRewrite: {
|
|
45
48
|
attempted: true,
|
|
46
|
-
changed: rewrite.changed,
|
|
49
|
+
changed: rewrite.changed || Boolean(routingSummary),
|
|
47
50
|
originalLength: event.systemPrompt.length,
|
|
48
|
-
rewrittenLength:
|
|
51
|
+
rewrittenLength: rewrittenPrompt.length,
|
|
49
52
|
removedSections: rewrite.removedSections,
|
|
50
53
|
warnings: rewrite.warnings,
|
|
51
54
|
},
|
|
52
55
|
};
|
|
53
56
|
|
|
54
|
-
return { systemPrompt:
|
|
57
|
+
return { systemPrompt: rewrittenPrompt };
|
|
55
58
|
});
|
|
56
59
|
}
|
|
@@ -5,6 +5,7 @@ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
|
5
5
|
import type { ContextManagerState } from "./state";
|
|
6
6
|
import { recordBlocked } from "./state";
|
|
7
7
|
import { resolveTakomiRoutingPolicy } from "../takomi-runtime/routing-policy";
|
|
8
|
+
import { approvedModelEquivalent, isTakomiModelApproved } from "../takomi-runtime/model-routing-defaults";
|
|
8
9
|
|
|
9
10
|
type Settings = {
|
|
10
11
|
takomi?: { modelRoutingPolicyFile?: string };
|
|
@@ -79,12 +80,21 @@ function isModelLike(value: string): boolean {
|
|
|
79
80
|
|| lower.includes("lmstudio/");
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function extractPreferredProvider(text: string): string | undefined {
|
|
84
|
+
const match = text.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
|
|
85
|
+
?? text.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
|
|
86
|
+
return match?.[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
function collectModelsFromPolicy(text: string): string[] {
|
|
90
|
+
// Providerless names such as "GPT-5.5" are intent labels unless the policy
|
|
91
|
+
// declares a preferred provider/router header.
|
|
83
92
|
const explicit = (text.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? []).filter(isModelLike);
|
|
93
|
+
const preferredProvider = extractPreferredProvider(text);
|
|
84
94
|
const inferred: string[] = [];
|
|
85
|
-
if (/gpt[- ]?5\.5/i.test(text)) inferred.push(
|
|
86
|
-
if (/gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push(
|
|
87
|
-
if (/gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push(
|
|
95
|
+
if (preferredProvider && /gpt[- ]?5\.5/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.5`);
|
|
96
|
+
if (preferredProvider && /gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4`);
|
|
97
|
+
if (preferredProvider && /gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4-mini`);
|
|
88
98
|
return unique([...explicit, ...inferred]);
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -98,8 +108,8 @@ async function loadSnapshot(cwd: string): Promise<ModelPolicySnapshot> {
|
|
|
98
108
|
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles };
|
|
99
109
|
}
|
|
100
110
|
|
|
101
|
-
function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<string, unknown>; key: string; value: string }> {
|
|
102
|
-
const refs: Array<{ holder: Record<string, unknown>; key: string; value: string }> = [];
|
|
111
|
+
function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<string, unknown>; key: string; value: string; index?: number }> {
|
|
112
|
+
const refs: Array<{ holder: Record<string, unknown>; key: string; value: string; index?: number }> = [];
|
|
103
113
|
function visit(value: unknown): void {
|
|
104
114
|
if (!value || typeof value !== "object") return;
|
|
105
115
|
if (Array.isArray(value)) {
|
|
@@ -111,7 +121,9 @@ function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<strin
|
|
|
111
121
|
if (typeof record[key] === "string") refs.push({ holder: record, key, value: record[key] });
|
|
112
122
|
}
|
|
113
123
|
if (Array.isArray(record.fallbackModels)) {
|
|
114
|
-
|
|
124
|
+
const fallbackModels = record.fallbackModels.filter((item): item is string => typeof item === "string");
|
|
125
|
+
record.fallbackModels = fallbackModels;
|
|
126
|
+
fallbackModels.forEach((value: string, index: number) => refs.push({ holder: record, key: "fallbackModels", value, index }));
|
|
115
127
|
}
|
|
116
128
|
for (const key of ["tasks", "chain"]) visit(record[key]);
|
|
117
129
|
}
|
|
@@ -119,9 +131,12 @@ function collectRequestedModelRefs(input: unknown): Array<{ holder: Record<strin
|
|
|
119
131
|
return refs;
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
134
|
+
function setModelRef(ref: { holder: Record<string, unknown>; key: string; value: string; index?: number }, value: string): void {
|
|
135
|
+
if (ref.key === "fallbackModels" && typeof ref.index === "number" && Array.isArray(ref.holder.fallbackModels)) {
|
|
136
|
+
ref.holder.fallbackModels[ref.index] = value;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
ref.holder[ref.key] = value;
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
function isModelFailure(text: string): boolean {
|
|
@@ -170,16 +185,16 @@ export function installModelPolicyGate(pi: ExtensionAPI, state: ContextManagerSt
|
|
|
170
185
|
const refs = collectRequestedModelRefs(event.input);
|
|
171
186
|
const corrections: string[] = [];
|
|
172
187
|
for (const ref of refs) {
|
|
173
|
-
if (
|
|
174
|
-
const equivalent =
|
|
188
|
+
if (isTakomiModelApproved(ref.value, approved)) continue;
|
|
189
|
+
const equivalent = approvedModelEquivalent(ref.value, approved);
|
|
175
190
|
if (equivalent) {
|
|
176
|
-
ref
|
|
191
|
+
setModelRef(ref, equivalent);
|
|
177
192
|
corrections.push(`${ref.value} -> ${equivalent}`);
|
|
178
193
|
continue;
|
|
179
194
|
}
|
|
180
195
|
const recovery = await askForInvalidModelRecovery(ctx, ref.value, approved);
|
|
181
196
|
if (recovery.action === "retry") {
|
|
182
|
-
ref
|
|
197
|
+
setModelRef(ref, recovery.model);
|
|
183
198
|
corrections.push(`${ref.value} -> ${recovery.model} (user selected recovery)`);
|
|
184
199
|
continue;
|
|
185
200
|
}
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "../../../src/pi-takomi-core";
|
|
8
8
|
import { commandHelp, completions, statusText, workflowPrompt } from "./command-text";
|
|
9
9
|
import type { TakomiSubagentController } from "./subagent-types";
|
|
10
|
-
import {
|
|
10
|
+
import { previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy, type RoutingPolicyInstallScope } from "./routing-policy";
|
|
11
11
|
import { collectTakomiStats, renderTakomiStats } from "./takomi-stats.js";
|
|
12
12
|
|
|
13
13
|
export type TakomiRuntimeCommandState = {
|
|
@@ -158,12 +158,29 @@ export function registerTakomiCommands(pi: ExtensionAPI, options: RegisterTakomi
|
|
|
158
158
|
const policyText = scopeMatch?.[2] ?? trimmed.replace(/^set\s+/i, "");
|
|
159
159
|
|
|
160
160
|
try {
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
:
|
|
166
|
-
|
|
161
|
+
const preview = previewTakomiRoutingPolicy(ctx.cwd, policyText, { scope });
|
|
162
|
+
const reviewPrompt = [
|
|
163
|
+
"Review this Takomi routing policy extraction before it is saved.",
|
|
164
|
+
"",
|
|
165
|
+
"Rules:",
|
|
166
|
+
"- Do not invent providers or model IDs not grounded in the policy.",
|
|
167
|
+
"- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
|
|
168
|
+
"- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
|
|
169
|
+
"- If the extraction is correct and safe, call takomi_apply_routing_policy with the exact policyText and scope below.",
|
|
170
|
+
"- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
|
|
171
|
+
"",
|
|
172
|
+
"Deterministic extraction:",
|
|
173
|
+
renderRoutingPolicyPreview(preview),
|
|
174
|
+
"",
|
|
175
|
+
"Original policy text:",
|
|
176
|
+
"```",
|
|
177
|
+
preview.policy,
|
|
178
|
+
"```",
|
|
179
|
+
"",
|
|
180
|
+
`Tool call to apply if safe: takomi_apply_routing_policy({ scope: ${JSON.stringify(scope)}, policyText: <original policy text> })`,
|
|
181
|
+
].join("\n");
|
|
182
|
+
ctx.ui.notify("Takomi routing extraction prepared. Sending it to the active model for review before saving.", "info");
|
|
183
|
+
pi.sendUserMessage(reviewPrompt);
|
|
167
184
|
} catch (error) {
|
|
168
185
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
169
186
|
}
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
getProfileDefaults,
|
|
57
57
|
loadTakomiProfile,
|
|
58
58
|
} from "./profile";
|
|
59
|
-
import { installTakomiRoutingPolicy, resolveTakomiRoutingPolicy } from "./routing-policy";
|
|
59
|
+
import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
|
|
60
60
|
|
|
61
61
|
type TakomiState = {
|
|
62
62
|
enabled: boolean;
|
|
@@ -799,6 +799,47 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
799
799
|
},
|
|
800
800
|
});
|
|
801
801
|
|
|
802
|
+
pi.registerTool({
|
|
803
|
+
name: "takomi_apply_routing_policy",
|
|
804
|
+
label: "Takomi Routing",
|
|
805
|
+
description: "Apply a Takomi model-routing policy after deterministic extraction and active-model review.",
|
|
806
|
+
promptSnippet: "Save reviewed Takomi model routing policy text to the active global or project policy file.",
|
|
807
|
+
promptGuidelines: [
|
|
808
|
+
"Use takomi_apply_routing_policy only after reviewing the deterministic extraction against the original routing policy text.",
|
|
809
|
+
"Do not call takomi_apply_routing_policy if the policy is ambiguous, invents providers, or maps to non-Takomi roles.",
|
|
810
|
+
],
|
|
811
|
+
parameters: Type.Object({
|
|
812
|
+
policyText: Type.String({ description: "Original routing policy text to save" }),
|
|
813
|
+
scope: Type.Optional(StringEnum(["global", "project"] as const)),
|
|
814
|
+
reviewNotes: Type.Optional(Type.String({ description: "Brief notes from the active-model review" })),
|
|
815
|
+
}),
|
|
816
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
817
|
+
const scope = params.scope ?? "global";
|
|
818
|
+
const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
|
|
819
|
+
const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
|
|
820
|
+
const scopeNote = scope === "global"
|
|
821
|
+
? "This global policy applies unless a project-local override exists."
|
|
822
|
+
: "This project-local policy overrides the global policy for the current project.";
|
|
823
|
+
return {
|
|
824
|
+
content: [{
|
|
825
|
+
type: "text",
|
|
826
|
+
text: [
|
|
827
|
+
`Takomi routing policy saved (${scope}).`,
|
|
828
|
+
"",
|
|
829
|
+
`Policy: ${result.policyPath}`,
|
|
830
|
+
`Settings: ${result.settingsPath}`,
|
|
831
|
+
"",
|
|
832
|
+
renderRoutingPolicyPreview(preview),
|
|
833
|
+
params.reviewNotes ? `\nReview notes:\n${params.reviewNotes}` : "",
|
|
834
|
+
"",
|
|
835
|
+
scopeNote,
|
|
836
|
+
].filter(Boolean).join("\n"),
|
|
837
|
+
}],
|
|
838
|
+
details: { result, preview, reviewNotes: params.reviewNotes },
|
|
839
|
+
};
|
|
840
|
+
},
|
|
841
|
+
});
|
|
842
|
+
|
|
802
843
|
pi.registerTool({
|
|
803
844
|
name: "takomi_workflow",
|
|
804
845
|
label: "Takomi Workflow",
|
|
@@ -1067,11 +1108,28 @@ ${stateJson}`
|
|
|
1067
1108
|
if (routingUpdateMatch) {
|
|
1068
1109
|
state.enabled = true;
|
|
1069
1110
|
try {
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1072
|
-
return { action: "transform", text:
|
|
1111
|
+
const cwd = runtimeCtx?.cwd ?? process.cwd();
|
|
1112
|
+
const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global" });
|
|
1113
|
+
return { action: "transform", text: [
|
|
1114
|
+
"Review this Takomi routing policy extraction before it is saved.",
|
|
1115
|
+
"",
|
|
1116
|
+
"Rules:",
|
|
1117
|
+
"- Do not invent providers or model IDs not grounded in the policy.",
|
|
1118
|
+
"- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
|
|
1119
|
+
"- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
|
|
1120
|
+
"- If the extraction is correct and safe, call takomi_apply_routing_policy with scope=global and the exact original policy text.",
|
|
1121
|
+
"- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
|
|
1122
|
+
"",
|
|
1123
|
+
"Deterministic extraction:",
|
|
1124
|
+
renderRoutingPolicyPreview(preview),
|
|
1125
|
+
"",
|
|
1126
|
+
"Original policy text:",
|
|
1127
|
+
"```",
|
|
1128
|
+
preview.policy,
|
|
1129
|
+
"```",
|
|
1130
|
+
].join("\n") };
|
|
1073
1131
|
} catch (error) {
|
|
1074
|
-
return { action: "transform", text: `Takomi routing policy
|
|
1132
|
+
return { action: "transform", text: `Takomi routing policy review failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
1075
1133
|
}
|
|
1076
1134
|
}
|
|
1077
1135
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
BUNDLED_TAKOMI_ROUTING_POLICY_PATH,
|
|
6
|
+
GLOBAL_PI_SETTINGS_PATH,
|
|
7
|
+
GLOBAL_TAKOMI_ROUTING_POLICY_PATH,
|
|
8
|
+
PROJECT_PI_SETTINGS_RELATIVE,
|
|
9
|
+
TAKOMI_ROUTING_POLICY_RELATIVE,
|
|
10
|
+
resolveTakomiRoutingPolicy,
|
|
11
|
+
} from "./routing-policy";
|
|
12
|
+
|
|
13
|
+
export const TAKOMI_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
|
|
14
|
+
|
|
15
|
+
type Settings = {
|
|
16
|
+
takomi?: { modelRoutingPolicyFile?: string };
|
|
17
|
+
subagents?: { agentOverrides?: Record<string, unknown> };
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type TakomiAgentModelDefault = {
|
|
21
|
+
agent: string;
|
|
22
|
+
model?: string;
|
|
23
|
+
thinking?: string;
|
|
24
|
+
fallbackModels?: string[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type TakomiModelRoutingSnapshot = {
|
|
28
|
+
approvedModels: string[];
|
|
29
|
+
preferredModels: string[];
|
|
30
|
+
sourceFiles: string[];
|
|
31
|
+
agentDefaults: TakomiAgentModelDefault[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
35
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function unique(values: string[]): string[] {
|
|
39
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readSettingsFile(filePath: string): Promise<Settings> {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(await fs.promises.readFile(filePath, "utf8")) as Settings;
|
|
45
|
+
} catch {
|
|
46
|
+
return {};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readSettingsFileSync(filePath: string): Settings {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8")) as Settings;
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mergeSettings(globalSettings: Settings, projectSettings: Settings): Settings {
|
|
59
|
+
const globalOverrides = asRecord(globalSettings.subagents?.agentOverrides);
|
|
60
|
+
const projectOverrides = asRecord(projectSettings.subagents?.agentOverrides);
|
|
61
|
+
return {
|
|
62
|
+
...globalSettings,
|
|
63
|
+
...projectSettings,
|
|
64
|
+
takomi: { ...(globalSettings.takomi ?? {}), ...(projectSettings.takomi ?? {}) },
|
|
65
|
+
subagents: {
|
|
66
|
+
...(globalSettings.subagents ?? {}),
|
|
67
|
+
...(projectSettings.subagents ?? {}),
|
|
68
|
+
agentOverrides: { ...globalOverrides, ...projectOverrides },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function readSettings(cwd: string): Promise<Settings> {
|
|
74
|
+
const globalSettings = await readSettingsFile(path.join(os.homedir(), ".pi", "agent", "settings.json"));
|
|
75
|
+
const projectSettings = await readSettingsFile(path.resolve(cwd, ".pi/settings.json"));
|
|
76
|
+
return mergeSettings(globalSettings, projectSettings);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readSettingsSync(cwd: string): Settings {
|
|
80
|
+
const globalSettings = readSettingsFileSync(path.join(os.homedir(), ".pi", "agent", "settings.json"));
|
|
81
|
+
const projectSettings = readSettingsFileSync(path.resolve(cwd, ".pi/settings.json"));
|
|
82
|
+
return mergeSettings(globalSettings, projectSettings);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function collectAgentDefaultsFromSettings(settings: Settings): TakomiAgentModelDefault[] {
|
|
86
|
+
const overrides = asRecord(settings.subagents?.agentOverrides);
|
|
87
|
+
const defaults: TakomiAgentModelDefault[] = [];
|
|
88
|
+
for (const [agent, value] of Object.entries(overrides)) {
|
|
89
|
+
const record = asRecord(value);
|
|
90
|
+
const fallbackModels = Array.isArray(record.fallbackModels)
|
|
91
|
+
? record.fallbackModels.filter((item): item is string => typeof item === "string" && item.trim().length > 0)
|
|
92
|
+
: undefined;
|
|
93
|
+
defaults.push({
|
|
94
|
+
agent,
|
|
95
|
+
model: typeof record.model === "string" ? record.model : undefined,
|
|
96
|
+
thinking: typeof record.thinking === "string" ? record.thinking : undefined,
|
|
97
|
+
fallbackModels,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return defaults;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function collectModelsFromDefaults(defaults: TakomiAgentModelDefault[]): string[] {
|
|
104
|
+
const models: string[] = [];
|
|
105
|
+
for (const record of defaults) {
|
|
106
|
+
if (record.model) models.push(stripThinkingSuffix(record.model).baseModel);
|
|
107
|
+
if (Array.isArray(record.fallbackModels)) {
|
|
108
|
+
for (const fallback of record.fallbackModels) models.push(stripThinkingSuffix(fallback).baseModel);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return models;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isModelLike(value: string): boolean {
|
|
115
|
+
const lower = value.toLowerCase();
|
|
116
|
+
return /(^|\/)(gpt|claude|gemini|o[0-9]|qwen|deepseek|llama|mistral|kimi|grok|sonnet|haiku|opus|codex|mini|max)/i.test(lower)
|
|
117
|
+
|| lower.includes("oauth-router/")
|
|
118
|
+
|| lower.includes("openai-codex/")
|
|
119
|
+
|| lower.includes("lmstudio/");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function extractPreferredProvider(text: string): string | undefined {
|
|
123
|
+
const match = text.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
|
|
124
|
+
?? text.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
|
|
125
|
+
return match?.[1];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function collectModelsFromPolicy(text: string): string[] {
|
|
129
|
+
// Provider-agnostic policy text like "GPT-5.5" expresses routing intent, not
|
|
130
|
+
// a concrete provider. A preferred provider/router header intentionally binds
|
|
131
|
+
// those intent labels to executable provider-qualified IDs.
|
|
132
|
+
const explicit = (text.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? []).filter(isModelLike);
|
|
133
|
+
const preferredProvider = extractPreferredProvider(text);
|
|
134
|
+
const inferred: string[] = [];
|
|
135
|
+
if (preferredProvider && /gpt[- ]?5\.5/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.5`);
|
|
136
|
+
if (preferredProvider && /gpt[- ]?5\.4(?!\s*mini)/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4`);
|
|
137
|
+
if (preferredProvider && /gpt[- ]?5\.4\s*mini/i.test(text)) inferred.push(`${preferredProvider}/gpt-5.4-mini`);
|
|
138
|
+
return unique([...explicit, ...inferred].map((model) => stripThinkingSuffix(model).baseModel));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readPolicyTextSync(filePath: string): string | undefined {
|
|
142
|
+
try {
|
|
143
|
+
const text = fs.readFileSync(filePath, "utf8").trim();
|
|
144
|
+
return text || undefined;
|
|
145
|
+
} catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolvePolicySync(cwd: string): { policyPath?: string; text?: string } {
|
|
151
|
+
const projectSettings = readSettingsFileSync(path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE));
|
|
152
|
+
const projectTakomi = asRecord(projectSettings.takomi);
|
|
153
|
+
const configuredProject = typeof projectTakomi.modelRoutingPolicyFile === "string"
|
|
154
|
+
? projectTakomi.modelRoutingPolicyFile
|
|
155
|
+
: TAKOMI_ROUTING_POLICY_RELATIVE;
|
|
156
|
+
const configuredProjectPath = path.isAbsolute(configuredProject) ? configuredProject : path.join(cwd, configuredProject);
|
|
157
|
+
const configuredProjectText = readPolicyTextSync(configuredProjectPath);
|
|
158
|
+
if (configuredProjectText) return { policyPath: configuredProjectPath, text: configuredProjectText };
|
|
159
|
+
|
|
160
|
+
const defaultProjectPath = path.join(cwd, TAKOMI_ROUTING_POLICY_RELATIVE);
|
|
161
|
+
if (path.resolve(defaultProjectPath) !== path.resolve(configuredProjectPath)) {
|
|
162
|
+
const defaultProjectText = readPolicyTextSync(defaultProjectPath);
|
|
163
|
+
if (defaultProjectText) return { policyPath: defaultProjectPath, text: defaultProjectText };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const globalSettings = readSettingsFileSync(GLOBAL_PI_SETTINGS_PATH);
|
|
167
|
+
const globalTakomi = asRecord(globalSettings.takomi);
|
|
168
|
+
const configuredGlobal = typeof globalTakomi.modelRoutingPolicyFile === "string"
|
|
169
|
+
? globalTakomi.modelRoutingPolicyFile
|
|
170
|
+
: GLOBAL_TAKOMI_ROUTING_POLICY_PATH;
|
|
171
|
+
const configuredGlobalPath = path.isAbsolute(configuredGlobal) ? configuredGlobal : path.join(os.homedir(), configuredGlobal);
|
|
172
|
+
const configuredGlobalText = readPolicyTextSync(configuredGlobalPath);
|
|
173
|
+
if (configuredGlobalText) return { policyPath: configuredGlobalPath, text: configuredGlobalText };
|
|
174
|
+
|
|
175
|
+
if (path.resolve(configuredGlobalPath) !== path.resolve(GLOBAL_TAKOMI_ROUTING_POLICY_PATH)) {
|
|
176
|
+
const globalText = readPolicyTextSync(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
|
|
177
|
+
if (globalText) return { policyPath: GLOBAL_TAKOMI_ROUTING_POLICY_PATH, text: globalText };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const bundledText = readPolicyTextSync(BUNDLED_TAKOMI_ROUTING_POLICY_PATH);
|
|
181
|
+
return bundledText ? { policyPath: BUNDLED_TAKOMI_ROUTING_POLICY_PATH, text: bundledText } : {};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function stripThinkingSuffix(model: string): { baseModel: string; thinkingSuffix: string } {
|
|
185
|
+
const colonIdx = model.lastIndexOf(":");
|
|
186
|
+
if (colonIdx === -1) return { baseModel: model, thinkingSuffix: "" };
|
|
187
|
+
const suffix = model.slice(colonIdx + 1).toLowerCase();
|
|
188
|
+
if (!(TAKOMI_THINKING_LEVELS as readonly string[]).includes(suffix)) return { baseModel: model, thinkingSuffix: "" };
|
|
189
|
+
return { baseModel: model.slice(0, colonIdx), thinkingSuffix: `:${suffix}` };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function modelFamily(model: string): string {
|
|
193
|
+
const baseModel = stripThinkingSuffix(model).baseModel;
|
|
194
|
+
return baseModel.split("/").at(-1)?.toLowerCase() ?? baseModel.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function isTakomiModelApproved(requested: string, approved: string[]): boolean {
|
|
198
|
+
const requestedBase = stripThinkingSuffix(requested).baseModel;
|
|
199
|
+
return approved.some((candidate) => stripThinkingSuffix(candidate).baseModel === requestedBase);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function approvedModelEquivalent(requested: string, approved: string[]): string | undefined {
|
|
203
|
+
const { thinkingSuffix } = stripThinkingSuffix(requested);
|
|
204
|
+
const requestedFamily = modelFamily(requested);
|
|
205
|
+
const equivalent = approved.find((candidate) => modelFamily(candidate) === requestedFamily);
|
|
206
|
+
return equivalent ? `${stripThinkingSuffix(equivalent).baseModel}${thinkingSuffix}` : undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function normalizeModelToApproved(requested: string | undefined, approved: string[], fallback?: string): string | undefined {
|
|
210
|
+
if (!requested) return fallback;
|
|
211
|
+
if (isTakomiModelApproved(requested, approved)) return requested;
|
|
212
|
+
return approvedModelEquivalent(requested, approved) ?? fallback ?? requested;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizedAgentKey(agent: string): string {
|
|
216
|
+
return agent.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const DEFAULT_ALIAS_KEYS: Record<string, string[]> = {
|
|
220
|
+
architect: ["architect", "planner", "oracle", "worker"],
|
|
221
|
+
orchestrator: ["orchestrator", "planner", "oracle", "worker"],
|
|
222
|
+
planner: ["planner", "oracle", "worker"],
|
|
223
|
+
coder: ["coder", "code", "worker", "delegate"],
|
|
224
|
+
code: ["code", "coder", "worker", "delegate"],
|
|
225
|
+
designer: ["designer", "design", "worker", "delegate"],
|
|
226
|
+
design: ["design", "designer", "worker", "delegate"],
|
|
227
|
+
reviewer: ["reviewer", "review", "oracle"],
|
|
228
|
+
review: ["review", "reviewer", "oracle"],
|
|
229
|
+
general: ["general", "worker"],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export function resolveAgentRoutingDefault(snapshot: TakomiModelRoutingSnapshot, agent: string): TakomiAgentModelDefault | undefined {
|
|
233
|
+
const normalized = normalizedAgentKey(agent);
|
|
234
|
+
const candidates = [normalized, ...(DEFAULT_ALIAS_KEYS[normalized] ?? []), "worker"].map(normalizedAgentKey);
|
|
235
|
+
return snapshot.agentDefaults.find((entry) => candidates.includes(normalizedAgentKey(entry.agent)));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function applyTakomiRoutingDefaults<T extends { agent: string; model?: string; fallbackModels?: string[]; thinking?: string }>(
|
|
239
|
+
task: T,
|
|
240
|
+
snapshot: TakomiModelRoutingSnapshot,
|
|
241
|
+
): T {
|
|
242
|
+
const defaults = resolveAgentRoutingDefault(snapshot, task.agent);
|
|
243
|
+
if (!snapshot.approvedModels.length && !defaults) return task;
|
|
244
|
+
const approved = snapshot.approvedModels;
|
|
245
|
+
const model = normalizeModelToApproved(task.model, approved, defaults?.model);
|
|
246
|
+
const thinking = task.thinking ?? defaults?.thinking;
|
|
247
|
+
const mergedFallbacks = unique([...(task.fallbackModels ?? []), ...(defaults?.fallbackModels ?? [])]);
|
|
248
|
+
const fallbackModels = unique(mergedFallbacks
|
|
249
|
+
.map((fallback) => normalizeModelToApproved(fallback, approved))
|
|
250
|
+
.filter((fallback): fallback is string => Boolean(fallback))
|
|
251
|
+
.filter((fallback) => !approved.length || isTakomiModelApproved(fallback, approved)));
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
...task,
|
|
255
|
+
...(model ? { model } : {}),
|
|
256
|
+
...(thinking ? { thinking } : {}),
|
|
257
|
+
...(fallbackModels.length ? { fallbackModels } : {}),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function loadTakomiModelRoutingSnapshot(cwd: string): Promise<TakomiModelRoutingSnapshot> {
|
|
262
|
+
const settings = await readSettings(cwd);
|
|
263
|
+
const agentDefaults = collectAgentDefaultsFromSettings(settings);
|
|
264
|
+
const settingsModels = collectModelsFromDefaults(agentDefaults);
|
|
265
|
+
const resolvedPolicy = await resolveTakomiRoutingPolicy(cwd);
|
|
266
|
+
const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
|
|
267
|
+
const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
|
|
268
|
+
const approvedModels = unique([...settingsModels, ...policyModels]);
|
|
269
|
+
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function loadTakomiModelRoutingSnapshotSync(cwd: string): TakomiModelRoutingSnapshot {
|
|
273
|
+
const settings = readSettingsSync(cwd);
|
|
274
|
+
const agentDefaults = collectAgentDefaultsFromSettings(settings);
|
|
275
|
+
const settingsModels = collectModelsFromDefaults(agentDefaults);
|
|
276
|
+
const resolvedPolicy = resolvePolicySync(cwd);
|
|
277
|
+
const sourceFiles = resolvedPolicy.policyPath ? [resolvedPolicy.policyPath] : [];
|
|
278
|
+
const policyModels = resolvedPolicy.text ? collectModelsFromPolicy(resolvedPolicy.text) : [];
|
|
279
|
+
const approvedModels = unique([...settingsModels, ...policyModels]);
|
|
280
|
+
return { approvedModels, preferredModels: settingsModels.length ? unique(settingsModels) : approvedModels, sourceFiles, agentDefaults };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function renderCompactTakomiModelRoutingSummary(snapshot: TakomiModelRoutingSnapshot): string {
|
|
284
|
+
if (!snapshot.approvedModels.length && !snapshot.agentDefaults.length) return "";
|
|
285
|
+
const defaultLines = snapshot.agentDefaults
|
|
286
|
+
.filter((entry) => entry.model)
|
|
287
|
+
.map((entry) => `- ${entry.agent}: ${entry.model}${entry.thinking ? ` (${entry.thinking})` : ""}${entry.fallbackModels?.length ? `; fallbacks ${entry.fallbackModels.join(", ")}` : ""}`);
|
|
288
|
+
return [
|
|
289
|
+
"Active Takomi subagent routing summary:",
|
|
290
|
+
snapshot.sourceFiles.length ? `Policy source: ${snapshot.sourceFiles.join(", ")}` : "Policy source: settings/defaults",
|
|
291
|
+
snapshot.approvedModels.length ? `Approved model IDs: ${snapshot.approvedModels.join(", ")}` : "Approved model IDs: none discovered",
|
|
292
|
+
"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.",
|
|
293
|
+
defaultLines.length ? "Role defaults:" : "",
|
|
294
|
+
...defaultLines,
|
|
295
|
+
].filter(Boolean).join("\n");
|
|
296
|
+
}
|
|
@@ -22,6 +22,15 @@ export type RoutingPolicyInstallResult = {
|
|
|
22
22
|
detectedDefaults: string[];
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
+
export type RoutingPolicyPreviewResult = {
|
|
26
|
+
scope: RoutingPolicyInstallScope;
|
|
27
|
+
policy: string;
|
|
28
|
+
policyPath: string;
|
|
29
|
+
settingsPath: string;
|
|
30
|
+
detectedDefaults: string[];
|
|
31
|
+
overrides: JsonObject;
|
|
32
|
+
};
|
|
33
|
+
|
|
25
34
|
export type RoutingPolicyInstallScope = "global" | "project";
|
|
26
35
|
export type RoutingPolicySource = "project" | "global" | "bundled" | "missing";
|
|
27
36
|
|
|
@@ -64,6 +73,25 @@ function normalizeForSettings(filePath: string): string {
|
|
|
64
73
|
return filePath.replaceAll(path.sep, "/");
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
function extractPreferredProvider(policy: string): string | undefined {
|
|
77
|
+
const match = policy.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
|
|
78
|
+
?? policy.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
|
|
79
|
+
return match?.[1];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findExplicitProviderModel(policy: string, family: RegExp): string | undefined {
|
|
83
|
+
const refs = policy.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? [];
|
|
84
|
+
return refs.find((ref) => family.test(ref));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function providerModel(preferredProvider: string | undefined, model: string): string | undefined {
|
|
88
|
+
return preferredProvider ? `${preferredProvider}/${model}` : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function withOptionalModel(model: string | undefined, thinking: string, extra: JsonObject = {}): JsonObject {
|
|
92
|
+
return model ? { model, thinking, ...extra } : { thinking, ...extra };
|
|
93
|
+
}
|
|
94
|
+
|
|
67
95
|
function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detected: string[] } {
|
|
68
96
|
const lower = policy.toLowerCase();
|
|
69
97
|
const has55 = /gpt[- ]?5\.5/.test(lower);
|
|
@@ -71,28 +99,29 @@ function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detect
|
|
|
71
99
|
const hasMini = /gpt[- ]?5\.4\s*mini/.test(lower);
|
|
72
100
|
if (!has55 && !has54 && !hasMini) return { overrides: {}, detected: [] };
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
102
|
+
// Keep generated settings provider-agnostic unless the policy explicitly
|
|
103
|
+
// declares provider-qualified models or a preferred provider/router header.
|
|
104
|
+
const preferredProvider = extractPreferredProvider(policy);
|
|
105
|
+
const model55 = findExplicitProviderModel(policy, /gpt[-_.]?5\.5/i) ?? providerModel(preferredProvider, "gpt-5.5");
|
|
106
|
+
const model54 = findExplicitProviderModel(policy, /gpt[-_.]?5\.4(?![-_.]?mini)/i) ?? providerModel(preferredProvider, "gpt-5.4");
|
|
107
|
+
const modelMini = findExplicitProviderModel(policy, /gpt[-_.]?5\.4[-_.]?mini/i) ?? providerModel(preferredProvider, "gpt-5.4-mini");
|
|
77
108
|
const overrides: JsonObject = {};
|
|
78
109
|
const detected: string[] = [];
|
|
79
110
|
|
|
80
111
|
if (has55) {
|
|
81
|
-
overrides.
|
|
82
|
-
overrides.
|
|
83
|
-
overrides.
|
|
84
|
-
detected.push(
|
|
112
|
+
overrides.orchestrator = withOptionalModel(model55, "high");
|
|
113
|
+
overrides.architect = withOptionalModel(model55, "high");
|
|
114
|
+
overrides.reviewer = withOptionalModel(model55, "high");
|
|
115
|
+
detected.push(model55 ? `orchestrator/architect/reviewer → ${model55} high` : "orchestrator/architect/reviewer → GPT-5.5 high intent");
|
|
85
116
|
}
|
|
86
117
|
if (has54) {
|
|
87
|
-
overrides.
|
|
88
|
-
overrides.
|
|
89
|
-
overrides
|
|
90
|
-
detected.push("
|
|
118
|
+
overrides.general = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
119
|
+
overrides.coder = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
120
|
+
overrides.designer = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
121
|
+
detected.push(model54 ? `general/coder/designer → ${model54} high` : "general/coder/designer → GPT-5.4 high intent");
|
|
91
122
|
}
|
|
92
123
|
if (hasMini) {
|
|
93
|
-
|
|
94
|
-
overrides.delegate = { model: modelMini, thinking: "high" };
|
|
95
|
-
detected.push("scout/delegate → GPT-5.4 Mini high");
|
|
124
|
+
detected.push(modelMini ? `GPT-5.4 Mini available for explicit small-task overrides: ${modelMini}` : "GPT-5.4 Mini available as small-task intent only");
|
|
96
125
|
}
|
|
97
126
|
return { overrides, detected };
|
|
98
127
|
}
|
|
@@ -148,7 +177,7 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
|
|
|
148
177
|
return { source: "missing" };
|
|
149
178
|
}
|
|
150
179
|
|
|
151
|
-
export
|
|
180
|
+
export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): RoutingPolicyPreviewResult {
|
|
152
181
|
const policy = extractQuotedPolicy(input);
|
|
153
182
|
if (!policy) throw new Error("No routing policy text found. Paste the policy after /takomi routing or inside triple quotes.");
|
|
154
183
|
|
|
@@ -159,6 +188,13 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
|
|
|
159
188
|
const settingsPath = scope === "project"
|
|
160
189
|
? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
|
|
161
190
|
: GLOBAL_PI_SETTINGS_PATH;
|
|
191
|
+
const { overrides, detected } = deriveSubagentDefaults(policy);
|
|
192
|
+
return { scope, policy, policyPath, settingsPath, detectedDefaults: detected, overrides };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {
|
|
196
|
+
const preview = previewTakomiRoutingPolicy(cwd, input, options);
|
|
197
|
+
const { scope, policy, policyPath, settingsPath, overrides, detectedDefaults } = preview;
|
|
162
198
|
await mkdir(path.dirname(policyPath), { recursive: true });
|
|
163
199
|
await mkdir(path.dirname(settingsPath), { recursive: true });
|
|
164
200
|
await writeFile(policyPath, `# Takomi Model Routing Policy\n\n${policy}\n`, "utf8");
|
|
@@ -170,7 +206,6 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
|
|
|
170
206
|
: normalizeForSettings(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
|
|
171
207
|
settings.takomi = takomi;
|
|
172
208
|
|
|
173
|
-
const { overrides, detected } = deriveSubagentDefaults(policy);
|
|
174
209
|
if (Object.keys(overrides).length > 0) {
|
|
175
210
|
const subagents = asObject(settings.subagents);
|
|
176
211
|
const existingOverrides = asObject(subagents.agentOverrides);
|
|
@@ -179,7 +214,22 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
|
|
|
179
214
|
}
|
|
180
215
|
|
|
181
216
|
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
182
|
-
return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults
|
|
217
|
+
return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function renderRoutingPolicyPreview(preview: RoutingPolicyPreviewResult): string {
|
|
221
|
+
const overrideLines = Object.entries(preview.overrides).map(([role, value]) => `- ${role}: ${JSON.stringify(value)}`);
|
|
222
|
+
return [
|
|
223
|
+
`Scope: ${preview.scope}`,
|
|
224
|
+
`Policy path: ${preview.policyPath}`,
|
|
225
|
+
`Settings path: ${preview.settingsPath}`,
|
|
226
|
+
"",
|
|
227
|
+
preview.detectedDefaults.length ? "Detected routing defaults:" : "Detected routing defaults: none",
|
|
228
|
+
...preview.detectedDefaults.map((item) => `- ${item}`),
|
|
229
|
+
"",
|
|
230
|
+
overrideLines.length ? "Settings overrides to write:" : "Settings overrides to write: none",
|
|
231
|
+
...overrideLines,
|
|
232
|
+
].join("\n");
|
|
183
233
|
}
|
|
184
234
|
|
|
185
235
|
export async function loadTakomiRoutingPolicy(cwd: string): Promise<string | undefined> {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SubagentState,
|
|
13
13
|
} from "./pi-subagents-internal";
|
|
14
14
|
import { resolveAgentName } from "./agent-aliases";
|
|
15
|
+
import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshotSync } from "../takomi-runtime/model-routing-defaults";
|
|
15
16
|
import type { TakomiSubagentToolParams, TakomiSubagentToolTask } from "./tool-runner";
|
|
16
17
|
|
|
17
18
|
type ToolUpdate = (partial: AgentToolResult<Details>) => void;
|
|
@@ -135,9 +136,18 @@ function defaultChildExtensions(): string[] {
|
|
|
135
136
|
return candidates.filter((candidate) => fs.existsSync(candidate));
|
|
136
137
|
}
|
|
137
138
|
|
|
138
|
-
function withTakomiAgentDefaults(agent: AgentConfig): AgentConfig {
|
|
139
|
+
function withTakomiAgentDefaults(agent: AgentConfig, cwd: string): AgentConfig {
|
|
140
|
+
const routed = applyTakomiRoutingDefaults({
|
|
141
|
+
agent: agent.name,
|
|
142
|
+
model: agent.model,
|
|
143
|
+
fallbackModels: agent.fallbackModels,
|
|
144
|
+
thinking: agent.thinking,
|
|
145
|
+
}, loadTakomiModelRoutingSnapshotSync(cwd));
|
|
139
146
|
return {
|
|
140
147
|
...agent,
|
|
148
|
+
model: routed.model,
|
|
149
|
+
fallbackModels: routed.fallbackModels,
|
|
150
|
+
thinking: routed.thinking,
|
|
141
151
|
systemPromptMode: agent.systemPromptMode ?? "replace",
|
|
142
152
|
inheritProjectContext: agent.inheritProjectContext ?? true,
|
|
143
153
|
inheritSkills: agent.inheritSkills ?? false,
|
|
@@ -147,7 +157,7 @@ function withTakomiAgentDefaults(agent: AgentConfig): AgentConfig {
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
function discoverUnifiedAgents(discoverPiAgents: any, cwd: string, scope: AgentScope): { agents: AgentConfig[] } {
|
|
150
|
-
return { agents: discoverPiAgents(cwd, scope).agents.map(withTakomiAgentDefaults) };
|
|
160
|
+
return { agents: discoverPiAgents(cwd, scope).agents.map((agent: AgentConfig) => withTakomiAgentDefaults(agent, cwd)) };
|
|
151
161
|
}
|
|
152
162
|
|
|
153
163
|
function agentNameSet(discoverPiAgents: any, cwd: string): Set<string> {
|
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import type { TakomiThinkingLevel } from "../../../src/pi-takomi-core";
|
|
4
4
|
import { loadTakomiProfile } from "../takomi-runtime/profile";
|
|
5
|
+
import { applyTakomiRoutingDefaults, loadTakomiModelRoutingSnapshot } from "../takomi-runtime/model-routing-defaults";
|
|
5
6
|
import { resolveAgentName } from "./agent-aliases";
|
|
6
7
|
import { discoverTakomiAgents, type TakomiAgentConfig, type TakomiAgentScope } from "./agents";
|
|
7
8
|
import { createTakomiDelegationPlan, renderTakomiDelegationPlan } from "./delegation-plan";
|
|
@@ -202,10 +203,11 @@ export async function executeTakomiSubagentTool(
|
|
|
202
203
|
const agents = discoverTakomiAgents(rootCwd, agentScope);
|
|
203
204
|
const byName = new Map<string, TakomiAgentConfig>(agents.map((agent) => [agent.name, agent]));
|
|
204
205
|
const mode = resolveMode(params);
|
|
205
|
-
const
|
|
206
|
+
const routingSnapshot = await loadTakomiModelRoutingSnapshot(rootCwd);
|
|
207
|
+
const tasks = resolveTasks(params).map((task) => applyTakomiRoutingDefaults({
|
|
206
208
|
...task,
|
|
207
209
|
agent: resolveAgentName(task.agent, byName),
|
|
208
|
-
}));
|
|
210
|
+
}, routingSnapshot));
|
|
209
211
|
|
|
210
212
|
if (!mode) {
|
|
211
213
|
return textResult(
|
package/.pi/settings.json
CHANGED
|
@@ -5,41 +5,39 @@
|
|
|
5
5
|
},
|
|
6
6
|
"subagents": {
|
|
7
7
|
"agentOverrides": {
|
|
8
|
-
"
|
|
9
|
-
"model": "oauth-router/gpt-5.
|
|
10
|
-
"thinking": "high"
|
|
8
|
+
"general": {
|
|
9
|
+
"model": "oauth-router/gpt-5.4",
|
|
10
|
+
"thinking": "high",
|
|
11
|
+
"fallbackModels": [
|
|
12
|
+
"oauth-router/gpt-5.5:low"
|
|
13
|
+
]
|
|
11
14
|
},
|
|
12
|
-
"
|
|
15
|
+
"orchestrator": {
|
|
13
16
|
"model": "oauth-router/gpt-5.5",
|
|
14
17
|
"thinking": "high"
|
|
15
18
|
},
|
|
16
|
-
"
|
|
19
|
+
"architect": {
|
|
17
20
|
"model": "oauth-router/gpt-5.5",
|
|
18
|
-
"thinking": "
|
|
21
|
+
"thinking": "high"
|
|
19
22
|
},
|
|
20
|
-
"
|
|
23
|
+
"designer": {
|
|
21
24
|
"model": "oauth-router/gpt-5.4",
|
|
22
25
|
"thinking": "high",
|
|
23
26
|
"fallbackModels": [
|
|
24
27
|
"oauth-router/gpt-5.5:low"
|
|
25
28
|
]
|
|
26
29
|
},
|
|
27
|
-
"
|
|
30
|
+
"coder": {
|
|
28
31
|
"model": "oauth-router/gpt-5.4",
|
|
29
|
-
"thinking": "high"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"thinking": "high"
|
|
34
|
-
},
|
|
35
|
-
"scout": {
|
|
36
|
-
"model": "oauth-router/gpt-5.4-mini",
|
|
37
|
-
"thinking": "high"
|
|
32
|
+
"thinking": "high",
|
|
33
|
+
"fallbackModels": [
|
|
34
|
+
"oauth-router/gpt-5.5:low"
|
|
35
|
+
]
|
|
38
36
|
},
|
|
39
|
-
"
|
|
40
|
-
"model": "oauth-router/gpt-5.
|
|
37
|
+
"reviewer": {
|
|
38
|
+
"model": "oauth-router/gpt-5.5",
|
|
41
39
|
"thinking": "high"
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
|
-
}
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "takomi",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.27",
|
|
4
4
|
"description": "🎯 Stop wrestling with AI. Start building with purpose. The artisan's toolkit for agent workflows, Codex skills, and original Takomi capabilities like 21st.dev integration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|