TL;DR

A headless AI agent runs without human intervention at runtime. Chatbots wait for people to ask things. Headless agents just work in the background, watching for conditions and taking action. Autogenesis shows what this enables: a living world simulation where NPCs act around the clock, generating narrative that persists between player sessions.


What is a Headless Agent?

A headless agent is a process that runs continuously, makes decisions without human input at runtime, and operates through infrastructure rather than a user interface. No chat window, no voice interface, no person waiting on the other side of a request. The agent runs on server infrastructure, monitors conditions, and takes action based on its programming.

Headless agents get work through APIs, event streams, or scheduled triggers. An API endpoint accepts a payload. An event bus publishes a message. A cron job fires on a schedule. The agent processes the input, applies its logic, and produces an output or side effect. It acts when conditions are met, not when a user initiates contact.

A headless agent runs on server infrastructure as a long-running process. It is not a client-side library that loads in a browser or a function that executes and terminates. The process starts, establishes its state, and stays running. It persists across network calls, across machine reboots, across days or months of operation.

The defining property is no human in the loop at runtime. This constraint drives every subsequent architectural decision. State must persist because there is no human to re-explain context on every operation. Concurrent writes must be safe because multiple agents may act simultaneously without coordination. Fault tolerance must be built in because the agent runs unattended, and infrastructure failures will occur without manual intervention to recover.

These three properties form the foundation: interaction through infrastructure, server-side deployment, and autonomous operation. The sections that follow explore how Autogenesis implements each requirement and what fails when they are not met.


What Problem Does a Headless Agent Solve?

A chatbot needs a human to drive every exchange. You ask, it answers, you ask again. State exists only for the duration of that conversation. Close the chat, and everything learned evaporates.

Autogenesis needed something different. NPCs act autonomously while players sleep. Military conflicts resolve across hours. Territory changes hands overnight. When a player returns days later, the world has moved on without them.

An agent that operates continuously, maintains state across time, and makes decisions without waiting for human input. Not a smarter chatbot. A fundamentally different architectural pattern.

The core issue: most AI systems are built for request-response. Each interaction is self-contained. Headless operation inverts this assumption. The system must persist across time, handle concurrent writes safely, and recover gracefully from infrastructure failures.


State Mutation: Why Every Write Goes Through the Mutex

When an NPC captures a territory, the game updates military points, territory ownership, and player resources. These mutations happen on background threads. Without coordination, two NPCs resolving simultaneously could corrupt the world state.

Autogenesis protects every mutation with WorldManager.worldMutex.withLock. This Kotlin coroutine mutex serializes access to the shared World object.

Territory transfer is atomic:

// WorldManager.kt — transferTerritory protected by mutex
private fun transferTerritory(
    territoryName: String,
    from: String,
    to: String,
    turnNumber: Int,
    timestampMillis: Long,
    skipAdjacencyCheck: Boolean = false
) {
    val territory = world.mapTiles.findTerritoryByName(territoryName) ?: return

    // Remove from previous owner's list
    if(targetFrom.isNotBlank()) {
        val oldPlayer = world.findPlayerByName(targetFrom)
        oldPlayer?.capturedTerritory?.removeIf { it.name.equals(territoryName, ignoreCase = true) }
        oldNpc?.capturedTerritory?.removeIf { it.name.equals(territoryName, ignoreCase = true) }
    }

    // Add to new owner's list
    if(newPlayer != null) {
        territory.ruler = newPlayer.name
        territory.isCaptured = true
        newPlayer.capturedTerritory.add(territory)
    }

    logTerritoryEvent(...)
}

Applied inside a mutex block:

// WorldManager.kt — applyNpcJudgeResults serialized
suspend fun applyNpcJudgeResults(npcName: String, results: Results) {
    worldMutex.withLock {
        applyNpcJudgeResultsUnlocked(npcName, results, turnNumber, timestampMillis)
    }
}

Every state mutation follows this pattern. Territory transfers, point adjustments, resource grants. The mutex is the boundary. Without it, concurrent mutations corrupt data silently.


Concurrent Writes: The Naive Write Problem

Picture two NPCs resolving simultaneously. NPC A attacks a territory. NPC B attacks a different territory. Both trigger state updates on the World object. Without coordination:

  1. NPC A reads World state (territory count = 5)
  2. NPC B reads World state (territory count = 5)
  3. NPC A writes (territory count = 6)
  4. NPC B writes (territory count = 6, but A’s change is lost)

This is the naive write problem. Two agents read the same snapshot, and the second write overwrites the first silently.

Autogenesis solves this with emplaceWithMutex, which acquires exclusive access before writing. Every ContextBank write goes through this method.

