Orchestration Layer
Shared from "Edward" on Inkdown
Overview
Think of it like a conductor leading an orchestra:
- Conductor = Orchestration layer
- Musicians = LLM, Docker sandbox, file system, package manager, build tools
- Music = The generated code
The orchestration layer makes sure everyone plays at the right time, in the right order.
2. The 5-Minute Overview
The Big Picture
The 3 Main Phases
3. Step-by-Step Flow
Step 1: User Sends Message
File: apps/web/stores/chatStream/useStartStream.ts
Step 2: API Admission
File: apps/api/services/runs/messageOrchestrator.service.ts
- Admission control prevents overload (global + per-user + per-chat limits)
- Run is persisted BEFORE execution (durable)
- Browser gets runId immediately for tracking
Step 3: Worker Picks Up Job
File: apps/api/services/runs/agent-run-worker/processor.ts
- Worker is independent (can restart without losing progress)
- Cancel signal via Redis pub/sub (fast)
- Events persisted to DB as they happen (resumable)
Step 4: Stream Session Setup
File: apps/api/services/chat/session/orchestrator/runStreamSession.orchestrator.ts
- Framework resolved before LLM call (better prompts)
- Token budget checked BEFORE calling LLM (fail fast)
- Post-generation validation + autofix (quality control)
Step 5: Agent Loop (Multi-Turn)
File: apps/api/services/chat/session/loop/agentLoop.runner.ts
- Tools were called but no file output yet
- No
<done> tag received
- Under turn budget
- Code/file output detected
<done> tag received
- Tool budget exceeded
- Max turns reached
- Client aborted
Step 6: Turn Execution (Stream + Parse)
File: apps/api/services/chat/session/loop/agentLoop.stream.ts
- Chunks processed as they arrive (not waiting for full response)
- Parser converts raw text → structured events
- Events trigger immediate side effects
Step 7: Parser State Machine
File: apps/api/lib/llm/parser.ts
| State | Trigger | Exit |
|---|
| TEXT | Default | <thinking>, <edward_sandbox>, <file> |
| THINKING | <thinking> | </thinking> |
| SANDBOX | <edward_sandbox> | </edward_sandbox> |
| FILE | <file path="..."> | </file> |
| INSTALL | <install> | </install> |
- Chunks can split tags across boundaries
- Need to handle incomplete output safely
- Can't just regex over full string
Step 8: Event Handler (Side Effects)
File: apps/api/services/chat/session/events/handler.ts
| Event | Action |
|---|
| SANDBOX_START | Provision Docker container |
| FILE_START | Prepare file path |
| FILE_CONTENT | Buffer to Redis |
| FILE_END | Sanitize file |
| SANDBOX_END | Flush buffers to disk |
| INSTALL_CONTENT | Queue npm install |
| COMMAND | Run shell command |
| WEB_SEARCH | Search web |
Step 9: Sandbox Write Flow (Buffered)
File: apps/api/services/sandbox/write/buffer.ts + flush.ts
Write (Buffered to Redis)
Flush (Redis → Container)
- Resilient to partial failures
- Can batch multiple writes
- Can replay/repair on failure
Step 10: Install Task Queue
File: apps/api/services/chat/session/loop/agentLoop.runner.ts
Step 11: Turn Outcome Decision
File: apps/api/services/chat/session/loop/agentLoop.turnOutcome.ts
Step 12: Finalize
File: apps/api/services/chat/session/orchestrator/runStreamSession.finalize.ts
4. Deep Dive: Each Layer
Layer 1: Message Orchestrator
Purpose: Admission control + queue + stream handoff
unifiedSendMessage() - Entry point
createAdmittedRun() - Create run with limits
enqueueAdmittedRun() - Queue to worker
streamRunEventsFromPersistence() - SSE to browser
- API key decryption fails
- Model/provider mismatch
- Run admission rejected (limits)
- Queue enqueue fails
Layer 2: Stream Session
Purpose: Framework resolve + message prep + token budget + finalize
resolveFramework() - Detect/prefer framework
prepareBaseMessages() - Build LLM context
composePrompt() - System prompt
computeTokenUsage() - Budget check
finalizeStreamSession() - Persist results
- Context limit exceeded
- Framework detection fails
- Finalize persistence fails
Layer 3: Agent Loop
Purpose: Multi-turn execution + outcome decisions
runAgentLoop() - Main loop
executeAgentTurnStream() - Single turn
resolveTurnOutcome() - Continue/stop decision
- Turn budget exceeded
- Max turns reached
- Abort signal received
- Continuation prompt fails
Layer 4: Parser
Purpose: Chunk → event conversion
createStreamParser() - State machine
process() - Parse chunk
flush() - Handle incomplete output
- Tag split across chunks
- Incomplete output
- State machine stuck
Layer 5: Event Handler
Purpose: Side effect execution
handleParserEvent() - Dispatch by type
handleFileContent() - Buffer writes
handleInstallContent() - Queue installs
handleCommandEvent() - Run commands
- Sandbox not provisioned
- File write fails
- Install conflicts
- Command timeout
Layer 6: Sandbox Write
Purpose: Buffered writes to container
writeSandboxFile() - Buffer to Redis
flushSandbox() - Redis → container
scheduleSandboxFlush() - Debounced flush
- Redis unavailable
- Docker exec fails
- Lock acquisition fails
- Container stopped
5. Key Files to Read
Core Orchestration
| File | Purpose |
|---|
apps/api/services/runs/messageOrchestrator.service.ts | Entry point |
apps/api/services/runs/agent-run-worker/processor.ts | Worker execution |
apps/api/services/chat/session/orchestrator/runStreamSession.orchestrator.ts | Stream session |
apps/api/services/chat/session/loop/agentLoop.runner.ts | Agent loop |
apps/api/services/chat/session/loop/agentLoop.stream.ts | Turn execution |
Parser + Events
| File | Purpose |
|---|
apps/api/lib/llm/parser.ts | State machine parser |
apps/api/services/chat/session/events/handler.ts | Event side effects |
apps/api/services/chat/session/loop/events.ts | Event processing |
apps/api/services/chat/session/loop/agentLoop.turnOutcome.ts | Continue/stop logic |
Sandbox
| File | Purpose |
|---|
apps/api/services/sandbox/write/buffer.ts | Redis buffering |
apps/api/services/sandbox/write/flush.ts | Flush to container |
apps/api/services/sandbox/write/flush.scheduler.ts | Debounced flush |
apps/api/services/chat/file.handlers.ts | File content handling |
6. Common Questions
Q: Why multi-turn loop instead of one LLM call?
A: Complex tasks need multiple steps:
Q: Why buffer writes to Redis instead of writing directly?
- Resilience: If Docker fails, buffer survives in Redis
- Performance: One flush vs many small writes
- Batching: Multiple chunks → one file write
Q: Why serialize installs?
A: Prevent race conditions:
Q: How does cancellation work?
-
Redis pub/sub (fast):
-
DB polling (backup):
Q: What happens if worker crashes mid-turn?
A: Checkpoint system allows resume:
Q: How are tokens budgeted?
Q: How does framework detection work?
Q: What's the difference between run_event and message?
7. Debugging Guide
Trace a Turn
Common Failures
| Symptom | Likely Cause | Fix |
|---|
| Context limit exceeded | Too much history | Truncate context |
| Tool budget exceeded | Too many tool calls | Reduce per-turn limit |
| Turn stuck in loop | No code output detected | Check parser, tags |
| Files not written | Flush failed | Check Redis, Docker |
| Install conflicts | Concurrent installs | Check queue serialization |
8. Summary
The Orchestration Flow in One Diagram
Key Takeaways
- Orchestration is layered - Each layer has a clear responsibility
- Multi-turn is essential - Complex tasks need iteration
- Buffering matters - Redis buffers make writes resilient
- Budgets prevent runaway - Token, tool, turn limits
- Events are durable - Persisted for replay/resume
- Cancellation is dual - Pub/sub + DB polling