OS Model API Reference

On Sun, 04 Oct 2026, by @lucasdicioccio, 664 words, 44 code snippets, 9 links, 0images.

Generated from documentation/OS-API.md, the repository is the canonical source and may be ahead of this page.

OS Model API Reference

Complete API reference for the Entity-Component-System (ECS) based OS architecture.

Table of Contents

  1. Core ECS Types
  2. World Operations
  3. Agent Operations
  4. Toolbox Operations
  5. Resource Management
  6. Concurrent Access
  7. Conversation Tracking
  8. OS Events
  9. Persistence Layer

Core ECS Types

EntityId

-- | Unique identifier for any entity in the system.
newtype EntityId = EntityId { unEntityId :: UUID }
    deriving (Eq, Ord, Show, Hashable, FromJSON, ToJSON)

-- | Generate a new unique EntityId.
newEntityId :: IO EntityId

Example:

eid1 <- newEntityId  -- Unique ID
eid2 <- newEntityId  -- Different unique ID
assert (eid1 /= eid2)  -- True

Phantom-Typed Entity IDs

Phantom types provide compile-time safety for entity operations:

newtype AgentId = AgentId { unAgentId :: EntityId }
newtype ToolboxId = ToolboxId { unToolboxId :: EntityId }
newtype ConversationId = ConversationId { unConversationId :: EntityId }
newtype TurnId = TurnId { unTurnId :: EntityId }
newtype ToolCallId = ToolCallId { unToolCallId :: EntityId }
newtype ResourceId = ResourceId { unResourceId :: EntityId }

Example:

eid <- newEntityId
let agentId = AgentId eid
let toolboxId = ToolboxId eid  -- Same underlying ID, different type

-- Type-safe: Can't mix up IDs
useAgent agentId       -- OK
useAgent toolboxId     -- Compile error!

Component Typeclass

-- | Type-level identifier for component types.
newtype ComponentTypeId = ComponentTypeId { unComponentTypeId :: Int }

-- | A component is any data type that can be attached to an entity.
class Component a where
    componentId :: Proxy a -> ComponentTypeId

Creating a custom component:

data MyComponent = MyComponent
    { mcValue :: Int
    , mcName :: Text
    } deriving (Show, Eq, Generic)

instance FromJSON MyComponent
instance ToJSON MyComponent

-- Choose a unique ID (check existing IDs to avoid collisions)
instance Component MyComponent where
    componentId _ = ComponentTypeId 100

World Operations

Creating and Managing Worlds

-- | The world contains all component stores.
newtype World = World { componentStores :: HashMap ComponentTypeId (TVar Any) }

-- | Create an empty world.
newWorld :: STM World

-- | Register a component store for a component type.
registerComponentStore :: (Component a, Typeable a) => World -> Proxy a -> STM World

Example:

import Data.Proxy (Proxy(..))

-- Create world with agent components
world <- atomically $ do
    w <- newWorld
    w' <- registerComponentStore w (Proxy @AgentConfig)
    w'' <- registerComponentStore w' (Proxy @AgentState)
    pure w''

Entity Operations

-- | Create a new entity.
createEntity :: IO EntityId

-- | Check if an entity exists (has any component).
entityExists :: World -> EntityId -> STM Bool

-- | Get all entities with a specific component.
allEntitiesWithComponent :: (Component a, Typeable a) => World -> STM [EntityId]

Example:

eid <- createEntity
exists <- atomically $ entityExists world eid  -- False

atomically $ setComponent world eid (AgentConfig "test" ...)
exists' <- atomically $ entityExists world eid  -- True

-- Find all agents
agentIds <- atomically $ allEntitiesWithComponent @AgentConfig world

Component Operations

-- | Get a component for an entity.
getComponent :: (Component a, Typeable a) => World -> EntityId -> STM (Maybe a)

-- | Set a component for an entity.
setComponent :: (Component a, Typeable a) => World -> EntityId -> a -> STM ()

-- | Modify a component for an entity.
modifyComponent :: (Component a, Typeable a) => World -> EntityId -> (a -> a) -> STM ()

-- | Remove a component from an entity.
removeComponent :: (Component a, Typeable a) => World -> EntityId -> STM ()

-- | Check if an entity has a specific component.
hasComponent :: (Component a, Typeable a) => World -> EntityId -> STM Bool

