<?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: João Miguel</title>
    <description>The latest articles on DEV Community by João Miguel (@espanhol).</description>
    <link>https://dev.to/espanhol</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3985484%2F4d5ff059-e267-40b9-8e8f-c315c33669e2.jpg</url>
      <title>DEV Community: João Miguel</title>
      <link>https://dev.to/espanhol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/espanhol"/>
    <language>en</language>
    <item>
      <title>The Claude API multi-agent loop, without the framework</title>
      <dc:creator>João Miguel</dc:creator>
      <pubDate>Mon, 15 Jun 2026 14:56:59 +0000</pubDate>
      <link>https://dev.to/espanhol/the-claude-api-multi-agent-loop-without-the-framework-2nh5</link>
      <guid>https://dev.to/espanhol/the-claude-api-multi-agent-loop-without-the-framework-2nh5</guid>
      <description>&lt;p&gt;Most Claude API tutorials show a single tool call. Most frameworks hide the loop behind abstractions you can't read. This post shows the loop directly — what actually happens between "Claude requests a tool" and "Claude finishes."&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop in plain English
&lt;/h2&gt;

&lt;p&gt;When you give Claude tools, a single API call isn't always enough. Claude decides whether to call a tool, you execute it, then you send the result back. Claude might call another tool, or it might answer. That cycle is the agent loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user message
    ↓
Claude responds
    ↓
stop_reason == "tool_use"?  →  execute tools  →  back to Claude
    ↓
stop_reason == "end_turn"
    ↓
return final text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The implementation
&lt;/h2&gt;

&lt;p&gt;The entire loop is in &lt;a href="https://github.com/espanhol6/claude-multiagent-loop/blob/main/agent.py" rel="noopener noreferrer"&gt;&lt;code&gt;agent.py&lt;/code&gt;&lt;/a&gt; — about 80 lines.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;tool_handlers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;round_num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_rounds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&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;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_extract_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tool_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&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;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_handlers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_extract_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the core. The rest of the file is &lt;code&gt;_call_tool&lt;/code&gt; (dispatch to your Python function) and &lt;code&gt;_extract_text&lt;/code&gt; (pull text blocks from the response).&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;Define tools in Anthropic's schema format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;TOOLS&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Read the contents of a file.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;File path to read&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&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;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Define handlers as plain Python functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

&lt;span class="n"&gt;tool_handlers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;read_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s in README.md?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tool_handlers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tool_handlers&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;h2&gt;
  
  
  Why not use a framework?
&lt;/h2&gt;

&lt;p&gt;Frameworks aren't wrong. But when something breaks in production — and it will — you want to know exactly what message went to Claude and exactly what came back. Abstractions make that harder.&lt;/p&gt;

&lt;p&gt;This implementation is meant to be read, modified, and owned. The loop is visible. You can add logging, approval gates, retry logic, or conditional execution exactly where you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two working examples
&lt;/h2&gt;

