<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: bigfish</title>
    <description>The latest articles on DEV Community by bigfish (@bigfish).</description>
    <link>https://dev.to/bigfish</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4142937%2F3e4af51c-6415-4791-a53e-75a83aa3d3e4.jpg</url>
      <title>DEV Community: bigfish</title>
      <link>https://dev.to/bigfish</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bigfish"/>
    <language>en</language>
    <item>
      <title>Building a Library-First AI Agent SDK in Rust</title>
      <dc:creator>bigfish</dc:creator>
      <pubDate>Wed, 07 Oct 2026 14:24:33 +0000</pubDate>
      <link>https://dev.to/bigfish/building-a-library-first-ai-agent-sdk-in-rust-1n26</link>
      <guid>https://dev.to/bigfish/building-a-library-first-ai-agent-sdk-in-rust-1n26</guid>
      <description>&lt;p&gt;I've been learning about the Pi agent and decided to reimplement it in Rust. What started as a performance experiment turned into something more interesting: a library-first agent SDK that you can embed directly into Rust applications.&lt;/p&gt;

&lt;p&gt;This post covers the architecture, the plugin ABI, and a complete minimal example you can run without an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Rust implementation?
&lt;/h2&gt;

&lt;p&gt;Pi is a minimal terminal coding harness. It's designed to be extended and reshaped by the user. But its runtime is TypeScript/Node.&lt;/p&gt;

&lt;p&gt;I was running an agent on a low-spec server and kept hitting memory limits. So I ported the core runtime to Rust. But the more useful goal emerged along the way: making the agent runtime embeddable, not just a faster CLI.&lt;/p&gt;

&lt;p&gt;If you're building a Rust service that needs to receive tasks, call models, execute custom tools, and stream progress to your own UI, you shouldn't have to spawn a subprocess or treat the terminal as the only entry point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Library-first architecture
&lt;/h2&gt;

&lt;p&gt;rpi is split into independent crates. Each layer can be used on its own:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crate&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rpi-ai&lt;/td&gt;
&lt;td&gt;Unified multi-provider LLM client (Anthropic Messages, OpenAI-compatible, custom gateways)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpi-agent&lt;/td&gt;
&lt;td&gt;Async streaming agent loop with events, hooks, queues, cancellation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpi-tools&lt;/td&gt;
&lt;td&gt;Built-in tools (read, write, edit, bash, grep, find, ls) with a replaceable execution environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpi-harness&lt;/td&gt;
&lt;td&gt;Session tree, JSONL persistence, context compaction, recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rpi-cli&lt;/td&gt;
&lt;td&gt;Terminal UI built on the same runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you only need the model and tool loop, use rpi-agent. If you need sessions and recovery, add rpi-harness. The CLI is just one consumer of the same libraries.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal agent in ~100 lines
&lt;/h2&gt;

&lt;p&gt;Here's the complete example. It uses a faux provider — a scripted model that needs no API key and no network. You can run it end-to-end.&lt;/p&gt;

&lt;p&gt;Cargo.toml:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[dependencies]
rpi-agent = "0.1"
rpi-ai = "0.1"
tokio = { version = "1", features = ["full"] }
tokio-util = "0.7"
serde_json = "1"
async-trait = "0.1"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;main.rs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use std::io::Write;
use std::sync::Arc;
use rpi_agent::{AgentBuilder, AgentEvent, AgentTool, AgentToolResult, TextContentOrImage};
use rpi_ai::providers::faux::{FauxProvider, FauxScript};
use rpi_ai::types::{Context, Schema, Tool};
use rpi_ai::{Model, Provider, SimpleStreamOptions};
use tokio::runtime::Handle;
use tokio_util::sync::CancellationToken;

// A custom tool: add two integers
struct AddTool { schema: Tool }

impl AddTool {
    fn new() -&amp;gt; Self {
        Self {
            schema: Tool {
                name: "add".to_string(),
                description: "Add two integers a and b.".to_string(),
                parameters: Schema(serde_json::json!({
                    "type": "object",
                    "properties": {
                        "a": { "type": "integer" },
                        "b": { "type": "integer" }
                    },
                    "required": ["a", "b"]
                })),
                constrained_sampling: None,
            },
        }
    }
}

#[async_trait::async_trait]
impl AgentTool for AddTool {
    fn schema(&amp;amp;self) -&amp;gt; &amp;amp;Tool { &amp;amp;self.schema }
    fn label(&amp;amp;self) -&amp;gt; &amp;amp;str { "Add" }