Example:

-- Set component
atomically $ setComponent world eid AgentConfig
    { agentName = "my-agent"
    , agentModel = ModelConfig "openai" "..." "gpt-4" "key1"
    , agentSystemPrompt = "You are helpful"
    , agentToolboxBindings = []
    }

-- Get component
mConfig <- atomically $ getComponent @AgentConfig world eid
case mConfig of
    Nothing -> putStrLn "No agent config"
    Just config -> putStrLn $ "Agent: " ++ show config.agentName

-- Modify component
atomically $ modifyComponent @AgentConfig world eid $
    \c -> c { agentName = "renamed-agent" }

-- Check and remove
hasIt <- atomically $ hasComponent @AgentConfig world eid
when hasIt $ atomically $ removeComponent @AgentConfig world eid

Agent Operations

Agent Configuration

data AgentConfig = AgentConfig
    { agentName :: Text                    -- ^ Human-readable name
    , agentModel :: ModelConfig            -- ^ LLM configuration
    , agentSystemPrompt :: Text            -- ^ System instructions
    , agentToolboxBindings :: [ToolboxBindingSpec]  -- ^ Bound toolboxes
    } deriving (Show, Eq, Generic)

data ModelConfig = ModelConfig
    { modelFlavor :: Text      -- ^ Provider (openai, mistral, etc.)
    , modelUrl :: Text         -- ^ API endpoint
    , modelName :: Text        -- ^ Model identifier
    , modelApiKeyId :: Text    -- ^ Key reference
    } deriving (Show, Eq, Generic)

Agent State

data AgentState = AgentState
    { agentStatus :: AgentStatus           -- ^ Current status
    , agentCurrentConversation :: Maybe ConversationId
    , agentCreatedAt :: UTCTime
    } deriving (Show, Eq, Generic)

data AgentStatus
    = AgentIdle                           -- ^ Available for work
    | AgentBusy TurnId                    -- ^ Executing a turn
    | AgentError Text                     -- ^ Error state
    deriving (Show, Eq, Generic)

Example: Creating an Agent

createAgent :: OS -> AgentConfig -> IO AgentId
createAgent os config = do
    eid <- createEntity
    now <- getCurrentTime
    
    atomically $ do
        setComponent (osWorld os) eid config
        setComponent (osWorld os) eid AgentState
            { agentStatus = AgentIdle
            , agentCurrentConversation = Nothing
            , agentCreatedAt = now
            }
    
    pure $ AgentId eid

-- Usage
let config = AgentConfig
    { agentName = "coder-agent"
    , agentModel = ModelConfig "openai" "https://api.openai.com/v1" "gpt-4" "openai-key"
    , agentSystemPrompt = "You are a helpful coding assistant"
    , agentToolboxBindings = []
    }
agentId <- createAgent os config

Toolbox Operations

Toolbox Configuration

data ToolboxConfig = ToolboxConfig
    { toolboxName :: Text           -- ^ Human-readable name
    , toolboxType :: ToolboxType    -- ^ Type of toolbox
    , toolboxSettings :: Value      -- ^ Type-specific settings
    } deriving (Show, Eq, Generic)

data ToolboxType
    = ToolboxTypeBash
    | ToolboxTypeMCP
    | ToolboxTypeOpenAPI
    | ToolboxTypePostgREST
    | ToolboxTypeSqlite
    | ToolboxTypeSystem
    | ToolboxTypeDeveloper
    | ToolboxTypeLua
    | ToolboxTypeSkills
    deriving (Show, Eq, Generic)

Toolbox State

data ToolboxState = ToolboxState
    { toolboxScope :: ResourceScope     -- ^ Resource lifetime scope
    , toolboxStatus :: ToolboxStatus    -- ^ Current status
    , toolboxResourceRef :: Maybe ResourceId  -- ^ Associated resource
    } deriving (Show, Eq, Generic)

data ToolboxStatus
    = ToolboxInitializing
    | ToolboxReady
    | ToolboxError Text
    | ToolboxDisposed
    deriving (Show, Eq, Generic)

Resource Scope

data ResourceScope
    = ScopeGlobal                       -- ^ Global/program scope
    | ScopeAgent AgentId                -- ^ Per-agent scope
    | ScopeConversation ConversationId  -- ^ Per-conversation scope
    deriving (Show, Eq, Generic)

