Skip to content
published Visibility internal Owner erik@uvilo.com Approver _ Created _ Updated _

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

RequirementStatus
Cron scheduling of agents✅ Inngest schedules → orchestrator → uvilo-mono / Bot execution
Project/skill discovery tool✅ forge-discovery MCP
Todo/checklist tooltodos tool group (createTodoList, createTodoItem, findTodoItems, updateTodoItem, etc.)
Spawning sub-agentsspawnAgent tool (sync + async modes)
Async job monitoring & recoverycheckAgentJob, 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:

spawnAgent({
  message: "<task prompt>",
  botId: "<bot_id>" | botGroup + botHandle,
  mode: "sync" | "async",
  convoName: "<optional label>",
  project: "<project name>",
  department: "<department name>",
  completionEvent: { name: "<inngest-event>" }  // async only
})
  • 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 jobId immediately. Use checkAgentJob in a later turn to retrieve status or results.

Support Endpoints & Tools

Tool / EndpointPurpose
spawnAgentCreate and run a sub-agent (sync or async)
checkAgentJobLook up AgentJob status, result, steps, usage, and message history
abortAgentCancel a running sub-agent at the next opportunity
switchAgentTransfer the current conversation to a different Bot

Persistence

  • Conversations and messages are persisted by uvilo-mono in its database (Postgres-backed).
  • Each spawnAgent call creates an AgentJob row 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 checkAgentJob to poll for status. The completionEvent option can fire an Inngest workflow on completion, eliminating the need for polling.
  • abortAgent stops 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 runAsForgeAgent option on spawnAgent runs 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:

OverrideDescription
modelOverrideSwap the Bot’s model (e.g., "openai:gpt-5-mini")
systemPromptOverrideReplace the Bot’s system prompt entirely
systemPromptAppendAppend extra instructions to the Bot’s system prompt
temperatureOverride the model temperature
reasoningEffortOverride reasoning effort (nonexhigh)
maxStepsMax 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

  1. Receive trigger from Inngest (or on-demand via POST /orchestrator/run)
  2. Authenticate with uvilo-mono using API-key auth
  3. Invoke the appropriate Bot via spawnAgent with task prompt + context
  4. For async jobs: monitor via checkAgentJob; on completion, parse result and update orchestration state
  5. 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.

ToolReturns
forge-discovery__list_departmentsAll department folders with README summaries
forge-discovery__list_dept_projectsProjects in one department (name, department, phase)
forge-discovery__get_projectFull project data: name, department, phase, and files with frontmatter metadata
forge-discovery__list_skillsAll available skills with descriptions
forge-discovery__get_skillFull SKILL.md content for a named skill
forge-discovery__findFuzzy 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

ParameterTypeDescription
messagestringThe initial user message to send to the sub-agent
botIdstringBot ID to spawn as (use this OR botGroup + botHandle)
botGroupstringBot group (e.g., "forge") — required with botHandle when botId is omitted
botHandlestringBot 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)
convoNamestringShort label for the spawned conversation sidebar entry. For project phase agents, use {phaseValue} - {Department} {ProjectName}
projectstringForge project context for the sub-agent job
departmentstringForge department context for the sub-agent job
completionEventobjectInngest event to fire on completion (async only): { name, data? }
runAsForgeAgentbooleanRun as the Forge agent service account (default: current chat user)
overridesobjectOverride 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

  1. After spawning an async sub-agent, the orchestrator records the jobId
  2. Use checkAgentJob(jobId) to retrieve current status (running, completed, failed, aborted)
  3. Set includePartial: true to get a partial-result placeholder if the job is still running
  4. Set includeMessages: true after completion to retrieve full message history and token usage

Intervention Conditions

ConditionAction
Job status is running beyond expected timeoutabortAgent(jobId) → restart with fresh spawnAgent and additional context
Job status is failedReview error from checkAgentJob; restart if partial work exists on filesystem
Job status is abortedDetermine cause; restart fresh with context from filesystem state
Maximum retries exceededCreate 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 jobId and filesystem state
  • Fresh Bot (when previous attempt was corrupted): spawn with conversationId: "new" equivalent (new spawnAgent call) 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)

AgentResponsibility
Page ManagerCreate/rename/move/delete pages + sidebar
Project WorkerExecute Plan tasks (implement, modify files, run builds)
Project EvaluatorEvaluate all phases — Vision_Eval, Spec_Eval, Plan_Eval, Execute_Eval, Extract_Eval, Verify
Project ThinkerDraft 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

  1. The user asks Forge Chat to do something project-related (create a project, approve a phase, check status)
  2. Forge Chat detects the project routing intent (configured in its Bot prompt)
  3. Forge Chat calls switchAgent({ botGroup: "forge", botHandle: "project-bot", reason: "..." })
  4. The conversation transfers to Project Runner under the user’s account
  5. 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:

ModeTriggerHuman Present?Behavior at Human Gates
InteractiveswitchAgent handoff from Forge ChatYesAsk the user directly for approval
AutonomousHourly cron or spawnAgentNoCreate 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

JobScheduleDescription
Project RunnerHourlyCheck active project phases
Task RunnerHourlyCheck for new agent todo items
Memory CompactionOnce dailyRun memory compaction agent
Infrastructure CheckMonthlyCheck 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

POST /orchestrator/run
{
  "botId": "<bot_id>",
  "message": "...",
  "mode": "sync" | "async"
}

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:

ColumnTypeDescription
idTEXT (agt_ prefix, Cuid2)Primary key
jobIdTEXTThe jobId returned by spawnAgent
botIdTEXTBot that was invoked
taskPromptTEXTThe task given to the agent
statusTEXT CHECKrunning | completed | failed | aborted
startedAtTIMESTAMPTZWhen the agent was invoked
completedAtTIMESTAMPTZWhen the agent finished
retryCountINTNumber of restart attempts
parentJobIdTEXT FKFor sub-agent tracking
projectTEXTProject name (e.g., Project_Automation); passed via spawnAgent project param
phaseTEXTPhase name (e.g., Execute_1_Started); passed via spawnAgent phase param
metadataJSONBArbitrary 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 }).

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:

<!-- include: Forge/Configs/Agents/Env_Snippet.md -->

This allows shared snippets and skills to be composed into agent prompts without runtime tool calls.