For pseudocode conventions, see the README.

3. Initialization & Lifecycle Sequences

Agent Construction (Builder Pattern)

SEQUENCE AgentConstruction
  1. BasicAgent::new(model_config: ModelConfig)
     - Stores model_config (provider identity: id, api_key, base_url, api protocol, cost rates)
     - Initializes messages = []
     - Initializes tools = []
     - Sets defaults: thinking = Off, tool_execution = Parallel, retry = default

  2. .with_system_prompt(text)
     - Stores system_prompt string

  3. .with_tools(vec)
     - Replaces or extends the tools list

  5. .with_context_config(config)
     - Enables automatic compaction before each turn

  6. .with_execution_limits(limits)
     - Enables turn/token/duration caps

  7. .with_skills(skill_set)
     - Appends skill XML index to system_prompt

  8. .with_mcp_server_stdio(cmd, args, env)     [async]
     - Spawns MCP subprocess
     - Calls initialize + tools/list over JSON-RPC
     - Wraps each discovered tool as McpToolAdapter (implements AgentTool)
     - Appends adapters to tools list

  9. .with_openapi_file/url/spec(...)           [async, feature-gated]
     - Parses OpenAPI spec
     - Generates one OpenApiToolAdapter per matching operation
     - Appends adapters to tools list

  10. Callbacks: .on_before_turn(f), .on_after_turn(f), .on_error(f)
      - Stores function pointers; called at appropriate points in run_loop

  11. .with_input_filter(filter)
      - Appends to input_filters list

  12. .with_compaction_strategy(strategy)
      - Sets context_config.compaction.in_memory_strategy (custom compaction implementation)

END SEQUENCE

Agent Run Lifecycle

SEQUENCE AgentRun (invoked by agent.prompt("..."))
  1. Acquire run lock (ensure not already streaming)
     - is_streaming ← true
     - Create new CancellationToken

  2. Build AgentContext from current Agent state
     - Snapshot: system_prompt, messages (copy), tools

  3. Build AgentLoopConfig from current Agent config
     - Wire get_steering_messages → drain steering_queue
     - Wire get_follow_up_messages → drain follow_up_queue

  4. Create event channel (tx, rx)

  5. SPAWN async task: agent_loop(prompts, context, config, tx, cancel)

  6. Return rx to caller immediately (non-blocking)
     - Caller consumes events: AgentStart, TurnStart/End, MessageStart/Update/End,
       ToolExecutionStart/Update/End, ProgressMessage, AgentEnd

  7. When AgentEnd received or channel closes:
     - Merge new_messages into Agent.messages
     - is_streaming ← false
     - CancellationToken dropped

END SEQUENCE

Abort Lifecycle

SEQUENCE AgentAbort (invoked by agent.abort())
  1. IF cancel token exists THEN
       cancel.cancel()  // signals all child tokens
  2. Agent loop checks cancel.is_cancelled() at:
     - Start of each outer/inner loop iteration
     - In BashTool's tokio::select! race
     - In ReadFileTool/WriteFileTool/EditFileTool before each I/O op
  3. Loop exits cleanly at next check point; AgentEnd NOT emitted on abort
     [AMBIGUOUS: AgentEnd may or may not be emitted depending on where
      in the loop cancellation is detected — Start/Done events from provider
      may still arrive before cancellation is noticed]
END SEQUENCE

Message Persistence

SEQUENCE MessagePersistence
  Save:
    1. agent.save_messages() → serde_json::to_string(agent.messages)
    2. Caller writes JSON string to disk/storage

  Restore:
    1. Caller reads JSON string from disk/storage
    2. agent.restore_messages(json_str) → serde_json::from_str(json_str) → Vec<AgentMessage>
    3. Agent.messages ← deserialized messages
    4. Next agent.prompt() continues from restored history

  All types in AgentMessage tree derive Serialize + Deserialize.
  JSON format: array of untagged AgentMessage items;
    Llm variant: has "role" field ("user", "assistant", "toolResult")
    Extension variant: has "role" field "extension" + "kind" + "data"