Example: Creating Toolboxes

-- Create a bash toolbox
let bashConfig = ToolboxConfig
    { toolboxName = "bash-tools"
    , toolboxType = ToolboxTypeBash
    , toolboxSettings = object ["directory" .= "./tools"]
    }
bashId <- createToolbox os bashConfig

-- Create a SQLite toolbox
let sqliteConfig = ToolboxConfig
    { toolboxName = "memory-db"
    , toolboxType = ToolboxTypeSqlite
    , toolboxSettings = object 
        [ "path" .= "./memory.db"
        , "access" .= "readwrite"
        ]
    }
sqliteId <- createToolbox os sqliteConfig

-- Bind toolboxes to agent
let agentConfig = AgentConfig
    { ...
    , agentToolboxBindings = 
        [ unToolboxId bashId
        , unToolboxId sqliteId
        ]
    }

Resource Management

Resource Types

data ResourceType
    = SqliteResource SqliteConfig
    | LuaResource LuaConfig
    | HttpResource HttpConfig
    | CustomResource Text Value
    deriving (Show, Eq)

data ResourceHandle = ResourceHandle
    { handleId :: ResourceId
    , handleCleanup :: IO ()
    , handleAccess :: (ResourceAccessor -> IO a) -> IO a
    }

data ResourceInfo = ResourceInfo
    { resourceId :: ResourceId
    , resourceScope :: ResourceScope
    , resourceType :: ResourceType
    , resourceCreatedAt :: UTCTime
    }

Resource Context

data ResourceContext = ResourceContext
    { contextScope :: [ScopeLevel]      -- ^ Active scope path
    , contextRegistry :: ResourceRegistry
    }

data ScopeLevel
    = ProgramScope
    | AgentScope AgentId
    | ToolboxScope ToolboxId
    | ConversationScope ConversationId
    | TurnScope TurnId
    | ToolCallScope ToolCallId
    deriving (Show, Eq)

Resource Operations

-- | Create a new resource.
createResource :: 
    ResourceContext -> 
    ResourceType -> 
    (ResourceId -> IO ResourceHandle) -> 
    IO ResourceId

-- | Cleanup all resources in a scope.
cleanupScope :: ResourceRegistry -> ScopeLevel -> IO ()

-- | Check if a resource is valid in given scopes.
isResourceValid :: ResourceScope -> [ScopeLevel] -> Bool

-- | Access a resource with a function.
withResource :: ResourceRegistry -> ResourceId -> (ResourceAccessor -> IO a) -> IO (Maybe a)

-- | Get total resource count.
getResourceCount :: ResourceRegistry -> IO Int

Example: Resource Lifecycle

import System.Agents.OS.Resources

-- Create registry and context
registry <- atomically newResourceRegistry
let ctx = ResourceContext [ProgramScope] registry

-- Create SQLite resource
rid <- createResource ctx (SqliteResource config) $ \rid -> do
    conn <- openConnection config
    pure ResourceHandle
        { handleId = rid
        , handleCleanup = do
            putStrLn "Closing SQLite connection"
            closeConnection conn
        , handleAccess = \f -> f (SqliteAccessor conn)
        }

-- Use the resource
result <- withResource registry rid $ \accessor -> do
    case accessor of
        SqliteAccessor conn -> queryDatabase conn "SELECT * FROM table"

-- Later, cleanup all agent resources
cleanupScope registry (AgentScope agentId)

Concurrent Access

Access Patterns

data AccessPattern
    = ExclusiveAccess      -- ^ Single accessor (TMVar)
    | ReadWriteAccess      -- ^ Multiple readers, single writer (RWLock)
    | PoolAccess Int       -- ^ Bounded pool (TBQueue)
    | StatelessAccess      -- ^ No synchronization needed
    deriving (Show, Eq)

data AccessControl = AccessControl
    { accessPattern :: AccessPattern
    , accessTimeout :: Maybe NominalDiffTime
    }

Resource Monad

-- | Monad for resource operations.
newtype ResourceM a = ResourceM { unResourceM :: ReaderT ResourceContext (ExceptT ResourceError IO) a }
    deriving (Functor, Applicative, Monad, MonadIO)

-- | Run a ResourceM computation.
runResourceM :: ResourceContext -> ResourceM a -> IO (Either ResourceError a)

