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
package/.pi/README.md
CHANGED
|
@@ -34,8 +34,8 @@ Inside Pi, use:
|
|
|
34
34
|
- `/takomi build [prompt]` to implement while cross-checking against approved UI/design artifacts
|
|
35
35
|
- `/takomi plan [title]` to create a Genesis-first orchestration session that can expand through Design and Build
|
|
36
36
|
- `/takomi mode direct`, `/takomi mode orchestrate`, or `/takomi mode review` to choose the session operating mode
|
|
37
|
-
- `/takomi gate auto` to continue approved plans automatically
|
|
38
|
-
- `/takomi gate review` to return to the user after each task with results and verification guidance
|
|
37
|
+
- `/takomi gate auto` to continue approved plans automatically and explicitly authorize project-local agents for this session
|
|
38
|
+
- `/takomi gate review` (or `/takomi gate manual`) to return to the user after each task with results and verification guidance and revoke that authorization
|
|
39
39
|
- `/takomi subagents on` or `/takomi subagents off` to allow or disable delegated subagents
|
|
40
40
|
- `/takomi subagents list` to list available Takomi agents without exposing the raw `subagent` tool
|
|
41
41
|
- `/takomi subagents status` to inspect active subagent state
|
|
@@ -109,7 +109,8 @@ So when working on packaging, agents should distinguish between:
|
|
|
109
109
|
- Direct `takomi_subagent` calls build a `TakomiDelegationPlan` before launch. In auto mode the plan launches immediately; in manual mode the plan is returned for review until `confirmLaunch=true` is supplied.
|
|
110
110
|
- The subagent tool supports `conversationId`, so reviewed work can be sent back to the same agent for continuation instead of restarting from scratch.
|
|
111
111
|
- The subagent tool supports Pi-style single, parallel `tasks`, sequential `chain`, explicit `context=fork|fresh`, `async=true`, and native read/control actions such as `action=list`, `action=status`, `action=doctor`, `action=interrupt`, and `action=resume`.
|
|
112
|
-
- The subagent tool supports `agentScope` values of `user`, `project`, and `both`; project-local agents require confirmation by default.
|
|
112
|
+
- The subagent tool supports `agentScope` values of `user`, `project`, and `both`; project-local agents require confirmation by default. The only session authorization bypasses are `TAKOMI_TRUST_PROJECT_AGENTS` and the explicit user `/takomi gate auto` provenance marker; model mode changes plus profile, default, or generic runtime `auto` state do not authorize project agents.
|
|
113
|
+
- The explicit user gate-auto marker is stored in session history so it survives resuming that same session. `/takomi gate review`, `/takomi gate manual`, review mode, and `/takomi-reset` append a revocation; it is not a cross-session or profile setting.
|
|
113
114
|
- The subagent tool also supports per-run `workflow`, `skills`, `model`, `fallbackModels`, `thinking`, `checklist`, `concurrency`, and `worktree` overrides.
|
|
114
115
|
- `takomi_board` records session/state/markdown artifacts only; it does not run subagents.
|
|
115
116
|
- Pi's default `subagent` tool remains owned by the user-level/default subagent extension to avoid tool-name conflicts; Takomi uses `takomi_subagent` as the preferred lifecycle-aware execution interface and renders it with the native Pi-style result surface.
|
|
@@ -41,12 +41,12 @@ The extension ships with two default upstream profiles:
|
|
|
41
41
|
- auth mode: OAuth
|
|
42
42
|
- oauth provider: `openai-codex`
|
|
43
43
|
- api: `openai-codex-responses`
|
|
44
|
-
- default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
|
|
44
|
+
- default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
|
|
45
45
|
|
|
46
46
|
2. `openai-compatible`
|
|
47
47
|
- auth mode: API key fallback
|
|
48
48
|
- api: `openai-responses`
|
|
49
|
-
- default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
|
|
49
|
+
- default models: `gpt-5.4-mini`, `gpt-5.4`, `gpt-5.5`, `gpt-5.6-luna`, `gpt-5.6-sol`, `gpt-5.6-terra`
|
|
50
50
|
|
|
51
51
|
Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoints, change model catalogs, or tune retry behavior. By default client-side transport failures such as `Codex SSE response headers timed out after 10000ms` retry the same account 5 times with exponential backoff (`5s`, `10s`, `20s`, `40s`, then capped at `60s`) before router failover. While this is happening, Pi's footer shows the active retry/failover/error state so the UI no longer looks frozen. These failures are recorded but do not cool down an account unless `clientNetworkPenaltyMs` is set above `0`.
|
|
52
52
|
|
|
@@ -62,7 +62,7 @@ Edit `~/.pi/agent/oauth-router/config.json` to add more upstreams, swap endpoint
|
|
|
62
62
|
- `/router-status`
|
|
63
63
|
- `/router-usage`
|
|
64
64
|
5. Select a model:
|
|
65
|
-
- `oauth-router/gpt-5.6-sol`
|
|
65
|
+
- `oauth-router/gpt-5.6-sol`
|
|
66
66
|
|
|
67
67
|
## Account storage format
|
|
68
68
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { createAccountFromUpstream } from "./oauth-flow.ts";
|
|
3
|
+
import { createRouterReportLines, createRouterReportWidget, ROUTER_REPORT_DISMISS_HINT, ROUTER_REPORT_WIDGET_KEY } from "./report-ui.ts";
|
|
3
4
|
import type { RouterStatusRow, RouterUsageSummary, RouterUsageWindowSummary, RoutingPolicyName, StoredRouterAccount } from "./types.ts";
|
|
4
5
|
import { RouterRuntime } from "./provider.ts";
|
|
5
6
|
|
|
@@ -273,16 +274,32 @@ export function formatUsageRawReport(runtime: RouterRuntime, accountId?: string)
|
|
|
273
274
|
return ["# oauth-router usage raw", "", ...rows].join("\n");
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
ctx.
|
|
283
|
-
|
|
277
|
+
export function emitRouterReport(ctx: ExtensionCommandContext, text: string) {
|
|
278
|
+
// A single key gives every report predictable replacement semantics. Add the
|
|
279
|
+
// dismiss affordance at this presentation boundary, not to formatter output,
|
|
280
|
+
// so model/source report text remains unchanged. Omitting options uses Pi's
|
|
281
|
+
// default above-editor placement for both TUI and RPC widgets.
|
|
282
|
+
const reportText = `${ROUTER_REPORT_DISMISS_HINT}\n\n${text}`;
|
|
283
|
+
if (ctx.mode === "tui") {
|
|
284
|
+
ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, createRouterReportWidget(reportText));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// RPC accepts string-array widgets only; ctx.hasUI is true there, so ctx.mode
|
|
288
|
+
// is the capability signal that prevents its visible report from being ignored.
|
|
289
|
+
ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, createRouterReportLines(reportText));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function clearRouterReport(ctx: ExtensionCommandContext) {
|
|
293
|
+
ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, undefined);
|
|
284
294
|
}
|
|
285
295
|
|
|
296
|
+
export type RouterReportEmitter = (ctx: ExtensionCommandContext, text: string) => void;
|
|
297
|
+
|
|
298
|
+
export type RouterReportControls = {
|
|
299
|
+
show: RouterReportEmitter;
|
|
300
|
+
clear: (ctx: ExtensionCommandContext) => void;
|
|
301
|
+
};
|
|
302
|
+
|
|
286
303
|
async function pickUpstream(runtime: RouterRuntime, ctx: ExtensionCommandContext, requestedId?: string) {
|
|
287
304
|
const upstreams = runtime.listUpstreams().filter((upstream) => upstream.enabled);
|
|
288
305
|
if (upstreams.length === 0) throw new Error("No enabled upstreams are configured");
|
|
@@ -389,12 +406,11 @@ function setFooterStatus(ctx: ExtensionCommandContext, runtime: RouterRuntime) {
|
|
|
389
406
|
ctx.ui.setStatus("oauth-router", `oauth-router ${healthy}/${rows.length || 0} healthy | ${runtime.getPolicy()}`);
|
|
390
407
|
}
|
|
391
408
|
|
|
392
|
-
function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[]) {
|
|
393
|
-
|
|
394
|
-
ctx.ui.notify(title, "info");
|
|
409
|
+
function showCommandHint(ctx: ExtensionCommandContext, title: string, lines: string[], showReport: RouterReportEmitter = emitRouterReport) {
|
|
410
|
+
showReport(ctx, [`# ${title}`, "", ...lines].join("\n"));
|
|
395
411
|
}
|
|
396
412
|
|
|
397
|
-
async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext) {
|
|
413
|
+
async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: ExtensionCommandContext, showReport: RouterReportEmitter = emitRouterReport) {
|
|
398
414
|
const [command = "help", first, ...rest] = parseArgs(args);
|
|
399
415
|
|
|
400
416
|
switch (command) {
|
|
@@ -419,18 +435,17 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
419
435
|
runtime.clearAccountHealth(duplicate.id);
|
|
420
436
|
const usageMessage = await refreshUsageAfterCredentialChange(runtime, duplicate.id);
|
|
421
437
|
setFooterStatus(ctx, runtime);
|
|
422
|
-
|
|
438
|
+
showReport(ctx, `Updated existing account ${duplicate.id} (${duplicate.label}) with fresh credentials instead of adding a duplicate.${usageMessage}`);
|
|
423
439
|
return;
|
|
424
440
|
}
|
|
425
441
|
runtime.addAccount(account);
|
|
426
442
|
const usageMessage = await refreshUsageAfterCredentialChange(runtime, account.id);
|
|
427
443
|
setFooterStatus(ctx, runtime);
|
|
428
|
-
|
|
429
|
-
ctx.ui.notify(`Added ${account.id}`, "info");
|
|
444
|
+
showReport(ctx, `Added account ${account.id} (${account.label}) for upstream ${upstream.id}.${usageMessage}`);
|
|
430
445
|
return;
|
|
431
446
|
}
|
|
432
447
|
case "list": {
|
|
433
|
-
|
|
448
|
+
showReport(ctx, formatAccountsReport(runtime));
|
|
434
449
|
setFooterStatus(ctx, runtime);
|
|
435
450
|
return;
|
|
436
451
|
}
|
|
@@ -443,7 +458,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
443
458
|
}
|
|
444
459
|
runtime.removeAccount(account.id);
|
|
445
460
|
setFooterStatus(ctx, runtime);
|
|
446
|
-
|
|
461
|
+
showReport(ctx, `Removed account ${account.id}.`);
|
|
447
462
|
return;
|
|
448
463
|
}
|
|
449
464
|
case "rename": {
|
|
@@ -451,7 +466,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
451
466
|
const label = await getRequiredLabel(ctx, account.label, rest.join(" "));
|
|
452
467
|
runtime.renameAccount(account.id, label);
|
|
453
468
|
setFooterStatus(ctx, runtime);
|
|
454
|
-
|
|
469
|
+
showReport(ctx, `Renamed account ${account.id} to ${label}.`);
|
|
455
470
|
return;
|
|
456
471
|
}
|
|
457
472
|
case "relogin": {
|
|
@@ -476,7 +491,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
476
491
|
runtime.clearAccountHealth(existing.id);
|
|
477
492
|
const usageMessage = await refreshUsageAfterCredentialChange(runtime, existing.id);
|
|
478
493
|
setFooterStatus(ctx, runtime);
|
|
479
|
-
|
|
494
|
+
showReport(ctx, `Re-logged account ${existing.id} (${existing.label}) and cleared auth state.${usageMessage}`);
|
|
480
495
|
return;
|
|
481
496
|
}
|
|
482
497
|
case "refresh": {
|
|
@@ -484,12 +499,12 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
484
499
|
const refreshed = await runtime.refreshAccount(account.id);
|
|
485
500
|
const usageMessage = await refreshUsageAfterCredentialChange(runtime, refreshed.id);
|
|
486
501
|
setFooterStatus(ctx, runtime);
|
|
487
|
-
|
|
502
|
+
showReport(ctx, `Refreshed account ${refreshed.id} (${refreshed.label}).${usageMessage}`);
|
|
488
503
|
return;
|
|
489
504
|
}
|
|
490
505
|
case "help":
|
|
491
506
|
default: {
|
|
492
|
-
|
|
507
|
+
showReport(
|
|
493
508
|
ctx,
|
|
494
509
|
[
|
|
495
510
|
"# oauth-router commands",
|
|
@@ -510,6 +525,7 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
510
525
|
"- /router-disable <id>",
|
|
511
526
|
"- /router-policy <round-robin|weighted-round-robin>",
|
|
512
527
|
"- /router-weight <id> <n>",
|
|
528
|
+
"- /router-clear (dismiss the current report)",
|
|
513
529
|
].join("\n"),
|
|
514
530
|
);
|
|
515
531
|
return;
|
|
@@ -517,16 +533,27 @@ async function handleRouterLogin(runtime: RouterRuntime, args: string, ctx: Exte
|
|
|
517
533
|
}
|
|
518
534
|
}
|
|
519
535
|
|
|
520
|
-
export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime) {
|
|
536
|
+
export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime, reportControls?: RouterReportControls) {
|
|
537
|
+
const showReport = reportControls?.show ?? emitRouterReport;
|
|
538
|
+
const clearReport = reportControls?.clear ?? clearRouterReport;
|
|
539
|
+
|
|
521
540
|
pi.registerCommand("router-login", {
|
|
522
541
|
description: "Manage oauth-router accounts",
|
|
523
|
-
handler: async (args, ctx) => handleRouterLogin(runtime, args || "", ctx),
|
|
542
|
+
handler: async (args, ctx) => handleRouterLogin(runtime, args || "", ctx, showReport),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
pi.registerCommand("router-clear", {
|
|
546
|
+
description: "Dismiss the current oauth-router report widget",
|
|
547
|
+
handler: async (_args, ctx) => {
|
|
548
|
+
clearReport(ctx);
|
|
549
|
+
setFooterStatus(ctx, runtime);
|
|
550
|
+
},
|
|
524
551
|
});
|
|
525
552
|
|
|
526
553
|
pi.registerCommand("router-status", {
|
|
527
554
|
description: "Show oauth-router health and routing state",
|
|
528
555
|
handler: async (_args, ctx) => {
|
|
529
|
-
|
|
556
|
+
showReport(ctx, formatStatusReport(runtime));
|
|
530
557
|
setFooterStatus(ctx, runtime);
|
|
531
558
|
},
|
|
532
559
|
});
|
|
@@ -534,7 +561,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
534
561
|
pi.registerCommand("router-accounts", {
|
|
535
562
|
description: "Show compact oauth-router account list",
|
|
536
563
|
handler: async (_args, ctx) => {
|
|
537
|
-
|
|
564
|
+
showReport(ctx, formatAccountsReport(runtime));
|
|
538
565
|
setFooterStatus(ctx, runtime);
|
|
539
566
|
},
|
|
540
567
|
});
|
|
@@ -543,7 +570,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
543
570
|
description: "Show oauth-router visual provider quota and local usage",
|
|
544
571
|
handler: async (args, ctx) => {
|
|
545
572
|
const [id] = parseArgs(args || "");
|
|
546
|
-
|
|
573
|
+
showReport(ctx, formatUsageReport(runtime, id));
|
|
547
574
|
setFooterStatus(ctx, runtime);
|
|
548
575
|
},
|
|
549
576
|
});
|
|
@@ -552,7 +579,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
552
579
|
description: "Show raw oauth-router usage/provider quota details",
|
|
553
580
|
handler: async (args, ctx) => {
|
|
554
581
|
const [id] = parseArgs(args || "");
|
|
555
|
-
|
|
582
|
+
showReport(ctx, formatUsageRawReport(runtime, id));
|
|
556
583
|
setFooterStatus(ctx, runtime);
|
|
557
584
|
},
|
|
558
585
|
});
|
|
@@ -561,7 +588,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
561
588
|
description: "Alias for visual oauth-router usage/quota",
|
|
562
589
|
handler: async (args, ctx) => {
|
|
563
590
|
const [id] = parseArgs(args || "");
|
|
564
|
-
|
|
591
|
+
showReport(ctx, formatUsageReport(runtime, id));
|
|
565
592
|
setFooterStatus(ctx, runtime);
|
|
566
593
|
},
|
|
567
594
|
});
|
|
@@ -584,7 +611,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
584
611
|
}
|
|
585
612
|
|
|
586
613
|
const report = formatUsageReport(runtime, target === "all" ? undefined : target);
|
|
587
|
-
|
|
614
|
+
showReport(ctx, failures.length ? [report, "", "## Refresh failures", ...failures].join("\n") : report);
|
|
588
615
|
if (failures.length) ctx.ui.notify(`oauth-router usage refreshed with ${failures.length} failure(s)`, "error");
|
|
589
616
|
setFooterStatus(ctx, runtime);
|
|
590
617
|
},
|
|
@@ -598,7 +625,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
598
625
|
const label = await getRequiredLabel(ctx, account.label, labelParts.join(" "));
|
|
599
626
|
runtime.renameAccount(account.id, label);
|
|
600
627
|
setFooterStatus(ctx, runtime);
|
|
601
|
-
|
|
628
|
+
showReport(ctx, `Renamed account ${account.id} to ${label}.`);
|
|
602
629
|
},
|
|
603
630
|
});
|
|
604
631
|
|
|
@@ -613,13 +640,13 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
613
640
|
}
|
|
614
641
|
runtime.removeAccount(account.id);
|
|
615
642
|
setFooterStatus(ctx, runtime);
|
|
616
|
-
|
|
643
|
+
showReport(ctx, `Deleted account ${account.id}.`);
|
|
617
644
|
},
|
|
618
645
|
});
|
|
619
646
|
|
|
620
647
|
pi.registerCommand("router-relogin", {
|
|
621
648
|
description: "Re-login and recover an oauth-router account",
|
|
622
|
-
handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx),
|
|
649
|
+
handler: async (args, ctx) => handleRouterLogin(runtime, `relogin ${args || ""}`, ctx, showReport),
|
|
623
650
|
});
|
|
624
651
|
|
|
625
652
|
pi.registerCommand("router-enable", {
|
|
@@ -629,7 +656,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
629
656
|
const account = await pickAccount(runtime, ctx, id, "Choose account to enable");
|
|
630
657
|
runtime.setEnabled(account.id, true);
|
|
631
658
|
setFooterStatus(ctx, runtime);
|
|
632
|
-
|
|
659
|
+
showReport(ctx, `Enabled account ${account.id}.`);
|
|
633
660
|
},
|
|
634
661
|
});
|
|
635
662
|
|
|
@@ -640,7 +667,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
640
667
|
const account = await pickAccount(runtime, ctx, id, "Choose account to disable");
|
|
641
668
|
runtime.setEnabled(account.id, false);
|
|
642
669
|
setFooterStatus(ctx, runtime);
|
|
643
|
-
|
|
670
|
+
showReport(ctx, `Disabled account ${account.id}.`);
|
|
644
671
|
},
|
|
645
672
|
});
|
|
646
673
|
|
|
@@ -655,7 +682,7 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
655
682
|
"",
|
|
656
683
|
"Usage: /router-policy <round-robin|weighted-round-robin>",
|
|
657
684
|
"Aliases: rr, wrr, weighted",
|
|
658
|
-
]);
|
|
685
|
+
], showReport);
|
|
659
686
|
return;
|
|
660
687
|
}
|
|
661
688
|
if (!policy) {
|
|
@@ -664,12 +691,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
664
691
|
"",
|
|
665
692
|
`Current policy: ${runtime.getPolicy()}`,
|
|
666
693
|
"Valid values: round-robin, weighted-round-robin",
|
|
667
|
-
]);
|
|
694
|
+
], showReport);
|
|
668
695
|
return;
|
|
669
696
|
}
|
|
670
697
|
runtime.setPolicy(policy);
|
|
671
698
|
setFooterStatus(ctx, runtime);
|
|
672
|
-
|
|
699
|
+
showReport(ctx, `Routing policy set to ${policy}.`);
|
|
673
700
|
},
|
|
674
701
|
});
|
|
675
702
|
|
|
@@ -684,12 +711,12 @@ export function registerRouterCommands(pi: ExtensionAPI, runtime: RouterRuntime)
|
|
|
684
711
|
weight = Number(response);
|
|
685
712
|
}
|
|
686
713
|
if (!Number.isFinite(weight)) {
|
|
687
|
-
showCommandHint(ctx, "router-weight", ["Usage: /router-weight <id> <n>", "Example: /router-weight acct_ab12cd34 3"]);
|
|
714
|
+
showCommandHint(ctx, "router-weight", ["Usage: /router-weight <id> <n>", "Example: /router-weight acct_ab12cd34 3"], showReport);
|
|
688
715
|
return;
|
|
689
716
|
}
|
|
690
717
|
runtime.setWeight(account.id, weight);
|
|
691
718
|
setFooterStatus(ctx, runtime);
|
|
692
|
-
|
|
719
|
+
showReport(ctx, `Updated weight for ${account.id} to ${Math.max(1, Math.floor(weight))}.`);
|
|
693
720
|
},
|
|
694
721
|
});
|
|
695
722
|
}
|
|
@@ -11,7 +11,7 @@ export const STATE_PATH = join(DATA_ROOT, "state.json");
|
|
|
11
11
|
|
|
12
12
|
const LEGACY_CODEX_CONTEXT_WINDOW = 272000;
|
|
13
13
|
const SAFE_CODEX_CONTEXT_WINDOW = 240000;
|
|
14
|
-
const CODEX_MODEL_IDS = new Set(["gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
|
|
14
|
+
const CODEX_MODEL_IDS = new Set(["gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"]);
|
|
15
15
|
|
|
16
16
|
const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
17
17
|
{
|
|
@@ -32,42 +32,42 @@ const DEFAULT_MODELS: RouterModelConfig[] = [
|
|
|
32
32
|
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
33
33
|
maxTokens: 128000,
|
|
34
34
|
},
|
|
35
|
-
{
|
|
36
|
-
id: "gpt-5.5",
|
|
35
|
+
{
|
|
36
|
+
id: "gpt-5.5",
|
|
37
37
|
name: "GPT-5.5",
|
|
38
38
|
reasoning: true,
|
|
39
39
|
input: ["text", "image"],
|
|
40
40
|
cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
41
41
|
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
42
|
-
maxTokens: 128000,
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
id: "gpt-5.6-luna",
|
|
46
|
-
name: "GPT-5.6 Luna",
|
|
47
|
-
reasoning: true,
|
|
48
|
-
input: ["text", "image"],
|
|
49
|
-
cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
|
|
50
|
-
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
51
|
-
maxTokens: 128000,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
id: "gpt-5.6-sol",
|
|
55
|
-
name: "GPT-5.6 Sol",
|
|
56
|
-
reasoning: true,
|
|
57
|
-
input: ["text", "image"],
|
|
58
|
-
cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
59
|
-
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
60
|
-
maxTokens: 128000,
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
id: "gpt-5.6-terra",
|
|
64
|
-
name: "GPT-5.6 Terra",
|
|
65
|
-
reasoning: true,
|
|
66
|
-
input: ["text", "image"],
|
|
67
|
-
cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.125 },
|
|
68
|
-
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
69
|
-
maxTokens: 128000,
|
|
70
|
-
},
|
|
42
|
+
maxTokens: 128000,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "gpt-5.6-luna",
|
|
46
|
+
name: "GPT-5.6 Luna",
|
|
47
|
+
reasoning: true,
|
|
48
|
+
input: ["text", "image"],
|
|
49
|
+
cost: { input: 1.00, output: 6.00, cacheRead: 0.10, cacheWrite: 1.25 },
|
|
50
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
51
|
+
maxTokens: 128000,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "gpt-5.6-sol",
|
|
55
|
+
name: "GPT-5.6 Sol",
|
|
56
|
+
reasoning: true,
|
|
57
|
+
input: ["text", "image"],
|
|
58
|
+
cost: { input: 5.00, output: 30.00, cacheRead: 0.50, cacheWrite: 6.25 },
|
|
59
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
60
|
+
maxTokens: 128000,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "gpt-5.6-terra",
|
|
64
|
+
name: "GPT-5.6 Terra",
|
|
65
|
+
reasoning: true,
|
|
66
|
+
input: ["text", "image"],
|
|
67
|
+
cost: { input: 2.50, output: 15.00, cacheRead: 0.25, cacheWrite: 3.125 },
|
|
68
|
+
contextWindow: SAFE_CODEX_CONTEXT_WINDOW,
|
|
69
|
+
maxTokens: 128000,
|
|
70
|
+
},
|
|
71
71
|
];
|
|
72
72
|
|
|
73
73
|
const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
|
|
@@ -79,7 +79,7 @@ const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
|
|
|
79
79
|
api: "openai-responses",
|
|
80
80
|
authMode: "api-key",
|
|
81
81
|
enabled: true,
|
|
82
|
-
modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
82
|
+
modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
id: "chatgpt-codex",
|
|
@@ -90,7 +90,7 @@ const DEFAULT_UPSTREAMS: RouterUpstreamConfig[] = [
|
|
|
90
90
|
authMode: "oauth",
|
|
91
91
|
oauthProviderId: "openai-codex",
|
|
92
92
|
enabled: true,
|
|
93
|
-
modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
93
|
+
modelIds: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
94
94
|
usageProbe: {
|
|
95
95
|
enabled: true,
|
|
96
96
|
timeoutMs: 8_000,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { emitRouterReport, formatStatusReport, registerRouterCommands, type RouterReportControls } from "./commands.ts";
|
|
3
|
+
import { ROUTER_REPORT_WIDGET_KEY } from "./report-ui.ts";
|
|
3
4
|
import { registerRouterProvider, RouterRuntime } from "./provider.ts";
|
|
4
5
|
import type { RouterUiEvent } from "./types.ts";
|
|
5
6
|
|
|
@@ -64,8 +65,33 @@ function notifyActivity(ctx: ExtensionContext, event: RouterUiEvent) {
|
|
|
64
65
|
ctx.ui.notify(message, level);
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false) {
|
|
68
|
+
function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyOnLoad = false): RouterReportControls {
|
|
68
69
|
let activeCtx: ExtensionContext | undefined;
|
|
70
|
+
let reportGeneration = 0;
|
|
71
|
+
let activeReport: { generation: number; ctx: ExtensionContext } | undefined;
|
|
72
|
+
|
|
73
|
+
const clearReport = (fallbackCtx?: ExtensionContext, clearFallback = false) => {
|
|
74
|
+
const report = activeReport;
|
|
75
|
+
const ctx = report?.ctx ?? (clearFallback ? fallbackCtx : undefined);
|
|
76
|
+
if (!ctx) return;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
ctx.ui.setWidget(ROUTER_REPORT_WIDGET_KEY, undefined);
|
|
80
|
+
} catch {
|
|
81
|
+
// Widget lifecycle is best-effort and must not interrupt agent activity.
|
|
82
|
+
} finally {
|
|
83
|
+
// A re-entrant command can replace the report while the old widget clears.
|
|
84
|
+
// Its newer generation remains active instead of being erased by this clear.
|
|
85
|
+
if (report && activeReport?.generation === report.generation) activeReport = undefined;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const showReport: RouterReportControls["show"] = (ctx, text) => {
|
|
90
|
+
const report = { generation: ++reportGeneration, ctx };
|
|
91
|
+
activeCtx = ctx;
|
|
92
|
+
activeReport = report;
|
|
93
|
+
emitRouterReport(ctx, text);
|
|
94
|
+
};
|
|
69
95
|
|
|
70
96
|
const setBaseStatus = (ctx: ExtensionContext) => {
|
|
71
97
|
activeCtx = ctx;
|
|
@@ -84,14 +110,29 @@ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyO
|
|
|
84
110
|
});
|
|
85
111
|
|
|
86
112
|
pi.on("session_start", async (_event, ctx) => {
|
|
113
|
+
// A session replacement owns a fresh transport, so clear the old transport
|
|
114
|
+
// before accepting reports for the new one.
|
|
115
|
+
clearReport(activeCtx, true);
|
|
87
116
|
setBaseStatus(ctx);
|
|
88
117
|
if (notifyOnLoad) ctx.ui.notify("oauth-router loaded", "info");
|
|
89
118
|
});
|
|
90
119
|
|
|
120
|
+
pi.on("input", async (event, ctx) => {
|
|
121
|
+
// Registered slash commands run before Pi emits input and return without an
|
|
122
|
+
// input event. Ordinary text/image input reaches this handler before its
|
|
123
|
+
// agent starts, which is the intended transient-dismissal boundary.
|
|
124
|
+
if (event.text.trim() || event.images?.length) clearReport(ctx);
|
|
125
|
+
});
|
|
126
|
+
|
|
91
127
|
pi.on("agent_start", async (_event, ctx) => {
|
|
128
|
+
clearReport(ctx);
|
|
92
129
|
setBaseStatus(ctx);
|
|
93
130
|
});
|
|
94
131
|
|
|
132
|
+
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
133
|
+
clearReport(ctx);
|
|
134
|
+
});
|
|
135
|
+
|
|
95
136
|
pi.on("turn_start", async (_event, ctx) => {
|
|
96
137
|
setBaseStatus(ctx);
|
|
97
138
|
});
|
|
@@ -105,26 +146,26 @@ function installRouterUiBridge(pi: ExtensionAPI, runtime: RouterRuntime, notifyO
|
|
|
105
146
|
});
|
|
106
147
|
|
|
107
148
|
pi.on("session_shutdown", async () => {
|
|
149
|
+
// The old context owns the RPC transport. Clear its report before releasing
|
|
150
|
+
// it so clients do not retain a widget across a session replacement.
|
|
151
|
+
clearReport(activeCtx, true);
|
|
108
152
|
activeCtx = undefined;
|
|
109
153
|
});
|
|
154
|
+
|
|
155
|
+
return { show: showReport, clear: (ctx) => clearReport(ctx, true) };
|
|
110
156
|
}
|
|
111
157
|
|
|
112
158
|
export default function (pi: ExtensionAPI) {
|
|
113
159
|
const runtime = new RouterRuntime();
|
|
114
160
|
|
|
115
161
|
registerRouterProvider(pi, runtime);
|
|
116
|
-
|
|
117
|
-
|
|
162
|
+
const reportControls = installRouterUiBridge(pi, runtime, false);
|
|
163
|
+
registerRouterCommands(pi, runtime, reportControls);
|
|
118
164
|
|
|
119
165
|
pi.registerCommand("router-debug-report", {
|
|
120
166
|
description: "Show a detailed oauth-router report in the UI only",
|
|
121
167
|
handler: async (_args, ctx) => {
|
|
122
|
-
|
|
123
|
-
ctx.ui.setWidget("oauth-router-report", [
|
|
124
|
-
"oauth-router debug report (UI-only; not sent to the agent)",
|
|
125
|
-
...report.split(/\r?\n/),
|
|
126
|
-
], { placement: "belowEditor" });
|
|
127
|
-
ctx.ui.notify("oauth-router debug report updated", "info");
|
|
168
|
+
reportControls.show(ctx, formatStatusReport(runtime));
|
|
128
169
|
},
|
|
129
170
|
});
|
|
130
171
|
}
|