Phase System Design v2

Extends envoy/design-phases with three key changes agreed 2026-02-28: 1. Model selection removed from the LLM entirely — the phase owns the model 2. Anti-loop mechanism via idle counter and dynamic phase list (no note mutation) 3. next_phases / force_next / one_shot fields added to phase notesMotivation: gpt-4.1-mini spent 16 consecutive iterations in gathering, never transitioningto working, because the model deciding whether to upgrade was the wrong tier to make that call.

1. Phase as a Complete Unit

A phase bundles everything needed to run that step of a task: - Instructions the LLM receives (the existing content field) - The model those instructions are sent to (new model field) - Transition guidance for the LLM (next_phases, force_next, one_shot)Phase note schema (additions to existing format): model : string — alias: 'nano', 'mini', or 'full' orchestrator resolves to actual model ID on phase entry LLM never sees or outputs this next_phases : list — preference-ordered list of recommended next phases, shown to LLM each iteration force_next : string — phase the orchestrator transitions to at idle_count=5 one_shot : bool — if true, phase is removed from available list once left (e.g. triage: you never return to triage mid-task)

2. Model Selection: Phase Owns It, LLM Does Not

next_model is REMOVED from the EnvoyResponse schema.The LLM's only job regarding model selection is to choose the next phase.The orchestrator reads model from the phase note on phase entry and uses it.The LLM never sees, outputs, or reasons about model names.Model aliases resolved by orchestrator: nano -> gpt-4.1-nano mini -> gpt-4.1-mini full -> gpt-5.2-chat-latestAliases are stable; the underlying model IDs can be updated in one place.If a phase note has no model field, the orchestrator falls back to the global default.This breaks the circular dependency: mini in gathering was deciding whether toupgrade itself — always choosing mini. Now gathering says model=mini, working saysmodel=full. The upgrade is automatic on phase transition, no LLM judgment required.

3. Anti-Loop: Idle Counter + Dynamic Phase List

An idle iteration is one where the LLM made no progress: - all gather requests were skipped (notes already in context) - no notes were written - no emails were sentidle_count tracks consecutive idle iterations in the current phase.It is stored in the continuation JSON (not in any shared note).It resets on phase change or when output is produced.IMPORTANT: idle_count lives in the continuation JSON, not in phase notes.Two concurrent Envoy instances each have their own continuation JSON andtherefore their own independent idle_count. No locking required.The anti-loop mechanism works through the presented list of available phases.As idle iterations accumulate, the option to stay in the current phase isprogressively degraded, then removed, then overridden: idle_count=0 next_phases from note; current phase NOT offered idle_count=1 current phase at bottom: 'you may remain' idle_count=2 current phase still listed: 'consider moving on' idle_count=3 current phase at tail: 'remaining is now discouraged' idle_count=4 current phase REMOVED from list; LLM has no legitimate way to stay idle_count=5 if LLM sets status=current_phase anyway, orchestrator overrides to force_next; model switches to target phase's modelNo phase notes are modified during a task run. The dynamic list is computedfresh each iteration by the orchestrator from the phase note and idle_count.

4. Available Phases Injected Each Iteration

Each iteration the orchestrator injects a context block listing available phases.Example at idle_count=2: === AVAILABLE PHASES === You may set status to any of these: 1. working <- recommended next 2. summarising 3. composing 4. complete (only when task is fully done) 5. escalate (only when blocked, needs human help) 6. gathering <- you may stay, but consider moving on (2 idle iterations) Current phase: gathering [idle: 2]At idle_count=4, gathering is absent. At idle_count=5, orchestrator ignoresthe LLM's status if it names the current phase and forces force_next.One-shot phases already left are excluded from the available list.complete and escalate are always available regardless of next_phases.The list is derived from the current phase's next_phases field.If next_phases is absent, the orchestrator falls back to the full list ofenvoy/states/* notes that exist.

5. Two Layers of Instruction

What instructions the LLM follows comes from two distinct sources:Phase note (static, general — same for every task using this phase): 'In the working phase: write notes iteratively, produce content, do not re-request notes already in context, transition to composing when done.'Task context (dynamic, specific to this run — assembled by orchestrator each iteration): 'We are writing a LinkedIn article about pwsafe. Gathered notes: pwsafe/story/01-07, ask. Sections written so far: 1, 2, 3. Next: complete sections 4-6, then transition to composing.'Task context comes from: working_note, continuation JSON progress summary,the original email, and notes already loaded into context.The phase note tells the LLM HOW to work (methodology).The task context tells it WHAT to work on (goal, progress, next step).Neither can substitute for the other.

6. Phase Note Self-Modification: Strategic Only

Phase notes may be updated by Envoy, but ONLY as a deliberate strategic decisionmade after completing a task — when Envoy has observed a pattern across multipleruns and judges that a permanent improvement is warranted.Valid long-term edits: - Reordering next_phases after learning a transition is almost always correct - Adjusting the model field after observing a phase consistently needs more capability - Adding a new phase note for a task type that keeps recurringNOT valid: modifying phase notes mid-task as a loop-breaking tactic.Reasons: - Changes persist beyond the current task, polluting subsequent runs - Two concurrent instances writing the same note would require locking - An in-flight LLM reacting to one task's friction is not the right authorThe anti-loop mechanism (idle_count + dynamic list) handles in-task pressure.Phase notes handle long-term design.

7. Implementation Priority

MUST (prevents recurrence of the 16-iteration gathering loop): 1. Add model field to all phase notes; add alias resolution to orchestrator; switch model on phase entry; remove next_model from EnvoyResponse schema 2. Add idle_count to continuation JSON; compute dynamic phase list each iteration; degrade current-phase option at idle 1-4; force transition at idle 5 3. Inject available phases context block each iterationGOOD (cleans up architecture): 4. Add next_phases, force_next, one_shot to all phase notes; orchestrator reads them to build the available list and perform forced transitionsLATER (full vision): 5. Arbitrary phase names: status enum -> free string, accept any envoy/states/{name}

Navigation

Phase Design v1 — original phase/state machine design (still current for state descriptions)TODO — implementation tasks derived from this designOrchestrator Implementation

version 1  ·  created 2026-02-28