takomi 2.1.43 → 2.1.45
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 +4 -3
- package/.pi/extensions/oauth-router/README.md +3 -3
- package/.pi/extensions/oauth-router/commands.ts +66 -39
- package/.pi/extensions/oauth-router/config.ts +34 -34
- package/.pi/extensions/oauth-router/index.ts +51 -10
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/oauth-router/scripts/vibe-verify.py +5 -5
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +20 -9
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +36 -15
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/commands.ts +45 -12
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +133 -56
- package/.pi/extensions/takomi-runtime/routing-policy.ts +14 -2
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +4 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +46 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +55 -46
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +158 -28
- package/.pi/takomi/model-routing.md +282 -3
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-harness.js +41 -1
- package/src/pi-takomi-core/types.ts +10 -0
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- package/src/skills-installer.js +4 -1
- package/src/takomi-stats.js +5 -5
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
getSessionPaths,
|
|
14
14
|
getNextTaskId,
|
|
15
15
|
getWorkflowDefinition,
|
|
16
|
-
listWorkflowDefinitions,
|
|
17
16
|
markStageExpanded,
|
|
18
17
|
normalizeSessionState,
|
|
19
18
|
renderMasterPlan,
|
|
@@ -34,7 +33,6 @@ import {
|
|
|
34
33
|
type VibeLifecycleStage,
|
|
35
34
|
} from "../../../src/pi-takomi-core";
|
|
36
35
|
import {
|
|
37
|
-
renderRuntimeStatus,
|
|
38
36
|
renderRuntimeWidget,
|
|
39
37
|
renderTakomiHeader,
|
|
40
38
|
TakomiFooterComponent,
|
|
@@ -51,12 +49,27 @@ import {
|
|
|
51
49
|
} from "./shared";
|
|
52
50
|
import { TakomiContextPanel, wireContextPanel } from "./context-panel";
|
|
53
51
|
import { registerTakomiCommands } from "./commands";
|
|
52
|
+
import { USER_GATE_AUTO_PROVENANCE_ENTRY } from "./gate-provenance";
|
|
54
53
|
import {
|
|
55
54
|
DEFAULT_TAKOMI_PROFILE,
|
|
56
55
|
getProfileDefaults,
|
|
57
56
|
loadTakomiProfile,
|
|
58
57
|
} from "./profile";
|
|
59
58
|
import { installTakomiRoutingPolicy, previewTakomiRoutingPolicy, renderRoutingPolicyPreview, resolveTakomiRoutingPolicy } from "./routing-policy";
|
|
59
|
+
import {
|
|
60
|
+
discoverWorkflowPlaybooks,
|
|
61
|
+
showWorkflowCatalogForBoard,
|
|
62
|
+
} from "./workflow-catalog";
|
|
63
|
+
import {
|
|
64
|
+
renderTakomiBoardCall,
|
|
65
|
+
renderTakomiBoardResult,
|
|
66
|
+
renderTakomiModeCall,
|
|
67
|
+
renderTakomiModeResult,
|
|
68
|
+
renderTakomiRoutingCall,
|
|
69
|
+
renderTakomiRoutingResult,
|
|
70
|
+
renderTakomiWorkflowCall,
|
|
71
|
+
renderTakomiWorkflowResult,
|
|
72
|
+
} from "./tool-renderers";
|
|
60
73
|
|
|
61
74
|
type TakomiModeSource = "idle" | "manual" | "model" | "board";
|
|
62
75
|
|
|
@@ -400,6 +413,24 @@ function getIncompleteChecklistItems(checklist?: OrchestratorTask["checklist"]):
|
|
|
400
413
|
.map((item) => item.text);
|
|
401
414
|
}
|
|
402
415
|
|
|
416
|
+
type BoardErrorSeverity = "warning" | "error";
|
|
417
|
+
|
|
418
|
+
function createBoardErrorResult(
|
|
419
|
+
text: string,
|
|
420
|
+
code: string,
|
|
421
|
+
severity: BoardErrorSeverity,
|
|
422
|
+
details: Record<string, unknown> = {},
|
|
423
|
+
) {
|
|
424
|
+
return {
|
|
425
|
+
content: [{ type: "text" as const, text }],
|
|
426
|
+
// Keep this semantic error independent of Pi's transport-level isError.
|
|
427
|
+
// Renderers use it to retain warning/error meaning even when Pi does not
|
|
428
|
+
// pass top-level result flags back into renderResult.
|
|
429
|
+
details: { ...details, error: { code, message: text, severity } },
|
|
430
|
+
isError: true,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
403
434
|
function getCompletionGateError(task: Pick<OrchestratorTask, "id" | "title" | "checklist">): string | undefined {
|
|
404
435
|
if (!task.checklist?.length) {
|
|
405
436
|
return `Task ${task.id} cannot be marked completed until it has a checklist.`;
|
|
@@ -689,10 +720,15 @@ function installTakomiFooter(ctx: ExtensionContext, stateRef: { current: TakomiS
|
|
|
689
720
|
ctx.ui.setFooter((tui, theme, footerData) => new TakomiFooterComponent(tui, theme, footerData, ctx, () => stateRef.current));
|
|
690
721
|
}
|
|
691
722
|
|
|
692
|
-
|
|
693
|
-
|
|
723
|
+
function hasVisibleRuntimeWidget(state: TakomiState): boolean {
|
|
724
|
+
return state.enabled && (state.modeSource ?? "idle") !== "idle";
|
|
725
|
+
}
|
|
694
726
|
|
|
695
|
-
async function refreshUi(
|
|
727
|
+
async function refreshUi(
|
|
728
|
+
ctx: ExtensionContext,
|
|
729
|
+
state: TakomiState,
|
|
730
|
+
footerStateRef: { current: TakomiState; context?: ExtensionContext },
|
|
731
|
+
) {
|
|
696
732
|
if (!ctx.hasUI) return;
|
|
697
733
|
ctx.ui.setTitle("Takomi");
|
|
698
734
|
ctx.ui.setHeader((_tui, theme) => ({
|
|
@@ -702,17 +738,25 @@ async function refreshUi(ctx: ExtensionContext, state: TakomiState) {
|
|
|
702
738
|
},
|
|
703
739
|
}));
|
|
704
740
|
footerStateRef.current = state;
|
|
705
|
-
|
|
741
|
+
|
|
742
|
+
// The mode indicator belongs in the widget above the editor. Keeping a
|
|
743
|
+
// second setStatus copy makes Pi's default footer duplicate it whenever a
|
|
744
|
+
// custom footer is replaced or a session is rebound.
|
|
745
|
+
ctx.ui.setStatus("takomi-runtime", undefined);
|
|
706
746
|
const widget = renderRuntimeWidget(ctx.ui.theme, state);
|
|
707
747
|
ctx.ui.setWidget("takomi-runtime", widget.length > 0 ? widget : undefined);
|
|
708
|
-
|
|
748
|
+
|
|
749
|
+
// A replacement session receives a fresh UI context even when extension
|
|
750
|
+
// modules remain cached. Install once per context, not once per module.
|
|
751
|
+
if (footerStateRef.context !== ctx) {
|
|
709
752
|
installTakomiFooter(ctx, footerStateRef);
|
|
710
|
-
footerStateRef.
|
|
753
|
+
footerStateRef.context = ctx;
|
|
711
754
|
}
|
|
712
755
|
}
|
|
713
756
|
|
|
714
757
|
export default function takomiRuntime(pi: ExtensionAPI) {
|
|
715
758
|
let state = cloneState(DEFAULT_STATE);
|
|
759
|
+
const footerStateRef: { current: TakomiState; context?: ExtensionContext } = { current: state };
|
|
716
760
|
const subagentController = getTakomiSubagentController();
|
|
717
761
|
const contextPanel = new TakomiContextPanel();
|
|
718
762
|
let runtimeCtx: ExtensionContext | undefined;
|
|
@@ -734,6 +778,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
734
778
|
pi.appendEntry(STATE_ENTRY, state);
|
|
735
779
|
}
|
|
736
780
|
|
|
781
|
+
// This is intentionally separate from generic runtime-state persistence.
|
|
782
|
+
// Only user slash-command handlers receive the recorder below.
|
|
783
|
+
function recordUserGateAutoProvenance(authorized: boolean): void {
|
|
784
|
+
pi.appendEntry(USER_GATE_AUTO_PROVENANCE_ENTRY, { authorized });
|
|
785
|
+
}
|
|
786
|
+
|
|
737
787
|
function syncContextPanelState() {
|
|
738
788
|
contextPanel.setRuntimeState({
|
|
739
789
|
role: state.role,
|
|
@@ -798,7 +848,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
798
848
|
mutator();
|
|
799
849
|
persistState();
|
|
800
850
|
syncContextPanelState();
|
|
801
|
-
await refreshUi(ctx, state);
|
|
851
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
802
852
|
const resolvedMessage = typeof message === "function" ? message() : message;
|
|
803
853
|
if (resolvedMessage) ctx.ui.notify(resolvedMessage, "info");
|
|
804
854
|
}
|
|
@@ -821,6 +871,7 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
821
871
|
registerTakomiCommands(pi, {
|
|
822
872
|
getState: () => state,
|
|
823
873
|
updateState,
|
|
874
|
+
recordUserGateAutoProvenance,
|
|
824
875
|
setStageAndWorkflow: (stage, options) => setStageAndWorkflow(state, stage, options),
|
|
825
876
|
hasGenesisArtifacts,
|
|
826
877
|
subagentController,
|
|
@@ -849,12 +900,13 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
849
900
|
},
|
|
850
901
|
resetRuntime: async (ctx) => {
|
|
851
902
|
await updateState(ctx, () => {
|
|
903
|
+
recordUserGateAutoProvenance(false);
|
|
852
904
|
state = cloneState(DEFAULT_STATE);
|
|
853
905
|
activeSubagentLabel = undefined;
|
|
854
906
|
activeSubagentAgent = undefined;
|
|
855
907
|
activeSubagentTask = undefined;
|
|
856
908
|
activeSubagentStatus = undefined;
|
|
857
|
-
}, "Takomi runtime state reset");
|
|
909
|
+
}, () => hasVisibleRuntimeWidget(state) ? "" : "Takomi runtime state reset");
|
|
858
910
|
subagentController.reset(ctx);
|
|
859
911
|
contextPanel.resetSession();
|
|
860
912
|
contextPanel.show(ctx);
|
|
@@ -910,9 +962,11 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
910
962
|
|
|
911
963
|
persistState();
|
|
912
964
|
syncContextPanelState();
|
|
913
|
-
await refreshUi(ctx, state);
|
|
965
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
914
966
|
const label = state.modeSource === "idle" ? "idle" : `${state.modeSource}:${state.stage ?? state.role}`;
|
|
915
|
-
|
|
967
|
+
const text = `Takomi mode set to ${label}${state.modeReason ? ` (${state.modeReason})` : ""}.`;
|
|
968
|
+
if (!hasVisibleRuntimeWidget(state)) ctx.ui.notify(text, "info");
|
|
969
|
+
return text;
|
|
916
970
|
}
|
|
917
971
|
|
|
918
972
|
pi.registerTool({
|
|
@@ -931,12 +985,13 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
931
985
|
}),
|
|
932
986
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
933
987
|
const text = await applyTakomiMode(ctx, params.mode, "model", params.reason);
|
|
934
|
-
ctx.ui.notify(text, "info");
|
|
935
988
|
return {
|
|
936
989
|
content: [{ type: "text", text }],
|
|
937
990
|
details: { mode: params.mode, source: state.modeSource, reason: state.modeReason, role: state.role, stage: state.stage, workflow: state.workflow },
|
|
938
991
|
};
|
|
939
992
|
},
|
|
993
|
+
renderCall: renderTakomiModeCall,
|
|
994
|
+
renderResult: (result, options, theme) => renderTakomiModeResult(result, options, theme),
|
|
940
995
|
});
|
|
941
996
|
|
|
942
997
|
pi.registerTool({
|
|
@@ -955,7 +1010,15 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
955
1010
|
}),
|
|
956
1011
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
957
1012
|
const scope = params.scope ?? "global";
|
|
958
|
-
const
|
|
1013
|
+
const availableModels = (() => {
|
|
1014
|
+
try {
|
|
1015
|
+
const available = (ctx as typeof ctx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } }).modelRegistry?.getAvailable?.() ?? [];
|
|
1016
|
+
return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
|
|
1017
|
+
} catch {
|
|
1018
|
+
return [];
|
|
1019
|
+
}
|
|
1020
|
+
})();
|
|
1021
|
+
const preview = previewTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope, availableModels });
|
|
959
1022
|
const result = await installTakomiRoutingPolicy(ctx.cwd, params.policyText, { scope });
|
|
960
1023
|
const scopeNote = scope === "global"
|
|
961
1024
|
? "This global policy applies unless a project-local override exists."
|
|
@@ -978,6 +1041,8 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
978
1041
|
details: { result, preview, reviewNotes: params.reviewNotes },
|
|
979
1042
|
};
|
|
980
1043
|
},
|
|
1044
|
+
renderCall: renderTakomiRoutingCall,
|
|
1045
|
+
renderResult: (result, options, theme) => renderTakomiRoutingResult(result, options, theme),
|
|
981
1046
|
});
|
|
982
1047
|
|
|
983
1048
|
pi.registerTool({
|
|
@@ -989,20 +1054,10 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
989
1054
|
workflow: Type.Optional(StringEnum(["vibe-genesis", "vibe-design", "vibe-build"] as const)),
|
|
990
1055
|
}),
|
|
991
1056
|
async execute(_toolCallId, params) {
|
|
992
|
-
|
|
993
|
-
const workflow = getWorkflowDefinition(params.workflow);
|
|
994
|
-
return {
|
|
995
|
-
content: [{ type: "text", text: `${workflow.title}\n\n${workflow.playbook}` }],
|
|
996
|
-
details: workflow,
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
const workflows = listWorkflowDefinitions();
|
|
1001
|
-
return {
|
|
1002
|
-
content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.purpose}`).join("\n") }],
|
|
1003
|
-
details: undefined,
|
|
1004
|
-
};
|
|
1057
|
+
return discoverWorkflowPlaybooks(params.workflow);
|
|
1005
1058
|
},
|
|
1059
|
+
renderCall: renderTakomiWorkflowCall,
|
|
1060
|
+
renderResult: (result, options, theme) => renderTakomiWorkflowResult(result, options, theme),
|
|
1006
1061
|
});
|
|
1007
1062
|
|
|
1008
1063
|
pi.registerTool({
|
|
@@ -1072,16 +1127,12 @@ export default function takomiRuntime(pi: ExtensionAPI) {
|
|
|
1072
1127
|
}),
|
|
1073
1128
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1074
1129
|
if (params.action === "show_workflows") {
|
|
1075
|
-
|
|
1076
|
-
return {
|
|
1077
|
-
content: [{ type: "text", text: workflows.map((workflow) => `${workflow.id}: ${workflow.playbook}`).join("\n\n") }],
|
|
1078
|
-
details: { workflows },
|
|
1079
|
-
};
|
|
1130
|
+
return showWorkflowCatalogForBoard();
|
|
1080
1131
|
}
|
|
1081
1132
|
|
|
1082
1133
|
if (params.action === "show_session") {
|
|
1083
1134
|
if (!params.sessionId) {
|
|
1084
|
-
return
|
|
1135
|
+
return createBoardErrorResult("sessionId is required for show_session", "missing-session-id", "warning");
|
|
1085
1136
|
}
|
|
1086
1137
|
assertSafeSessionId(params.sessionId);
|
|
1087
1138
|
const paths = getSessionPaths(ctx.cwd, params.sessionId);
|
|
@@ -1100,13 +1151,18 @@ ${stateJson}`
|
|
|
1100
1151
|
|
|
1101
1152
|
if (params.action === "update_task") {
|
|
1102
1153
|
if (!params.sessionId || !params.taskId) {
|
|
1103
|
-
return
|
|
1154
|
+
return createBoardErrorResult("sessionId and taskId are required for update_task", "missing-task-context", "warning");
|
|
1104
1155
|
}
|
|
1105
1156
|
assertSafeTaskId(params.taskId);
|
|
1106
1157
|
const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
|
|
1107
1158
|
const idx = sessionState.tasks.findIndex((task) => task.id === params.taskId);
|
|
1108
1159
|
if (idx === -1) {
|
|
1109
|
-
return
|
|
1160
|
+
return createBoardErrorResult(
|
|
1161
|
+
`Task ${params.taskId} not found in session ${params.sessionId}`,
|
|
1162
|
+
"task-not-found",
|
|
1163
|
+
"error",
|
|
1164
|
+
{ sessionId: params.sessionId, taskId: params.taskId },
|
|
1165
|
+
);
|
|
1110
1166
|
}
|
|
1111
1167
|
const current = sessionState.tasks[idx];
|
|
1112
1168
|
const checklist = resolveChecklistState(current.checklist, params.checklist, params.checklistUpdates);
|
|
@@ -1119,16 +1175,12 @@ ${stateJson}`
|
|
|
1119
1175
|
if (params.status === "completed") {
|
|
1120
1176
|
const completionGateError = getCompletionGateError(nextTask);
|
|
1121
1177
|
if (completionGateError) {
|
|
1122
|
-
return {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
checklist: nextTask.checklist,
|
|
1129
|
-
},
|
|
1130
|
-
isError: true,
|
|
1131
|
-
};
|
|
1178
|
+
return createBoardErrorResult(completionGateError, "completion-gate", "warning", {
|
|
1179
|
+
sessionId: params.sessionId,
|
|
1180
|
+
taskId: current.id,
|
|
1181
|
+
incompleteChecklistItems: getIncompleteChecklistItems(nextTask.checklist),
|
|
1182
|
+
checklist: nextTask.checklist,
|
|
1183
|
+
});
|
|
1132
1184
|
}
|
|
1133
1185
|
}
|
|
1134
1186
|
sessionState.tasks[idx] = nextTask;
|
|
@@ -1137,7 +1189,7 @@ ${stateJson}`
|
|
|
1137
1189
|
state.modeReason = state.modeReason ?? "board task update";
|
|
1138
1190
|
persistState();
|
|
1139
1191
|
syncContextPanelState();
|
|
1140
|
-
await refreshUi(ctx, state);
|
|
1192
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
1141
1193
|
const nextState = buildSessionState(
|
|
1142
1194
|
sessionState.sessionId,
|
|
1143
1195
|
sessionState.title,
|
|
@@ -1166,7 +1218,11 @@ ${stateJson}`
|
|
|
1166
1218
|
|
|
1167
1219
|
if (params.action === "expand_stage") {
|
|
1168
1220
|
if (!params.sessionId || !params.stage || !params.tasks?.length) {
|
|
1169
|
-
return
|
|
1221
|
+
return createBoardErrorResult(
|
|
1222
|
+
"sessionId, stage, and at least one task are required for expand_stage",
|
|
1223
|
+
"invalid-expansion",
|
|
1224
|
+
"warning",
|
|
1225
|
+
);
|
|
1170
1226
|
}
|
|
1171
1227
|
|
|
1172
1228
|
const { state: sessionState } = await loadSessionState(ctx.cwd, params.sessionId);
|
|
@@ -1197,7 +1253,7 @@ ${stateJson}`
|
|
|
1197
1253
|
state.modeReason = `expanded ${params.stage} stage`;
|
|
1198
1254
|
persistState();
|
|
1199
1255
|
syncContextPanelState();
|
|
1200
|
-
await refreshUi(ctx, state);
|
|
1256
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
1201
1257
|
|
|
1202
1258
|
return {
|
|
1203
1259
|
content: [{ type: "text", text: `Expanded ${params.stage} stage in session ${nextState.sessionId}.\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks)}` }],
|
|
@@ -1242,13 +1298,15 @@ ${stateJson}`
|
|
|
1242
1298
|
state.modeReason = "orchestrator session";
|
|
1243
1299
|
persistState();
|
|
1244
1300
|
syncContextPanelState();
|
|
1245
|
-
await refreshUi(ctx, state);
|
|
1301
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
1246
1302
|
|
|
1247
1303
|
return {
|
|
1248
1304
|
content: [{ type: "text", text: `Created Takomi orchestrator session ${nextState.sessionId} in hybrid mode\n\nDocs: ${paths.root}\nState: ${paths.stateFile}\n\n${buildTaskRows(nextState.tasks) || "No tasks provided."}` }],
|
|
1249
1305
|
details: { sessionId: nextState.sessionId, paths, tasks: nextState.tasks, lifecycle: nextState.lifecycle, mode: nextState.mode },
|
|
1250
1306
|
};
|
|
1251
1307
|
},
|
|
1308
|
+
renderCall: renderTakomiBoardCall,
|
|
1309
|
+
renderResult: (result, options, theme, context) => renderTakomiBoardResult(result, options, theme, context?.args),
|
|
1252
1310
|
});
|
|
1253
1311
|
|
|
1254
1312
|
pi.on("input", async (event) => {
|
|
@@ -1263,20 +1321,39 @@ ${stateJson}`
|
|
|
1263
1321
|
state.enabled = true;
|
|
1264
1322
|
try {
|
|
1265
1323
|
const cwd = runtimeCtx?.cwd ?? process.cwd();
|
|
1266
|
-
const
|
|
1324
|
+
const availableModels = (() => {
|
|
1325
|
+
try {
|
|
1326
|
+
const available = (runtimeCtx as typeof runtimeCtx & { modelRegistry?: { getAvailable?: () => Array<{ provider?: string; id?: string; name?: string }> } })?.modelRegistry?.getAvailable?.() ?? [];
|
|
1327
|
+
return available.map((model) => `${model.provider ? `${model.provider}/` : ""}${model.id ?? model.name ?? ""}`).filter(Boolean);
|
|
1328
|
+
} catch {
|
|
1329
|
+
return [];
|
|
1330
|
+
}
|
|
1331
|
+
})();
|
|
1332
|
+
const preview = previewTakomiRoutingPolicy(cwd, text, { scope: "global", availableModels });
|
|
1333
|
+
const activePolicy = await resolveTakomiRoutingPolicy(cwd);
|
|
1334
|
+
const replacementWarning = activePolicy.text && activePolicy.text.trim().length > preview.policy.trim().length * 2
|
|
1335
|
+
? `WARNING: This input is much shorter than the active policy (${preview.policy.length} vs ${activePolicy.text.length} characters). It will replace the file, not merge into it. Inspect any referenced full source before applying.`
|
|
1336
|
+
: "The supplied text replaces the policy file exactly; it is not merged with the current policy.";
|
|
1267
1337
|
return { action: "transform", text: [
|
|
1268
1338
|
"Review this Takomi routing policy extraction before it is saved.",
|
|
1269
1339
|
"",
|
|
1270
1340
|
"Rules:",
|
|
1271
1341
|
"- Do not invent providers or model IDs not grounded in the policy.",
|
|
1272
|
-
"- Providerless names
|
|
1273
|
-
"-
|
|
1274
|
-
"-
|
|
1275
|
-
"-
|
|
1342
|
+
"- Providerless names are valid routing intent. Require a provider only for executable model overrides.",
|
|
1343
|
+
"- Check the available Pi model registry below before asking whether a named model exists or what its exact ID is.",
|
|
1344
|
+
"- Conditional task routes need not be forced into role-wide agentOverrides.",
|
|
1345
|
+
"- Valid role-wide Takomi overrides are: general, orchestrator, architect, designer, coder, reviewer. Other headings may be policy concepts or execution routes.",
|
|
1346
|
+
"- Preserve the user's complete authored policy. If this is a summary/excerpt and a referenced full source exists, inspect and apply that source rather than overwriting it with the excerpt.",
|
|
1347
|
+
"- If correct and safe, call takomi_apply_routing_policy with scope=global and the complete intended policy text.",
|
|
1348
|
+
"- Ask only for unresolved provider/account choices or genuine ambiguity; do not ask for facts available in registry or files.",
|
|
1276
1349
|
"",
|
|
1277
1350
|
"Deterministic extraction:",
|
|
1278
1351
|
renderRoutingPolicyPreview(preview),
|
|
1279
1352
|
"",
|
|
1353
|
+
replacementWarning,
|
|
1354
|
+
"",
|
|
1355
|
+
availableModels.length ? `Available Pi models:\n${availableModels.map((model) => `- ${model}`).join("\n")}` : "Available Pi models: registry unavailable; inspect it before asking the user if possible.",
|
|
1356
|
+
"",
|
|
1280
1357
|
"Original policy text:",
|
|
1281
1358
|
"```",
|
|
1282
1359
|
preview.policy,
|
|
@@ -1356,7 +1433,7 @@ ${stateJson}`
|
|
|
1356
1433
|
}
|
|
1357
1434
|
persistState();
|
|
1358
1435
|
syncContextPanelState();
|
|
1359
|
-
await refreshUi(ctx, state);
|
|
1436
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
1360
1437
|
|
|
1361
1438
|
const routingPolicy = await resolveTakomiRoutingPolicy(runtimeCwd);
|
|
1362
1439
|
const optionalFeatureContext = (() => {
|
|
@@ -1458,7 +1535,7 @@ ${stateJson}`
|
|
|
1458
1535
|
|
|
1459
1536
|
syncContextPanelState();
|
|
1460
1537
|
contextPanel.rebuildFromSession(ctx);
|
|
1461
|
-
await refreshUi(ctx, state);
|
|
1538
|
+
await refreshUi(ctx, state, footerStateRef);
|
|
1462
1539
|
contextPanel.show(ctx);
|
|
1463
1540
|
flushPendingSubagentEvents();
|
|
1464
1541
|
});
|
|
@@ -205,7 +205,7 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
|
|
|
205
205
|
return { source: "missing" };
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): RoutingPolicyPreviewResult {
|
|
208
|
+
export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope; availableModels?: string[] } = {}): RoutingPolicyPreviewResult {
|
|
209
209
|
const policy = extractQuotedPolicy(input);
|
|
210
210
|
if (!policy) throw new Error("No routing policy text found. Paste the policy after /takomi routing or inside triple quotes.");
|
|
211
211
|
|
|
@@ -217,7 +217,19 @@ export function previewTakomiRoutingPolicy(cwd: string, input: string, options:
|
|
|
217
217
|
? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
|
|
218
218
|
: GLOBAL_PI_SETTINGS_PATH;
|
|
219
219
|
const { overrides, detected } = deriveSubagentDefaults(policy);
|
|
220
|
-
|
|
220
|
+
|
|
221
|
+
// Resolve named model families from Pi's registry for review visibility. These
|
|
222
|
+
// are conditional routing intents, not role-wide defaults, so do not invent
|
|
223
|
+
// agentOverrides merely to make the extraction non-empty.
|
|
224
|
+
const availableModels = [...new Set(options.availableModels ?? [])];
|
|
225
|
+
for (const alias of ["luna", "sol", "terra"]) {
|
|
226
|
+
if (!new RegExp(`\\b${alias}\\b`, "i").test(policy)) continue;
|
|
227
|
+
const matches = availableModels.filter((model) => new RegExp(`(?:^|/)gpt[-_.]?5\\.6[-_.]?${alias}$`, "i").test(model));
|
|
228
|
+
if (matches.length === 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent resolves to ${matches[0]} (conditional route)`);
|
|
229
|
+
else if (matches.length > 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent matches available models: ${matches.join(", ")}`);
|
|
230
|
+
else detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} remains a providerless conditional routing intent`);
|
|
231
|
+
}
|
|
232
|
+
return { scope, policy, policyPath, settingsPath, detectedDefaults: [...new Set(detected)], overrides };
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {
|
|
@@ -15,29 +15,47 @@ const pc = {
|
|
|
15
15
|
magenta: ansi(35, 39),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// USD per million tokens: input, cache read, output, cache write.
|
|
19
|
+
// OAuth-router's configured catalog is merged over these fallbacks at runtime,
|
|
20
|
+
// keeping Stats aligned with custom/new models without another hard-coded edit.
|
|
18
21
|
const PRICES = {
|
|
19
|
-
'gpt-5.
|
|
20
|
-
'gpt-5.
|
|
21
|
-
'gpt-5.
|
|
22
|
-
'gpt-5.
|
|
23
|
-
'gpt-5.
|
|
24
|
-
'gpt-5.
|
|
25
|
-
'gpt-5-
|
|
26
|
-
'gpt-5.
|
|
27
|
-
'gpt-5.
|
|
28
|
-
'gpt-5': [1.25, 0.125, 10.00],
|
|
29
|
-
'gpt-5
|
|
30
|
-
'gpt-
|
|
31
|
-
'gpt-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
22
|
+
'gpt-5.6-luna': [1.00, 0.10, 6.00, 1.25],
|
|
23
|
+
'gpt-5.6-sol': [5.00, 0.50, 30.00, 6.25],
|
|
24
|
+
'gpt-5.6-terra': [2.50, 0.25, 15.00, 3.125],
|
|
25
|
+
'gpt-5.5': [5.00, 0.50, 30.00, 6.25],
|
|
26
|
+
'gpt-5.4': [2.50, 0.25, 15.00, 3.125],
|
|
27
|
+
'gpt-5.4-mini': [0.75, 0.075, 4.50, 0.9375],
|
|
28
|
+
'gpt-5.4-nano': [0.20, 0.02, 1.25, 0.25],
|
|
29
|
+
'gpt-5.3-codex': [2.50, 0.25, 15.00, 3.125],
|
|
30
|
+
'gpt-5.2-codex': [1.75, 0.175, 14.00, 2.1875],
|
|
31
|
+
'gpt-5-codex': [1.25, 0.125, 10.00, 1.5625],
|
|
32
|
+
'gpt-5.2': [1.75, 0.175, 14.00, 2.1875],
|
|
33
|
+
'gpt-5.1': [1.25, 0.125, 10.00, 1.5625],
|
|
34
|
+
'gpt-5': [1.25, 0.125, 10.00, 1.5625],
|
|
35
|
+
'gpt-5-mini': [0.25, 0.025, 2.00, 0.3125],
|
|
36
|
+
'gpt-4.1': [2.00, 0.50, 8.00, 2.00],
|
|
37
|
+
'gpt-4o': [2.50, 1.25, 10.00, 2.50],
|
|
38
|
+
'o4-mini': [1.10, 0.275, 4.40, 1.10],
|
|
39
|
+
'claude-sonnet-4-6': [3.00, 0.30, 15.00, 3.75],
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
|
|
37
43
|
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
38
44
|
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
39
|
-
function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
|
|
40
|
-
function cost(model, input, cache, output,
|
|
45
|
+
function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
|
|
46
|
+
function cost(model, input, cache, output, cacheWrite = 0, prices = PRICES) { const p = prices[model]; if (!p) return 0; return (input*p[0] + cache*p[1] + output*p[2] + cacheWrite*(p[3] ?? p[0])) / 1_000_000; }
|
|
47
|
+
async function loadPrices(home) {
|
|
48
|
+
const prices = { ...PRICES };
|
|
49
|
+
const configPath = path.join(home, '.pi', 'agent', 'oauth-router', 'config.json');
|
|
50
|
+
try {
|
|
51
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
52
|
+
for (const model of config.models || []) {
|
|
53
|
+
if (!model?.id || !model?.cost) continue;
|
|
54
|
+
prices[model.id] = [model.cost.input || 0, model.cost.cacheRead || 0, model.cost.output || 0, model.cost.cacheWrite ?? model.cost.input ?? 0];
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return prices;
|
|
58
|
+
}
|
|
41
59
|
function fmtTokens(n) { if (n >= 1e9) return `${(n/1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n/1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n/1e3).toFixed(1)}K`; return String(Math.round(n || 0)); }
|
|
42
60
|
function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
|
|
43
61
|
function ms(n) { if (!n) return '-'; const s = Math.round(n/1000); if (s < 60) return `${s}s`; const m = Math.floor(s/60); if (m < 60) return `${m}m ${s%60}s`; const h = Math.floor(m/60); return `${h}h ${m%60}m`; }
|
|
@@ -117,7 +135,7 @@ function pushTask(taskRows, task) {
|
|
|
117
135
|
taskRows.push(task);
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
|
|
138
|
+
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = [], prices = PRICES) {
|
|
121
139
|
for (const file of await files(root)) {
|
|
122
140
|
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
123
141
|
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
|
|
@@ -170,7 +188,7 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
170
188
|
}
|
|
171
189
|
}
|
|
172
190
|
const u = msg && msg.usage;
|
|
173
|
-
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0,
|
|
191
|
+
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, cacheWrite: +u.cacheWrite||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, +u.cacheWrite||0, prices) });
|
|
174
192
|
}
|
|
175
193
|
} catch {
|
|
176
194
|
continue;
|
|
@@ -193,18 +211,19 @@ export async function collectTakomiStats(opts = {}) {
|
|
|
193
211
|
const home = opts.home || os.homedir();
|
|
194
212
|
const cwd = opts.cwd || process.cwd();
|
|
195
213
|
const rawEvents = [], rawSessions = [], rawTasks = [];
|
|
214
|
+
const prices = await loadPrices(home);
|
|
196
215
|
const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
|
|
197
216
|
const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
|
|
198
|
-
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
|
|
199
|
-
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
|
|
200
|
-
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
|
|
217
|
+
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks, prices);
|
|
218
|
+
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks, prices);
|
|
219
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks, prices);
|
|
201
220
|
const sinceDay = parseSince(opts.since);
|
|
202
221
|
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
203
222
|
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
204
223
|
const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
|
|
205
224
|
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
206
225
|
const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
|
|
207
|
-
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
226
|
+
let totals = { input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
208
227
|
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
209
228
|
for (const e of events) {
|
|
210
229
|
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
@@ -214,7 +233,7 @@ export async function collectTakomiStats(opts = {}) {
|
|
|
214
233
|
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
215
234
|
continue;
|
|
216
235
|
}
|
|
217
|
-
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
236
|
+
totals.input += e.input; totals.cache += e.cache; totals.cacheWrite += e.cacheWrite || 0; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
218
237
|
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
219
238
|
}
|
|
220
239
|
const byAgent = new Map(); let longestRun = null;
|
|
@@ -509,7 +528,8 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
509
528
|
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
510
529
|
const cards1 = [
|
|
511
530
|
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
512
|
-
statCard(fmtTokens(stats.totals.cache), 'Cache
|
|
531
|
+
statCard(fmtTokens(stats.totals.cache), 'Cache Reads'),
|
|
532
|
+
statCard(fmtTokens(stats.totals.cacheWrite), 'Cache Writes'),
|
|
513
533
|
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
514
534
|
statCard(String(stats.sessions), 'Sessions'),
|
|
515
535
|
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
@@ -677,7 +697,7 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
677
697
|
lines.push('');
|
|
678
698
|
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
679
699
|
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
680
|
-
lines.push(' ' + pc.dim('Costs
|
|
700
|
+
lines.push(' ' + pc.dim('Costs use the oauth-router catalog when available; cache reads and writes are priced separately.'));
|
|
681
701
|
lines.push('');
|
|
682
702
|
|
|
683
703
|
return lines.join('\n');
|