Arcen Agent v1.0.0
⌘K
⌘K

Architecture Guide

This document describes the architectural flow of Arcen Agent, detailing how the agent loop executes tools, resolves dependencies, and communicates across processes.


1. File Dependency Chain

Arcen uses an import-time registration pattern to load tools. This ensures that adding a new tool requires no changes to core orchestrators.

┌──────────────────┐
│  tools/registry  │  (Defines registry container; has no dependencies)
└────────┬─────────┘


┌──────────────────┐
│   tools/*.py     │  (Import-time registry.register() calls register schemas)
└────────┬─────────┘


┌──────────────────┐
│   model_tools    │  (Triggers auto-discovery imports and handles execution)
└────────┬─────────┘


┌──────────────────┐
│    AIAgent,      │  (AIAgent, CLI, gateway platforms inherit schemas
│   CLI, Gateway   │   and invoke handlers via model_tools)
└──────────────────┘

2. The Core AIAgent Loop (run_agent.py)

The conversational loop resides inside AIAgent.run_conversation() in run_agent.py. It is entirely synchronous and runs inside an iteration budget wrapper.

Here is a simplified representation of the loop execution:

def run_conversation(self, user_message: str):
    messages = [{"role": "user", "content": user_message}]
    api_call_count = 0

    while (api_call_count < self.max_iterations and self.iteration_budget.remaining > 0) \
            or self._budget_grace_call:
        
        # Check if the user requested an interrupt (e.g. Ctrl+C in CLI)
        if self._interrupt_requested:
            break
            
        # 1. Fetch completion from model provider
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=self.get_tool_schemas()
        )
        
        # 2. Extract and print reasoning content if present
        if hasattr(response, "reasoning_content") and response.reasoning_content:
            self.display_reasoning(response.reasoning_content)
            
        # 3. Handle Tool calls
        if response.tool_calls:
            for tool_call in response.tool_calls:
                # Executes tool and returns JSON string wrapper
                result_str = handle_function_call(
                    tool_call.name, 
                    tool_call.args, 
                    task_id=self.task_id
                )
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result_str
                })
            api_call_count += 1
        else:
            # 4. Return final content output when no tool calls are returned
            return response.content

Budget & Interrupt Handling

  • Grace Call: If the agent runs out of iterations while in the middle of executing a critical file modification, the loop is granted one single grace API call (self._budget_grace_call) to cleanly commit and wrap up before halting.
  • SQLite State Recording: Every message and tool output is immediately serialized and persisted to the SQLite database via SessionDB to prevent loss on unexpected shutdowns.

3. TUI stdio JSON-RPC Transport

The modern TUI (ui-tui) runs as a Node.js process rendering React components using the Ink library. It starts a persistent Python subprocess running tui_gateway/server.py.

┌───────────────────────┐                    ┌───────────────────────┐
│     Node (Ink UI)     │                    │  Python tui_gateway   │
│                       ├───────────────────►│                       │
│  e.g. prompt.submit   │   JSON-RPC over   │  e.g. AIAgent.chat()  │
│                       │       stdio        │                       │
│  ◄────────────────────┤                    │◄──────────────────────┤
│  message.delta events │                    │  tool.start events    │
└───────────────────────┘                    └───────────────────────┘
  • Transport: Communication happens entirely over stdio. Commands and events are serialized as single-line, newline-terminated JSON-RPC payloads.
  • Slash command fallbacks: Built-in commands like /clear or /quit are intercepted locally by TypeScript. Any unrecognized slash commands are forwarded down to the Python gateway CLI parser to ensure complete command parity.