Thread-safe context writes in the validator:

// validator.kt — preInitFunction thread-safe context population
setPreInitFunction {
    val playerJson = serialize(player)
    // Acquire exclusive access before writing player state
    ContextBank.emplaceWithMutex("player_data", ContextWindow().apply {
        contextElements.add(playerJson)
    })

    val worldContextData = WorldContextData(
        mapTiles = world.mapTiles.associate { it.name to TileContextData(...) },
        rules = world.worldRules,
        recentHistory = WorldManager.getRecentHistory(1),
        // Anti-retcon data for detecting state contradictions
        depletedOrDestroyedResources = depletedResources,
        defeatedNpcs = defeatedNpcs,
        destroyedTerritories = world.destroyedTerritories.toList()
    )
    // Serialize world state before mutex-protected write
    ContextBank.emplaceWithMutex("world_context", ContextWindow().apply {
        contextElements.add(serialize(worldContextData))
    })

    // NPC ownership verification from Player.capturedNemesis
    val npcData = NpcContextData(playerOwnedNpcs = ownedNpcs)
    ContextBank.emplaceWithMutex("npc_data", ContextWindow().apply {
        contextElements.add(serialize(npcData))
    })

    ContextBank.emplaceWithMutex("user prompt", newContextWindow)
}

The same pattern protects NPC validator context writes:

// npcValidationAgent.kt — NPC prompt stored with mutex
setPreInitFunction {
    val newContextWindow = ContextWindow().apply {
        contextElements.add(it.text)
    }
    ContextBank.emplaceWithMutex("npc prompt", newContextWindow)
}

emplaceWithMutex ensures that when multiple agents write to ContextBank simultaneously, each write completes fully before the next one starts. No lost updates. No corrupted state.


Counter-Play Cascade: Depth-by-Depth Processing

When an NPC attacks a player, that player gets a counter-play window. The response doesn’t end there though. The player’s response might target another NPC, which triggers another counter-play window, and so on. This is the cascade.

The cascade processes by depth, not by agent. All defenders at depth 0 respond to the initial attack. Their responses are collected. Then defenders at depth 1 respond to those responses. This continues until no new defenders enter the system or a cycle is detected.

Cycle detection prevents infinite loops:

// npcOrchestrator.kt — NpcCascadeState tracks the chain
private data class NpcCascadeState(
    val attackerName: String,
    val attackerAction: String,
    val attackerPlayer: Player?,
    val targets: List<Player>,
    val cascadeDepth: Int
)

// Cascade initialization in handleNpcCounterPlay
val cascadeQueue = mutableListOf(
    NpcCascadeState(
        attackerName = npc.name,
        attackerAction = finalActionText,
        attackerPlayer = null,
        targets = initialTargets,
        cascadeDepth = 0
    )
)

while(cascadeQueue.isNotEmpty()) {
    val current = cascadeQueue.removeAt(0)

    // Check for back-edges to prevent cycles
    val newTargets = detected.targets.mapNotNull { targetName ->
        WorldManager.world.activePlayers.find { ... }
    }.filter { nextTarget ->
        val backEdge = "${responseData.player.name}→${nextTarget.name}"
        when {
            // Reject if this target already attacked us (cycle detected)
            nextTarget.name == current.attackerName -> false
            // Reject if we've already seen this edge (cycle detected)
            targetingChain.contains(backEdge) -> false
            else -> true
        }
    }

    if(newTargets.isNotEmpty()) {
        // Add new cascade level
        cascadeQueue.add(
            NpcCascadeState(
                attackerName = responseData.player.name,
                attackerAction = responseData.thirdPersonResponse,
                attackerPlayer = responseData.player,
                targets = newTargets,
                cascadeDepth = current.cascadeDepth + 1
            )
        )
    }
}

The targetingChain set tracks edges that have already appeared. If A→B appears, B→A is rejected as a cycle. This ensures the cascade terminates even when agents target each other.

Defender point costs are deducted inside the mutex:

// Counter-play point deduction inside mutex protection
if(!isDemo) {
    WorldManager.worldMutex.withLock {
        activeDefenders.forEach { defender ->
            when(playType.type) {
                PlayType.Military -> defender.militaryPoints -= 50
                PlayType.Diplomatic -> defender.diplomacyPoints -= 50
                PlayType.Research -> defender.researchPoints -= 50
                else -> {}
            }
        }
    }
}

Pipeline Architecture: Composing Agents from Building Blocks

Autogenesis NPCs are not classes with methods. They are Pipeline objects assembled from factory functions. Each pipeline chains BedrockMultimodalPipe instances together.

The validator builds a three-stage pipeline:

