<?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: HiveGate</title>
    <description>The latest articles on DEV Community by HiveGate (@hivegate-).</description>
    <link>https://dev.to/hivegate-</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%2F4187344%2Ff04796c1-5a92-4c81-8828-3c9bfef9d5aa.png</url>
      <title>DEV Community: HiveGate</title>
      <link>https://dev.to/hivegate-</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hivegate-"/>
    <language>en</language>
    <item>
      <title>From BizGenie to HiveGate: where our agent runtime began</title>
      <dc:creator>HiveGate</dc:creator>
      <pubDate>Tue, 10 Nov 2026 16:00:01 +0000</pubDate>
      <link>https://dev.to/hivegate-/from-bizgenie-to-hivegate-where-our-agent-runtime-began-3g7e</link>
      <guid>https://dev.to/hivegate-/from-bizgenie-to-hivegate-where-our-agent-runtime-began-3g7e</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;HiveGate&lt;/a&gt; wasn't written from scratch. It started in May 2025 from &lt;a href="https://github.com/agno-agi/agent-api" rel="noopener noreferrer"&gt;agent-api&lt;/a&gt;, Agno's template for serving agents with FastAPI and Postgres. It became the agent backend for a product called BizGenie, and later we opened it up and gave it a new name.&lt;/p&gt;

&lt;p&gt;This post covers where it came from, which parts are still the ones we built for BizGenie, and what we use it for now. The part with code is the approval flow: a tool marked &lt;code&gt;requires_confirmation&lt;/code&gt;, a run that comes back &lt;code&gt;PAUSED&lt;/code&gt;, and the commit call that resumes it. That flow dates from October 2025 and works the same way in HiveGate 0.3.1.&lt;/p&gt;

&lt;h2&gt;
  
  
  What BizGenie asked of the backend
&lt;/h2&gt;

&lt;p&gt;BizGenie was a B2B SaaS platform with two kinds of agents. Each employee got a personalized AI copilot, and there were shared, role-based experts on top.&lt;/p&gt;

&lt;p&gt;That shape asks more of a backend than a single agent in a script does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A personal copilot works for one person. When it reads mail or books a meeting, it has to do that in that person's account, with that person's credentials.&lt;/li&gt;
&lt;li&gt;A shared expert is one agent with many users. Each conversation with it still belongs to one person, so sessions and history are per user.&lt;/li&gt;
&lt;li&gt;An agent that can send email on someone's behalf needs a stop before it sends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is about the model. Agno already gave us the agent itself: model calls, tools, memory and teams. The work was everything around it, which is what HiveGate is today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The timeline
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;May 2025:&lt;/strong&gt; the backend starts from Agno's agent-api template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;June 2025:&lt;/strong&gt; BizGenie's agents are added to it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August 2025:&lt;/strong&gt; the &lt;code&gt;/v2&lt;/code&gt; routes that HiveGate still serves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 2025:&lt;/strong&gt; the Google and Microsoft workspace toolkits (mail, calendar, contacts, drive) and &lt;code&gt;requires_confirmation&lt;/code&gt; on the tools that write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;End of 2025:&lt;/strong&gt; open-source work begins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;September 2026:&lt;/strong&gt; the project is renamed HiveGate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;October 31, 2026:&lt;/strong&gt; 0.3.1, the first release on PyPI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What carried over: approval before a write
&lt;/h2&gt;

&lt;p&gt;Letting an agent search an inbox is low risk. Letting it send an email or delete a message is not. Agno lets a toolkit name the tools that need a person's confirmation, and when the model calls one, Agno stops the run before the tool executes.&lt;/p&gt;

&lt;p&gt;This is how HiveGate's email toolkit declares its writes, in &lt;code&gt;toolkits/email.py&lt;/code&gt; (trimmed):&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="nc"&gt;Toolkit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"EmailToolkit"&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_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;requires_confirmation_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"create_draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"send_draft"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"trash_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"delete_email_permanently"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"modify_labels"&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;&lt;code&gt;search_emails&lt;/code&gt; and &lt;code&gt;list_drafts&lt;/code&gt; aren't on the list, so they run without a pause. The calendar, contacts and drive toolkits follow the same pattern, and so can your own:&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;from&lt;/span&gt; &lt;span class="n"&gt;agno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Toolkit&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyServiceToolkit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Toolkit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;super&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="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my_service_toolkit"&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="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;my_action&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;auth_required&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;requires_confirmation_tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"my_action"&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;auth&lt;/span&gt; &lt;span class="k"&gt;else&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="n"&gt;stop_after_tool_call_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="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"auth_required"&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;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
        &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The run pauses
&lt;/h3&gt;

&lt;p&gt;A chat request to an agent with the email toolkit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST http://localhost:8000/v2/agents/customer-support/chat \
  -H &lt;span class="s2"&gt;"X-API-Key: $HIVEGATE_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{
    "message": "Send Jane the password reset instructions.",
    "stream": false,
    "user_id": "user-42",
    "session_id": "sess-abc123",
    "timezone": "America/New_York",
    "locale": "en-US"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the model decides to call &lt;code&gt;send_email&lt;/code&gt;, the response isn't a finished answer. It has &lt;code&gt;status: "PAUSED"&lt;/code&gt;, a &lt;code&gt;run_id&lt;/code&gt;, and the proposed call with its arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"customer-support"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"sess-abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"PAUSED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"run-f47ac10b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"tools"&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="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"tc-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"requires_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"tool_args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"jane@acme.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Password Reset"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Nothing has been sent yet. Your app shows &lt;code&gt;tool_name&lt;/code&gt; and &lt;code&gt;tool_args&lt;/code&gt; to the user in whatever form fits: a confirmation card, an editable form, a message in Slack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approve, edit or deny
&lt;/h3&gt;

