<?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: Paw from Oz</title>
    <description>The latest articles on DEV Community by Paw from Oz (@pawfromoz).</description>
    <link>https://dev.to/pawfromoz</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%2F4036916%2F7d8c7b46-1060-440d-bfb5-0f1daf99ff58.png</url>
      <title>DEV Community: Paw from Oz</title>
      <link>https://dev.to/pawfromoz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pawfromoz"/>
    <language>en</language>
    <item>
      <title>Stoke: A kill switch for runaway AI agents</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Mon, 20 Jul 2026 19:00:58 +0000</pubDate>
      <link>https://dev.to/pawfromoz/stoke-a-kill-switch-for-runaway-ai-agents-p0k</link>
      <guid>https://dev.to/pawfromoz/stoke-a-kill-switch-for-runaway-ai-agents-p0k</guid>
      <description>&lt;p&gt;I've been building with AI coding agents daily — Claude Code, OpenCode, custom agents. They're productive, but they have a failure mode that costs real money: they retry, loop, and fan out on metered API keys. Billing alerts fire hours after the damage is done.&lt;/p&gt;

&lt;p&gt;Observability tools (Langfuse, Helicone, Portkey) tell you what happened. None of them &lt;strong&gt;stop it from happening&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stoke
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://stokegate.com" rel="noopener noreferrer"&gt;Stoke&lt;/a&gt; is a single Rust binary (~5.5 MB, zero runtime dependencies) that sits between your AI agents and model providers. It enforces policy &lt;strong&gt;before&lt;/strong&gt; any provider is called:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hard budget caps&lt;/strong&gt; — per-API-key spend limits in USD. Over the limit = 429 Budget exceeded, not an alert. Streamed responses accrue spend too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop detection&lt;/strong&gt; — exact prompt-hash matching plus optional semantic similarity. 5 similar requests from one key in 60 seconds = blocked for 120 seconds. Catches agents stuck in retry loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — per-key requests-per-minute over a sliding window.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight: &lt;strong&gt;enforce, don't just observe.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What else it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node-aware routing&lt;/strong&gt; — polls your Ollama nodes, places requests on warm machines, balances by live load, fails over automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Federation&lt;/strong&gt; — one Stoke can front another across machines, with a hop guard that prevents cycles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-routing&lt;/strong&gt; — send &lt;code&gt;"model": "auto"&lt;/code&gt; and Stoke scores every (model, node) candidate on cost, predicted latency, and your preferences. Every response carries a receipt explaining the decision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Messages API&lt;/strong&gt; — point Claude Code at Stoke with &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. Same enforcement, then passthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugin system&lt;/strong&gt; — built-in PII redaction, audit logging, prompt harness + HTTP webhooks in any language + JS/TS via deno_core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-closed auth&lt;/strong&gt; — no keys configured and no dev flag = every request rejected. There is no accidentally-open state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;Every Python proxy (LiteLLM, OpenRouter) adds latency and memory overhead — 500MB+ RAM, Postgres, Redis. Stoke is 5.5 MB, starts in milliseconds, uses ~10 MB RAM, and has zero runtime dependencies. It's a single binary you can curl-pipe-sh install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-machine setup
&lt;/h2&gt;

&lt;p&gt;If you have Ollama running on two machines (say a MacBook and a Mac Studio), Stoke routes between them automatically — preferring the machine where the model is already loaded in RAM, balancing by in-flight count, and measuring real time-to-first-token per node.&lt;/p&gt;

&lt;p&gt;Or use federation: run a Stoke on each machine, keep Ollama on 127.0.0.1, and front them with a &lt;code&gt;type = "stoke"&lt;/code&gt; provider. Stoke is the only network-facing door, and it requires auth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickstart
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://stokegate.com/install | sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--version&lt;/span&gt; nightly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minimal config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;routing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"single"&lt;/span&gt;
&lt;span class="py"&gt;default_model&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"qwen3:8b"&lt;/span&gt;

&lt;span class="nn"&gt;[server]&lt;/span&gt;
&lt;span class="py"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8787&lt;/span&gt;