    async fn execute(
        &amp;amp;self,
        _tool_call_id: &amp;amp;str,
        params: serde_json::Value,
        _signal: CancellationToken,
        _on_update: Arc&amp;lt;dyn Fn(rpi_agent::ToolResultPartial) + Send + Sync&amp;gt;,
    ) -&amp;gt; Result&amp;lt;AgentToolResult, rpi_agent::AgentError&amp;gt; {
        let a = params.get("a").and_then(|v| v.as_i64()).unwrap_or(0);
        let b = params.get("b").and_then(|v| v.as_i64()).unwrap_or(0);
        Ok(AgentToolResult::text((a + b).to_string()))
    }
}

// Bridge async Provider to sync StreamFn
fn provider_stream_fn(provider: Arc&amp;lt;dyn Provider&amp;gt;) -&amp;gt; rpi_agent::StreamFn {
    rpi_agent::stream_fn(
        move |model: &amp;amp;Model, ctx: &amp;amp;Context, opts: &amp;amp;SimpleStreamOptions| {
            let provider = Arc::clone(&amp;amp;provider);
            let model = model.clone();
            let ctx = ctx.clone();
            let opts = opts.clone();
            tokio::task::block_in_place(|| {
                Handle::current().block_on(async move {
                    provider.stream_simple(&amp;amp;model, &amp;amp;ctx, &amp;amp;opts).await
                })
            })
        },
    )
}

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;(), Box&amp;lt;dyn std::error::Error&amp;gt;&amp;gt; {
    // Script the model: first call add, then give the answer
    let script = FauxScript::new()
        .with_tool_call("add", serde_json::json!({ "a": 2, "b": 40 }))
        .with_text("2 + 40 = 42");
    let provider = FauxProvider::new(script);
    let model = provider.default_model().clone();

    let agent = AgentBuilder::new()
        .model(model)
        .system_prompt("You are a minimal agent. Use tools when needed.")
        .tools(vec![Arc::new(AddTool::new())])
        .stream_fn(provider_stream_fn(provider))
        .build()?;

    let mut events = agent.subscribe();

    println!("&amp;gt; user: What is 2 + 40?\n");
    agent.prompt("What is 2 + 40?").await?;

    loop {
        let event = events.recv().await?;
        match event {
            AgentEvent::MessageUpdate {
                assistant_message_event:
                    rpi_ai::types::AssistantMessageEvent::TextDelta { delta, .. },
                ..
            } =&amp;gt; {
                print!("{delta}");
                std::io::stdout().flush().ok();
            }
            AgentEvent::ToolExecutionStart { tool_name, args, .. } =&amp;gt; {
                println!("→ tool {tool_name}({args})");
            }
            AgentEvent::ToolExecutionEnd { result, is_error, .. } =&amp;gt; {
                let tag = if is_error { "tool error" } else { "tool result" };
                println!("← {tag}: {}", result_text(&amp;amp;result));
            }
            AgentEvent::AgentEnd { messages } =&amp;gt; {
                println!("\n--- done: {} new message(s) ---", messages.len());
                break;
            }
            _ =&amp;gt; {}
        }
    }
    Ok(())
}