&lt;p&gt;The decision goes to &lt;code&gt;POST /v2/agents/{agent_id}/chat/commit&lt;/code&gt; with the &lt;code&gt;run_id&lt;/code&gt;, the same &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;session_id&lt;/code&gt;, and one entry per pending tool in &lt;code&gt;updated_tools&lt;/code&gt;. Here the reviewer changed the subject before approving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST http://localhost:8000/v2/agents/customer-support/chat/commit \
  -H &lt;span class="s2"&gt;"X-API-Key: $HIVEGATE_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{
    "run_id": "run-f47ac10b",
    "stream": false,
    "user_id": "user-42",
    "session_id": "sess-abc123",
    "updated_tools": [
      {
        "tool_call_id": "tc-001",
        "confirmed": true,
        "tool_args": {"to": "jane@acme.com", "subject": "Password Reset Instructions"},
        "confirmation_note": "Clearer subject"
      }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run resumes, the email goes out through the user's own Gmail or Outlook account, and the response has the agent's final message. HiveGate copies only &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;tool_args&lt;/code&gt; (or &lt;code&gt;args&lt;/code&gt;) and &lt;code&gt;confirmation_note&lt;/code&gt; from each entry onto the paused call. Anything else is ignored.&lt;/p&gt;

&lt;p&gt;Set &lt;code&gt;"confirmed": false&lt;/code&gt; on every pending tool and the run doesn't resume at all. The response is &lt;code&gt;status: "cancelled"&lt;/code&gt; with the content &lt;code&gt;"Tool execution cancelled by user."&lt;/code&gt;, and no model call is made.&lt;/p&gt;

&lt;p&gt;The same thing from a Python backend:&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;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="no"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:8000/v2/agents/customer-support"&lt;/span&gt;
&lt;span class="no"&gt;HEADERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"X-API-Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="no"&gt;HIVEGATE_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;dict&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="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"stream"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"updated_tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;,&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;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"{BASE}/chat/commit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"The paused run is gone. Ask the agent again."&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="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;h3&gt;
  
  
  The limits that came with it
&lt;/h3&gt;

&lt;p&gt;Two things about this flow are worth knowing before you rely on it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Paused runs live in process memory.&lt;/strong&gt; If the process restarts, or the commit lands on another replica, commit returns 410 Gone ("Confirmation time window has elapsed"). Run a single replica, or route each session to the same instance, when you use approvals. The conversation itself is in Postgres, so the user keeps their history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP tools have no approval gate yet.&lt;/strong&gt; Tools from an MCP server run without a pause. That's &lt;a href="https://github.com/hivegate-ai/hivegate/issues/93" rel="noopener noreferrer"&gt;#93&lt;/a&gt; on the &lt;a href="https://github.com/hivegate-ai/hivegate/issues/72" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HiveGate also records the API key that made the commit, not the person who clicked approve. If you need the reviewer's identity, store it in your own database before you call commit, or put it in &lt;code&gt;confirmation_note&lt;/code&gt;, which is saved with the tool execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else carried over: each user's own accounts
&lt;/h2&gt;

&lt;p&gt;The workspace toolkits from October 2025 are the other half of the copilot idea. An agent that books a meeting for a user does it in that user's Google or Microsoft calendar, not in a shared account.&lt;/p&gt;

&lt;p&gt;Your app runs the OAuth consent and hands HiveGate the tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST http://localhost:8000/v2/users/user-42/tokens \
  -H &lt;span class="s2"&gt;"X-API-Key: $HIVEGATE_KEY"&lt;/span&gt; \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{
    "integration_key": "google_calendar",
    "provider": "google",
    "token_type": "oauth2",
    "token_data": {
      "access_token": "ya29.a0...",
      "refresh_token": "1//0g...",
      "expires_in": 3599,
      "client_id": "1234-abc.apps.googleusercontent.com",
      "client_secret": "GOCSPX-..."
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HiveGate encrypts the token data with Fernet before it's written, refreshes the access token when a tool asks for it close to expiry, and gives it to the tool at the moment it runs. The model never sees it. The &lt;a href="https://hivegate.dev/blog/per-user-google-microsoft-oauth-ai-agents/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=2026-W45" rel="noopener noreferrer"&gt;per-user OAuth post&lt;/a&gt; has the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  What HiveGate is now
&lt;/h2&gt;

&lt;p&gt;Around that core, HiveGate today has agents, prompts and skills managed as data through the API, supervisor teams with Docker and Kubernetes workers (including Claude Code), per-tenant knowledge in Qdrant, a record of tokens and USD cost for every model call under &lt;code&gt;/usage&lt;/code&gt;, Claude prompt caching, HTTP MCP servers in chat, OpenTelemetry and Sentry tracing, and one-click deploys to Railway, Render and Koyeb. It's MIT-licensed, and since 0.3.1 it installs from PyPI.&lt;/p&gt;

&lt;p&gt;It's also no longer only ours. Six first-time contributors have joined since late September, with work that ranges from fixing the Qdrant healthcheck to working out how GitHub's and Atlassian's hosted MCP servers do OAuth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we use it today
&lt;/h2&gt;

&lt;p&gt;We build our current AI projects on HiveGate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A personal-finance app&lt;/strong&gt; whose agents propose and classify budgets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://joyfulchess.com" rel="noopener noreferrer"&gt;Joyful Chess&lt;/a&gt;&lt;/strong&gt;, a chess-learning project in the works. Its AI tutor is built on HiveGate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's our own use. It tells you what we test HiveGate against day to day. It isn't evidence that anyone else runs it, and we won't present it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you don't need it
&lt;/h2&gt;

&lt;p&gt;Agno's own &lt;a href="https://docs.agno.com/agent-os/introduction" rel="noopener noreferrer"&gt;AgentOS&lt;/a&gt; also serves agents over HTTP, with sessions, human approval, RBAC and tracing. If your agents are defined in code and AgentOS covers what you need, use AgentOS.&lt;/p&gt;

&lt;p&gt;HiveGate is for the case where agents and prompts are data you change over an API, each user connecting their own Google or Microsoft account, approvals that happen inside your product over REST, and a need to know what each user cost you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool install hivegate-ai
mkdir my-agents &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; my-agents &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; hivegate init
&lt;span class="c"&gt;# set GOOGLE_API_KEY in .env&lt;/span&gt;
docker compose up -d &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; hivegate serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://127.0.0.1:8000/docs&lt;/code&gt;. The paused-run and commit endpoints are documented in the &lt;a href="https://hivegate.dev/docs/api-agents/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=2026-W45" rel="noopener noreferrer"&gt;agents API docs&lt;/a&gt;, and the toolkits and their confirmation lists in the &lt;a href="https://hivegate.dev/docs/toolkits/?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=2026-W45" rel="noopener noreferrer"&gt;toolkits docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If this is useful, a star helps other people find the project: &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;⭐ github.com/hivegate-ai/hivegate&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>AI agent REST API from pip install: HiveGate 0.3.1</title>
      <dc:creator>HiveGate</dc:creator>
      <pubDate>Wed, 04 Nov 2026 16:00:01 +0000</pubDate>
      <link>https://dev.to/hivegate-/ai-agent-rest-api-from-pip-install-hivegate-031-1fe9</link>
      <guid>https://dev.to/hivegate-/ai-agent-rest-api-from-pip-install-hivegate-031-1fe9</guid>
      <description>&lt;p&gt;Until last week, trying &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;HiveGate&lt;/a&gt; meant cloning the repo, copying &lt;code&gt;.env.example&lt;/code&gt;, running a setup script and starting the server from the checkout. That's fine if you want to work on HiveGate. It's a lot to ask if you only want to see whether it fits your agents.&lt;/p&gt;

&lt;p&gt;HiveGate 0.3.1, released on October 31, is the first release on PyPI. You install it as a tool, write a starter config with one command, start Postgres and Qdrant, and serve. This post walks through that path from an empty directory to an AI agent REST API with per-user sessions, a paused tool call you approve over HTTP, and a cost record for every model call. It ends with what changes when you take it off your laptop.&lt;/p&gt;

&lt;p&gt;HiveGate is an MIT-licensed FastAPI service built on &lt;a href="https://github.com/agno-agi/agno" rel="noopener noreferrer"&gt;Agno&lt;/a&gt;. You write the agent. HiveGate serves it. It is not a request-routing API gateway like Kong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11 or newer, and &lt;a href="https://docs.astral.sh/uv/" rel="noopener noreferrer"&gt;uv&lt;/a&gt; or &lt;a href="https://pipx.pypa.io/" rel="noopener noreferrer"&gt;pipx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker, for Postgres and Qdrant&lt;/li&gt;
&lt;li&gt;An API key for a model provider. The seeded demo agents use Gemini, so &lt;code&gt;GOOGLE_API_KEY&lt;/code&gt; is the shortest path. OpenAI, Anthropic, xAI, Z.ai and DeepSeek keys also work.&lt;/li&gt;
&lt;/ul&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;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;hivegate-ai   &lt;span class="c"&gt;# or: pipx install hivegate-ai&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The package is &lt;code&gt;hivegate-ai&lt;/code&gt; because PyPI refused &lt;code&gt;hivegate&lt;/code&gt; as too similar to an existing project. The command is still &lt;code&gt;hivegate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install it as a tool rather than into a project's virtual environment. The package's top-level modules have generic names (&lt;code&gt;api&lt;/code&gt;, &lt;code&gt;agents&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;), and they can clash with other libraries in a shared environment. &lt;code&gt;uv tool&lt;/code&gt; and &lt;code&gt;pipx&lt;/code&gt; give it an environment of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write the config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-agents &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-agents
hivegate init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init&lt;/code&gt; writes four files: &lt;code&gt;compose.yaml&lt;/code&gt;, &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;db/migrations/setup.sql&lt;/code&gt; and &lt;code&gt;sql/sample_data.sql&lt;/code&gt;. The &lt;code&gt;.env&lt;/code&gt; is not a copy of the example file. Three values are filled in for a first local run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;AUTH_DISABLED=true&lt;/code&gt;, so you can call &lt;code&gt;/v2&lt;/code&gt; without an API key while the server only listens on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ADMIN_SECRET&lt;/code&gt;, set to a random 32-byte value instead of the placeholder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SECRET_TOKEN_ENC_KEY&lt;/code&gt;, a fresh Fernet key. HiveGate encrypts the per-user OAuth tokens it stores with this key, so keep it with your data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running &lt;code&gt;init&lt;/code&gt; again refuses to overwrite anything. With &lt;code&gt;--force&lt;/code&gt; it rewrites the compose files but still keeps an existing &lt;code&gt;.env&lt;/code&gt;, because that file holds your API keys and the key your stored tokens are encrypted with.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;.env&lt;/code&gt; and set your model key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GOOGLE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Start the stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;   &lt;span class="c"&gt;# Postgres + Qdrant, seeds demo agents&lt;/span&gt;
hivegate serve         &lt;span class="c"&gt;# http://127.0.0.1:8000/docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose starts Postgres and Qdrant, creates the schema and seeds a handful of demo agents and teams. Since 0.3.1 it publishes their ports on &lt;code&gt;127.0.0.1&lt;/code&gt; only, so the databases aren't reachable from your network by accident.&lt;/p&gt;

&lt;p&gt;One known wart: &lt;code&gt;docker compose ps&lt;/code&gt; reports Qdrant as unhealthy even though it works. The healthcheck calls &lt;code&gt;curl&lt;/code&gt;, which the Qdrant image doesn't have, so &lt;code&gt;docker compose up -d --wait&lt;/code&gt; fails too. That's &lt;a href="https://github.com/hivegate-ai/hivegate/issues/135" rel="noopener noreferrer"&gt;#135&lt;/a&gt;, a good first issue if you want to fix it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;hivegate serve&lt;/code&gt; loads &lt;code&gt;.env&lt;/code&gt; from the current directory and starts the API on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;. If no model key is set, it starts anyway and prints a warning, because agents can't answer without one. The interactive OpenAPI docs are at &lt;code&gt;/docs&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  First calls
&lt;/h2&gt;

&lt;p&gt;Check that it's up, then list the agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/health
curl http://localhost:8000/v2/agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the seeded agents, including &lt;code&gt;demo-assistant&lt;/code&gt;. Chat with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents/demo-assistant/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "message": "What is the capital of France?",
    "stream": false,
    "user_id": "user-1",
    "session_id": "session-1",
    "timezone": "UTC",
    "locale": "en-US"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response carries the answer and the identifiers you'll use later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The capital of France is Paris."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"demo-assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"session-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini-3-flash-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"completed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-f47ac10b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Leave out &lt;code&gt;"stream": false&lt;/code&gt; and you get server-sent events instead, which is what a chat UI wants.&lt;/p&gt;

&lt;p&gt;Every chat names a &lt;code&gt;user_id&lt;/code&gt; and a &lt;code&gt;session_id&lt;/code&gt;. Send another message with the same pair and the agent sees the earlier turns, because history lives in Postgres, not in the process. Use a different &lt;code&gt;user_id&lt;/code&gt; and that user gets their own sessions and memory. Nothing in your agent code has to track who is who.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are data
&lt;/h2&gt;

&lt;p&gt;The demo agents came from SQL, but you create your own over the same API. An agent is an id, a name and a prompt template, plus optional settings for memory, history and reasoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "id": "my-first-agent",
    "name": "My First Agent",
    "template": "You are a helpful assistant that answers questions concisely.",
    "description": "A simple Q&amp;amp;A agent",
    "tags": ["qa", "general"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's available at &lt;code&gt;/v2/agents/my-first-agent/chat&lt;/code&gt; straight away, with no redeploy. Changing a prompt is an API call, not a release. The &lt;a href="https://hivegate.dev/docs/api-agents/" rel="noopener noreferrer"&gt;agents API docs&lt;/a&gt; list every field.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tool call that waits for a person
&lt;/h2&gt;

&lt;p&gt;Letting an agent search an inbox is low risk. Letting it send an email is not. HiveGate's built-in Google and Microsoft toolkits mark every write as &lt;code&gt;requires_confirmation&lt;/code&gt;: &lt;code&gt;send_email&lt;/code&gt;, &lt;code&gt;trash_email&lt;/code&gt;, &lt;code&gt;schedule_meeting&lt;/code&gt; and the rest. Your own Agno toolkits opt in with &lt;code&gt;requires_confirmation_tools=[...]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When the model calls one of those tools, the run stops before the tool executes, and the chat response says so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAUSED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"run-f47ac10b"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tc-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requires_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jane@acme.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Password Reset"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has been sent yet. Show the pending call wherever your users are, then send the decision back. The reviewer can edit the arguments, not only say yes or no:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents/my-first-agent/chat/commit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "run_id": "run-f47ac10b",
    "stream": false,
    "user_id": "user-1",
    "session_id": "session-1",
    "updated_tools": [
      {
        "tool_call_id": "tc-001",
        "confirmed": true,
        "tool_args": {"to": "jane@acme.com", "subject": "Password Reset Instructions"}
      }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;"confirmed": false&lt;/code&gt; and the run ends with &lt;code&gt;status: "cancelled"&lt;/code&gt; without calling the tool. Since 0.3.1 the same works for team runs, which used to pause and then couldn't be committed (#116).&lt;/p&gt;

&lt;p&gt;Two limits to know. Paused runs live in the server's memory, so a restart or a request that lands on another replica gets &lt;code&gt;410 Gone&lt;/code&gt;. Run a single replica, or route each session to the same instance, when you use approvals. And tools from MCP servers don't have an approval gate yet; that's &lt;a href="https://github.com/hivegate-ai/hivegate/issues/72" rel="noopener noreferrer"&gt;roadmap item #72&lt;/a&gt;. The &lt;a href="https://hivegate.dev/blog/human-approval-agent-tool-calls/" rel="noopener noreferrer"&gt;human approval post&lt;/a&gt; covers the flow in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did that cost?
&lt;/h2&gt;

&lt;p&gt;Every model call is metered: input, output, cache-read and cache-write tokens, and the USD cost from a versioned price table. The read-only &lt;code&gt;/usage&lt;/code&gt; API groups it by agent, user, tenant, model, API key or status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"http://localhost:8000/usage/summary?window=24h&amp;amp;group_by=user"&lt;/span&gt;
curl &lt;span class="s2"&gt;"http://localhost:8000/usage/calls?order=cost&amp;amp;limit=10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/usage/calls&lt;/code&gt; returns the most expensive calls first, with session and run ids, so you can find the conversation behind a spike. With a Claude model, prompt caching is on by default in 0.3.1, and the cache-read tokens show up in the same records. The &lt;a href="https://hivegate.dev/blog/llm-token-usage-cost-tracking/" rel="noopener noreferrer"&gt;token cost post&lt;/a&gt; shows the queries behind a per-user cost report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Off your laptop
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AUTH_DISABLED=true&lt;/code&gt; is only acceptable while nothing else can reach the server, and &lt;code&gt;hivegate serve&lt;/code&gt; enforces that. Ask it to listen on another host with auth off and it refuses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;hivegate serve &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0
Refusing to serve on 0.0.0.0 with AUTH_DISABLED on: every route would be open. Remove it from .env.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It refuses the placeholder &lt;code&gt;ADMIN_SECRET&lt;/code&gt; the same way. Remove &lt;code&gt;AUTH_DISABLED&lt;/code&gt; from &lt;code&gt;.env&lt;/code&gt;, restart, and create an API key with the admin secret &lt;code&gt;init&lt;/code&gt; generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/admin/api-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Admin-Secret: &lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "Frontend", "owner_id": "team-frontend", "scopes": ["agents:read", "agents:chat"]}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw key (&lt;code&gt;agk_...&lt;/code&gt;) is returned once and stored as a SHA-256 hash. Send it as &lt;code&gt;X-API-Key&lt;/code&gt; on every &lt;code&gt;/v2&lt;/code&gt; call. For the cost dashboard, a key with only the &lt;code&gt;usage:read&lt;/code&gt; scope can read &lt;code&gt;/usage&lt;/code&gt; and nothing else. The &lt;a href="https://hivegate.dev/docs/authentication/" rel="noopener noreferrer"&gt;authentication docs&lt;/a&gt; have the details.&lt;/p&gt;

&lt;p&gt;For a hosted deployment you don't need the package at all. The prebuilt image deploys in one click to Railway, Render or Koyeb from the &lt;a href="https://hivegate.dev/deploy/" rel="noopener noreferrer"&gt;deploy page&lt;/a&gt;, and the &lt;a href="https://hivegate.dev/docs/deployment/" rel="noopener noreferrer"&gt;deployment docs&lt;/a&gt; cover a production setup of your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool upgrade hivegate-ai
hivegate migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;migrate&lt;/code&gt; reads &lt;code&gt;.env&lt;/code&gt; from the current directory and applies any database changes the new version ships. Coming from a clone of 0.3.0, apply &lt;code&gt;db/migrations/011_token_usage_cost.sql&lt;/code&gt; (it adds the metering columns), and note that &lt;code&gt;compose.yaml&lt;/code&gt; now reads &lt;code&gt;DB_PASS&lt;/code&gt; and &lt;code&gt;DB_DATABASE&lt;/code&gt; instead of &lt;code&gt;DB_PASSWORD&lt;/code&gt; and &lt;code&gt;DB_NAME&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else is in 0.3.1
&lt;/h2&gt;

&lt;p&gt;Everything merged since 0.3.0 on September 29:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Per-call token cost metering and the &lt;code&gt;/usage&lt;/code&gt; API (#109)&lt;/li&gt;
&lt;li&gt;Prompt caching for Claude models (#110)&lt;/li&gt;
&lt;li&gt;Approvable paused team runs (#116)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error_code: "provider_billing"&lt;/code&gt; and a plain message when a provider account is out of credits, instead of the provider's raw error (#125)&lt;/li&gt;
&lt;li&gt;A warning that names the server when an MCP server with an unsupported transport is dropped, instead of the agent silently getting fewer tools (#146, from first-time contributor @coughlmao)&lt;/li&gt;
&lt;li&gt;Dependency upgrades that clear the open security advisories (#134)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full list is in the &lt;a href="https://github.com/hivegate-ai/hivegate/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;changelog&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you don't need HiveGate
&lt;/h2&gt;

&lt;p&gt;Agno ships its own &lt;a href="https://docs.agno.com/agent-os/introduction" rel="noopener noreferrer"&gt;AgentOS&lt;/a&gt;, which serves agents defined in code over HTTP with sessions, human approval, RBAC and tracing. If your agents live in your codebase and AgentOS covers what you need, use AgentOS. It's maintained by the Agno team and tracks Agno releases.&lt;/p&gt;

&lt;p&gt;HiveGate is for the case where agents and prompts are data you change over an API, each of your users connects their own Google or Microsoft account, approvals happen in your product over REST, and you need to know what each user cost you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv tool &lt;span class="nb"&gt;install &lt;/span&gt;hivegate-ai
&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-agents &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-agents &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; hivegate init
&lt;span class="c"&gt;# set GOOGLE_API_KEY in .env&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; hivegate serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://127.0.0.1:8000/docs&lt;/code&gt;. The &lt;a href="https://hivegate.dev/docs/quickstart/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; and the rest of the &lt;a href="https://hivegate.dev/docs/" rel="noopener noreferrer"&gt;docs&lt;/a&gt; go from there, and issues and questions are welcome on &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If this is useful, a star helps other people find the project: &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;⭐ github.com/hivegate-ai/hivegate&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>fastapi</category>
    </item>
    <item>
      <title>Human approval before risky agent tool calls</title>
      <dc:creator>HiveGate</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:00:03 +0000</pubDate>
      <link>https://dev.to/hivegate-/human-approval-before-risky-agent-tool-calls-1hk8</link>
      <guid>https://dev.to/hivegate-/human-approval-before-risky-agent-tool-calls-1hk8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://hivegate.dev/blog/human-approval-agent-tool-calls/" rel="noopener noreferrer"&gt;hivegate.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Letting an agent search an inbox is low risk. Letting it send an email, cancel a meeting or permanently delete a message is not. A wrong recipient or a hallucinated date becomes a real-world mistake. The usual answer is human-in-the-loop: the agent proposes the call, a person reviews it, and only then does it run.&lt;/p&gt;

&lt;p&gt;This post shows how that works in &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;HiveGate&lt;/a&gt;, an open-source FastAPI service for &lt;a href="https://github.com/agno-agi/agno" rel="noopener noreferrer"&gt;Agno&lt;/a&gt; agents. The run pauses, your app gets the proposed call, and a second request approves, edits or denies it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which tools pause
&lt;/h2&gt;

&lt;p&gt;Agno lets a toolkit mark tools as &lt;code&gt;requires_confirmation&lt;/code&gt;. When the model calls one, Agno stops the run before executing it. HiveGate's built-in toolkits mark every write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; &lt;code&gt;send_email&lt;/code&gt;, &lt;code&gt;create_draft&lt;/code&gt;, &lt;code&gt;send_draft&lt;/code&gt;, &lt;code&gt;trash_email&lt;/code&gt;, &lt;code&gt;delete_email_permanently&lt;/code&gt;, &lt;code&gt;modify_labels&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calendar:&lt;/strong&gt; &lt;code&gt;schedule_meeting&lt;/code&gt;, &lt;code&gt;schedule_meeting_find_time&lt;/code&gt;, &lt;code&gt;cancel_meeting&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contacts:&lt;/strong&gt; &lt;code&gt;create_contact&lt;/code&gt;, &lt;code&gt;update_contact&lt;/code&gt;, &lt;code&gt;delete_contact&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drive:&lt;/strong&gt; &lt;code&gt;read_file&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workers:&lt;/strong&gt; &lt;code&gt;run_claude_code&lt;/code&gt;, &lt;code&gt;run_managed_agent&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read-only tools such as &lt;code&gt;search_emails&lt;/code&gt; and &lt;code&gt;list_events&lt;/code&gt; run without a pause. Your own Agno toolkits opt in the same way, through &lt;code&gt;requires_confirmation_tools=[...]&lt;/code&gt;. Tools from MCP servers don't have an approval gate yet. That's on the &lt;a href="https://github.com/hivegate-ai/hivegate/issues/72" rel="noopener noreferrer"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the run pauses
&lt;/h2&gt;

&lt;p&gt;A normal chat request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents/support-agent/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$HIVEGATE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "message": "Email jordan@acme.example that the renewal call moves to Thursday 3pm.",
    "user_id": "user-1842",
    "session_id": "inbox-1",
    "timezone": "America/Chicago",
    "locale": "en-US",
    "stream": false,
    "user_profile": {
      "profile_id": "user-1842",
      "email": "dana@acme.example",
      "full_name": "Dana Lee",
      "role": "Account manager",
      "tenant_id": "acme"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of a finished answer, the response has &lt;code&gt;status: "PAUSED"&lt;/code&gt;, a &lt;code&gt;run_id&lt;/code&gt;, and the pending call with its arguments (trimmed here):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"agent_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"support-agent"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"inbox-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PAUSED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"run_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6f1c9a2e-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_8Jx2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"send_email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"requires_confirmation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tool_args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"jordan@acme.example"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Renewal call moved to Thursday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"body_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hi Jordan, the renewal call is now Thursday at 3pm CT. Thanks, Dana"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has been sent yet. With &lt;code&gt;"stream": true&lt;/code&gt;, you get the same data as an SSE event whose &lt;code&gt;event&lt;/code&gt; field is &lt;code&gt;RunPaused&lt;/code&gt;, and then the stream ends.&lt;/p&gt;

&lt;p&gt;Show &lt;code&gt;tool_name&lt;/code&gt; and &lt;code&gt;tool_args&lt;/code&gt; to the user, or to whoever reviews actions for them, in whatever form fits your app: a confirmation card, an editable form, a Slack message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: approve, edit or deny
&lt;/h2&gt;

&lt;p&gt;The decision goes to &lt;code&gt;POST /v2/agents/{agent_id}/chat/commit&lt;/code&gt; with the &lt;code&gt;run_id&lt;/code&gt;, the same &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;session_id&lt;/code&gt;, and one entry per pending tool in &lt;code&gt;updated_tools&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approve as proposed:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents/support-agent/chat/commit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$HIVEGATE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "run_id": "6f1c9a2e-...",
    "user_id": "user-1842",
    "session_id": "inbox-1",
    "stream": false,
    "updated_tools": [
      {"tool_call_id": "call_8Jx2...", "confirmed": true}
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent resumes, the email is sent through the user's own Gmail or Outlook account, and the response has the agent's final message, &lt;code&gt;status: "completed"&lt;/code&gt; and the token usage for the resumed part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edit, then approve.&lt;/strong&gt; The reviewer fixed the time, so send the corrected arguments with the approval. &lt;code&gt;tool_args&lt;/code&gt; replaces the proposed arguments, so send the full set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_8Jx2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"tool_args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"jordan@acme.example"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Renewal call moved to Thursday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"body_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Hi Jordan, the renewal call is now Thursday at 4pm CT. Thanks, Dana"&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"confirmation_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Corrected time to 4pm"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Deny:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"tool_call_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_8Jx2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"confirmation_note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wrong recipient"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If every pending tool is denied, HiveGate doesn't resume the run at all. It returns &lt;code&gt;status: "cancelled"&lt;/code&gt; with the content &lt;code&gt;"Tool execution cancelled by user."&lt;/code&gt;, and no model call is made. If you approve some tools and deny others, the run resumes: Agno executes the approved ones, skips the denied ones, and tells the model they were rejected.&lt;/p&gt;

&lt;p&gt;The fields HiveGate accepts in each &lt;code&gt;updated_tools&lt;/code&gt; entry are &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;tool_args&lt;/code&gt; (or &lt;code&gt;args&lt;/code&gt;) and &lt;code&gt;confirmation_note&lt;/code&gt;. Anything else is ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same thing from Python
&lt;/h2&gt;

&lt;p&gt;A minimal backend helper, with the review step left to your UI:&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;httpx&lt;/span&gt;

&lt;span class="n"&gt;BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/v2/agents/support-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HEADERS&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;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HIVEGATE_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;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;user&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;session_id&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="n"&gt;body&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&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_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;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile_id&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;session_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;session_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;timezone&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;UTC&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;locale&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;en&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;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&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_profile&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&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paused&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;user&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;decisions&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="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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;decisions: one {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, optional &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_args&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confirmation_note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;} per pending tool.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;body&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;run_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;paused&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&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_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;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profile_id&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;session_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;paused&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_id&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;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updated_tools&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;,&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;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/chat/commit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;HEADERS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Paused run is gone; ask the agent again.&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="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cancel my 2pm with Jordan.&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&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inbox-1&lt;/span&gt;&lt;span class="sh"&gt;"&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;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PAUSED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools&lt;/span&gt;&lt;span class="sh"&gt;"&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;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requires_confirmation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;review_in_ui&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# your code: returns approve/edit/deny per tool
&lt;/span&gt;    &lt;span class="n"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reply&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the chat request named a &lt;code&gt;model&lt;/code&gt;, send the same &lt;code&gt;model&lt;/code&gt; on commit. HiveGate uses it to find the paused agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where paused runs live
&lt;/h2&gt;

&lt;p&gt;A paused run is held in the memory of the HiveGate process that paused it, until it's committed. If the process restarts, or the commit lands on a different replica, commit returns &lt;strong&gt;410 Gone&lt;/strong&gt; ("Confirmation time window has elapsed"). Two practical consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a single replica, or route each session to the same instance, when you use approvals.&lt;/li&gt;
&lt;li&gt;Treat a 410 as "the proposal expired": ask the agent again rather than retrying the commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conversation itself is stored in Postgres, so the user doesn't lose their history when a pending action expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit trail
&lt;/h2&gt;

&lt;p&gt;Here is what HiveGate records about a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The run, in the agent's session.&lt;/strong&gt; Agno stores each run in the agent's Postgres session table. A resumed run's tool executions carry the arguments that actually ran (including any edits), &lt;code&gt;requires_confirmation&lt;/code&gt;, &lt;code&gt;confirmed&lt;/code&gt; and the &lt;code&gt;confirmation_note&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost of the resumed run.&lt;/strong&gt; The resumed part is metered like any other call: a row in &lt;code&gt;token_usage&lt;/code&gt; with the user, session, API key name, number of tool calls, status and USD cost. You can list them with &lt;code&gt;GET /usage/calls?agent=support-agent&amp;amp;order=recent&lt;/code&gt;, which needs a key with the &lt;code&gt;usage:read&lt;/code&gt; scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs.&lt;/strong&gt; Every commit is logged with its &lt;code&gt;run_id&lt;/code&gt;, and a fully denied run is logged as such. Set &lt;code&gt;OTEL_TRACING_BACKEND=otlp&lt;/code&gt; and &lt;code&gt;OTEL_LOGGING_BACKEND=otlp&lt;/code&gt; (Sentry and Logtail are also supported) to send traces and logs to your observability stack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it doesn't record is &lt;em&gt;which person&lt;/em&gt; clicked approve. HiveGate sees the API key that made the commit, not your end user's identity. If you need that for compliance, record the reviewer and the decision in your own database before you call commit. You can also put the reviewer in &lt;code&gt;confirmation_note&lt;/code&gt;, which is stored with the tool execution. A fully denied run is never resumed, so it leaves no usage row. Your own record and the log line are the trace for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on &lt;code&gt;/v2/approvals&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;HiveGate also has an approvals inbox at &lt;code&gt;/v2/approvals&lt;/code&gt;, used by the supervisor platform. &lt;code&gt;GET /v2/approvals&lt;/code&gt; lists pending requests, and &lt;code&gt;POST /v2/approvals/{job_id}/approve&lt;/code&gt;, &lt;code&gt;/deny&lt;/code&gt; or &lt;code&gt;/decide&lt;/code&gt; records a decision. It can push requests out through notification plugins (webhook, Slack, Telegram, Discord and WhatsApp are built in; &lt;code&gt;GET /v2/approvals/plugins/list&lt;/code&gt; shows them). Those requests are also held in process memory. For the agent chat flow in this post, the commit call is what resumes the run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Deploy HiveGate from the &lt;a href="https://hivegate.dev/deploy/" rel="noopener noreferrer"&gt;deploy page&lt;/a&gt;. Create an agent tagged &lt;code&gt;available_tools:email&lt;/code&gt;, store a test Gmail token for one user (see the &lt;a href="https://hivegate.dev/blog/per-user-google-microsoft-oauth-ai-agents/" rel="noopener noreferrer"&gt;OAuth post&lt;/a&gt;), and ask the agent to email you. The run will pause.&lt;/li&gt;
&lt;li&gt;The commit endpoint is in &lt;code&gt;api/routes/v2/agents.py&lt;/code&gt; and the Agno resume logic is in &lt;code&gt;agents/hitl.py&lt;/code&gt; in the &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>agents</category>
    </item>
    <item>
      <title>Deploying Agno agents to production as a REST API</title>
      <dc:creator>HiveGate</dc:creator>
      <pubDate>Tue, 20 Oct 2026 16:00:02 +0000</pubDate>
      <link>https://dev.to/hivegate-/deploying-agno-agents-to-production-as-a-rest-api-1a1d</link>
      <guid>https://dev.to/hivegate-/deploying-agno-agents-to-production-as-a-rest-api-1a1d</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://hivegate.dev/blog/deploy-agno-agents-production-rest-api/" rel="noopener noreferrer"&gt;hivegate.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://github.com/agno-agi/agno" rel="noopener noreferrer"&gt;Agno&lt;/a&gt; agent that works in a script is a few lines of Python. Turning it into something a web or mobile app can call for thousands of users takes more: an HTTP endpoint, a conversation per user, history that survives restarts, API keys, model keys kept on the server, and a container you can deploy.&lt;/p&gt;

&lt;p&gt;This post shows how &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;HiveGate&lt;/a&gt;, an MIT-licensed FastAPI service built on Agno, handles each of those. It also covers when you should skip HiveGate and use Agno's own AgentOS.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: you may not need HiveGate
&lt;/h2&gt;

&lt;p&gt;Agno ships &lt;a href="https://docs.agno.com/agent-os/introduction" rel="noopener noreferrer"&gt;AgentOS&lt;/a&gt;, which serves agents defined in code over HTTP, with sessions, human approval, RBAC and tracing:&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;from&lt;/span&gt; &lt;span class="n"&gt;agno.agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agno.db.postgres&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PostgresDb&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agno.models.google&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Gemini&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agno.os&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentOS&lt;/span&gt;

&lt;span class="n"&gt;support_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support-agent&lt;/span&gt;&lt;span class="sh"&gt;"&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="nc"&gt;Gemini&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-3-flash-preview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PostgresDb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg://ai:ai@localhost:5432/ai&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;add_history_to_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentOS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;support_agent&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;get_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your agents live in your codebase and that covers what you need, use AgentOS. It's maintained by the Agno team and tracks Agno releases.&lt;/p&gt;

&lt;p&gt;HiveGate is for a different case: agents that act &lt;em&gt;for your users&lt;/em&gt; in their own Google or Microsoft accounts, and agents, prompts and skills that you manage as data through an API instead of shipping code for each change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents are records, not code
&lt;/h2&gt;

&lt;p&gt;In HiveGate you create an agent with a POST. The &lt;code&gt;template&lt;/code&gt; becomes the agent's system prompt, stored as a versioned prompt in Postgres. The &lt;code&gt;config&lt;/code&gt; block maps onto Agno's &lt;code&gt;Agent&lt;/code&gt; options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$HIVEGATE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "id": "support-agent",
    "name": "support-agent",
    "template": "You are the support assistant for Acme. Answer briefly and cite the help center when you can.",
    "description": "Customer support for Acme",
    "tags": ["available_tools:email,calendar"],
    "config": {
      "enable_memory": true,
      "enable_history": true,
      "num_history_runs": 3
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;available_tools:&lt;/code&gt; tag switches on HiveGate's built-in toolkits for this agent. Leave it out and the agent gets none of them. Agents that need other tools can point at MCP servers over Streamable HTTP through &lt;code&gt;config.worker_config.mcp_servers&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Changing the prompt later is a &lt;code&gt;PUT /v2/prompts/support_agent&lt;/code&gt; (the prompt ID is the agent's name in snake case). The next chat picks up the new template, so a prompt fix doesn't need a redeploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chatting, with sessions per user
&lt;/h2&gt;

&lt;p&gt;Every chat request names a &lt;code&gt;user_id&lt;/code&gt; and a &lt;code&gt;session_id&lt;/code&gt;. Both are required, as are &lt;code&gt;timezone&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/v2/agents/support-agent/chat &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$HIVEGATE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "message": "My invoice from last month looks wrong.",
    "user_id": "user-1842",
    "session_id": "ticket-5521",
    "timezone": "America/New_York",
    "locale": "en-US",
    "stream": false
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The non-streaming response is a JSON object with &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;session_id&lt;/code&gt;, &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, the &lt;code&gt;tools&lt;/code&gt; the agent called, and &lt;code&gt;token_usage&lt;/code&gt; for that call.&lt;/p&gt;

&lt;p&gt;Here is what happens to those two IDs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;History.&lt;/strong&gt; Agno stores each run in a Postgres session table, one table per agent. With &lt;code&gt;enable_history&lt;/code&gt; on, the last &lt;code&gt;num_history_runs&lt;/code&gt; runs of the same session go back into the model's context. A follow-up with the same &lt;code&gt;session_id&lt;/code&gt; continues the conversation, even after a restart or on another replica.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory.&lt;/strong&gt; With &lt;code&gt;enable_memory&lt;/code&gt; on, Agno's memory manager keeps facts about the user across sessions, keyed by &lt;code&gt;user_id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent cache.&lt;/strong&gt; HiveGate builds the Agno &lt;code&gt;Agent&lt;/code&gt; once per (prompt, agent, model, user, session) and caches it in the process, so later turns skip setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Per-request context goes into Agno's &lt;code&gt;session_state&lt;/code&gt;. HiveGate fills it from the request: the user's name and profile (from &lt;code&gt;user_profile&lt;/code&gt;), &lt;code&gt;timezone&lt;/code&gt; and &lt;code&gt;locale&lt;/code&gt;. If you send a &lt;code&gt;tenant_profile&lt;/code&gt;, knowledge search is filtered to that tenant's documents, so one deployment can serve several customers without mixing their data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stream&lt;/code&gt; defaults to &lt;code&gt;true&lt;/code&gt;. The response is Server-Sent Events, one &lt;code&gt;event: message&lt;/code&gt; per Agno event, each with a JSON &lt;code&gt;data&lt;/code&gt; line:&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;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="n"&gt;payload&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;message&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;Summarize my open tickets.&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_id&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-1842&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;session_id&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;ticket-5521&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;timezone&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;UTC&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;locale&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;en&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&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;http://localhost:8000/v2/agents/support-agent/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&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;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HIVEGATE_KEY&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&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;line&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="nf"&gt;iter_lines&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&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;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data: &lt;/span&gt;&lt;span class="sh"&gt;"&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;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RunContent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&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="nf"&gt;get&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;print&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="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;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a model call fails mid-stream, you get an &lt;code&gt;event: error&lt;/code&gt; with a generic message. The details stay in the server log, so provider internals don't leak to clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking the model per request
&lt;/h2&gt;

&lt;p&gt;A request can set &lt;code&gt;model&lt;/code&gt;. Otherwise HiveGate uses &lt;code&gt;DEFAULT_CHAT_MODEL&lt;/code&gt;, and falls back to &lt;code&gt;gemini-3-flash-preview&lt;/code&gt; if that's unset. Besides pinned IDs such as &lt;code&gt;claude-sonnet-4-6&lt;/code&gt;, it accepts aliases like &lt;code&gt;anthropic:sonnet-latest&lt;/code&gt; or &lt;code&gt;google:flash-latest&lt;/code&gt;. An alias resolves to the vendor's newest model in that tier and never moves you up to a pricier tier. Model keys stay in the server's environment; clients never see them.&lt;/p&gt;

&lt;h2&gt;
  
  
  API keys and cost per user
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/v2/*&lt;/code&gt; requires an &lt;code&gt;X-API-Key&lt;/code&gt;. Create keys with the admin secret:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/admin/api-keys &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Admin-Secret: &lt;/span&gt;&lt;span class="nv"&gt;$ADMIN_SECRET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name": "acme-web", "rate_limit": 1000}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw key is in the &lt;code&gt;api_key&lt;/code&gt; field of the response, and it's shown only once. For local work, &lt;code&gt;AUTH_DISABLED=true&lt;/code&gt; skips the check. Never set it on a deployment other people can reach.&lt;/p&gt;

&lt;p&gt;Every model call is metered: tokens, cache reads, tool calls, duration and USD cost go into a &lt;code&gt;token_usage&lt;/code&gt; table, including failed and abandoned runs. To see which users cost the most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$USAGE_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"http://localhost:8000/usage/summary?window=7d&amp;amp;group_by=user"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The usage endpoints accept a key with the &lt;code&gt;usage:read&lt;/code&gt; scope. A key whose only scope is &lt;code&gt;usage:read&lt;/code&gt; is refused on &lt;code&gt;/v2&lt;/code&gt;, so a dashboard key can't call agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it
&lt;/h2&gt;

&lt;p&gt;Locally, Docker Compose starts Postgres and Qdrant and seeds demo agents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/hivegate-ai/hivegate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;hivegate
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env    &lt;span class="c"&gt;# set GOOGLE_API_KEY, uncomment AUTH_DISABLED=true&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
./scripts/dev_setup.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./scripts/start_server.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a server, use the prebuilt multi-arch image &lt;code&gt;ghcr.io/hivegate-ai/hivegate:latest&lt;/code&gt;. The &lt;a href="https://hivegate.dev/deploy/" rel="noopener noreferrer"&gt;deploy page&lt;/a&gt; has one-click buttons for Render, Railway and Koyeb, plus Kubernetes and AWS, Azure and GCP manifests. The settings that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variable&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DB_HOST&lt;/code&gt;, &lt;code&gt;DB_PORT&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;DB_PASS&lt;/code&gt;, &lt;code&gt;DB_DATABASE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Postgres for agents, sessions, tokens and usage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ADMIN_SECRET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Protects &lt;code&gt;/admin/*&lt;/code&gt;, where API keys are created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GOOGLE_API_KEY&lt;/code&gt; / &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; / &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;At least one model provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SECRET_TOKEN_ENC_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fernet key that encrypts stored user OAuth tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QDRANT_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional, for per-tenant knowledge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;GET /health&lt;/code&gt; is the health check. The deploy page ends with a five-step smoke test: health, list agents, create an agent, one real LLM call, then cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to know before scaling out
&lt;/h2&gt;

&lt;p&gt;Conversation history is in Postgres, so any replica can serve the next turn. Two things live in process memory: the cached &lt;code&gt;Agent&lt;/code&gt; objects, and runs that are paused waiting for a human to approve a tool call. A paused run has to be resumed on the process that paused it. If you use approvals, run one replica or route each session to the same instance. The &lt;a href="https://hivegate.dev/blog/human-approval-agent-tool-calls/" rel="noopener noreferrer"&gt;post on approvals&lt;/a&gt; covers that flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use which
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Agents defined in code, one team, standard Agno features: &lt;strong&gt;AgentOS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Agents that act in each user's Gmail, Outlook or calendar; agents and prompts edited through an API; per-tenant knowledge; supervisor teams with container workers: &lt;strong&gt;HiveGate&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Deploy in one click from the &lt;a href="https://hivegate.dev/deploy/" rel="noopener noreferrer"&gt;deploy page&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Read the source, open an issue or star the repo on &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this is useful, a star helps other people find the project: &lt;a href="https://github.com/hivegate-ai/hivegate" rel="noopener noreferrer"&gt;⭐ github.com/hivegate-ai/hivegate&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>fastapi</category>
    </item>
  </channel>
</rss>