&lt;span class="nn"&gt;[[providers]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"local"&lt;/span&gt;
&lt;span class="py"&gt;base_url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://127.0.0.1:11434/v1"&lt;/span&gt;
&lt;span class="py"&gt;tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"local"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it:&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;STOKE_API_KEYS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;stk-mykey ./target/release/stoke
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point any OpenAI-compatible client at &lt;code&gt;http://localhost:8787/v1&lt;/code&gt; — OpenCode, Cursor, custom apps. Budget caps, rate limits, and loop detection apply to every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Status
&lt;/h2&gt;

&lt;p&gt;Pre-release, MIT licensed, EU-based. Dogfooded daily on a real two-Mac + Ollama setup. Built in the open at &lt;a href="https://github.com/Ozperium/stoke" rel="noopener noreferrer"&gt;github.com/Ozperium/stoke&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The smoke scripts (&lt;code&gt;./scripts/smoke.sh&lt;/code&gt;, &lt;code&gt;./scripts/smoke_federation.sh&lt;/code&gt;) run the full system end-to-end with mock Ollama nodes — no Ollama required to verify the claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic to OpenAI translation (so Claude Code can run against local models)&lt;/li&gt;
&lt;li&gt;Homebrew formula and Docker images&lt;/li&gt;
&lt;li&gt;Per-key loop-detection thresholds&lt;/li&gt;
&lt;li&gt;OpenTelemetry traces and Prometheus metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback welcome. The repo is &lt;a href="https://github.com/Ozperium/stoke" rel="noopener noreferrer"&gt;github.com/Ozperium/stoke&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>rust</category>
      <category>ai</category>
      <category>llm</category>
    </item>
    <item>
      <title>Testing AI Agents with AgentSpec: Jest for Non-Deterministic Behavior</title>
      <dc:creator>Paw from Oz</dc:creator>
      <pubDate>Sun, 19 Jul 2026 19:13:47 +0000</pubDate>
      <link>https://dev.to/pawfromoz/testing-ai-agents-with-agentspec-jest-for-non-deterministic-behavior-55kg</link>
      <guid>https://dev.to/pawfromoz/testing-ai-agents-with-agentspec-jest-for-non-deterministic-behavior-55kg</guid>
      <description>&lt;p&gt;AI agents are non-deterministic. This is both their superpower and their biggest testing challenge.&lt;/p&gt;

&lt;p&gt;When you change a prompt, swap a model, or update a tool definition, the agent's behavior shifts in ways you can't predict. Traditional test tools (Jest, Vitest, Playwright) are built for deterministic code — they expect exact string matching. AI agents don't work that way.&lt;/p&gt;

&lt;p&gt;Last week, I changed a system prompt in a support agent. It still passed all our Jest tests (the function still returned a string). But the agent had stopped offering password reset help — a regression that only surfaced when a user complained.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;AgentSpec&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AgentSpec?
&lt;/h2&gt;

&lt;p&gt;AgentSpec is a testing framework designed specifically for AI agents. It provides assertions that handle non-deterministic output, behavior diff reports that show what changed, and CI/CD integration out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions for AI output
&lt;/h2&gt;

&lt;p&gt;Instead of exact string matching, AgentSpec provides assertions that work with the variability of AI responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# agentspec.yaml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;agent"&lt;/span&gt;
&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handles&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;expired"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refresh&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;token"&lt;/span&gt;
      &lt;span class="na"&gt;not_contains&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;don't&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;know"&lt;/span&gt;
      &lt;span class="na"&gt;max_latency_ms&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;routes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;billing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;question"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;want&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;refund"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contains_any&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;billing"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;support&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;team"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;tool_called&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer_to_billing"&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;on-topic"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;semantically_similar&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;credentials"&lt;/span&gt;
      &lt;span class="na"&gt;min_confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Available assertions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;contains / not_contains&lt;/strong&gt; — substring checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;contains_any / contains_all&lt;/strong&gt; — flexible multi-string matching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;regex&lt;/strong&gt; — pattern matching (error codes, IDs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tool_called&lt;/strong&gt; — verify specific tools were invoked&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;max_latency_ms / max_tokens&lt;/strong&gt; — performance and cost gates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;semantically_similar&lt;/strong&gt; — word-overlap similarity (no API needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;json_path&lt;/strong&gt; — extract and verify fields from JSON agent output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llm_judge&lt;/strong&gt; — use a local LLM to evaluate output quality&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Behavior diff reports
&lt;/h2&gt;

&lt;p&gt;This is the killer feature. AgentSpec stores the last passing output per test. When behavior changes, you see exactly what shifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠️ REGRESSION support agent &amp;gt; handles expired token
  ⚠  Behavior REGRESSED — test was passing, now failing
  + added: your, token, has, expired, please, contact, support
  - removed: you, need, refresh, the, token, settings, security
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of just knowing a test failed, you know WHAT changed. This turns debugging from "what broke?" into "the model stopped saying 'refresh token' and started saying 'renew credentials'."&lt;/p&gt;

&lt;h2&gt;
  
  
  LLM-as-judge with local models
&lt;/h2&gt;

&lt;p&gt;The most powerful assertion is &lt;code&gt;llm_judge&lt;/code&gt; — use an LLM to evaluate whether a response meets quality criteria that can't be expressed as string matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;helpful"&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;How&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;do&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;my&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password?"&lt;/span&gt;
    &lt;span class="na"&gt;expect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;llm_judge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;should&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;explain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;how&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;reset&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;password"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, AgentSpec uses a local Ollama model as the judge — no API costs, no data leaving your machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--judge-endpoint&lt;/span&gt; http://127.0.0.1:11434/v1/chat/completions &lt;span class="nt"&gt;--judge-model&lt;/span&gt; qwen2.5:7b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing real agents
&lt;/h2&gt;

&lt;p&gt;AgentSpec includes an HTTP agent adapter to test any HTTP-accessible AI agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentspec run &lt;span class="nt"&gt;--endpoint&lt;/span&gt; https://my-agent.example.com/chat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It POSTs &lt;code&gt;{input: "..."}&lt;/code&gt; to your endpoint and reads the response. Works with any agent that exposes an HTTP API.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD integration
&lt;/h2&gt;

&lt;p&gt;AgentSpec is designed to drop into any CI pipeline:&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="c"&gt;# Exit code 1 on failure&lt;/span&gt;
agentspec run &lt;span class="nt"&gt;--ci&lt;/span&gt;

&lt;span class="c"&gt;# JUnit XML for CI systems&lt;/span&gt;
agentspec run &lt;span class="nt"&gt;--junit&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; results.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's also a GitHub Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;agentspec/action@v1&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test-dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @ozperium/agentspec
agentspec init
agentspec run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. YAML test definitions, assertions for non-deterministic output, diff reports, CI integration. No cloud, no account, no telemetry.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/Ozperium/agentspec" rel="noopener noreferrer"&gt;Ozperium/agentspec&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Landing:&lt;/strong&gt; &lt;a href="https://agentspec.pages.dev" rel="noopener noreferrer"&gt;agentspec.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love feedback on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is YAML the right format, or would you prefer TS test definitions?&lt;/li&gt;
&lt;li&gt;What assertions are you missing for your AI agents?&lt;/li&gt;
&lt;li&gt;Would you use LLM-as-judge with a local model?&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>testing</category>
      <category>agents</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
