DEV Community

bigfish
bigfish

Posted on AI-assisted

Rust-native coding-agent runtime

Today I finished the implementation of pi-rust. Now I want to share how to build an AI agent.

From my perspective, the core of an agent is the agent loop. The flow looks like this:

Read steering messages
        ↓
Call the model
        ↓
Get assistant message
        ↓
Check for tool calls
        ├─ No: check whether to stop
        └─ Yes: execute tools
                ↓
           Write tool result
                ↓
           Call the model again
Enter fullscreen mode Exit fullscreen mode

A simplified version of the code looks like this:

while has_more_tool_calls || !pending_messages.is_empty() {
    // 1. Inject steering/follow-up messages

    // 2. Call the model and get the assistant response
    let message = stream_assistant_response(...).await?;

    // 3. Check stop_reason
    if message.stop_reason == StopReason::Error {
        ...
    }

    // 4. Extract tool calls from the assistant message
    let tool_calls = message.content.iter()
        .filter_map(|content| match content {
            Content::ToolCall(tool_call) => Some(tool_call.clone()),
            _ => None,
        })
        .collect();

    // 5. Execute tools
    let batch = execute_tool_calls(...).await?;

    // 6. Add ToolResultMessage back to the context
    current_context.messages.push(...);

    // 7. Decide whether to continue to the next round
}
Enter fullscreen mode Exit fullscreen mode

Another question arises: how do we parse messages from different providers, such as Anthropic, OpenAI, and other completion protocols? The response is streamed, so we need to process it in a special way.

while let Some(event) = response.next().await {
    match &event {
        AssistantMessageEvent::Start { partial } => { ... }

        AssistantMessageEvent::TextDelta { partial, .. } => { ... }

        AssistantMessageEvent::ThinkingDelta { partial, .. } => { ... }

        AssistantMessageEvent::ToolCallDelta { partial, .. } => { ... }

        AssistantMessageEvent::Done { .. }
        | AssistantMessageEvent::Error { .. } => { ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

The streamed message sequence can look like this:

Start
  ↓
TextStart
  ↓
TextDelta
  ↓
TextDelta
  ↓
ToolCallStart
  ↓
ToolCallDelta
  ↓
Done
Enter fullscreen mode Exit fullscreen mode

That's the core idea behind pi-rust: keep the agent loop simple, and normalize provider-specific streaming events into one consistent event model.

If you're building something similar, I'd love to hear how you handle provider differences and streaming tool calls.

Top comments (0)

Some comments have been hidden by the post's author - find out more