takomi 2.1.30 → 2.1.32

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.
Files changed (28) hide show
  1. package/.pi/extensions/notify-sound/index.ts +262 -262
  2. package/.pi/extensions/oauth-router/README.md +2 -1
  3. package/.pi/extensions/oauth-router/index.ts +130 -41
  4. package/.pi/extensions/oauth-router/provider.ts +115 -3
  5. package/.pi/extensions/oauth-router/types.ts +18 -0
  6. package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +1 -1
  7. package/.pi/extensions/takomi-context-manager/index.ts +1 -1
  8. package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +1 -1
  9. package/.pi/extensions/takomi-context-manager/policy-tools.ts +1 -1
  10. package/.pi/extensions/takomi-context-manager/prerequisite-gates.ts +1 -1
  11. package/.pi/extensions/takomi-context-manager/skill-tools.ts +1 -1
  12. package/.pi/extensions/takomi-runtime/commands.ts +1 -1
  13. package/.pi/extensions/takomi-runtime/context-panel.ts +708 -708
  14. package/.pi/extensions/takomi-runtime/index.ts +2 -2
  15. package/.pi/extensions/takomi-runtime/shared.ts +1 -1
  16. package/.pi/extensions/takomi-runtime/subagent-controller.ts +1 -1
  17. package/.pi/extensions/takomi-runtime/subagent-render.ts +1 -1
  18. package/.pi/extensions/takomi-runtime/subagent-types.ts +11 -11
  19. package/.pi/extensions/takomi-runtime/ui.ts +2 -2
  20. package/.pi/extensions/takomi-subagents/agents.ts +1 -1
  21. package/.pi/extensions/takomi-subagents/index.ts +1 -1
  22. package/.pi/extensions/takomi-subagents/native-render.ts +3 -3
  23. package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +2 -2
  24. package/.pi/extensions/takomi-subagents/tool-runner.ts +1 -1
  25. package/package.json +5 -9
  26. package/src/doctor.js +1 -1
  27. package/src/pi-harness.js +34 -11
  28. package/src/pi-optional-features.js +1 -6
