Agent Core
The Agent Core is the brain of an agent. It defines the reasoning loop — how the agent observes its environment, decides what to do, and acts on that decision. Different agent types need different reasoning patterns, but they all implement the same trait.
The AgentCore trait
#[async_trait]pub trait AgentCore: Send + Sync { /// Identifier for this core type (e.g., "react", "embodied") fn core_type(&self) -> &str; /// Run one full turn: user input to final response. async fn run_turn( &mut self, input: TurnInput, ctx: &mut AgentContext, ) -> Result<TurnOutput, AgentError>; /// Called once when the agent session starts. async fn init(&mut self, ctx: &mut AgentContext) -> Result<(), AgentError> { Ok(()) } /// Called when the agent session ends. async fn shutdown(&mut self, ctx: &mut AgentContext) -> Result<(), AgentError> { Ok(()) }}
The trait is deliberately minimal. A core receives input and context, returns output. Everything it needs — LLM access, tool dispatch, memory, policy — comes through AgentContext:
pub struct AgentContext { pub provider: Box<dyn LlmProvider>, // LLM inference pub tools: Box<dyn ToolDispatcher>, // Tool execution pub memory: Box<dyn MemoryPlugin>, // Persistent state pub policy: Box<dyn RuntimePolicy>, // Execution rules pub events: EventSender, // UI event stream pub config: AgentConfig, // Configuration}
The core doesn't construct or own any of these — the AgentRuntime injects them. This means cores are testable in isolation (mock the context) and composable without coupling.
ReAct core (first citizen)
The default core. Extracted from the current AgentLoop, this is the reasoning pattern that ships first.
Pattern: Reason → Act → Observe → Repeat
User prompt │ ▼┌─────────┐ ┌──────────┐ ┌───────────┐│ Reason │────▶│ Act │────▶│ Observe │──┐│ (LLM) │◀───│ (Tool) │◀───│ (Result) │ │└─────────┘ └──────────┘ └───────────┘ │ │ │ │◀───────────────────────────────────────────┘ │ (loop until done) ▼Final response
How it works inside run_turn():
- Build the message history with memory context
- Call
ctx.provider.chat_completion()with available tools - If the LLM returns tool calls:
- Check
ctx.policy.check_permission()for each tool - Execute via
ctx.tools.dispatch() - Append results to history
- Go to step 2
- Check
- If the LLM returns text only → return as the final response
- If the token budget is exceeded → compact context and continue
pub struct ReActCore { system_prompt: String, max_tokens: u32, token_budget: u32, compaction_threshold: f32, // compact at 80% of budget}
This core runs on the host environment directly. No sub-agent orchestration, no nested cores. One LLM, one tool at a time, one turn at a time.
Lifecycle
Every core follows the same lifecycle, managed by AgentRuntime:
Create ──▶ init() ──▶ run_turn() ──▶ run_turn() ──▶ ... ──▶ shutdown() │ │ │ Load prompts, Flush memory, │ │ warm caches cleanup state │
init()— Called once when the session starts. Load system prompts, warm any caches, initialize state.run_turn()— Called for each user message. The core has full control over how many LLM calls and tool calls it makes within a turn.shutdown()— Called when the session ends. Flush pending memory writes, release resources.
Event streaming
Cores emit events through ctx.events so the TUI (or any UI) can render in real-time:
pub enum AgentEvent { /// Streaming text token from LLM TextDelta(String), /// Tool call started ToolCallStart { name: String, args: Value }, /// Tool call completed ToolCallEnd { name: String, output: Value, duration_ms: u64 }, /// Token usage update Usage(TokenUsage), /// Context was compacted ContextCompacted { removed_turns: usize },}
The TUI doesn't know which core is running. It subscribes to the event stream and renders whatever comes through.
Future cores
The AgentCore trait enables fundamentally different reasoning patterns without changing the framework.
Embodied core (second citizen — robot)
The current Jan Agent already has vision providers and robot tools. The Embodied core wraps these in a continuous sense-plan-act loop:
Sense (camera) ──▶ Plan (LLM) ──▶ Act (robot.move) ──▶ Sense ──▶ ...
Key differences from ReAct:
- Continuous loop with a fixed cycle time (e.g., 100ms)
- Sensor input (vision frames) injected automatically each cycle
- Shorter planning horizon — one action per cycle, not multi-step
- Spatial memory instead of conversation memory
pub struct EmbodiedCore { sensors: Vec<Box<dyn SensorPlugin>>, control_frequency: Duration,}
Plan-Execute core (future)
Two-phase reasoning: generate a plan first, then execute steps one by one. Better for complex multi-step tasks.
Plan (LLM) ──▶ Execute step 1 ──▶ Execute step 2 ──▶ ... ──▶ Replan if needed
Reflexion core (future)
Self-evaluating: act, then critique the result, then try again with the reflection. Converges toward better answers.
Adding a new core requires implementing one trait. No changes to the framework, the plugin registry, or the TUI. The new core automatically gets access to all registered tools, memory backends, and providers.
Configuration
Cores are selected and configured through AgentConfig:
{ "core": { "type": "react", "system_prompt": "You are a helpful assistant.", "max_tokens": 4096, "token_budget": 32000, "compaction_threshold": 0.8 }}
The type field selects which core implementation to instantiate. Core-specific settings are passed through as-is.