-- | Resource errors.
data ResourceError
    = ResourceNotFound ResourceId
    | ResourceBusy ResourceId
    | ResourceClosed ResourceId
    | ResourceAccessTimeout ResourceId NominalDiffTime
    | ResourceInvalidAccess ResourceId Text
    deriving (Show, Eq)

Access Operations

-- | Execute with exclusive access.
withExclusive :: ResourceId -> ResourceM a -> ResourceM a

-- | Execute with read access.
withRead :: ResourceId -> ResourceM a -> ResourceM a

-- | Execute with write access.
withWrite :: ResourceId -> ResourceM a -> ResourceM a

-- | Execute with pooled resource.
withPooled :: ResourceId -> ResourceM a -> ResourceM a

-- | Execute without synchronization.
withStateless :: ResourceId -> ResourceM a -> ResourceM a

Example: Concurrent Access Patterns

import System.Agents.OS.Concurrent

-- SQLite with WAL mode (supports concurrent reads)
sqliteRid <- createSqliteResource ctx config True  -- WAL mode enabled

-- Multiple concurrent reads (safe)
forConcurrently_ [1..10] $ \_ -> do
    result <- runResourceM ctx $ withRead sqliteRid $ do
        queryData
    print result

-- Exclusive write (blocks readers)
runResourceM ctx $ withWrite sqliteRid $ do
    modifyData

-- Lua interpreter (requires exclusive access)
luaRid <- createLuaResource ctx config
result <- runResourceM ctx $ withExclusive luaRid $ do
    runScript "return 1 + 1"

-- HTTP connection pool
httpRid <- createHttpPool ctx 10  -- 10 connections
results <- forConcurrently urls $ \url ->
    runResourceM ctx $ withPooled httpRid $ do
        fetchUrl url

Initialization Helpers

-- | Initialize access control.
initAccessControl :: AccessPattern -> Maybe NominalDiffTime -> AccessControl

-- | Initialize SQLite access (WAL mode flag).
initSqliteAccess :: Bool -> IO SqliteAccess

-- | Initialize Lua access.
initLuaAccess :: IO LuaAccess

-- | Initialize HTTP access.
initHttpAccess :: Int -> ResourceId -> IO HttpAccess

Conversation Tracking

Conversation Components

data ConversationConfig = ConversationConfig
    { conversationTitle :: Maybe Text
    , conversationMetadata :: Map Text Value
    } deriving (Show, Eq, Generic)

data ConversationState = ConversationState
    { conversationAgentId :: AgentId
    , conversationStatus :: ConversationStatus
    , conversationStartedAt :: UTCTime
    , conversationLastActivity :: TVar UTCTime
    } deriving (Generic)

data ConversationStatus
    = ConversationActive
    | ConversationPaused
    | ConversationCompleted
    | ConversationError Text
    deriving (Show, Eq, Generic)

Turn Components

data TurnConfig = TurnConfig
    { turnConversationId :: ConversationId
    , turnParentTurnId :: Maybe TurnId
    } deriving (Show, Eq, Generic)

data TurnState = TurnState
    { turnStatus :: TurnStatus
    , turnStartedAt :: UTCTime
    , turnCompletedAt :: Maybe UTCTime
    } deriving (Show, Eq, Generic)

data TurnStatus
    = TurnStarting
    | TurnProcessing
    | TurnWaitingForTools
    | TurnCompleted
    | TurnError Text
    deriving (Show, Eq, Generic)

Tool Call Components

data ToolCallConfig = ToolCallConfig
    { toolCallTurnId :: TurnId
    , toolCallParentId :: Maybe ToolCallId
    , toolCallName :: Text
    , toolCallInput :: Value
    } deriving (Show, Eq, Generic)

data ToolCallState = ToolCallState
    { toolCallStatus :: ToolCallStatus
    , toolCallResult :: Maybe Value
    , toolCallStartedAt :: UTCTime
    , toolCallCompletedAt :: Maybe UTCTime
    } deriving (Show, Eq, Generic)

data ToolCallStatus
    = ToolCallPending
    | ToolCallExecuting
    | ToolCallCompleted
    | ToolCallFailed Text
    deriving (Show, Eq, Generic)

Lineage Tracking

data Lineage = Lineage { unLineage :: [LineageFrame] }
    deriving (Show, Eq)