// validator.kt — buildValidator returns a Pipeline
fun buildValidator(player: Player): Pipeline {
    // Stage 1: Legality checker — evaluates action against game rules
    val legalityCheckerPipe = BedrockMultimodalPipe().apply {
        setModel(BedrockConfig.qwenCoder30B)
        setJsonOutput(`Legal?`())
        pullGlobalContext()
        setPageKey("player_data, world_context, local_adjacency, npc_data, other_players")
        forceSaveSnapshot()
        // Rule checking logic: narrative control, world consistency, NPC ownership
    }

    // Stage 2: Legality rectifier — fixes illegal actions
    val legalityRectifierPipe = BedrockMultimodalPipe().apply {
        setJsonInput(`Legal?`())
        pullGlobalContext()
        setPageKey("user prompt, history, player statistics")
        // Preserves intent while making action legal
    }

    // Stage 3: Style reapply — converts to third person narrative
    val styleReapplyPipe = BedrockMultimodalPipe().apply {
        setJsonOutput(ThirdPersonChanges())
        setTemperature(1.0)
        setTopP(0.8)
        // Formats output without expanding or truncating
    }

    return Pipeline().apply {
        add(legalityCheckerPipe)
        add(legalityRectifierPipe)
        add(styleReapplyPipe)
    }
}

The NPC validator follows the same pattern:

// npcValidationAgent.kt — buildNPCValidator for NPC actions
fun buildNPCValidator(): Pipeline {
    val npcLegalityCheckerPipe = BedrockMultimodalPipe().apply {
        setModel(BedrockConfig.qwen235B)
        setJsonOutput(`NPCLegal?`())
        // Rule 1: No narrative control (NPCs participate, don't dictate)
        // Rule 2: No god-mode (NPCs attempt actions, don't decide outcomes)
    }

    val npcLegalityRectifierPipe = BedrockMultimodalPipe().apply {
        setJsonInput(`NPCLegal?`())
        // Transforms controlling actions into participating actions
    }

    val npcStyleReapplyPipe = BedrockMultimodalPipe().apply {
        setJsonOutput(NPCThirdPersonChanges())
        // Formats to third person without content changes
    }

    return npcValidator.apply {
        add(npcLegalityCheckerPipe)
        add(npcLegalityRectifierPipe)
        add(npcStyleReapplyPipe)
    }
}

The judge pipeline evaluates outcomes:

// judge.kt — buildJudge creates pass/fail and consequence pipeline
fun buildJudge(
    player: Player? = null,
    targetActor: Actor? = null,
    knownOutcome: Boolean? = null,
    actionIntent: String? = null,
    targetData: ActionTargetTypeObj? = null,
): Pipeline {
    // Store context for the judge
    if(player != null) {
        ContextBank.emplace("player stats", ContextWindow().apply {
            contextElements.add(serialize(player))
        })
    }

    val judge = Pipeline()

    // Stage 1: Pass or fail — did the action succeed?
    val passOrFailPipe = BedrockMultimodalPipe().apply {
        setJsonOutput(`Victory?`())
        setPageKey("previous turn, user prompt, player stats")
        setSystemPrompt("Your role is to determine whether the player succeeded.")
    }

    // Stage 2: Gains/losses — what did they win or lose?
    val gainsAndLossesPipe = BedrockMultimodalPipe().apply {
        setJsonOutput(Results())
        // Evaluates territory, resources, and stat changes
    }

    // Stage 3: Karma — positive or negative world impact?
    val karmaPipe = BedrockMultimodalPipe().apply {
        setJsonOutput(KarmaResult())
        // Adjusts world karma points up or down
    }

    // Stage 4: Stat changes — what buffs or debuffs apply?
    val statChangePipe = BedrockMultimodalPipe().apply {
        setJsonOutput(MultiActorStatChanges())
        // Applies stat modifications to all affected actors
    }

    return judge.apply {
        add(passOrFailPipe)
        add(gainsAndLossesPipe)
        add(karmaPipe)
        add(statChangePipe)
    }
}

Each factory function returns a configured Pipeline. The orchestrator does not care about the internal structure. It just executes the pipeline and gets a result.


Parallel Execution: Splitter Runs Agents Simultaneously

Some checks must run in parallel. When validating a player action, Autogenesis runs the validator pipeline and the railroad detection pipeline simultaneously. Waiting for one to finish before starting the other would double the latency.

Splitter executes multiple pipelines concurrently:

