@phi-code-admin/phi-code 0.62.1 → 0.63.0
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/extensions/phi/init.ts +10 -417
- package/extensions/phi/orchestrator.ts +156 -39
- package/package.json +1 -1
package/extensions/phi/init.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Phi Init Extension - Interactive setup wizard for Phi Code
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - benchmark: Test available models with /benchmark, then assign (10-15 min)
|
|
7
|
-
* - manual: User assigns each model role interactively
|
|
4
|
+
* Detects providers (API keys + local endpoints), then lets the user
|
|
5
|
+
* manually assign models to each agent role (code, debug, plan, explore, test, review).
|
|
8
6
|
*
|
|
9
7
|
* Creates ~/.phi/agent/ structure with routing, agents, and memory.
|
|
10
8
|
*/
|
|
@@ -13,7 +11,7 @@ import type { ExtensionAPI } from "phi-code";
|
|
|
13
11
|
import { writeFile, mkdir, copyFile, readdir, access, readFile } from "node:fs/promises";
|
|
14
12
|
import { join, resolve } from "node:path";
|
|
15
13
|
import { homedir } from "node:os";
|
|
16
|
-
import { existsSync
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
17
15
|
|
|
18
16
|
// ─── Types ───────────────────────────────────────────────────────────────
|
|
19
17
|
|
|
@@ -367,270 +365,10 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
367
365
|
|
|
368
366
|
// ─── MODE: Auto ──────────────────────────────────────────────────
|
|
369
367
|
|
|
370
|
-
// ─── Model Intelligence Database ─────────────────────────────────
|
|
371
|
-
|
|
372
|
-
interface ModelProfile {
|
|
373
|
-
id: string;
|
|
374
|
-
capabilities: {
|
|
375
|
-
coding: number; // 0-100 score for code generation
|
|
376
|
-
reasoning: number; // 0-100 score for debugging/planning
|
|
377
|
-
speed: number; // 0-100 score for fast tasks
|
|
378
|
-
general: number; // 0-100 overall score
|
|
379
|
-
};
|
|
380
|
-
hasReasoning: boolean;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
368
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
* Falls back to name-based heuristics if OpenRouter is unreachable.
|
|
369
|
+
* Manual mode is the only setup mode.
|
|
370
|
+
* User assigns each model to each agent role interactively.
|
|
387
371
|
*/
|
|
388
|
-
async function fetchModelProfiles(modelIds: string[]): Promise<Map<string, ModelProfile>> {
|
|
389
|
-
const profiles = new Map<string, ModelProfile>();
|
|
390
|
-
|
|
391
|
-
try {
|
|
392
|
-
const controller = new AbortController();
|
|
393
|
-
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
394
|
-
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
395
|
-
signal: controller.signal,
|
|
396
|
-
});
|
|
397
|
-
clearTimeout(timeout);
|
|
398
|
-
|
|
399
|
-
if (res.ok) {
|
|
400
|
-
const data = await res.json() as any;
|
|
401
|
-
const orModels: any[] = data.data || [];
|
|
402
|
-
|
|
403
|
-
for (const modelId of modelIds) {
|
|
404
|
-
// Try exact match first, then fuzzy match by base name
|
|
405
|
-
const baseName = modelId.replace(/:.+$/, "").split("/").pop()?.toLowerCase() || modelId.toLowerCase();
|
|
406
|
-
const match = orModels.find((m: any) => {
|
|
407
|
-
const mId = m.id?.toLowerCase() || "";
|
|
408
|
-
const mName = m.name?.toLowerCase() || "";
|
|
409
|
-
return mId.includes(baseName) || mName.includes(baseName);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
if (match) {
|
|
413
|
-
const desc = (match.description || "").toLowerCase();
|
|
414
|
-
const name = (match.name || "").toLowerCase();
|
|
415
|
-
const hasReasoning = (match.supported_parameters || []).includes("reasoning")
|
|
416
|
-
|| (match.supported_parameters || []).includes("include_reasoning");
|
|
417
|
-
|
|
418
|
-
// Score based on description keywords and model characteristics
|
|
419
|
-
let coding = 50, reasoning = 50, speed = 50, general = 60;
|
|
420
|
-
|
|
421
|
-
// Coding signals
|
|
422
|
-
if (/cod(e|ing|ex)|program|implement|refactor|software engineer/.test(desc) || /coder|codex|codestral/.test(name)) {
|
|
423
|
-
coding = 85;
|
|
424
|
-
}
|
|
425
|
-
// Reasoning signals
|
|
426
|
-
if (hasReasoning || /reason|think|logic|step.by.step|complex/.test(desc) || /o1|o3|pro|opus/.test(name)) {
|
|
427
|
-
reasoning = 85;
|
|
428
|
-
}
|
|
429
|
-
// Speed signals (smaller/cheaper models)
|
|
430
|
-
const pricing = match.pricing || {};
|
|
431
|
-
const promptCost = parseFloat(pricing.prompt || "0.01");
|
|
432
|
-
if (promptCost < 0.001 || /fast|flash|mini|small|haiku|lite|instant/.test(name)) {
|
|
433
|
-
speed = 85;
|
|
434
|
-
}
|
|
435
|
-
// General quality (larger context = usually better)
|
|
436
|
-
const ctx = match.context_length || 0;
|
|
437
|
-
if (ctx >= 200000) general = 80;
|
|
438
|
-
if (ctx >= 1000000) general = 90;
|
|
439
|
-
if (/frontier|flagship|most.advanced|best|state.of.the.art/.test(desc)) general = 90;
|
|
440
|
-
|
|
441
|
-
profiles.set(modelId, { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning });
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
} catch {
|
|
446
|
-
// OpenRouter unreachable — will fall back to heuristics
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Fill in any models not found in OpenRouter with name-based heuristics
|
|
450
|
-
for (const modelId of modelIds) {
|
|
451
|
-
if (!profiles.has(modelId)) {
|
|
452
|
-
profiles.set(modelId, classifyByName(modelId));
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return profiles;
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Fallback: classify model by name patterns when OpenRouter data is unavailable.
|
|
461
|
-
*/
|
|
462
|
-
function classifyByName(modelId: string): ModelProfile {
|
|
463
|
-
const l = modelId.toLowerCase();
|
|
464
|
-
let coding = 50, reasoning = 50, speed = 50, general = 55;
|
|
465
|
-
let hasReasoning = false;
|
|
466
|
-
|
|
467
|
-
if (/coder|code|codestral/.test(l)) coding = 80;
|
|
468
|
-
if (/max|pro|plus|opus|large|o1|o3/.test(l)) { reasoning = 80; general = 75; }
|
|
469
|
-
if (/mini|flash|fast|small|haiku|lite/.test(l)) { speed = 80; }
|
|
470
|
-
if (/o1|o3|deepseek-r1|qwq/.test(l)) { hasReasoning = true; reasoning = 85; }
|
|
471
|
-
|
|
472
|
-
return { id: modelId, capabilities: { coding, reasoning, speed, general }, hasReasoning };
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Auto-assign models using OpenRouter rankings + models.dev data.
|
|
477
|
-
* Works with ANY provider — cloud, local, or mixed.
|
|
478
|
-
*
|
|
479
|
-
* Strategy:
|
|
480
|
-
* 1. Fetch model profiles from OpenRouter (free, no API key needed)
|
|
481
|
-
* 2. Score each model for coding, reasoning, speed, and general tasks
|
|
482
|
-
* 3. Assign best model per role based on scores
|
|
483
|
-
* 4. Fall back to name-based heuristics if OpenRouter is unreachable
|
|
484
|
-
* 5. Single model? → everything uses that model (still works!)
|
|
485
|
-
*/
|
|
486
|
-
async function autoMode(availableModels: string[], ctx?: any): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
487
|
-
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
488
|
-
|
|
489
|
-
if (availableModels.length === 0) {
|
|
490
|
-
const fb = { preferred: "default", fallback: "default" };
|
|
491
|
-
for (const role of TASK_ROLES) assignments[role.key] = fb;
|
|
492
|
-
assignments["default"] = fb;
|
|
493
|
-
return assignments;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
if (availableModels.length === 1) {
|
|
497
|
-
const single = { preferred: availableModels[0], fallback: availableModels[0] };
|
|
498
|
-
for (const role of TASK_ROLES) assignments[role.key] = single;
|
|
499
|
-
assignments["default"] = single;
|
|
500
|
-
return assignments;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
// Fetch intelligence from OpenRouter
|
|
504
|
-
if (ctx) ctx.ui.notify("📊 Fetching model rankings from OpenRouter...", "info");
|
|
505
|
-
const profiles = await fetchModelProfiles(availableModels);
|
|
506
|
-
|
|
507
|
-
// Find best model for each capability
|
|
508
|
-
function bestFor(capability: keyof ModelProfile["capabilities"]): string {
|
|
509
|
-
let best = availableModels[0], bestScore = 0;
|
|
510
|
-
for (const id of availableModels) {
|
|
511
|
-
const p = profiles.get(id);
|
|
512
|
-
if (p && p.capabilities[capability] > bestScore) {
|
|
513
|
-
bestScore = p.capabilities[capability];
|
|
514
|
-
best = id;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
return best;
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function secondBestFor(capability: keyof ModelProfile["capabilities"], excludeId: string): string {
|
|
521
|
-
let best = availableModels.find(m => m !== excludeId) || excludeId;
|
|
522
|
-
let bestScore = 0;
|
|
523
|
-
for (const id of availableModels) {
|
|
524
|
-
if (id === excludeId) continue;
|
|
525
|
-
const p = profiles.get(id);
|
|
526
|
-
if (p && p.capabilities[capability] > bestScore) {
|
|
527
|
-
bestScore = p.capabilities[capability];
|
|
528
|
-
best = id;
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return best;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
const bestCoder = bestFor("coding");
|
|
535
|
-
const bestReasoner = bestFor("reasoning");
|
|
536
|
-
const bestFast = bestFor("speed");
|
|
537
|
-
const bestGeneral = bestFor("general");
|
|
538
|
-
|
|
539
|
-
assignments["code"] = { preferred: bestCoder, fallback: secondBestFor("coding", bestCoder) };
|
|
540
|
-
assignments["debug"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
|
|
541
|
-
assignments["plan"] = { preferred: bestReasoner, fallback: secondBestFor("reasoning", bestReasoner) };
|
|
542
|
-
assignments["explore"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
|
|
543
|
-
assignments["test"] = { preferred: bestFast, fallback: secondBestFor("speed", bestFast) };
|
|
544
|
-
assignments["review"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
|
|
545
|
-
assignments["default"] = { preferred: bestGeneral, fallback: secondBestFor("general", bestGeneral) };
|
|
546
|
-
|
|
547
|
-
// Show what was assigned and why
|
|
548
|
-
if (ctx) {
|
|
549
|
-
ctx.ui.notify("📊 Model rankings applied:", "info");
|
|
550
|
-
for (const role of TASK_ROLES) {
|
|
551
|
-
const a = assignments[role.key];
|
|
552
|
-
const p = profiles.get(a.preferred);
|
|
553
|
-
const scores = p ? `(coding:${p.capabilities.coding} reasoning:${p.capabilities.reasoning} speed:${p.capabilities.speed})` : "";
|
|
554
|
-
ctx.ui.notify(` ${role.label}: ${a.preferred} ${scores}`, "info");
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
return assignments;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// ─── MODE: Benchmark ─────────────────────────────────────────────
|
|
562
|
-
|
|
563
|
-
async function benchmarkMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
564
|
-
// Check if benchmark results already exist
|
|
565
|
-
const benchmarkPath = join(phiDir, "benchmark", "results.json");
|
|
566
|
-
let existingResults: any = null;
|
|
567
|
-
try {
|
|
568
|
-
await access(benchmarkPath);
|
|
569
|
-
const content = await readFile(benchmarkPath, "utf-8");
|
|
570
|
-
existingResults = JSON.parse(content);
|
|
571
|
-
} catch {
|
|
572
|
-
// No existing results
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
if (existingResults?.results?.length > 0) {
|
|
576
|
-
const useExisting = await ctx.ui.confirm(
|
|
577
|
-
"Use existing benchmarks?",
|
|
578
|
-
`Found ${existingResults.results.length} benchmark results from a previous run. Use them?`
|
|
579
|
-
);
|
|
580
|
-
if (useExisting) {
|
|
581
|
-
ctx.ui.notify("📊 Using existing benchmark results for model assignment.\n", "info");
|
|
582
|
-
return assignFromBenchmark(existingResults.results, availableModels);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// No existing results or user declined — run benchmarks now
|
|
587
|
-
ctx.ui.notify("🧪 Benchmark mode: launching model tests...", "info");
|
|
588
|
-
ctx.ui.notify("This tests each model with 6 coding tasks via real API calls.", "info");
|
|
589
|
-
ctx.ui.notify("⏱️ Estimated time: 2-3 minutes per model.\n", "info");
|
|
590
|
-
|
|
591
|
-
// Trigger benchmark via sendUserMessage — this runs /benchmark all
|
|
592
|
-
// which saves results to the same results.json path
|
|
593
|
-
pi.sendUserMessage("/benchmark all");
|
|
594
|
-
ctx.ui.notify("⏳ Benchmarks started. Once complete, run `/phi-init` again and select benchmark mode to use the results.\n", "info");
|
|
595
|
-
ctx.ui.notify("💡 The benchmark runs in the background. You'll see live results in the terminal.\n", "info");
|
|
596
|
-
|
|
597
|
-
// Return auto mode assignments as temporary defaults
|
|
598
|
-
// (will be overwritten when user re-runs /phi-init with benchmark results)
|
|
599
|
-
ctx.ui.notify("📋 Setting auto-mode defaults while benchmarks run...\n", "info");
|
|
600
|
-
return autoMode(availableModels, ctx);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
function assignFromBenchmark(results: any[], availableModels: string[]): Record<string, { preferred: string; fallback: string }> {
|
|
604
|
-
const assignments: Record<string, { preferred: string; fallback: string }> = {};
|
|
605
|
-
|
|
606
|
-
// Sort by total score
|
|
607
|
-
const sorted = [...results].sort((a: any, b: any) => (b.totalScore || 0) - (a.totalScore || 0));
|
|
608
|
-
const bestOverall = sorted[0]?.modelId || availableModels[0];
|
|
609
|
-
const secondBest = sorted[1]?.modelId || bestOverall;
|
|
610
|
-
|
|
611
|
-
// Find best per category
|
|
612
|
-
function bestForCategory(category: string): string {
|
|
613
|
-
let best = { id: bestOverall, score: 0 };
|
|
614
|
-
for (const r of results) {
|
|
615
|
-
const catScore = r.categories?.[category]?.score ?? 0;
|
|
616
|
-
if (catScore > best.score) {
|
|
617
|
-
best = { id: r.modelId, score: catScore };
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
return best.id;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
assignments["code"] = { preferred: bestForCategory("code-gen"), fallback: secondBest };
|
|
624
|
-
assignments["debug"] = { preferred: bestForCategory("debug"), fallback: secondBest };
|
|
625
|
-
assignments["plan"] = { preferred: bestForCategory("planning"), fallback: secondBest };
|
|
626
|
-
assignments["explore"] = { preferred: bestForCategory("speed"), fallback: secondBest };
|
|
627
|
-
assignments["test"] = { preferred: bestForCategory("speed"), fallback: secondBest };
|
|
628
|
-
assignments["review"] = { preferred: bestForCategory("orchestration"), fallback: secondBest };
|
|
629
|
-
assignments["default"] = { preferred: bestOverall, fallback: secondBest };
|
|
630
|
-
|
|
631
|
-
return assignments;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
372
|
// ─── MODE: Manual ────────────────────────────────────────────────
|
|
635
373
|
|
|
636
374
|
async function manualMode(availableModels: string[], ctx: any): Promise<Record<string, { preferred: string; fallback: string }>> {
|
|
@@ -670,7 +408,7 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
670
408
|
// ─── Command ─────────────────────────────────────────────────────
|
|
671
409
|
|
|
672
410
|
pi.registerCommand("phi-init", {
|
|
673
|
-
description: "Initialize Phi Code — interactive setup wizard
|
|
411
|
+
description: "Initialize Phi Code — interactive setup wizard",
|
|
674
412
|
handler: async (args, ctx) => {
|
|
675
413
|
try {
|
|
676
414
|
ctx.ui.notify("╔══════════════════════════════════════╗", "info");
|
|
@@ -811,29 +549,10 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
811
549
|
const allModels = getAllAvailableModels(providers);
|
|
812
550
|
ctx.ui.notify(`\n✅ **${allModels.length} models** available from ${available.length} provider(s).\n`, "info");
|
|
813
551
|
|
|
814
|
-
// 2.
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
"manual — Choose each model yourself",
|
|
819
|
-
];
|
|
820
|
-
const modeChoice = await ctx.ui.select("Setup mode", modeOptions);
|
|
821
|
-
const mode = (modeChoice ?? "").startsWith("benchmark") ? "benchmark"
|
|
822
|
-
: (modeChoice ?? "").startsWith("manual") ? "manual"
|
|
823
|
-
: "auto";
|
|
824
|
-
|
|
825
|
-
ctx.ui.notify(`\n📋 Mode: **${mode}**\n`, "info");
|
|
826
|
-
|
|
827
|
-
// 3. Get assignments based on mode
|
|
828
|
-
let assignments: Record<string, { preferred: string; fallback: string }>;
|
|
829
|
-
|
|
830
|
-
if (mode === "auto") {
|
|
831
|
-
assignments = await autoMode(allModels, ctx);
|
|
832
|
-
} else if (mode === "benchmark") {
|
|
833
|
-
assignments = await benchmarkMode(allModels, ctx);
|
|
834
|
-
} else {
|
|
835
|
-
assignments = await manualMode(allModels, ctx);
|
|
836
|
-
}
|
|
552
|
+
// 2. Assign models to agents (manual)
|
|
553
|
+
ctx.ui.notify(`\n📋 **Assign a model to each agent role:**\n`, "info");
|
|
554
|
+
|
|
555
|
+
const assignments = await manualMode(allModels, ctx);
|
|
837
556
|
|
|
838
557
|
// 4. Create directory structure
|
|
839
558
|
ctx.ui.notify("\n📁 Creating directories...", "info");
|
|
@@ -882,130 +601,4 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
882
601
|
},
|
|
883
602
|
});
|
|
884
603
|
|
|
885
|
-
// ─── API Key Management Command ─────────────────────────────────
|
|
886
|
-
|
|
887
|
-
pi.registerCommand("api-key", {
|
|
888
|
-
description: "Set or view API keys (usage: /api-key set <provider> <key> | /api-key list)",
|
|
889
|
-
handler: async (args, ctx) => {
|
|
890
|
-
const parts = args.trim().split(/\s+/);
|
|
891
|
-
const action = parts[0]?.toLowerCase();
|
|
892
|
-
|
|
893
|
-
const PROVIDERS: Record<string, { envVar: string; name: string }> = {
|
|
894
|
-
alibaba: { envVar: "ALIBABA_CODING_PLAN_KEY", name: "Alibaba Coding Plan" },
|
|
895
|
-
dashscope: { envVar: "DASHSCOPE_API_KEY", name: "DashScope (Alibaba)" },
|
|
896
|
-
openai: { envVar: "OPENAI_API_KEY", name: "OpenAI" },
|
|
897
|
-
anthropic: { envVar: "ANTHROPIC_API_KEY", name: "Anthropic" },
|
|
898
|
-
google: { envVar: "GOOGLE_API_KEY", name: "Google" },
|
|
899
|
-
openrouter: { envVar: "OPENROUTER_API_KEY", name: "OpenRouter" },
|
|
900
|
-
groq: { envVar: "GROQ_API_KEY", name: "Groq" },
|
|
901
|
-
brave: { envVar: "BRAVE_API_KEY", name: "Brave Search" },
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
if (!action || action === "help") {
|
|
905
|
-
ctx.ui.notify(`**🔑 API Key Management**
|
|
906
|
-
|
|
907
|
-
Usage:
|
|
908
|
-
/api-key set <provider> <key> — Set an API key for this session
|
|
909
|
-
/api-key list — Show configured providers
|
|
910
|
-
/api-key providers — List all supported providers
|
|
911
|
-
|
|
912
|
-
Supported providers: ${Object.keys(PROVIDERS).join(", ")}
|
|
913
|
-
|
|
914
|
-
Example:
|
|
915
|
-
/api-key set alibaba sk-sp-xxx
|
|
916
|
-
/api-key set openai sk-xxx
|
|
917
|
-
|
|
918
|
-
Keys set with /api-key are active for the current session.
|
|
919
|
-
For persistence, set environment variables:
|
|
920
|
-
• Windows: setx ALIBABA_CODING_PLAN_KEY "your-key"
|
|
921
|
-
• Linux/Mac: export ALIBABA_CODING_PLAN_KEY="your-key"`, "info");
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
if (action === "list") {
|
|
926
|
-
let found = 0;
|
|
927
|
-
let msg = "**🔑 Configured API Keys:**\n";
|
|
928
|
-
for (const [id, info] of Object.entries(PROVIDERS)) {
|
|
929
|
-
const key = process.env[info.envVar];
|
|
930
|
-
if (key) {
|
|
931
|
-
const masked = key.substring(0, 6) + "..." + key.substring(key.length - 4);
|
|
932
|
-
msg += ` ✅ ${info.name} (${id}): ${masked}\n`;
|
|
933
|
-
found++;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
if (found === 0) {
|
|
937
|
-
msg += " ❌ No API keys configured\n";
|
|
938
|
-
msg += "\nUse `/api-key set <provider> <key>` to add one.";
|
|
939
|
-
}
|
|
940
|
-
ctx.ui.notify(msg, "info");
|
|
941
|
-
return;
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
if (action === "providers") {
|
|
945
|
-
let msg = "**Supported Providers:**\n";
|
|
946
|
-
for (const [id, info] of Object.entries(PROVIDERS)) {
|
|
947
|
-
const status = process.env[info.envVar] ? "✅" : "⬜";
|
|
948
|
-
msg += ` ${status} **${id}** — ${info.name} (${info.envVar})\n`;
|
|
949
|
-
}
|
|
950
|
-
ctx.ui.notify(msg, "info");
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
if (action === "set") {
|
|
955
|
-
const provider = parts[1]?.toLowerCase();
|
|
956
|
-
const key = parts.slice(2).join(" ").trim();
|
|
957
|
-
|
|
958
|
-
if (!provider || !key) {
|
|
959
|
-
ctx.ui.notify("Usage: /api-key set <provider> <key>\nExample: /api-key set alibaba sk-sp-xxx", "warning");
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
const info = PROVIDERS[provider];
|
|
964
|
-
if (!info) {
|
|
965
|
-
ctx.ui.notify(`Unknown provider "${provider}". Supported: ${Object.keys(PROVIDERS).join(", ")}`, "error");
|
|
966
|
-
return;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
// Set in current process environment
|
|
970
|
-
process.env[info.envVar] = key;
|
|
971
|
-
if (provider === "alibaba") {
|
|
972
|
-
process.env.DASHSCOPE_API_KEY = key;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
// Persist to models.json
|
|
976
|
-
const modelsJsonPath = join(homedir(), ".phi", "agent", "models.json");
|
|
977
|
-
let modelsConfig: any = { providers: {} };
|
|
978
|
-
try {
|
|
979
|
-
const existing = readFileSync(modelsJsonPath, "utf-8");
|
|
980
|
-
modelsConfig = JSON.parse(existing);
|
|
981
|
-
} catch { /* file doesn't exist yet */ }
|
|
982
|
-
|
|
983
|
-
// Find provider config from detectProviders
|
|
984
|
-
const providerDefs = detectProviders();
|
|
985
|
-
const providerDef = providerDefs.find(p => p.envVar === info.envVar);
|
|
986
|
-
if (providerDef) {
|
|
987
|
-
const providerId = providerDef.name.toLowerCase().replace(/\s+/g, "-");
|
|
988
|
-
modelsConfig.providers[providerId] = {
|
|
989
|
-
baseUrl: providerDef.baseUrl,
|
|
990
|
-
api: "openai-completions",
|
|
991
|
-
apiKey: key,
|
|
992
|
-
models: await Promise.all(providerDef.models.map(async (id: string) => {
|
|
993
|
-
const spec = await getModelSpec(id);
|
|
994
|
-
return {
|
|
995
|
-
id, name: id, reasoning: spec.reasoning, input: ["text"],
|
|
996
|
-
contextWindow: spec.contextWindow, maxTokens: spec.maxTokens,
|
|
997
|
-
};
|
|
998
|
-
})),
|
|
999
|
-
};
|
|
1000
|
-
writeFileSync(modelsJsonPath, JSON.stringify(modelsConfig, null, 2), "utf-8");
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
const masked = key.substring(0, 6) + "..." + key.substring(key.length - 4);
|
|
1004
|
-
ctx.ui.notify(`✅ **${info.name}** API key saved: ${masked}\n\n📁 Saved to \`~/.phi/agent/models.json\` (persistent)\n⚠️ **Restart phi** for the new models to load.`, "info");
|
|
1005
|
-
return;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
ctx.ui.notify("Unknown action. Use: /api-key set|list|providers|help", "warning");
|
|
1009
|
-
},
|
|
1010
|
-
});
|
|
1011
604
|
}
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
* Orchestrator Extension - Full-cycle project planning and execution
|
|
3
3
|
*
|
|
4
4
|
* WORKFLOW (single command):
|
|
5
|
-
* /plan <description> →
|
|
6
|
-
*
|
|
5
|
+
* /plan <description> → 5 sequential agent phases → each with its own model
|
|
6
|
+
*
|
|
7
|
+
* The orchestrator uses event-driven phase chaining:
|
|
8
|
+
* 1. Send phase 1 message with model A
|
|
9
|
+
* 2. Detect when agent goes idle (output event + polling)
|
|
10
|
+
* 3. Switch to model B, send phase 2
|
|
11
|
+
* 4. Repeat until all 5 phases complete
|
|
7
12
|
*
|
|
8
13
|
* Commands:
|
|
9
|
-
* /plan — Full workflow: plan + execute with
|
|
10
|
-
* /run — Re-execute an existing plan
|
|
14
|
+
* /plan — Full workflow: plan + execute with agents
|
|
15
|
+
* /run — Re-execute an existing plan
|
|
11
16
|
* /plans — List plans and their execution status
|
|
12
|
-
*
|
|
13
|
-
* Sub-agent execution:
|
|
14
|
-
* Each task spawns a separate `phi` CLI process with:
|
|
15
|
-
* - Its own system prompt (from the agent .md file)
|
|
16
|
-
* - Its own model (from routing.json)
|
|
17
|
-
* - Its own context (isolated, no shared history)
|
|
18
|
-
* - Its own tool access (read, write, edit, bash, etc.)
|
|
19
|
-
* Results are collected into progress.md and reported to the user.
|
|
20
17
|
*/
|
|
21
18
|
|
|
22
19
|
import { Type } from "@sinclair/typebox";
|
|
@@ -451,10 +448,141 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
451
448
|
},
|
|
452
449
|
});
|
|
453
450
|
|
|
451
|
+
// ─── Orchestration State ─────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
interface OrchestratorPhase {
|
|
454
|
+
key: string;
|
|
455
|
+
label: string;
|
|
456
|
+
model: string;
|
|
457
|
+
fallback: string;
|
|
458
|
+
instruction: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
let phaseQueue: OrchestratorPhase[] = [];
|
|
462
|
+
let orchestrationActive = false;
|
|
463
|
+
let idlePollTimer: ReturnType<typeof setInterval> | null = null;
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Load routing config and build phase queue with model assignments.
|
|
467
|
+
*/
|
|
468
|
+
function buildPhases(description: string): OrchestratorPhase[] {
|
|
469
|
+
// Read routing.json for model assignments
|
|
470
|
+
const routingPath = join(homedir(), ".phi", "agent", "routing.json");
|
|
471
|
+
let routing: any = { routes: {}, default: { model: "default" } };
|
|
472
|
+
try {
|
|
473
|
+
routing = JSON.parse(readFileSync(routingPath, "utf-8"));
|
|
474
|
+
} catch { /* no routing config */ }
|
|
475
|
+
|
|
476
|
+
function getModel(routeKey: string): { preferred: string; fallback: string } {
|
|
477
|
+
const route = routing.routes?.[routeKey];
|
|
478
|
+
return {
|
|
479
|
+
preferred: route?.preferredModel || routing.default?.model || "default",
|
|
480
|
+
fallback: route?.fallback || routing.default?.model || "default",
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const explore = getModel("explore");
|
|
485
|
+
const plan = getModel("plan");
|
|
486
|
+
const code = getModel("code");
|
|
487
|
+
const test = getModel("test");
|
|
488
|
+
const review = getModel("review");
|
|
489
|
+
|
|
490
|
+
return [
|
|
491
|
+
{
|
|
492
|
+
key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
|
|
493
|
+
instruction: `Analyze the project requirements and existing codebase. Identify what exists, what's needed, and any constraints.\n\n**Project:** ${description}\n\nList files, read key ones, check dependencies. Return a structured summary.`,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
key: "plan", label: "📐 Phase 2 — PLAN", model: plan.preferred, fallback: plan.fallback,
|
|
497
|
+
instruction: `Design the architecture for this project. Define file structure, tech choices, and implementation approach.\n\n**Project:** ${description}\n\nBe specific: list every file to create with its purpose.`,
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
key: "code", label: "💻 Phase 3 — CODE", model: code.preferred, fallback: code.fallback,
|
|
501
|
+
instruction: `Implement the COMPLETE project. Create ALL files with production-quality code.\n\n**Project:** ${description}\n\n**Rules:**\n- Create every file needed\n- No placeholders, no TODOs, no stubs\n- Every function must be fully implemented\n- Follow the architecture from the previous planning phase`,
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
key: "test", label: "🧪 Phase 4 — TEST", model: test.preferred, fallback: test.fallback,
|
|
505
|
+
instruction: `Test the implementation. Run the code, check for errors, verify it works.\n\n**Project:** ${description}\n\nFix any errors you find. Ensure the project runs correctly.`,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
key: "review", label: "🔍 Phase 5 — REVIEW", model: review.preferred, fallback: review.fallback,
|
|
509
|
+
instruction: `Review the code quality, security, and performance. Fix any issues.\n\n**Project:** ${description}\n\nCheck: error handling, edge cases, code style, documentation.`,
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Switch model for the current phase.
|
|
516
|
+
* Tries preferred model first, then fallback.
|
|
517
|
+
*/
|
|
518
|
+
async function switchModelForPhase(phase: OrchestratorPhase, ctx: any): Promise<string> {
|
|
519
|
+
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
520
|
+
const preferred = available.find((m: any) => m.id === phase.model);
|
|
521
|
+
const fallback = available.find((m: any) => m.id === phase.fallback);
|
|
522
|
+
const target = preferred || fallback;
|
|
523
|
+
|
|
524
|
+
if (target && target.id !== ctx.model?.id) {
|
|
525
|
+
const switched = await pi.setModel(target);
|
|
526
|
+
if (switched) return target.id;
|
|
527
|
+
}
|
|
528
|
+
return ctx.model?.id || phase.model;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Send the next phase in the queue.
|
|
533
|
+
* Called after the agent goes idle (previous phase complete).
|
|
534
|
+
*/
|
|
535
|
+
function sendNextPhase(ctx: any) {
|
|
536
|
+
if (phaseQueue.length === 0) {
|
|
537
|
+
// All phases done
|
|
538
|
+
orchestrationActive = false;
|
|
539
|
+
ctx.ui.notify(`\n✅ **All 5 phases complete!**`, "info");
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const phase = phaseQueue.shift()!;
|
|
544
|
+
|
|
545
|
+
// Switch model and send
|
|
546
|
+
switchModelForPhase(phase, ctx).then((modelId) => {
|
|
547
|
+
ctx.ui.notify(`\n${phase.label} → \`${modelId}\``, "info");
|
|
548
|
+
setTimeout(() => pi.sendUserMessage(phase.instruction), 200);
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ─── Output Event — Phase Chaining ───────────────────────────────
|
|
553
|
+
|
|
554
|
+
pi.on("output", async (_event, ctx) => {
|
|
555
|
+
if (!orchestrationActive || phaseQueue.length === 0) return;
|
|
556
|
+
|
|
557
|
+
// Debounce: clear any existing poll timer
|
|
558
|
+
if (idlePollTimer) {
|
|
559
|
+
clearInterval(idlePollTimer);
|
|
560
|
+
idlePollTimer = null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Poll for idle state (agent finishes tool calls + response)
|
|
564
|
+
let attempts = 0;
|
|
565
|
+
idlePollTimer = setInterval(() => {
|
|
566
|
+
attempts++;
|
|
567
|
+
if (attempts > 120) { // 60 seconds max
|
|
568
|
+
clearInterval(idlePollTimer!);
|
|
569
|
+
idlePollTimer = null;
|
|
570
|
+
ctx.ui.notify("⚠️ Orchestrator timeout — phase took too long.", "warning");
|
|
571
|
+
orchestrationActive = false;
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
if (ctx.isIdle()) {
|
|
575
|
+
clearInterval(idlePollTimer!);
|
|
576
|
+
idlePollTimer = null;
|
|
577
|
+
sendNextPhase(ctx);
|
|
578
|
+
}
|
|
579
|
+
}, 500);
|
|
580
|
+
});
|
|
581
|
+
|
|
454
582
|
// ─── /plan Command — Full workflow ───────────────────────────────
|
|
455
583
|
|
|
456
584
|
pi.registerCommand("plan", {
|
|
457
|
-
description: "Plan AND execute a project with
|
|
585
|
+
description: "Plan AND execute a project — 5 phases, each with its own model from routing.json",
|
|
458
586
|
handler: async (args, ctx) => {
|
|
459
587
|
const description = args.trim();
|
|
460
588
|
|
|
@@ -477,35 +605,24 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
477
605
|
const specFile = `spec-${ts}.md`;
|
|
478
606
|
await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
|
|
479
607
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
## Your workflow (follow these phases in order):
|
|
608
|
+
// Build phases with model assignments from routing.json
|
|
609
|
+
const phases = buildPhases(description);
|
|
610
|
+
phaseQueue = phases.slice(1); // Queue phases 2-5
|
|
611
|
+
orchestrationActive = true;
|
|
612
|
+
const firstPhase = phases[0];
|
|
486
613
|
|
|
487
|
-
|
|
488
|
-
Analyze the project requirements and any existing codebase. List what exists, what's needed, and constraints.
|
|
614
|
+
ctx.ui.notify(`📋 **Orchestrator started** — 5 phases with model routing\n`, "info");
|
|
489
615
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
### Phase 4 — 🧪 TEST
|
|
497
|
-
Run the code. Verify it works. Fix any errors you find.
|
|
498
|
-
|
|
499
|
-
### Phase 5 — 🔍 REVIEW
|
|
500
|
-
Check code quality. Fix any issues. Ensure everything is polished.
|
|
501
|
-
|
|
502
|
-
## Rules
|
|
503
|
-
- Complete ALL 5 phases in this turn
|
|
504
|
-
- Create every file needed for a working project
|
|
505
|
-
- Announce each phase as you start it (e.g. "## Phase 1 — Exploring...")
|
|
506
|
-
- Do NOT stop after planning — implement everything`;
|
|
616
|
+
// Show the plan
|
|
617
|
+
for (const p of phases) {
|
|
618
|
+
ctx.ui.notify(` ${p.label} → \`${p.model}\``, "info");
|
|
619
|
+
}
|
|
620
|
+
ctx.ui.notify("", "info");
|
|
507
621
|
|
|
508
|
-
|
|
622
|
+
// Switch model and send first phase after handler returns
|
|
623
|
+
const modelId = await switchModelForPhase(firstPhase, ctx);
|
|
624
|
+
ctx.ui.notify(`${firstPhase.label} → \`${modelId}\``, "info");
|
|
625
|
+
setTimeout(() => pi.sendUserMessage(firstPhase.instruction), 200);
|
|
509
626
|
},
|
|
510
627
|
});
|
|
511
628
|
|