data LineageFrame = LineageFrame
    { frameType :: FrameType
    , frameEntityId :: EntityId
    , frameTimestamp :: UTCTime
    } deriving (Show, Eq)

data FrameType
    = ProgramFrame
    | AgentFrame
    | ToolboxFrame
    | ConversationFrame
    | TurnFrame
    | ToolCallFrame
    deriving (Show, Eq, Enum, Bounded)

Lineage Operations

-- | Empty lineage.
emptyLineage :: Lineage

-- | Push a frame onto the lineage.
pushLineage :: FrameType -> EntityId -> UTCTime -> Lineage -> Lineage

-- | Get lineage depth.
lineageDepth :: Lineage -> Int

-- | Get the most recent frame.
lineageHead :: Lineage -> Maybe LineageFrame

-- | Get the oldest frame.
lineageRoot :: Lineage -> Maybe LineageFrame

-- | Build context from lineage.
buildLineageContext :: Lineage -> LineageContext

-- | Find frames by type.
findFramesByType :: FrameType -> Lineage -> [LineageFrame]

-- | Check if in specific context.
isInConversation :: Lineage -> Bool
isInTurn :: Lineage -> Bool
currentFrameType :: Lineage -> Maybe FrameType

Example: Conversation and Lineage

import System.Agents.OS.Conversation

-- Create conversation
convId <- createEntity
now <- getCurrentTime
lastActivity <- newTVarIO now

atomically $ do
    setComponent world convId ConversationConfig
        { conversationTitle = Just "My Chat"
        , conversationMetadata = Map.empty
        }
    setComponent world convId ConversationState
        { conversationAgentId = agentId
        , conversationStatus = ConversationActive
        , conversationStartedAt = now
        , conversationLastActivity = lastActivity
        }

-- Create turn
turnId <- createEntity
atomically $ do
    setComponent world turnId TurnConfig
        { turnConversationId = convId
        , turnParentTurnId = Nothing
        }
    setComponent world turnId TurnState
        { turnStatus = TurnStarting
        , turnStartedAt = now
        , turnCompletedAt = Nothing
        }

-- Build lineage
let lineage = pushLineage ConversationFrame (unConversationId convId) now $
              pushLineage TurnFrame (unTurnId turnId) now $
              emptyLineage

-- Check depth
print $ lineageDepth lineage  -- 2

-- Find conversation frames
let convFrames = findFramesByType ConversationFrame lineage

OS Events

Subcall lifecycle and tool-call activity are reported through a single emission mechanism, OSEmission (System.Agents.OS.Events), and the session runner (System.Agents.Host.Runner) is the mechanism’s one consumer that matters in practice: it turns each OSEmission into an EventBody (System.Agents.Protocol) and publishes it on the owning session’s event stream, which every runner client (the in-process TUI, the HTTP/SSE server) subscribes to. There is no more OSEvent type or ctxEventQueue; System.Agents.Session.Base.Agent.ctxEmit / System.Agents.Tools.Context.ToolExecutionContext.ctxEmit is the one hook (see todos/os-as-standalone-server.md, Phase 2c and Phase 3c).

OSEmission Type

-- | The single in-library event emission type.
data OSEmission
    = EmitSubcallStarted SessionId SessionId Text Int
      -- ^ Parent session id, child session id, the helper's slug, call depth.
    | EmitSubcallCompleted SessionId (Maybe Text)
      -- ^ Child session id, the subcall's result text (when it succeeded).
    | EmitSubcallFailed SessionId Text
      -- ^ Child session id, the failure message.
    | EmitToolCallActivity ToolCallActivity
    | EmitError Text
      -- ^ A hook (e.g. a before/after tool-call command hook) failed
      -- outside of the normal tool-call result path. Not a session
      -- failure: the run continues.
    deriving (Show)

ToolCallActivity (also in System.Agents.OS.Events) carries a tool call’s session/conversation/tool-call ids, the LLM provider’s own call id when known, the tool name, a ToolCallPhase (ToolCallStarted, ToolCallProgressed Value, ToolCallCompleted, ToolCallFailed Text, ToolCallCancelled) and a timestamp.

Subcall Event Lifecycle

Parent Conversation
       │
       ▼ triggers agent call