END SEQUENCE


BasicAgent::new and BasicAgent::prompt (src/agents/basic_agent.rs)

Purpose: Construct a BasicAgent and start a run. These are the primary application-facing entry points.

FUNCTION BasicAgent::new(model_config: ModelConfig) -> BasicAgent
  RETURN BasicAgent {
    model_config: model_config,       // complete provider identity: id, api_key, base_url, api, cost
    system_prompt: "",
    thinking_level: Off,
    max_tokens: None,
    temperature: None,
    messages: [],
    tools: [],
    steering_queue: Arc(Mutex([])),
    follow_up_queue: Arc(Mutex([])),
    steering_mode: QueueMode::OneAtATime,
    follow_up_mode: QueueMode::OneAtATime,
    context_config: Some(ContextConfig::default()),
    execution_limits: Some(ExecutionLimits::default()),
    cache_config: CacheConfig::default(),
    tool_execution: Parallel,
    retry_config: RetryConfig::default(),
    before_turn: None,
    after_turn: None,
    on_error: None,
    input_filters: [],
    // compaction strategies are now inside context_config.compaction (G5)
    cancel: None,
    is_streaming: false
  }
END FUNCTION

FUNCTION Agent::prompt(text: String) -> UnboundedReceiver<AgentEvent>
  RETURN Agent::prompt_messages([AgentMessage::Llm(Message::user(text))])
END FUNCTION

FUNCTION Agent::prompt_messages(messages: Vec<AgentMessage>) -> UnboundedReceiver<AgentEvent>
  (tx, rx) ← new unbounded channel
  SPAWN Agent::prompt_messages_with_sender(messages, tx)
  RETURN rx
END FUNCTION

FUNCTION Agent::prompt_messages_with_sender(
  messages: Vec<AgentMessage>,
  tx: EventSender<AgentEvent>
) [async]

  // Guard: panics if already streaming
  ASSERT NOT self.is_streaming,
    "Agent is already streaming. Use steer() or follow_up()."

  self.is_streaming ← true
  self.cancel ← Some(CancellationToken::new())

  // Build context snapshot for this run
  context ← AgentContext {
    system_prompt: self.system_prompt.clone(),
    messages: self.messages.clone(),
    tools: self.tools  // borrowed
  }

  // Wire queue closures — capture Arc pointers
  steering_arc ← Arc::clone(self.steering_queue)
  followup_arc ← Arc::clone(self.follow_up_queue)

  config ← AgentLoopConfig {
    provider: self.provider,
    model: self.model,
    api_key: self.api_key,
    thinking_level: self.thinking_level,
    max_tokens: self.max_tokens,
    temperature: self.temperature,
    model_config: self.model_config,
    get_steering_messages: closure {
      LOCK(steering_arc)
      MATCH self.steering_mode
        CASE OneAtATime → IF queue non-empty THEN [queue.remove(0)] ELSE []
        CASE All        → queue.drain_all()
      UNLOCK
    },
    get_follow_up_messages: closure {
      LOCK(followup_arc)
      MATCH self.follow_up_mode
        CASE OneAtATime → IF queue non-empty THEN [queue.remove(0)] ELSE []
        CASE All        → queue.drain_all()
      UNLOCK
    },
    context_config: self.context_config,  // includes compaction strategies (G5)
    execution_limits: self.execution_limits,
    cache_config: self.cache_config,
    tool_execution: self.tool_execution,
    retry_config: self.retry_config,
    before_turn: self.before_turn,
    after_turn: self.after_turn,
    on_error: self.on_error,
    input_filters: self.input_filters
  }

  new_messages ← AWAIT agent_loop(messages, context, config, tx, self.cancel.unwrap())

  // Merge new messages back into Agent.messages
  self.messages.extend(new_messages)

  self.is_streaming ← false
  self.cancel ← None

END FUNCTION