@@ -1,41 +1,130 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { registerRouterCommands, formatStatusReport } from "./commands.ts";
3
- import { registerRouterProvider, RouterRuntime } from "./provider.ts";
4
-
5
- function updateFooter(pi: ExtensionAPI, runtime: RouterRuntime, notify = false) {
6
- pi.on("session_start", async (_event, ctx) => {
7
- const rows = runtime.getStatusRows();
8
- const healthy = rows.filter((row) => {
9
- if (!row.enabled) return false;
10
- if (row.authHealth !== "ok") return false;
11
- if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
12
- if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
13
- return true;
14
- }).length;
15
-
16
- ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
17
- if (notify) {
18
- ctx.ui.notify("oauth-router loaded", "info");
19
- }
20
- });
21
- }
22
-
23
- export default function (pi: ExtensionAPI) {
24
- const runtime = new RouterRuntime();
25
-
26
- registerRouterProvider(pi, runtime);
27
- registerRouterCommands(pi, runtime);
28
- updateFooter(pi, runtime, false);
29
-
30
- pi.registerCommand("router-debug-report", {
31
- description: "Emit a detailed oauth-router report",
32
- handler: async () => {
33
- pi.sendMessage({
34
- customType: "oauth-router",
35
- content: formatStatusReport(runtime),
36
- display: true,
37
- details: { source: "oauth-router", debug: true },
38
- });
39
- },
40
- });
41
- }
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { registerRouterCommands, formatStatusReport } from "./commands.ts";
3
+ import { registerRouterProvider, RouterRuntime } from "./provider.ts";
4
+ import type { RouterUiEvent } from "./types.ts";
5
+
6
+ function getHealthSummary(runtime: RouterRuntime) {
7
+ const rows = runtime.getStatusRows();
8
+ const healthy = rows.filter((row) => {
9
+ if (!row.enabled) return false;
10
+ if (row.authHealth !== "ok") return false;
11
+ if (row.cooldownUntil && row.cooldownUntil > Date.now()) return false;
12
+ if (row.penaltyUntil && row.penaltyUntil > Date.now()) return false;
13
+ return true;
14
+ }).length;
15
+
16
+ return `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`;
17
+ }
18
+
19
+ function formatDuration(ms?: number): string {
20
+ if (!ms || ms <= 0) return "now";
21
+ if (ms < 1_000) return `${Math.round(ms)}ms`;
22
+ return `${Math.round(ms / 1_000)}s`;
23
+ }
24
+
25
+ function truncate(text: string | undefined, max = 110): string {
26
+ if (!text) return "";
27
+ const clean = text.replace(/\s+/g, " ").trim();
28
+ return clean.length > max ? `${clean.slice(0, Math.max(0, max - 1))}…` : clean;
29
+ }
30
+
31
+ function formatRoute(event: RouterUiEvent): string {
32
+ const parts = [event.accountLabel || event.accountId, event.upstreamId].filter(Boolean);
33
+ return parts.length ? parts.join(" @ ") : "router";
34
+ }
35
+
36
+ function formatActivityStatus(event: RouterUiEvent): string {
37
+ const route = formatRoute(event);
38
+ const model = event.modelId ? `${event.modelId} ` : "";
39
+ const reason = truncate(event.message, 70);
40
+
41
+ switch (event.phase) {
42
+ case "attempt": {
43
+ const retry = event.retryAttempt && event.maxRetries
44
+ ? ` retry ${event.retryAttempt}/${event.maxRetries}`
45
+ : "";
46
+ return `oauth-router contacting ${model}via ${route}${retry}`;
47
+ }
48
+ case "retry":
49
+ return `oauth-router retry ${event.retryAttempt}/${event.maxRetries} in ${formatDuration(event.delayMs)} | ${reason || event.failureKind || "network error"}`;
50
+ case "failover":
51
+ return `oauth-router failover from ${route} | ${reason || event.failureKind || "upstream error"}`;
52
+ case "success":
53
+ return `oauth-router ok: ${model}via ${route}`;
54
+ case "error":
55
+ return `oauth-router error | ${reason || event.failureKind || "request failed"}`;
56
+ }
57
+ }
58
+
59
+ function notifyActivity(ctx: ExtensionContext, event: RouterUiEvent) {
60
+ if (event.phase === "attempt" || event.phase === "success") return;
61
+
62
+ const message = formatActivityStatus(event);
63
+ const level = event.phase === "error" ? "error" : "info";
64
+ ctx.ui.notify(message, level);
65
+ }
66
+
67
+ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false) {
68
+ let activeCtx: ExtensionContext | undefined;
69
+
70
+ const setBaseStatus = (ctx: ExtensionContext) => {
71
+ activeCtx = ctx;
72
+ if (ctx.hasUI) ctx.ui.setStatus("oauth-router", getHealthSummary(runtime));
73
+ };
74
+
75
+ runtime.setUiReporter((event) => {
76
+ const ctx = activeCtx;
77
+ if (!ctx?.hasUI) return;
78
+ try {
79
+ ctx.ui.setStatus("oauth-router", formatActivityStatus(event));
80
+ notifyActivity(ctx, event);
81
+ } catch {
82
+ // Best-effort UI only; never disturb provider streaming.
83
+ }
84
+ });
85
+
86
+ pi.on("session_start", async (_event, ctx) => {
87
+ setBaseStatus(ctx);
88
+ if (notifyOnLoad) ctx.ui.notify("oauth-router loaded", "info");
89
+ });
90
+
91
+ pi.on("agent_start", async (_event, ctx) => {
92
+ setBaseStatus(ctx);
93
+ });
94
+
95
+ pi.on("turn_start", async (_event, ctx) => {
96
+ setBaseStatus(ctx);
97
+ });
98
+
99
+ pi.on("turn_end", async (_event, ctx) => {
100
+ setBaseStatus(ctx);
101
+ });
102
+
103
+ pi.on("agent_end", async (_event, ctx) => {
104
+ setBaseStatus(ctx);
105
+ });
106
+
107
+ pi.on("session_shutdown", async () => {
108
+ activeCtx = undefined;
109
+ });
110
+ }
111
+
112
+ export default function (pi: ExtensionAPI) {
113
+ const runtime = new RouterRuntime();
114
+
115
+ registerRouterProvider(pi, runtime);
116
+ registerRouterCommands(pi, runtime);
117
+ installRouterUiBridge(pi, runtime, false);
118
+
119
+ pi.registerCommand("router-debug-report", {
120
+ description: "Emit a detailed oauth-router report",
121
+ handler: async () => {
122
+ pi.sendMessage({
123
+ customType: "oauth-router",
124
+ content: formatStatusReport(runtime),
125
+ display: true,
126
+ details: { source: "oauth-router", debug: true },
127
+ });
128
+ },
129
+ });
130
+ }
@@ -25,6 +25,8 @@ import type {
25
25
  RouterErrorMessageInput,
26
26
  RouterModelConfig,
27
27
  RouterStatusRow,
28
+ RouterUiEvent,
29
+ RouterUiReporter,
28
30
  RouterUpstreamConfig,
29
31
  RoutingPolicyName,
30
32
  StoredRouterAccount,
@@ -176,6 +178,7 @@ export class RouterRuntime {
176
178
  private config: RouterConfig;
177
179
  private accounts: RouterAccountStore;
178
180
  private state: RouterStateStore;
181
+ private uiReporter?: RouterUiReporter;
179
182
 
180
183
  constructor() {
181
184
  this.config = loadRouterConfig();
@@ -190,6 +193,18 @@ export class RouterRuntime {
190
193
  this.state.pruneAccountIds(this.accounts.list().map((account) => account.id));
191
194
  }
192
195
 
196
+ setUiReporter(reporter?: RouterUiReporter) {
197
+ this.uiReporter = reporter;
198
+ }
199
+
200
+ private emitUi(event: RouterUiEvent) {
201
+ try {
202
+ this.uiReporter?.(event);
203
+ } catch {
204
+ // UI updates must never affect model streaming or retry behavior.
205
+ }
206
+ }
207
+
193
208
  getConfig(): RouterConfig {
194
209
  this.reloadConfig();
195
210
  return this.config;
@@ -468,17 +483,40 @@ export class RouterRuntime {
468
483
  ): Promise<{ completed: boolean; emittedMeaningfulOutput: boolean; failure?: FailureClassification }> {
469
484
  for (let retryIndex = 0; ; retryIndex += 1) {
470
485
  const result = await this.trySingleAccount(selection, outerStream, context, options);
486
+ const failure = result.failure;
471
487
  const canRetry =
472
488
  !result.completed &&
473
489
  !result.emittedMeaningfulOutput &&
474
- result.failure?.kind === "client-network" &&
490
+ failure?.kind === "client-network" &&
475
491
  retryIndex < this.config.clientNetworkMaxRetries;
476
492
 
477
- if (!canRetry) return result;
493
+ if (!canRetry || !failure) return result;
478
494
 
479
495
  const delayMs = this.getClientNetworkRetryDelayMs(retryIndex);
496
+ this.emitUi({
497
+ phase: "retry",
498
+ modelId,
499
+ accountId: selection.account.id,
500
+ accountLabel: selection.account.label,
501
+ upstreamId: selection.account.upstreamId,
502
+ failureKind: failure.kind,
503
+ status: failure.status,
504
+ message: failure.message,
505
+ retryAttempt: retryIndex + 1,
506
+ maxRetries: this.config.clientNetworkMaxRetries,
507
+ delayMs,
508
+ });
480
509
  await sleep(delayMs, options?.signal);
481
510
  this.state.markAttempt(selection.account.id, modelId);
511
+ this.emitUi({
512
+ phase: "attempt",
513
+ modelId,
514
+ accountId: selection.account.id,
515
+ accountLabel: selection.account.label,
516
+ upstreamId: selection.account.upstreamId,
517
+ retryAttempt: retryIndex + 2,
518
+ maxRetries: this.config.clientNetworkMaxRetries,
519
+ });
482
520
  }
483
521
  }
484
522
 
@@ -521,7 +559,11 @@ export class RouterRuntime {
521
559
  reason: "aborted",
522
560
  error: { ...event.error, stopReason: "aborted", errorMessage: message },
523
561
  });
524
- return { completed: true, emittedMeaningfulOutput };
562
+ return {
563
+ completed: true,
564
+ emittedMeaningfulOutput,
565
+ failure: { kind: "fatal", status: responseStatus, message },
566
+ };
525
567
  }
526
568
 
527
569
  const failure = classifyFailure(responseStatus, responseHeaders, message, this.config);
@@ -596,6 +638,13 @@ export class RouterRuntime {
596
638
  const message = lastFailure
597
639
  ? `No healthy oauth-router accounts remaining after failover. Last error: ${lastFailure.message}`
598
640
  : `No healthy oauth-router accounts are configured for model ${model.id}. Add accounts with /router-login add.`;
641
+ this.emitUi({
642
+ phase: "error",
643
+ modelId: model.id,
644
+ failureKind: lastFailure?.kind,
645
+ status: lastFailure?.status,
646
+ message,
647
+ });
599
648
  outer.push({ type: "error", reason: "error", error: createErrorMessage({ model, message }) });
600
649
  outer.end();
601
650
  return;
@@ -604,6 +653,13 @@ export class RouterRuntime {
604
653
  this.state.advanceCursor(policy, picked.nextCursor);
605
654
  tried.add(picked.selected.account.id);
606
655
  this.state.markAttempt(picked.selected.account.id, model.id);
656
+ this.emitUi({
657
+ phase: "attempt",
658
+ modelId: model.id,
659
+ accountId: picked.selected.account.id,
660
+ accountLabel: picked.selected.account.label,
661
+ upstreamId: picked.selected.account.upstreamId,
662
+ });
607
663
 
608
664
  let selection: DelegateSelection;
609
665
  try {
@@ -617,11 +673,39 @@ export class RouterRuntime {
617
673
  }
618
674
  lastFailure = { kind: "auth", status: 401, message };
619
675
  this.markFailure(picked.selected.account.id, lastFailure);
676
+ this.emitUi({
677
+ phase: "failover",
678
+ modelId: model.id,
679
+ accountId: picked.selected.account.id,
680
+ accountLabel: picked.selected.account.label,
681
+ upstreamId: picked.selected.account.upstreamId,
682
+ failureKind: lastFailure.kind,
683
+ status: lastFailure.status,
684
+ message: lastFailure.message,
685
+ });
620
686
  continue;
621
687
  }
622
688
 
623
689
  const result = await this.tryAccountWithClientNetworkRetries(selection, outer, context, model.id, options);
624
690
  if (result.completed) {
691
+ this.emitUi(result.failure
692
+ ? {
693
+ phase: "error",
694
+ modelId: model.id,
695
+ accountId: selection.account.id,
696
+ accountLabel: selection.account.label,
697
+ upstreamId: selection.account.upstreamId,
698
+ failureKind: result.failure.kind,
699
+ status: result.failure.status,
700
+ message: result.failure.message,
701
+ }
702
+ : {
703
+ phase: "success",
704
+ modelId: model.id,
705
+ accountId: selection.account.id,
706
+ accountLabel: selection.account.label,
707
+ upstreamId: selection.account.upstreamId,
708
+ });
625
709
  outer.end();
626
710
  return;
627
711
  }
@@ -630,13 +714,41 @@ export class RouterRuntime {
630
714
  lastFailure = result.failure;
631
715
 
632
716
  if (emittedMeaningfulOutput) {
717
+ this.emitUi({
718
+ phase: "error",
719
+ modelId: model.id,
720
+ accountId: selection.account.id,
721
+ accountLabel: selection.account.label,
722
+ upstreamId: selection.account.upstreamId,
723
+ failureKind: lastFailure?.kind,
724
+ status: lastFailure?.status,
725
+ message: lastFailure?.message ?? "Upstream failed after partial output; failover is unsafe.",
726
+ });
633
727
  outer.end();
634
728
  return;
635
729
  }
730
+
731
+ if (lastFailure) {
732
+ this.emitUi({
733
+ phase: "failover",
734
+ modelId: model.id,
735
+ accountId: selection.account.id,
736
+ accountLabel: selection.account.label,
737
+ upstreamId: selection.account.upstreamId,
738
+ failureKind: lastFailure.kind,
739
+ status: lastFailure.status,
740
+ message: lastFailure.message,
741
+ });
742
+ }
636
743
  }
637
744
  } catch (error) {
638
745
  const message = error instanceof Error ? error.message : String(error);
639
746
  const aborted = isAbortLike(message, options?.signal);
747
+ this.emitUi({
748
+ phase: "error",
749
+ modelId: model.id,
750
+ message: aborted ? "Request was aborted" : message,
751
+ });
640
752
  outer.push({ type: "error", reason: aborted ? "aborted" : "error", error: createErrorMessage({ model, message, stopReason: aborted ? "aborted" : "error" }) });
641
753
  outer.end();
642
754
  }
@@ -231,6 +231,24 @@ export interface RouterStreamInput {
231
231
  options?: SimpleStreamOptions;
232
232
  }
233
233
 
234
+ export type RouterUiEventPhase = "attempt" | "retry" | "failover" | "success" | "error";
235
+
236
+ export interface RouterUiEvent {
237
+ phase: RouterUiEventPhase;
238
+ modelId?: string;
239
+ accountId?: string;
240
+ accountLabel?: string;
241
+ upstreamId?: string;
242
+ failureKind?: FailureClassification["kind"];
243
+ status?: number;
244
+ message?: string;
245
+ retryAttempt?: number;
246
+ maxRetries?: number;
247
+ delayMs?: number;
248
+ }
249
+
250
+ export type RouterUiReporter = (event: RouterUiEvent) => void;
251
+
234
252
  export interface RouterErrorMessageInput {
235
253
  model: Model<Api>;
236
254
  message: string;
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import type { ContextManagerState } from "./state";
4
4
  import { renderReport } from "./diagnostics";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { loadConfig, DEFAULT_CONFIG } from "./config";
3
3
  import { createState } from "./state";
4
4
  import { collectSkillsFromOptions, collectSkillsFromXml, mergeSkills } from "./skill-registry";
@@ -1,7 +1,7 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
4
+ import type { ExtensionAPI } from "@earendil-works/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";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
3
  import type { ContextManagerState } from "./state";
4
4
  import { syncReportLedger } from "./state";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { ContextManagerConfig } from "./types";
3
3
  import type { ContextManagerState } from "./state";
4
4
  import { recordBlocked, syncReportLedger } from "./state";
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
3
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
4
  import { Type } from "typebox";
5
5
  import type { ContextManagerState } from "./state";
6
6
  import { findSkill, normalizeName, sortedSkills } from "./skill-registry";
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type {
3
3
  TakomiLaunchMode,
4
4
  TakomiRole,