swink-agent 0.8.0

Core scaffolding for running LLM-powered agentic loops
Documentation
# Quickstart: Agent Loop

**Feature**: 004-agent-loop

## Prerequisites

- Features 001-003 complete (workspace, types, traits)
- Rust 1.88 toolchain

## Build & Test

```bash
cargo build -p swink-agent
cargo test -p swink-agent
cargo clippy -p swink-agent -- -D warnings
```

## Usage Examples

### Simple Single-Turn Loop

```rust
use swink_agent::{
    agent_loop, AgentContext, AgentLoopConfig, AgentMessage,
    ContentBlock, LlmMessage, ModelSpec, StreamOptions,
};
use tokio_util::sync::CancellationToken;
use futures::StreamExt;

// Set up context with a user message
let context = AgentContext {
    system_prompt: "You are helpful.".into(),
    messages: vec![AgentMessage::Llm(LlmMessage::User {
        content: vec![ContentBlock::Text { text: "Hello!".into() }],
        timestamp: SystemTime::now(),
    })],
    tools: vec![],
};

// Configure the loop
let config = AgentLoopConfig {
    model: ModelSpec { provider: "mock".into(), model_id: "test".into(), .. },
    stream_options: StreamOptions::default(),
    retry_strategy: Box::new(DefaultRetryStrategy::default()),
    convert_to_llm: Arc::new(|msg| match msg {
        AgentMessage::Llm(llm) => Some(llm.clone()),
        AgentMessage::Custom(_) => None,
    }),
    transform_context: None, // identity — no pruning
    get_api_key: None,
    get_steering_messages: None,
    get_follow_up_messages: None,
};

// Run the loop
let token = CancellationToken::new();
let mut stream = agent_loop(vec![], context, config, token);

while let Some(event) = stream.next().await {
    match event {
        AgentEvent::MessageUpdate { delta } => { /* handle streaming delta */ }
        AgentEvent::AgentEnd { messages } => { /* done */ }
        _ => {}
    }
}
```

### Cancellation

```rust
let token = CancellationToken::new();
let token_clone = token.clone();

// Cancel after 5 seconds
tokio::spawn(async move {
    tokio::time::sleep(Duration::from_secs(5)).await;
    token_clone.cancel();
});

let mut stream = agent_loop(vec![], context, config, token);
// Stream will emit AgentEnd with StopReason::Aborted when cancelled
```

## Verification Checklist

- [ ] Single-turn conversation emits events in correct lifecycle order
- [ ] Tool calls execute concurrently (not sequentially)
- [ ] Steering interrupts cancel remaining tools
- [ ] Follow-up messages cause loop continuation
- [ ] Error/abort exits skip follow-up polling
- [ ] Retry strategy consulted for transient failures
- [ ] Context overflow triggers emergency recovery (compact + retry), second failure surfaces error
- [ ] Max tokens recovery replaces incomplete tool calls
- [ ] Cancellation produces clean aborted shutdown