takomi 2.1.26 → 2.1.28
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 +10 -0
- 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/diagnostics.ts +1 -1
- package/.pi/extensions/takomi-context-manager/extension-conflicts.ts +14 -3
- 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/context-panel.ts +583 -282
- package/.pi/extensions/takomi-runtime/index.ts +101 -6
- 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-runtime/takomi-stats.js +44 -16
- 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/README.md +34 -4
- package/package.json +4 -2
- package/src/cli.js +326 -33
- package/src/harness.js +132 -16
- package/src/pi-harness.js +27 -6
- package/src/pi-optional-features.js +195 -0
- package/src/postinstall.js +27 -0
- package/src/skills-catalog.js +245 -0
- package/src/skills-installer.js +244 -101
- package/src/skills-selection-tui.js +200 -0
- package/src/store.js +418 -240
- package/src/takomi-stats.js +44 -16
|
@@ -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;
|
|
@@ -84,6 +84,9 @@ const STATE_ENTRY = "takomi-runtime-state";
|
|
|
84
84
|
|
|
85
85
|
let activeProfile: TakomiProfile = DEFAULT_TAKOMI_PROFILE;
|
|
86
86
|
let activeSubagentLabel: string | undefined;
|
|
87
|
+
let activeSubagentAgent: string | undefined;
|
|
88
|
+
let activeSubagentTask: string | undefined;
|
|
89
|
+
let activeSubagentStatus: string | undefined;
|
|
87
90
|
|
|
88
91
|
const ThinkingSchema = Type.Union([
|
|
89
92
|
Type.Literal("off"),
|
|
@@ -665,18 +668,27 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
665
668
|
launchMode: state.launchMode,
|
|
666
669
|
planMode: state.planMode,
|
|
667
670
|
activeSubagent: activeSubagentLabel,
|
|
671
|
+
activeSubagentAgent,
|
|
672
|
+
activeSubagentTask,
|
|
673
|
+
activeSubagentStatus,
|
|
668
674
|
});
|
|
669
675
|
}
|
|
670
676
|
|
|
671
677
|
async function applySubagentRuntimeEvent(event: TakomiSubagentRuntimeEvent, ctx: ExtensionContext): Promise<void> {
|
|
672
678
|
if (event.type === "start") {
|
|
673
679
|
activeSubagentLabel = `${event.state.agent}: ${event.state.taskLabel}`;
|
|
680
|
+
activeSubagentAgent = event.state.agent;
|
|
681
|
+
activeSubagentTask = event.state.taskLabel;
|
|
682
|
+
activeSubagentStatus = event.state.status ?? "running";
|
|
674
683
|
syncContextPanelState();
|
|
675
684
|
} else if ((event.type === "update" || event.type === "complete" || event.type === "block") && event.patch) {
|
|
676
685
|
const model = event.patch.model ? ` @ ${event.patch.model}` : "";
|
|
677
686
|
const thinking = event.patch.thinking ? ` (${event.patch.thinking})` : "";
|
|
678
687
|
const label = event.patch.summary?.split(/\r?\n/).find(Boolean);
|
|
679
688
|
if (label) activeSubagentLabel = `${label}${model}${thinking}`;
|
|
689
|
+
if (event.patch.agent) activeSubagentAgent = event.patch.agent;
|
|
690
|
+
if (event.patch.taskLabel) activeSubagentTask = event.patch.taskLabel;
|
|
691
|
+
activeSubagentStatus = event.type === "complete" ? "completed" : event.type === "block" ? "blocked" : event.patch.status ?? activeSubagentStatus;
|
|
680
692
|
syncContextPanelState();
|
|
681
693
|
}
|
|
682
694
|
switch (event.type) {
|
|
@@ -761,6 +773,9 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
761
773
|
await updateState(ctx, () => {
|
|
762
774
|
state = cloneState(DEFAULT_STATE);
|
|
763
775
|
activeSubagentLabel = undefined;
|
|
776
|
+
activeSubagentAgent = undefined;
|
|
777
|
+
activeSubagentTask = undefined;
|
|
778
|
+
activeSubagentStatus = undefined;
|
|
764
779
|
}, "Takomi runtime state reset");
|
|
765
780
|
subagentController.reset(ctx);
|
|
766
781
|
contextPanel.resetSession();
|
|
@@ -799,6 +814,47 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
799
814
|
},
|
|
800
815
|
});
|
|
801
816
|
|
|
817
|
+
pi.registerTool({
|
|
818
|
+
name: "takomi_apply_routing_policy",
|
|
819
|
+
label: "Takomi Routing",
|
|
820
|
+
description: "Apply a Takomi model-routing policy after deterministic extraction and active-model review.",
|
|
821
|
+
promptSnippet: "Save reviewed Takomi model routing policy text to the active global or project policy file.",
|
|
822
|
+
promptGuidelines: [
|
|
823
|
+
"Use takomi_apply_routing_policy only after reviewing the deterministic extraction against the original routing policy text.",
|
|
824
|
+
"Do not call takomi_apply_routing_policy if the policy is ambiguous, invents providers, or maps to non-Takomi roles.",
|
|
825
|
+
],
|
|
826
|
+
parameters: Type.Object({
|
|
827
|
+
policyText: Type.String({ description: "Original routing policy text to save" }),
|
|
828
|
+
scope: Type.Optional(StringEnum(["global", "project"] as const)),
|
|
829
|
+
reviewNotes: Type.Optional(Type.String({ description: "Brief notes from the active-model review" })),
|
|
830
|
+
}),
|
|
831
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
832
|
+
const scope = params.scope ?? "global";
|
|
833
|
+
const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
|
|
834
|
+
const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
|
|
835
|
+
const scopeNote = scope === "global"
|
|
836
|
+
? "This global policy applies unless a project-local override exists."
|
|
837
|
+
: "This project-local policy overrides the global policy for the current project.";
|
|
838
|
+
return {
|
|
839
|
+
content: [{
|
|
840
|
+
type: "text",
|
|
841
|
+
text: [
|
|
842
|
+
`Takomi routing policy saved (${scope}).`,
|
|
843
|
+
"",
|
|
844
|
+
`Policy: ${result.policyPath}`,
|
|
845
|
+
`Settings: ${result.settingsPath}`,
|
|
846
|
+
"",
|
|
847
|
+
renderRoutingPolicyPreview(preview),
|
|
848
|
+
params.reviewNotes ? `\nReview notes:\n${params.reviewNotes}` : "",
|
|
849
|
+
"",
|
|
850
|
+
scopeNote,
|
|
851
|
+
].filter(Boolean).join("\n"),
|
|
852
|
+
}],
|
|
853
|
+
details: { result, preview, reviewNotes: params.reviewNotes },
|
|
854
|
+
};
|
|
855
|
+
},
|
|
856
|
+
});
|
|
857
|
+
|
|
802
858
|
pi.registerTool({
|
|
803
859
|
name: "takomi_workflow",
|
|
804
860
|
label: "Takomi Workflow",
|
|
@@ -1067,11 +1123,28 @@ ${stateJson}`
|
|
|
1067
1123
|
if (routingUpdateMatch) {
|
|
1068
1124
|
state.enabled = true;
|
|
1069
1125
|
try {
|
|
1070
|
-
const
|
|
1071
|
-
const
|
|
1072
|
-
return { action: "transform", text:
|
|
1126
|
+
const cwd = runtimeCtx?.cwd ?? process.cwd();
|
|
1127
|
+
const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global" });
|
|
1128
|
+
return { action: "transform", text: [
|
|
1129
|
+
"Review this Takomi routing policy extraction before it is saved.",
|
|
1130
|
+
"",
|
|
1131
|
+
"Rules:",
|
|
1132
|
+
"- Do not invent providers or model IDs not grounded in the policy.",
|
|
1133
|
+
"- Providerless names like GPT-5.5 are routing intent unless a preferred provider/router is declared.",
|
|
1134
|
+
"- Valid Takomi roles are: general, orchestrator, architect, designer, coder, reviewer.",
|
|
1135
|
+
"- If the extraction is correct and safe, call takomi_apply_routing_policy with scope=global and the exact original policy text.",
|
|
1136
|
+
"- If it is ambiguous or wrong, explain what the user should clarify and do not call the tool.",
|
|
1137
|
+
"",
|
|
1138
|
+
"Deterministic extraction:",
|
|
1139
|
+
renderRoutingPolicyPreview(preview),
|
|
1140
|
+
"",
|
|
1141
|
+
"Original policy text:",
|
|
1142
|
+
"```",
|
|
1143
|
+
preview.policy,
|
|
1144
|
+
"```",
|
|
1145
|
+
].join("\n") };
|
|
1073
1146
|
} catch (error) {
|
|
1074
|
-
return { action: "transform", text: `Takomi routing policy
|
|
1147
|
+
return { action: "transform", text: `Takomi routing policy review failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
1075
1148
|
}
|
|
1076
1149
|
}
|
|
1077
1150
|
|
|
@@ -1144,6 +1217,24 @@ ${stateJson}`
|
|
|
1144
1217
|
}
|
|
1145
1218
|
|
|
1146
1219
|
const routingPolicy = await resolveTakomiRoutingPolicy(runtimeCwd);
|
|
1220
|
+
const optionalFeatureContext = (() => {
|
|
1221
|
+
try {
|
|
1222
|
+
const tools = typeof (pi as { getAllTools?: () => Array<{ name?: string }> }).getAllTools === "function"
|
|
1223
|
+
? (pi as { getAllTools: () => Array<{ name?: string }> }).getAllTools()
|
|
1224
|
+
: [];
|
|
1225
|
+
const toolNames = new Set(tools.map((tool) => tool.name).filter(Boolean));
|
|
1226
|
+
const guidance: string[] = [];
|
|
1227
|
+
if (toolNames.has("ask_user_question")) {
|
|
1228
|
+
guidance.push("Takomi Interview is available: when Genesis, Design, or ambiguous planning would otherwise require guessing, use ask_user_question to ask concise structured questions before proceeding.");
|
|
1229
|
+
}
|
|
1230
|
+
if (toolNames.has("todo")) {
|
|
1231
|
+
guidance.push("Takomi Todo is available as an optional live overlay. You may use todo for short-lived execution visibility, but takomi_board remains the durable lifecycle/task source of truth.");
|
|
1232
|
+
}
|
|
1233
|
+
return guidance.join("\n");
|
|
1234
|
+
} catch {
|
|
1235
|
+
return "";
|
|
1236
|
+
}
|
|
1237
|
+
})();
|
|
1147
1238
|
const modelPreflightContext = (() => {
|
|
1148
1239
|
try {
|
|
1149
1240
|
const available = typeof (ctx as { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable === "function"
|
|
@@ -1165,6 +1256,7 @@ ${stateJson}`
|
|
|
1165
1256
|
routingPolicy.text
|
|
1166
1257
|
? `${routingPolicy.source === "bundled" ? "Bundled" : "Project"} Takomi model routing policy is active. Apply it when choosing parent/subagent models and escalation levels:\n\n${routingPolicy.text}`
|
|
1167
1258
|
: "No Takomi routing policy file was found. Users can install one with `/takomi routing <policy>` or by saying `Update Takomi routing logic: \"\"\"...\"\"\"`.",
|
|
1259
|
+
optionalFeatureContext,
|
|
1168
1260
|
modelPreflightContext,
|
|
1169
1261
|
`Execution mode: ${route.executionMode}. Session recommendation: ${route.sessionRecommendation}.`,
|
|
1170
1262
|
`Takomi execution gate: ${effectiveState.launchMode === "manual" ? "review" : "auto"}. In review gate mode, show the delegation plan before launching and return to the user after each task with results, verification guidance, and the recommended next step.`,
|
|
@@ -1199,8 +1291,10 @@ ${stateJson}`
|
|
|
1199
1291
|
runtimeCtx = ctx;
|
|
1200
1292
|
activeProfile = await loadTakomiProfile(ctx.cwd);
|
|
1201
1293
|
activeSubagentLabel = undefined;
|
|
1294
|
+
activeSubagentAgent = undefined;
|
|
1295
|
+
activeSubagentTask = undefined;
|
|
1296
|
+
activeSubagentStatus = undefined;
|
|
1202
1297
|
subagentController.reset(ctx);
|
|
1203
|
-
contextPanel.resetSession();
|
|
1204
1298
|
const entries = ctx.sessionManager.getEntries();
|
|
1205
1299
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
1206
1300
|
const entry = entries[i] as { type: string; customType?: string; data?: TakomiState };
|
|
@@ -1220,6 +1314,7 @@ ${stateJson}`
|
|
|
1220
1314
|
}
|
|
1221
1315
|
|
|
1222
1316
|
syncContextPanelState();
|
|
1317
|
+
contextPanel.rebuildFromSession(ctx);
|
|
1223
1318
|
await refreshUi(ctx, state);
|
|
1224
1319
|
contextPanel.show(ctx);
|
|
1225
1320
|
flushPendingSubagentEvents();
|
|
@@ -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> {
|