@phi-code-admin/phi-code 0.62.2 → 0.63.1
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 +246 -41
- 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,226 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
451
448
|
},
|
|
452
449
|
});
|
|
453
450
|
|
|
451
|
+
// ─── Orchestration State ─────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
interface AgentDef {
|
|
454
|
+
name: string;
|
|
455
|
+
tools: string[];
|
|
456
|
+
systemPrompt: string;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
interface OrchestratorPhase {
|
|
460
|
+
key: string;
|
|
461
|
+
label: string;
|
|
462
|
+
model: string;
|
|
463
|
+
fallback: string;
|
|
464
|
+
agent: AgentDef | null;
|
|
465
|
+
instruction: string;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let phaseQueue: OrchestratorPhase[] = [];
|
|
469
|
+
let orchestrationActive = false;
|
|
470
|
+
let idlePollTimer: ReturnType<typeof setInterval> | null = null;
|
|
471
|
+
let activeAgentPrompt: string | null = null;
|
|
472
|
+
let activeAgentTools: string[] | null = null;
|
|
473
|
+
let savedTools: string[] | null = null;
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Parse agent .md file with YAML frontmatter
|
|
477
|
+
*/
|
|
478
|
+
function loadAgentDef(name: string): AgentDef | null {
|
|
479
|
+
const dirs = [
|
|
480
|
+
join(process.cwd(), ".phi", "agents"),
|
|
481
|
+
join(homedir(), ".phi", "agent", "agents"),
|
|
482
|
+
];
|
|
483
|
+
for (const dir of dirs) {
|
|
484
|
+
const filePath = join(dir, `${name}.md`);
|
|
485
|
+
try {
|
|
486
|
+
const content = readFileSync(filePath, "utf-8");
|
|
487
|
+
const fmMatch = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/);
|
|
488
|
+
if (!fmMatch) continue;
|
|
489
|
+
const fields: Record<string, string> = {};
|
|
490
|
+
for (const line of fmMatch[1].split("\n")) {
|
|
491
|
+
const m = line.match(/^(\w+):\s*(.*)$/);
|
|
492
|
+
if (m) fields[m[1]] = m[2].trim();
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
name: fields.name || name,
|
|
496
|
+
tools: (fields.tools || "").split(",").map(t => t.trim()).filter(Boolean),
|
|
497
|
+
systemPrompt: fmMatch[2].trim(),
|
|
498
|
+
};
|
|
499
|
+
} catch { continue; }
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Load routing config and build phase queue with model assignments + agent definitions.
|
|
506
|
+
*/
|
|
507
|
+
function buildPhases(description: string): OrchestratorPhase[] {
|
|
508
|
+
const routingPath = join(homedir(), ".phi", "agent", "routing.json");
|
|
509
|
+
let routing: any = { routes: {}, default: { model: "default" } };
|
|
510
|
+
try {
|
|
511
|
+
routing = JSON.parse(readFileSync(routingPath, "utf-8"));
|
|
512
|
+
} catch { /* no routing config */ }
|
|
513
|
+
|
|
514
|
+
function getModel(routeKey: string): { preferred: string; fallback: string } {
|
|
515
|
+
const route = routing.routes?.[routeKey];
|
|
516
|
+
return {
|
|
517
|
+
preferred: route?.preferredModel || routing.default?.model || "default",
|
|
518
|
+
fallback: route?.fallback || routing.default?.model || "default",
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const explore = getModel("explore");
|
|
523
|
+
const plan = getModel("plan");
|
|
524
|
+
const code = getModel("code");
|
|
525
|
+
const test = getModel("test");
|
|
526
|
+
const review = getModel("review");
|
|
527
|
+
|
|
528
|
+
return [
|
|
529
|
+
{
|
|
530
|
+
key: "explore", label: "🔍 Phase 1 — EXPLORE", model: explore.preferred, fallback: explore.fallback,
|
|
531
|
+
agent: loadAgentDef("explore"),
|
|
532
|
+
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.`,
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
key: "plan", label: "📐 Phase 2 — PLAN", model: plan.preferred, fallback: plan.fallback,
|
|
536
|
+
agent: loadAgentDef("plan"),
|
|
537
|
+
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.`,
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
key: "code", label: "💻 Phase 3 — CODE", model: code.preferred, fallback: code.fallback,
|
|
541
|
+
agent: loadAgentDef("code"),
|
|
542
|
+
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`,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
key: "test", label: "🧪 Phase 4 — TEST", model: test.preferred, fallback: test.fallback,
|
|
546
|
+
agent: loadAgentDef("test"),
|
|
547
|
+
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.`,
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
key: "review", label: "🔍 Phase 5 — REVIEW", model: review.preferred, fallback: review.fallback,
|
|
551
|
+
agent: loadAgentDef("review"),
|
|
552
|
+
instruction: `Review the code quality, security, and performance. Fix any issues.\n\n**Project:** ${description}\n\nCheck: error handling, edge cases, code style, documentation.`,
|
|
553
|
+
},
|
|
554
|
+
];
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Switch model for the current phase.
|
|
559
|
+
*/
|
|
560
|
+
async function switchModelForPhase(phase: OrchestratorPhase, ctx: any): Promise<string> {
|
|
561
|
+
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
562
|
+
const preferred = available.find((m: any) => m.id === phase.model);
|
|
563
|
+
const fallback = available.find((m: any) => m.id === phase.fallback);
|
|
564
|
+
const target = preferred || fallback;
|
|
565
|
+
|
|
566
|
+
if (target && target.id !== ctx.model?.id) {
|
|
567
|
+
const switched = await pi.setModel(target);
|
|
568
|
+
if (switched) return target.id;
|
|
569
|
+
}
|
|
570
|
+
return ctx.model?.id || phase.model;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Activate agent for a phase: set system prompt + restrict tools.
|
|
575
|
+
*/
|
|
576
|
+
function activateAgent(phase: OrchestratorPhase, ctx: any) {
|
|
577
|
+
if (phase.agent) {
|
|
578
|
+
// Save current tools for restoration
|
|
579
|
+
if (!savedTools) {
|
|
580
|
+
savedTools = pi.getActiveTools();
|
|
581
|
+
}
|
|
582
|
+
// Set agent's system prompt (will be injected via before_agent_start)
|
|
583
|
+
activeAgentPrompt = phase.agent.systemPrompt;
|
|
584
|
+
// Restrict tools to agent's allowed tools
|
|
585
|
+
if (phase.agent.tools.length > 0) {
|
|
586
|
+
activeAgentTools = phase.agent.tools;
|
|
587
|
+
pi.setActiveTools(phase.agent.tools);
|
|
588
|
+
}
|
|
589
|
+
} else {
|
|
590
|
+
activeAgentPrompt = null;
|
|
591
|
+
activeAgentTools = null;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Deactivate agent: restore tools, clear prompt override.
|
|
597
|
+
*/
|
|
598
|
+
function deactivateAgent() {
|
|
599
|
+
activeAgentPrompt = null;
|
|
600
|
+
activeAgentTools = null;
|
|
601
|
+
if (savedTools) {
|
|
602
|
+
pi.setActiveTools(savedTools);
|
|
603
|
+
savedTools = null;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Send the next phase in the queue.
|
|
609
|
+
*/
|
|
610
|
+
function sendNextPhase(ctx: any) {
|
|
611
|
+
if (phaseQueue.length === 0) {
|
|
612
|
+
orchestrationActive = false;
|
|
613
|
+
deactivateAgent();
|
|
614
|
+
ctx.ui.notify(`\n✅ **All 5 phases complete!**`, "info");
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const phase = phaseQueue.shift()!;
|
|
619
|
+
|
|
620
|
+
switchModelForPhase(phase, ctx).then((modelId) => {
|
|
621
|
+
activateAgent(phase, ctx);
|
|
622
|
+
const agentName = phase.agent?.name || phase.key;
|
|
623
|
+
ctx.ui.notify(`\n${phase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
|
|
624
|
+
setTimeout(() => pi.sendUserMessage(phase.instruction), 200);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ─── System Prompt Injection — Agent personas ────────────────────
|
|
629
|
+
|
|
630
|
+
pi.on("before_agent_start", async (event, _ctx) => {
|
|
631
|
+
if (!orchestrationActive || !activeAgentPrompt) {
|
|
632
|
+
return { };
|
|
633
|
+
}
|
|
634
|
+
// Replace system prompt with the active agent's prompt
|
|
635
|
+
return { systemPrompt: activeAgentPrompt };
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
// ─── Output Event — Phase Chaining ───────────────────────────────
|
|
639
|
+
|
|
640
|
+
pi.on("output", async (_event, ctx) => {
|
|
641
|
+
if (!orchestrationActive || phaseQueue.length === 0) return;
|
|
642
|
+
|
|
643
|
+
if (idlePollTimer) {
|
|
644
|
+
clearInterval(idlePollTimer);
|
|
645
|
+
idlePollTimer = null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let attempts = 0;
|
|
649
|
+
idlePollTimer = setInterval(() => {
|
|
650
|
+
attempts++;
|
|
651
|
+
if (attempts > 120) {
|
|
652
|
+
clearInterval(idlePollTimer!);
|
|
653
|
+
idlePollTimer = null;
|
|
654
|
+
ctx.ui.notify("⚠️ Orchestrator timeout — phase took too long.", "warning");
|
|
655
|
+
orchestrationActive = false;
|
|
656
|
+
deactivateAgent();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
if (ctx.isIdle()) {
|
|
660
|
+
clearInterval(idlePollTimer!);
|
|
661
|
+
idlePollTimer = null;
|
|
662
|
+
sendNextPhase(ctx);
|
|
663
|
+
}
|
|
664
|
+
}, 500);
|
|
665
|
+
});
|
|
666
|
+
|
|
454
667
|
// ─── /plan Command — Full workflow ───────────────────────────────
|
|
455
668
|
|
|
456
669
|
pi.registerCommand("plan", {
|
|
457
|
-
description: "Plan AND execute a project with
|
|
670
|
+
description: "Plan AND execute a project — 5 phases, each with its own model from routing.json",
|
|
458
671
|
handler: async (args, ctx) => {
|
|
459
672
|
const description = args.trim();
|
|
460
673
|
|
|
@@ -477,36 +690,28 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
477
690
|
const specFile = `spec-${ts}.md`;
|
|
478
691
|
await writeFile(join(plansDir, specFile), `# ${description}\n\n**Created:** ${new Date().toLocaleString()}\n`, "utf-8");
|
|
479
692
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
## Your workflow (follow these phases in order):
|
|
486
|
-
|
|
487
|
-
### Phase 1 — 🔍 EXPLORE
|
|
488
|
-
Analyze the project requirements and any existing codebase. List what exists, what's needed, and constraints.
|
|
489
|
-
|
|
490
|
-
### Phase 2 — 📐 PLAN
|
|
491
|
-
Design the architecture, file structure, and tech choices. Be specific about file names and structure.
|
|
693
|
+
// Build phases with model assignments + agent definitions
|
|
694
|
+
const phases = buildPhases(description);
|
|
695
|
+
phaseQueue = phases.slice(1); // Queue phases 2-5
|
|
696
|
+
orchestrationActive = true;
|
|
697
|
+
const firstPhase = phases[0];
|
|
492
698
|
|
|
493
|
-
|
|
494
|
-
Implement EVERYTHING. Create ALL files with complete, production-quality code. No placeholders, no TODOs, no "implement later". Every file must be fully functional.
|
|
699
|
+
ctx.ui.notify(`📋 **Orchestrator started** — 5 phases with model routing + agent roles\n`, "info");
|
|
495
700
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
setTimeout(() => pi.sendUserMessage(
|
|
701
|
+
// Show the plan
|
|
702
|
+
for (const p of phases) {
|
|
703
|
+
const agentName = p.agent?.name || p.key;
|
|
704
|
+
const toolCount = p.agent?.tools.length || 0;
|
|
705
|
+
ctx.ui.notify(` ${p.label} → \`${p.model}\` (agent: ${agentName}, ${toolCount} tools)`, "info");
|
|
706
|
+
}
|
|
707
|
+
ctx.ui.notify("", "info");
|
|
708
|
+
|
|
709
|
+
// Switch model and activate agent for first phase
|
|
710
|
+
const modelId = await switchModelForPhase(firstPhase, ctx);
|
|
711
|
+
activateAgent(firstPhase, ctx);
|
|
712
|
+
const agentName = firstPhase.agent?.name || firstPhase.key;
|
|
713
|
+
ctx.ui.notify(`${firstPhase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
|
|
714
|
+
setTimeout(() => pi.sendUserMessage(firstPhase.instruction), 200);
|
|
510
715
|
},
|
|
511
716
|
});
|
|
512
717
|
|