Forge Agent Orchestration
Multi-agent orchestration that schedules, dispatches, monitors, and recovers AI agents — enabling cron-driven project management, supervised sub-agent delegation, and self-healing execution within the Forge stack.
Goal
A single agent cannot handle every task. Orchestration coordinates multiple specialized agents to work together on complex, multi-step workflows: scheduling agents on cron, dispatching sub-agents for focused work, monitoring their progress, and restarting them when they stall. The orchestrator is a thin dispatcher — it triggers agents and handles failures; it does not replace the uvilo-mono agent runtime.
Requirements Summary
| Requirement | Status |
|---|---|
| Cron scheduling of agents | ✅ Inngest schedules → orchestrator → uvilo-mono / Bot execution |
| Project/skill discovery tool | ✅ forge-discovery MCP |
| Todo/checklist tool | todos tool group (createTodoList, createTodoItem, findTodoItems, updateTodoItem, etc.) |
| Spawning sub-agents | ✅ spawnAgent tool (sync + async modes) |
| Async job monitoring & recovery | ✅ checkAgentJob, abortAgent, timeout/reaper behavior |
| Sub-agent transcript review | ✅ Conversation history in uvilo-mono; durable project reports on filesystem |
| Project Runner agent | ✅ Checks project phases; initiates next phase or notifies user |
| Task Runner agent | ✅ Checks agent todo list; dispatches SRP agents |
| Single-Responsibility agents | ✅ Page Manager, Project Worker/Evaluator/Thinker |
Execution Layer — uvilo-mono Bot API
All orchestrator-driven agent execution uses the uvilo-mono Bot API. Bots are identified by botId or botGroup + botHandle. Execution creates an AgentJob row in Postgres and runs the Bot’s model, system prompt, and tools in an isolated conversation.
Invocation Protocol
spawnAgent is the primary invocation mechanism — available as a tool to any running agent:
- Sync mode — blocks until the sub-agent completes; returns the full result in the same turn.
- Async mode — starts the sub-agent in the background and returns a
jobIdimmediately. UsecheckAgentJobin a later turn to retrieve status or results.
Support Endpoints & Tools
| Tool / Endpoint | Purpose |
|---|---|
spawnAgent | Create and run a sub-agent (sync or async) |
checkAgentJob | Look up AgentJob status, result, steps, usage, and message history |
abortAgent | Cancel a running sub-agent at the next opportunity |
switchAgent | Transfer the current conversation to a different Bot |
Persistence
- Conversations and messages are persisted by uvilo-mono in its database (Postgres-backed).
- Each
spawnAgentcall creates anAgentJobrow with status tracking (running,completed,failed,aborted). checkAgentJob(jobId, { includeMessages: true })returns the full message history and token usage after completion.- Project reports and deliverables are written to the filesystem as durable markdown files.
Streaming & Status
- In sync mode, the caller receives the result directly after the sub-agent finishes.
- In async mode, the caller uses
checkAgentJobto poll for status. ThecompletionEventoption can fire an Inngest workflow on completion, eliminating the need for polling. abortAgentstops a running sub-agent at the next opportunity; partial results or in-progress tool calls may be discarded.
Auth & Execution
- The orchestrator authenticates to uvilo-mono using API-key authentication (service account).
- No JWT lifecycle management is needed — API keys do not expire on short timers.
- The
runAsForgeAgentoption onspawnAgentruns the sub-agent as the Forge agent service account rather than the current chat user.
Parameter Control
The orchestrator can control agent behavior through spawnAgent overrides:
| Override | Description |
|---|---|
modelOverride | Swap the Bot’s model (e.g., "openai:gpt-5-mini") |
systemPromptOverride | Replace the Bot’s system prompt entirely |
systemPromptAppend | Append extra instructions to the Bot’s system prompt |
temperature | Override the model temperature |
reasoningEffort | Override reasoning effort (none–xhigh) |
maxSteps | Max tool-use steps the sub-agent may take (default 500) |
Unknown/extra parameters are silently ignored. Agent behavior should be encoded in the Bot’s configuration and the task prompt — not in undocumented API parameters.
Orchestrator Service — Dispatcher
A Railway web service (TypeScript, Hono) that serves as the thin orchestration layer between scheduling and uvilo-mono Bot execution.
Core Loop
- Receive trigger from Inngest (or on-demand via
POST /orchestrator/run) - Authenticate with uvilo-mono using API-key auth
- Invoke the appropriate Bot via
spawnAgentwith task prompt + context - For async jobs: monitor via
checkAgentJob; on completion, parse result and update orchestration state - On stall/timeout:
abortAgent→ restart with fresh context
API-Key Auth
The orchestrator authenticates using a long-lived API key stored as a Railway environment variable. No token refresh cycle is needed. The key grants access to uvilo-mono’s Bot execution endpoints on behalf of the Forge service account.
Migration Complete
The migration from the former runtime to uvilo-mono is complete. All agent execution, conversation persistence, and sub-agent spawning now go through uvilo-mono’s Bot API and the spawnAgent / checkAgentJob / abortAgent tooling.
Discovery MCP Server
forge-discovery — a read-only MCP server backed by filesystem reads. Provides structured, relational queries for project/skill discovery without filesystem grepping. Tools may need to be loaded if deferred; use toolSearch to find available tools and loadTool to activate them.
| Tool | Returns |
|---|---|
forge-discovery__list_departments | All department folders with README summaries |
forge-discovery__list_dept_projects | Projects in one department (name, department, phase) |
forge-discovery__get_project | Full project data: name, department, phase, and files with frontmatter metadata |
forge-discovery__list_skills | All available skills with descriptions |
forge-discovery__get_skill | Full SKILL.md content for a named skill |
forge-discovery__find | Fuzzy match by partial name across departments, projects, and skills |
Tools are discovered at runtime via toolSearch and activated with loadTool. No YAML configuration or container-specific transport setup is needed — tools execute within the agent’s tool runtime.
Sub-Agent Spawning — spawnAgent Tool
A tool available to any running agent that creates and runs a sub-agent in its own isolated conversation. The sub-agent gets its own model, system prompt, and tools, and can execute multi-step workflows.
Sync mode: Blocks until the sub-agent completes and returns the full result in the same turn. Use when the caller needs the result before continuing.
Async mode: Starts the sub-agent in the background and returns a jobId immediately. The caller ends its turn after spawning — do not call checkAgentJob to poll in the same turn. Use checkAgentJob in a later turn to retrieve status or results. Optionally specify completionEvent to trigger an Inngest workflow on completion.
spawnAgent Parameters
| Parameter | Type | Description |
|---|---|---|
message | string | The initial user message to send to the sub-agent |
botId | string | Bot ID to spawn as (use this OR botGroup + botHandle) |
botGroup | string | Bot group (e.g., "forge") — required with botHandle when botId is omitted |
botHandle | string | Bot handle within group (e.g., "task-runner") — required with botGroup when botId is omitted |
mode | "sync" | "async" | Sync: block until complete. Async: return jobId immediately (default: async) |
convoName | string | Short label for the spawned conversation sidebar entry. For project phase agents, use {phaseValue} - {Department} {ProjectName} |
project | string | Forge project context for the sub-agent job |
department | string | Forge department context for the sub-agent job |
completionEvent | object | Inngest event to fire on completion (async only): { name, data? } |
runAsForgeAgent | boolean | Run as the Forge agent service account (default: current chat user) |
overrides | object | Override model, system prompt, temperature, reasoning effort, max steps |
Transcript Review
Sub-agent conversations are persisted by uvilo-mono and accessible via checkAgentJob(jobId, { includeMessages: true }) after completion. The jobId links to the full message history with tool-call details and token usage. Project deliverables are written to the filesystem as durable markdown files in the project’s output directory.
Async Job Monitoring & Recovery
The current monitoring system tracks async AgentJobs, detects stalls, and recovers failed or timed-out executions.
Monitoring Behavior
- After spawning an async sub-agent, the orchestrator records the
jobId - Use
checkAgentJob(jobId)to retrieve current status (running,completed,failed,aborted) - Set
includePartial: trueto get a partial-result placeholder if the job is still running - Set
includeMessages: trueafter completion to retrieve full message history and token usage
Intervention Conditions
| Condition | Action |
|---|---|
Job status is running beyond expected timeout | abortAgent(jobId) → restart with fresh spawnAgent and additional context |
Job status is failed | Review error from checkAgentJob; restart if partial work exists on filesystem |
Job status is aborted | Determine cause; restart fresh with context from filesystem state |
| Maximum retries exceeded | Create a 🚫 todo item for the user via createTodoItem with project, phase, blocker details, and work-product link |
Continuation Strategy
- Same Bot, new conversation (preferred when partial work exists): spawn the same Bot with a task prompt that references the prior
jobIdand filesystem state - Fresh Bot (when previous attempt was corrupted): spawn with
conversationId: "new"equivalent (newspawnAgentcall) and include task + state summary in the message
Context Is a Cache, Not State
The agent must reconstruct its situation from the filesystem and database state alone. If it can’t recover from a restart by reading the current state of files and todo items, the architecture has a single point of failure. The monitoring system doesn’t pass accumulated context — it relies on the filesystem as the source of truth.
Agent Types
Project Runner
Checks active project phases. When a phase completes, initiates the next phase or creates a todo item for the user via the todos tool group. Uses master + clone pattern: one master agent + clones using different models for resilience and comparison.
Task Runner
Checks agent todo items via the todos tool group and dispatches the correct SRP agent. Uses same master + clone pattern. Dispatches only — does not execute tasks itself.
SRP Agents (Single-Responsibility Principle)
| Agent | Responsibility |
|---|---|
| Page Manager | Create/rename/move/delete pages + sidebar |
| Project Worker | Execute Plan tasks (implement, modify files, run builds) |
| Project Evaluator | Evaluate all phases — Vision_Eval, Spec_Eval, Plan_Eval, Execute_Eval, Extract_Eval, Verify |
| Project Thinker | Draft Vision, Research, Spec, Plans; run Extract |
SRP agents have minimal system prompts and least-privilege tool access. They are invocable from complex agents (Project/Task Runner) via spawnAgent.
Agent Handoffs
The switchAgent tool enables real-time conversation transfer from one Bot to another. This gives users an interactive entry point for project work without waiting for the hourly cron.
How It Works
- The user asks Forge Chat to do something project-related (create a project, approve a phase, check status)
- Forge Chat detects the project routing intent (configured in its Bot prompt)
- Forge Chat calls
switchAgent({ botGroup: "forge", botHandle: "project-bot", reason: "..." }) - The conversation transfers to Project Runner under the user’s account
- Project Runner operates in Interactive Mode — the user is present and can respond in real-time
Forge Chat Routing Rules
Forge Chat hands off to Project Runner when the user requests:
- Creating a project
- Approving a phase
- Making a research or eval decision
- Checking project status
- Anything referencing a specific project by name
Forge Chat does not hand off for:
- One-off tasks unrelated to a project lifecycle phase
- General system questions
- File edits outside a project plan
Interactive vs Autonomous Mode
Project Runner operates in two modes, detected from context:
| Mode | Trigger | Human Present? | Behavior at Human Gates |
|---|---|---|---|
| Interactive | switchAgent handoff from Forge Chat | Yes | Ask the user directly for approval |
| Autonomous | Hourly cron or spawnAgent | No | Create a todo item for the user and stop |
In Interactive Mode, the user is present under their own account. Project Runner can ask questions and get real-time approvals. This is the primary way humans steer projects.
In Autonomous Mode, Project Runner reconciles stalled projects and advances phases that don’t require human gates. It checks all active projects hourly via forge-discovery and handles phase transitions per the routing table.
Cron Scheduling
Inngest handles scheduling. It triggers the orchestrator on each schedule, which then spawns the appropriate Bot via spawnAgent.
Schedule Configuration
| Job | Schedule | Description |
|---|---|---|
| Project Runner | Hourly | Check active project phases |
| Task Runner | Hourly | Check for new agent todo items |
| Memory Compaction | Once daily | Run memory compaction agent |
| Infrastructure Check | Monthly | Check infrastructure configuration |
Schedule task prompts live in uvilo-mono orchestrator/src/schedules.ts, with Inngest bindings in orchestrator/src/inngest.ts. Treat these prompts as active workflow instructions: when project queue behavior, recovery sources, human-gate routing, or work-product links change, update schedule prompts in the same change set as Bot prompts, Skills, and Knowledge. Schedule prompts must use the todos tool group and durable work-product/report links; they must not reference retired flat-file queues or WIP recovery.
Prerequisite: Inngest Cloud account is required for scheduled execution. Without it, the orchestrator runs in INNGEST_DEV=1 mode (local dev server only). Create an Inngest Cloud account, obtain signing key + event key, and configure the Railway service.
On-Demand Triggering
Data Storage
Filesystem (Source of Truth for Documents)
Project documents remain as markdown files in the git repo: Phase, Vision, Research, Spec, Plans, Execute_State, evaluation reports, Verification, Extract_Eval, Runs, Changelog, and Learnings. The filesystem is the source of truth for project structure and document content. Recovery state is reconstructed from the Phase file, current durable work-products, persisted evaluation or verification reports, Execute_State files, saved conversation or AgentJob metadata, git status, and relevant todo items.
Postgres — forge Database
Railway VectorDB Postgres instance with a separate forge database for orchestration state. Connection: FORGE_DB_URL env var.
AgentJob table:
| Column | Type | Description |
|---|---|---|
| id | TEXT (agt_ prefix, Cuid2) | Primary key |
| jobId | TEXT | The jobId returned by spawnAgent |
| botId | TEXT | Bot that was invoked |
| taskPrompt | TEXT | The task given to the agent |
| status | TEXT CHECK | running | completed | failed | aborted |
| startedAt | TIMESTAMPTZ | When the agent was invoked |
| completedAt | TIMESTAMPTZ | When the agent finished |
| retryCount | INT | Number of restart attempts |
| parentJobId | TEXT FK | For sub-agent tracking |
| project | TEXT | Project name (e.g., Project_Automation); passed via spawnAgent project param |
| phase | TEXT | Phase name (e.g., Execute_1_Started); passed via spawnAgent phase param |
| metadata | JSONB | Arbitrary key-value |
Naming conventions: PascalCase tables, camelCase columns, Cuid2 IDs with 3-letter prefix, Table_column_idx index names.
Conversation Persistence
Conversations and messages are persisted by uvilo-mono in its database. The jobId in AgentJob references the uvilo-mono conversation record. Full message history is retrievable via checkAgentJob(jobId, { includeMessages: true }).
Typesense (Semantic Search)
The existing Typesense index provides semantic search across all repo content. forge-discovery complements it with structured, relational queries.
Include Directive
Agent prompt files support <!-- include: path --> directives, where path is relative to the repo root. When building the Bot’s prompt, each directive is replaced with the referenced file’s content (frontmatter stripped). Includes are resolved recursively up to depth 10; cycles and missing files produce errors.
Example:
This allows shared snippets and skills to be composed into agent prompts without runtime tool calls.