// gameplayOrchestrator.kt — validation runs two pipelines in parallel
val validationSplitter = Splitter().apply {
    enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))

    val validatorPipeline = buildValidator(player).apply {
        pipelineName = "validator"
        enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))
        enablePipeTimeout(
            applyRecursively = true,
            duration = 180000,
            autoRetry = true,
            retryLimit = 5
        )
    }
    addPipeline("validator", validatorPipeline)
    addContent("validator", MultimodalContent(effectiveTurnAction))

    val railroadDetectorPipeline = buildRailroadDetectionAgent(player, effectiveTurnAction).apply {
        pipelineName = "railroad"
        enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))
        enablePipeTimeout(
            applyRecursively = true,
            duration = 180000,
            autoRetry = true,
            retryLimit = 5
        )
    }
    addPipeline("railroad", railroadDetectorPipeline)
    addContent("railroad", MultimodalContent(effectiveTurnAction))

    init(true)
    streamPipelineOutputToAgentWorkBuffer(connectionId, this)
}

// Execute both in parallel, wait for both to complete
val validationResults = validationSplitter.execute()

Splitter collects results from both pipelines. The orchestrator accesses them via analysisSplitter.results.contents. If one pipeline fails, the other continues, though the overall result may be marked incomplete.

Assessment runs in parallel with resource detection:

// gameplayOrchestrator.kt — analysis phase parallelism
val analysisSplitter = Splitter().apply {
    enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))

    val analysisPipeline = buildPassFailAgent(player).apply {
        pipelineName = "analysis"
        enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))
    }
    attachProgressHooks(analysisPipeline, 3)
    analysisPipeline.init(true)
    addPipeline("analysis", analysisPipeline)
    addContent("analysis", MultimodalContent(narrativeText))

    val resourcesPipeline = buildResourceUsageDetectorAgent(player, effectiveTurnAction).apply {
        pipelineName = "resources"
        enableTracing(TraceConfig(enabled = true, detailLevel = TraceDetailLevel.DEBUG))
    }
    addPipeline("resources", resourcesPipeline)
    addContent("resources", MultimodalContent(narrativeText))
}

Both pipelines execute against the same narrative text. The results aggregate into analysisSplitter.results.contents, which the orchestrator reads to determine the final outcome.


Failure Modes Grounded in Autogenesis Patterns

Headless agents fail in ways chatbots never encounter. Each failure mode has a corresponding Autogenesis pattern that prevents it.

Concurrency corruption occurs without mutex protection. Concurrent writes corrupt shared state. Agent A writes, Agent B writes before A’s change is visible, one update vanishes. Autogenesis prevents this with emplaceWithMutex wrapping every ContextBank write, and worldMutex.withLock protecting every World mutation. The naive write problem cannot occur when every write holds the mutex.

Deadlock happens when agents wait on each other forever. A holds the mutex, B waits for A’s result, A waits for B’s action, neither proceeds. Autogenesis prevents this with cascade depth tracking and cycle detection. The targetingChain set in handleNpcCounterPlay tracks edges and rejects back-edges. If A→B→A would form, the second edge is rejected. The cascade terminates before deadlock forms.

Livelock occurs when agents consume resources without making progress. They retry indefinitely, consuming CPU without accomplishing anything. Autogenesis prevents this with enablePipeTimeout on every pipeline. The timeout fires after 180 seconds, triggers auto-retry with retryLimit = 5. After five failures, the pipeline propagates the error instead of retrying.

State inconsistency happens when agents produce different outputs that cannot be reconciled. The funnel must aggregate diverse responses into coherent state. Autogenesis prevents this with the refinement pipeline. After cascade collection, every response goes through buildResponseRefinementAgent, which converts raw defender responses into consistent third-person prose. The aggregation step then combines refined text, not raw agent output.

Failure recovery is about what happens when a node fails. Work is lost if state is not persisted. ContextBank survives restarts because it stores serialized context windows. When the process restarts, ContextBank.getContextFromBank restores the full state. The worldMutex ensures no partial writes corrupt state during recovery.


Closing: What This Enables

Autogenesis runs NPCs as headless TPipe pipelines. Each NPC turn executes through the validator, target detector, writing agent, and judge. No human drives any of it. The counter-play cascade processes defender responses depth-by-depth with cycle detection. Concurrent writes go through emplaceWithMutex. State mutations go through WorldManager.worldMutex.withLock.

This is what headless architecture looks like when it works.

The world persists while players sleep. NPCs act, make decisions, pursue goals. Military conflicts resolve. Territory changes hands. When a player returns hours later, they find a world that has been evolving without them.

Without headless agents, this game cannot exist. With them, it runs continuously.


Next Steps

Headless agents are not the future. They are the present. Systems that operate continuously, handle concurrent operations safely, and survive failures gracefully are built on architectures that treat these requirements as foundational. Autogenesis demonstrates what is possible when the infrastructure supports headless operation from the ground up.