┌───────────────────────────┐
│ EmitSubcallStarted         │ ctxEmit'd when the subcall begins
│ - parent session id        │
│ - child session id         │
│ - agent slug                │
│ - depth                    │
└──────────────┬─────────────┘
               │
               ▼
        Agent execution
               │
               ▼ (completion)
┌───────────────────────────┐
│ EmitSubcallCompleted        │ ctxEmit'd on success
│ - child session id         │
│ - result                   │
└─────────────────────────────┘
               │
               ▼ (or failure)
┌───────────────────────────┐
│ EmitSubcallFailed           │ ctxEmit'd on error
│ - child session id         │
│ - error                    │
└─────────────────────────────┘

Phase 3c retires the old OSEvent_SubcallProgress (it used to carry a whole Session, snapshotted after every step, purely for the TUI’s benefit): there is no runner-event equivalent. Phase 5 gives a real sub-agent session its own SessionUpdated events instead, once prompt_agent_* spawns through spawnSession rather than running inline.

The Runner’s Event Stream

System.Agents.Host.Runner.toEventBody is the one place that converts OSEmission into Protocol.EventBody:

toEventBody :: OSEmission -> EventBody
toEventBody = \case
    EmitSubcallStarted parent child slug depth -> SubcallStarted parent child slug depth
    EmitSubcallCompleted child result -> SubcallCompleted child result
    EmitSubcallFailed child msg -> SubcallFailed child msg
    EmitToolCallActivity activity -> ToolCallProgressed activity
    EmitError msg -> HookFailed msg

newAgent installs withEmit (emit runner sid . toEventBody) on every root agent it builds, so every emission from that session (and its subcalls, which inherit ctxEmit the same way they inherit ctxWorld) ends up on sid’s own event stream, wire-encoded with kind subcall.started / subcall.completed / subcall.failed / tool.progressed / hook.failed (see documentation/agents-server.md’s event table for the full list, including the runner’s own run.*/session.* kinds that do not originate from OSEmission at all).

Using ctxEmit Outside a Runner

A local (non-runner) consumer that used to drain ctxEventQueue – a CLI path, a test – installs System.Agents.OS.Events.queueEmitter (or newQueueEmitter, which also allocates the queue) as ctxEmit instead, and reads emissions back off the queue:

import System.Agents.OS.Events (newQueueEmitter, OSEmission (..))

(queue, emitter) <- newQueueEmitter
let agent' = agent{ctxEmit = Just emitter}
-- ... run agent' ...
emissions <- atomically $ flushTQueue queue

ToolExecutionContext Extensions

For subcall visibility, the ToolExecutionContext includes OS integration fields:

data ToolExecutionContext = ToolExecutionContext
    { -- ... existing fields ...
    , ctxWorld :: Maybe World
    -- ^ Optional OS World for ECS operations. When present, subcalls
    -- can insert entities and components into the OS.
    , ctxEmit :: Maybe (OSEmission -> IO ())
    -- ^ Optional emission hook. When present, subcalls emit events to
    -- notify a runner (or other local consumer) of their lifecycle.
    , ctxParentConversation :: Maybe ConversationId
    -- ^ Optional parent conversation ID for subcalls. When present,
    -- indicates this context is for a nested agent invocation.
    }

Helper Functions:

-- | Get the subcall depth (0 if not a subcall).
getSubcallDepth :: ToolExecutionContext -> Int

-- | Check if this context represents a subcall.
isSubcallContext :: ToolExecutionContext -> Bool

-- | Create a nested context for subcall execution.
mkSubcallContext ::
    ToolExecutionContext ->
    Maybe World ->
    ConversationId ->
    ToolExecutionContext

TUI Integration

In the TUI, runner Events are turned into AppEvents by bridgeRunnerEvents (System.Agents.TUI.Core), which maps each EventBody kind onto its AppEvent counterpart – subcall.started / subcall.completed / subcall.failed / tool.progressed map straight across, translating SessionId to ConversationId (they are the same UUID). hook.failed has no dedicated view yet (it is not a session failure, so it is not surfaced as one); see documentation/tui.md.


Persistence Layer

Removed. System.Agents.OS.Persistence was deleted (D1 in todos/os-as-standalone-server.md): sessions persist through the runner’s SessionBackend (SQLite or Postgres), not through ECS components.


Common Patterns

