@phi-code-admin/phi-code 0.62.2 → 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/orchestrator.ts +156 -40
- package/package.json +1 -1
|
@@ -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,36 +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
|
-
//
|
|
509
|
-
|
|
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);
|
|
510
626
|
},
|
|
511
627
|
});
|
|
512
628
|
|