&lt;p&gt;The repo includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;example_research.py&lt;/code&gt;&lt;/strong&gt; — an agent with &lt;code&gt;search&lt;/code&gt; and &lt;code&gt;read_page&lt;/code&gt; tools (swap in your real implementations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;example_code.py&lt;/code&gt;&lt;/strong&gt; — an agent with &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;, and &lt;code&gt;list_files&lt;/code&gt; tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both run end-to-end with real Claude API calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;anthropic
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
python example_code.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;The repo: &lt;strong&gt;&lt;a href="https://github.com/espanhol6/claude-multiagent-loop" rel="noopener noreferrer"&gt;github.com/espanhol6/claude-multiagent-loop&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This pattern is what I used as the foundation for &lt;a href="https://github.com/espanhol6" rel="noopener noreferrer"&gt;Cluster OS Jarvis&lt;/a&gt; — a production multi-agent framework with SSE streaming, up to 6 tool-calling rounds, and cron-scheduled autonomous agents. The loop here is the simplified, standalone version.&lt;/p&gt;

&lt;p&gt;If you're building something with Claude and want to understand what's happening under the hood before adding abstractions, this is a good starting point.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://linkedin.com/in/jo%C3%A3o-espanhol-miguel" rel="noopener noreferrer"&gt;João Daniel Espanhol Miguel&lt;/a&gt; — AI engineer, Lisbon. Also wrote about &lt;a href="https://dev.to/espanhol/debugging-a-silent-native-crash-when-combining-faster-whisper-and-winrt-on-windows-2ik9"&gt;debugging a silent native crash in ctranslate2 + WinRT&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>claude</category>
    </item>
    <item>
      <title>Debugging a silent native crash when combining faster-whisper and WinRT on Windows</title>
      <dc:creator>João Miguel</dc:creator>
      <pubDate>Mon, 15 Jun 2026 11:54:23 +0000</pubDate>
      <link>https://dev.to/espanhol/debugging-a-silent-native-crash-when-combining-faster-whisper-and-winrt-on-windows-2ik9</link>
      <guid>https://dev.to/espanhol/debugging-a-silent-native-crash-when-combining-faster-whisper-and-winrt-on-windows-2ik9</guid>
      <description>&lt;p&gt;I was building a voice assistant: local speech recognition via faster-whisper, reasoning via the Claude API, and local TTS via Windows WinRT. The kind of setup where the only data that leaves your machine is the conversation text.&lt;/p&gt;

&lt;p&gt;Everything worked in isolation. But when I put it all together, the process died silently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The crash
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exit code 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Python traceback. No error message. Just... gone.&lt;/p&gt;

&lt;p&gt;I'd seen exit code 1 before — that's a normal exception. Exit code 5 is different. On Windows, it corresponds to a native access violation (&lt;code&gt;0xC0000005&lt;/code&gt;): something in C or C++ tried to read or write memory it doesn't own. Python never gets a chance to catch it because the crash happens below the interpreter, in native code.&lt;/p&gt;

&lt;p&gt;The frustrating part: I couldn't reproduce it consistently by commenting things out. Sometimes it crashed. Sometimes it didn't. And I had no output to work with — even my &lt;code&gt;print&lt;/code&gt; statements at the top of the file weren't appearing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first insight: unbuffered output
&lt;/h2&gt;

&lt;p&gt;When Python crashes with a native exception, stdout may not be flushed. Any &lt;code&gt;print()&lt;/code&gt; output that was buffered in Python's I/O layer disappears with the process.&lt;/p&gt;

&lt;p&gt;The fix for debugging: run Python with &lt;code&gt;-u&lt;/code&gt; (unbuffered):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-u&lt;/span&gt; your_script.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces every &lt;code&gt;print()&lt;/code&gt; to go directly to the OS. Now I had output — not much, but enough to see that the last line printed was always inside &lt;code&gt;voz.py&lt;/code&gt;, right after the WinRT imports.&lt;/p&gt;

&lt;p&gt;The crash was happening at module load time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Binary search on imports
&lt;/h2&gt;

&lt;p&gt;Module-level crashes in Python are tricky. There's no stack trace, no exception, no line number. The only signal is: &lt;em&gt;which module was loading when the process died?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I narrowed it down by bisection. Comment out half the imports. Run. See which half crashes. Repeat.&lt;/p&gt;

&lt;p&gt;After a few rounds, I had isolated the culprit to two specific imports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ctranslate2&lt;/span&gt;                                      &lt;span class="c1"&gt;# faster-whisper's backend
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;winrt.windows.media.speechsynthesis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpeechSynthesizer&lt;/span&gt;   &lt;span class="c1"&gt;# WinRT TTS
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each one, imported alone, worked perfectly. Together, in the wrong order, the process crashed every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's actually happening
&lt;/h2&gt;

&lt;p&gt;Both &lt;code&gt;ctranslate2&lt;/code&gt; and WinRT load native DLLs into the Python process at import time. This is standard — Python extensions (.pyd files) are just DLLs.&lt;/p&gt;

&lt;p&gt;The problem is what those DLLs do at load time:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WinRT's COM runtime&lt;/strong&gt; sets up thread-local and process-global state when it initializes. It makes certain assumptions about the state of the Win32 heap and thread apartments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ctranslate2&lt;/strong&gt;, when it initializes its CPU backend (or tries to detect GPU capabilities), also does low-level memory operations. If WinRT has already set up certain process-global state that ctranslate2 doesn't expect to find, the access violation follows.&lt;/p&gt;

&lt;p&gt;The exact root cause would require reading the ctranslate2 and WinRT DLL source. But the empirical fix is unambiguous:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load ctranslate2 before WinRT, and the crash never happens.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;In the module where you use WinRT, force &lt;code&gt;ctranslate2&lt;/code&gt; to load its native libraries first — even if you don't use it in that file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# voz.py
&lt;/span&gt;
&lt;span class="c1"&gt;# IMPORTANT: ctranslate2 must be imported before any winrt module.
# Loading WinRT first causes a native crash (exit code 5, 0xC0000005)
# when ctranslate2 later tries to initialize its CPU backend.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ctranslate2&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: F401
&lt;/span&gt;&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ImportError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# ctranslate2 not installed — WinRT alone works fine
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;winrt.windows.media.speechsynthesis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpeechSynthesizer&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;winrt.windows.storage.streams&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DataReader&lt;/span&gt;
&lt;span class="c1"&gt;# ... rest of the module
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;try/except ImportError&lt;/code&gt; makes this safe: if ctranslate2 isn't installed, the WinRT import proceeds normally.&lt;/p&gt;




&lt;h2&gt;
  
  
  What didn't work
&lt;/h2&gt;

&lt;p&gt;Before finding this, I tried:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adding explicit &lt;code&gt;import ctranslate2&lt;/code&gt; at the top of the entry point&lt;/strong&gt; — not enough; the &lt;em&gt;order within each module&lt;/em&gt; still matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping WinRT in a try/except&lt;/strong&gt; — the crash bypasses Python's exception handling entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running in a subprocess&lt;/strong&gt; — deferred the crash, didn't prevent it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checking for version conflicts&lt;/strong&gt; — no version combination fixed it; it's a load-order issue, not a version issue&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to know if this is your bug
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You're using &lt;code&gt;faster-whisper&lt;/code&gt; (or anything that imports &lt;code&gt;ctranslate2&lt;/code&gt;) &lt;strong&gt;and&lt;/strong&gt; WinRT speech synthesis in the same Python process on Windows&lt;/li&gt;
&lt;li&gt;The process exits with code 5, or silently with no output&lt;/li&gt;
&lt;li&gt;The crash disappears when you remove one of the two components&lt;/li&gt;
&lt;li&gt;Standard Python debugging tools show nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The fix:&lt;/strong&gt; in whichever file imports WinRT first, add &lt;code&gt;import ctranslate2&lt;/code&gt; before the WinRT imports.&lt;/p&gt;




&lt;h2&gt;
  
  
  The project
&lt;/h2&gt;

&lt;p&gt;This came up while building &lt;strong&gt;&lt;a href="https://github.com/espanhol6/gemeo-conselheiro" rel="noopener noreferrer"&gt;Gémeo Conselheiro&lt;/a&gt;&lt;/strong&gt; — a voice AI assistant where your laptop does all the heavy lifting: local STT via faster-whisper, local TTS via WinRT (Microsoft Hélia, pt-PT), and only the conversation text goes to the Claude API. There's also a phone call mode that connects your phone over Wi-Fi for hands-free conversation.&lt;/p&gt;

&lt;p&gt;If you're building a similar local-first voice AI stack on Windows and hit this, I hope this saves you the time I spent on it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;João Daniel Espanhol Miguel — &lt;a href="https://github.com/espanhol6" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://linkedin.com/in/jo%C3%A3o-espanhol-miguel" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>microsoft</category>
      <category>ai</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