Pattern 1: Agent with Multiple Toolboxes

createFullAgent :: OS -> Text -> [ToolboxId] -> IO AgentId
createFullAgent os name toolboxes = do
    let config = AgentConfig
        { agentName = name
        , agentModel = ModelConfig "openai" "..." "gpt-4" "key"
        , agentSystemPrompt = "You are helpful"
        , agentToolboxBindings = map unToolboxId toolboxes
        }
    createAgent os config

-- Usage
bashTb <- createToolbox os bashConfig
sqlTb <- createToolbox os sqliteConfig
httpTb <- createToolbox os httpConfig

agentId <- createFullAgent os "multi-tool-agent" [bashTb, sqlTb, httpTb]

Pattern 2: Forking a Conversation

forkConversation :: World -> ConversationId -> IO ConversationId
forkConversation world convId = do
    -- Get original config
    Just config <- atomically $ getComponent @ConversationConfig world (unConversationId convId)
    
    -- Create new conversation
    newConvId <- createEntity
    now <- getCurrentTime
    lastActivity <- newTVarIO now
    
    atomically $ do
        setComponent world newConvId config
        setComponent world newConvId ConversationState
            { conversationAgentId = ...
            , conversationStatus = ConversationActive
            , conversationStartedAt = now
            , conversationLastActivity = lastActivity
            }
    
    -- Copy turn history
    turns <- getConversationTurns world convId
    forM_ turns $ \turnId -> do
        Just turnConfig <- atomically $ getComponent @TurnConfig world (unTurnId turnId)
        newTurnId <- createEntity
        atomically $ setComponent world newTurnId turnConfig
            { turnConversationId = ConversationId newConvId
            }
    
    pure $ ConversationId newConvId

Pattern 3: Resource Pool with Timeout

withPooledTimeout :: ResourceContext -> ResourceId -> NominalDiffTime -> ResourceM a -> ResourceM a
withPooledTimeout ctx rid timeout action = do
    currentTimeout <- getResourceTimeout rid
    -- Temporarily set timeout
    updateResourceTimeout rid (Just timeout)
    result <- withPooled rid action
    -- Restore original timeout
    updateResourceTimeout rid currentTimeout
    pure result

Pattern 4: Transaction with Multiple Components

updateAgentAndToolbox :: World -> AgentId -> ToolboxId -> AgentConfig -> ToolboxConfig -> STM ()
updateAgentAndToolbox world agentId toolboxId agentConfig toolboxConfig = do
    -- Both updates happen atomically
    setComponent world (unAgentId agentId) agentConfig
    setComponent world (unToolboxId toolboxId) toolboxConfig

-- Usage
atomically $ updateAgentAndToolbox world agentId toolboxId newAgentConfig newToolboxConfig

Type Index

Core Types

  • EntityId - Base entity identifier
  • AgentId, ToolboxId, ConversationId, TurnId, ToolCallId, ResourceId - Phantom-typed IDs
  • ComponentTypeId - Component type identifier
  • World - ECS world container

Component Types

  • AgentConfig, AgentState, AgentStatus - Agent components
  • ToolboxConfig, ToolboxState, ToolboxBinding - Toolbox components
  • ConversationConfig, ConversationState, ConversationStatus - Conversation components
  • TurnConfig, TurnState, TurnStatus - Turn components
  • ToolCallConfig, ToolCallState, ToolCallStatus - Tool call components
  • Message, MessageRole - Message components
  • Lineage, LineageFrame, FrameType - Lineage components

Resource Types

  • ResourceType, ResourceHandle, ResourceInfo - Resource definitions
  • ResourceContext, ScopeLevel, ResourceScope - Scope management
  • ResourceRegistry - Resource storage

Concurrent Types

  • AccessPattern, AccessControl - Access patterns
  • ResourceM - Resource monad
  • ResourceError - Error types
  • SyncPrimitive, ExclusiveLock, ReadWriteLock, PoolLock - Lock types

Event Types

  • OSEmission - the single emission type: subcall lifecycle, tool-call activity, hook failures
  • ToolCallActivity, ToolCallPhase - background tool-call activity payloads
  • SessionProgress - Session progress tracking

Persistence Types

  • PersistenceHandle, PersistenceBackendType - Backend types
  • Persistable - Persistence typeclass
  • EntityQuery - Query specification