fn result_text(result: &amp;amp;AgentToolResult) -&amp;gt; String {
    result.content.iter().map(|c| match c {
        TextContentOrImage::Text(t) =&amp;gt; t.text.clone(),
        TextContentOrImage::Image(_) =&amp;gt; "[image]".to_string(),
    }).collect::&amp;lt;Vec&amp;lt;_&amp;gt;&amp;gt;().join("\n")
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Run it with:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cargo run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;To use a real model, swap &lt;code&gt;FauxProvider&lt;/code&gt; for &lt;code&gt;rpi_ai::providers::anthropic&lt;/code&gt; or an OpenAI-compatible provider. The rest of the code stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extensions: native Rust cdylibs
&lt;/h2&gt;

&lt;p&gt;rpi extensions are compiled Rust dynamic libraries (.so/.dll/.dylib), loaded at runtime via libloading through a stable C ABI. The contract lives in rpi-plugin-sdk.&lt;/p&gt;

&lt;p&gt;The ABI is a hand-defined C ABI because Rust doesn't have a stable ABI. The two sides may be compiled with different Rust versions or crate versions, so no Rust type with a non-C repr or a Drop impl can cross. Owned data crosses as ptr+len with an explicit free function.&lt;/p&gt;

&lt;p&gt;There are already 15 extension packages in rpi-package, organized into five categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Extensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Task planning&lt;/td&gt;
&lt;td&gt;rpi-todo, rpi-goal, rpi-plan-mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human interaction&lt;/td&gt;
&lt;td&gt;rpi-ask-user, rpi-permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code analysis&lt;/td&gt;
&lt;td&gt;rpi-codegraph, rpi-lens, rpi-subagents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web tools&lt;/td&gt;
&lt;td&gt;rpi-websearch, rpi-webfetch, rpi-firecrawl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Other&lt;/td&gt;
&lt;td&gt;rpi-mcp-adapter, rpi-background-tasks, rpi-memory, rpi-voice&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Install an extension with:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rpi install rpi-todo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;This downloads from crates.io, compiles the cdylib for your platform, and places it in ~/.rpi/agent/extensions. The CLI discovers it automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's not there yet
&lt;/h2&gt;

&lt;p&gt;Pi ecosystem compatibility. Existing Pi extensions won't run directly. There's a Pi npm/Git bridge in beta, but it's not stable.&lt;/p&gt;

&lt;p&gt;It's 0.1.x. The core loop, providers, tools, harness, and CLI all work. The plugin ABI is stable, but if I break it, extensions break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer"&gt;https://github.com/bigfish1913/pi-rust&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://rpi.laofu.online/docs.html" rel="noopener noreferrer"&gt;https://rpi.laofu.online/docs.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Extensions: &lt;a href="https://github.com/pi-rust/rpi-package" rel="noopener noreferrer"&gt;https://github.com/pi-rust/rpi-package&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy to answer questions about the architecture, the plugin ABI, or the provider bridging.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>I reimplemented the Pi agent in Rust: 10 faster startup, 7 less memory</title>
      <dc:creator>bigfish</dc:creator>
      <pubDate>Thu, 01 Oct 2026 11:36:58 +0000</pubDate>
      <link>https://dev.to/bigfish/i-reimplemented-the-pi-agent-in-rust-10x-faster-startup-7x-less-memory-3gpn</link>
      <guid>https://dev.to/bigfish/i-reimplemented-the-pi-agent-in-rust-10x-faster-startup-7x-less-memory-3gpn</guid>
      <description>&lt;p&gt;I've been learning about the Pi agent and decided to rewrite it in Rust. Now that I've finished a lot of the work, I'd like to show how much faster Rust (rpi) is than TypeScript (Pi). Here are the results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;rpi (Rust)&lt;/th&gt;
&lt;th&gt;Pi (TypeScript)&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;17.5 ms&lt;/td&gt;
&lt;td&gt;169.5 ms&lt;/td&gt;
&lt;td&gt;9.7× faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;17.7 ms&lt;/td&gt;
&lt;td&gt;189.5 ms&lt;/td&gt;
&lt;td&gt;10.7× faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSS when ready&lt;/td&gt;
&lt;td&gt;12.0 MiB&lt;/td&gt;
&lt;td&gt;91.5 MiB&lt;/td&gt;
&lt;td&gt;7.6× smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install size&lt;/td&gt;
&lt;td&gt;21.8 MiB&lt;/td&gt;
&lt;td&gt;~385 MiB&lt;/td&gt;
&lt;td&gt;~18× smaller&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rust is really cool!&lt;/p&gt;

&lt;p&gt;Github:&lt;a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer"&gt;https://github.com/bigfish1913/pi-rust&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Rust-native coding-agent runtime</title>
      <dc:creator>bigfish</dc:creator>
      <pubDate>Mon, 28 Sep 2026 15:11:45 +0000</pubDate>
      <link>https://dev.to/bigfish/rust-native-coding-agent-runtime-501i</link>
      <guid>https://dev.to/bigfish/rust-native-coding-agent-runtime-501i</guid>
      <description>&lt;p&gt;Today I finished the implementation of &lt;strong&gt;&lt;a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer"&gt;pi-rust&lt;/a&gt;&lt;/strong&gt;. Now I want to share how to build an AI agent.&lt;/p&gt;

&lt;p&gt;From my perspective, the core of an agent is the &lt;strong&gt;agent loop&lt;/strong&gt;. The flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified version of the code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;has_more_tool_calls&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;pending_messages&lt;/span&gt;&lt;span class="nf"&gt;.is_empty&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Inject steering/follow-up messages&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Call the model and get the assistant response&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;stream_assistant_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Check stop_reason&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="py"&gt;.stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;StopReason&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Extract tool calls from the assistant message&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="py"&gt;.content&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ToolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Execute tools&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execute_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 6. Add ToolResultMessage back to the context&lt;/span&gt;
    &lt;span class="n"&gt;current_context&lt;/span&gt;&lt;span class="py"&gt;.messages&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 7. Decide whether to continue to the next round&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TextDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ThinkingDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ToolCallDelta&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;AssistantMessageEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The streamed message sequence can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start
  ↓
TextStart
  ↓
TextDelta
  ↓
TextDelta
  ↓
ToolCallStart
  ↓
ToolCallDelta
  ↓
Done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the core idea behind &lt;strong&gt;&lt;a href="https://github.com/bigfish1913/pi-rust" rel="noopener noreferrer"&gt;pi-rust&lt;/a&gt;&lt;/strong&gt;: keep the agent loop simple, and normalize provider-specific streaming events into one consistent event model.&lt;/p&gt;

&lt;p&gt;If you're building something similar, I'd love to hear how you handle provider differences and streaming tool calls.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>opensource</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
