<?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: Quietai.dev</title>
    <description>The latest articles on DEV Community by Quietai.dev (@joosep_aru).</description>
    <link>https://dev.to/joosep_aru</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3945942%2Fdf70fff8-cdcf-4220-8396-d1bb0e4092f9.png</url>
      <title>DEV Community: Quietai.dev</title>
      <link>https://dev.to/joosep_aru</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joosep_aru"/>
    <language>en</language>
    <item>
      <title>Two agents passing strings to each other is not a multi-agent system — it's a pipeline, and the distinction matters</title>
      <dc:creator>Quietai.dev</dc:creator>
      <pubDate>Sun, 31 May 2026 12:14:23 +0000</pubDate>
      <link>https://dev.to/joosep_aru/two-agents-passing-strings-to-each-other-is-not-a-multi-agent-system-its-a-pipeline-and-the-1i0</link>
      <guid>https://dev.to/joosep_aru/two-agents-passing-strings-to-each-other-is-not-a-multi-agent-system-its-a-pipeline-and-the-1i0</guid>
      <description>&lt;p&gt;In the previous two posts I built a minimal Claude agent (Module 1) and then gave it multiple tools (Module 2). This is Module 3 — adding a second agent that critiques the first one's work and loops until approval. The system works. The output is better than single-agent. But building it changed what I think the word "multi-agent" actually buys you, and I want to be specific about where the real architectural line sits.&lt;br&gt;
The setup&lt;br&gt;
Two Python functions, each making a single Anthropic API call with a different system prompt:&lt;br&gt;
pythondef run_designer(game_idea: str, criticism: str = None) -&amp;gt; str:&lt;br&gt;
    if criticism:&lt;br&gt;
        messages = [&lt;br&gt;
            {"role": "user", "content": f"Design a game based on this idea: {game_idea}"},&lt;br&gt;
            {"role": "assistant", "content": "I'll design this game now..."},&lt;br&gt;
            {"role": "user", "content": f"A critic reviewed your design and said: {criticism}\n\nRevise the design addressing all criticism points."}&lt;br&gt;
        ]&lt;br&gt;
    else:&lt;br&gt;
        messages = [&lt;br&gt;
            {"role": "user", "content": f"Design a game based on this idea: {game_idea}"}&lt;br&gt;
        ]&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=2048,
    system="You are a senior game designer. [...]",
    messages=messages
)
return response.content[0].text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;def run_critic(design: str) -&amp;gt; tuple[str, bool]:&lt;br&gt;
    response = client.messages.create(&lt;br&gt;
        model="claude-sonnet-4-6",&lt;br&gt;
        max_tokens=1024,&lt;br&gt;
        system="You are a brutal but fair game design critic. [...] At the end of your review you MUST write either: VERDICT: APPROVED or VERDICT: NEEDS REVISION",&lt;br&gt;
        messages=[{"role": "user", "content": f"Review this game design:\n\n{design}"}]&lt;br&gt;
    )&lt;br&gt;
    review = response.content[0].text&lt;br&gt;
    approved = "VERDICT: APPROVED" in review&lt;br&gt;
    return review, approved&lt;br&gt;
And the main loop:&lt;br&gt;
pythoncriticism = None&lt;br&gt;
for round_num in range(1, max_rounds + 1):&lt;br&gt;
    design = run_designer(game_idea, criticism)&lt;br&gt;
    review, approved = run_critic(design)&lt;br&gt;
    if approved:&lt;br&gt;
        save_final_design(game_idea, design)&lt;br&gt;
        break&lt;br&gt;
    criticism = review&lt;br&gt;
That single line — criticism = review — is the "agent-to-agent communication" in this system. The Critic's response text becomes part of the Designer's input on the next iteration. There is nothing else. No shared state, no message bus, no protocol, no orchestrator.&lt;br&gt;
It works, and I want to be honest about that&lt;br&gt;
In my Krenholm test run, the Critic rejected round 1 and round 2 and approved round 3. The final document was meaningfully better than what Module 2 produced — sharper scope decisions, a clever reframe of a production constraint as a thematic choice, and a tighter primary mechanic. The Critic's reviews surfaced real problems. The Designer's revisions actually addressed them rather than defending the original drafts.&lt;br&gt;
There is genuine value in having specialist prompts review each other. A "critic" prompt with explicit instructions to find real problems produces more useful pushback than asking the same model to self-review inside one prompt. That is a real and useful pattern.&lt;br&gt;
It is also, mechanically, a pipeline of two stateless API calls.&lt;br&gt;
Where the term "multi-agent" turned out to be bigger than what I built&lt;br&gt;
Before this, when I read "multi-agent system," I assumed it meant something like:&lt;/p&gt;

&lt;p&gt;Agents that maintain internal state independent of each other&lt;br&gt;
Some form of inter-agent communication protocol&lt;br&gt;
Coordination logic that exists between agents, not inside any one of them&lt;br&gt;
Often: parallelism, dynamic agent creation, emergent collective behaviour&lt;/p&gt;

&lt;p&gt;What this system actually has:&lt;/p&gt;

&lt;p&gt;No state. Each API call is independent. Conversation history is a Python list I assemble fresh on each iteration.&lt;br&gt;
No protocol. One agent's output is a string. The next agent receives a string. The string format is whatever I typed into an f-string.&lt;br&gt;
No coordination logic between agents. The for loop is the coordination. It runs sequentially and checks for one keyword.&lt;br&gt;
No parallelism, no dynamic agents, no shared memory.&lt;/p&gt;

&lt;p&gt;The "agents" don't know about each other. Each individual API call sees text, a system prompt, and is asked to respond. The Designer doesn't know there is a Critic. The Critic doesn't know there is a Designer. I know there are two of them, because I named the variables.&lt;br&gt;
This isn't a complaint about the model — the model is doing extraordinary work. It's a complaint about the label.&lt;br&gt;
What the architectural line actually is&lt;br&gt;
After building this, my working definition of a real multi-agent system (i.e. the thing Module 4 is going to attempt):&lt;/p&gt;

&lt;p&gt;An orchestrator that does not know in advance how many specialists it will invoke&lt;br&gt;
Dynamic decisions about which specialists to call, in what order, based on intermediate results&lt;br&gt;
Retry and reroute when a specialist's output is unusable&lt;br&gt;
Some form of persistent state that outlives any single API call&lt;br&gt;
Specialists that can themselves invoke tools or sub-specialists&lt;/p&gt;

&lt;p&gt;What I built in Module 3 has none of those. It is a two-stage pipeline with a feedback loop. Useful, deployable, much cheaper than dynamic orchestration. But the gap between "structured prompt pipeline" and "multi-agent system" is wider than I think the current vocabulary admits.&lt;br&gt;
One implementation note worth recording&lt;br&gt;
The VERDICT: APPROVED / VERDICT: NEEDS REVISION pattern at the end of the Critic's system prompt is doing a lot of load-bearing work. It is a structured-output hack — I'm scanning the Critic's free-text response for one of two literal substrings to drive the control flow:&lt;br&gt;
pythonapproved = "VERDICT: APPROVED" in review&lt;br&gt;
This works because the system prompt instructs the model to always end with one of those exact strings. If you remove that instruction, you have to start parsing free text more carefully, and the control flow gets brittle fast. For prompt-driven control flow in general, having the model emit a structured tag at a known location is much more reliable than asking it to "respond with yes or no."&lt;br&gt;
Where this goes&lt;br&gt;
Module 4 will attempt actual orchestration — an agent that receives a goal and figures out what subtasks exist, which specialists to call, and what to do when an output is unusable. None of that decision-making is in Module 3.&lt;br&gt;
Code, the full three-round Krenholm transcript, the final approved design doc: github.com/quietaidev-collab/zero-to-agent&lt;br&gt;
If you've built dynamic orchestration in production: how much harder is the code actually? I'd value calibration before I start.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>My agent re-ran a tool it didn't like the output of — multi-tool agents and the thing the docs don't tell you about tool descriptions</title>
      <dc:creator>Quietai.dev</dc:creator>
      <pubDate>Wed, 27 May 2026 19:47:49 +0000</pubDate>
      <link>https://dev.to/joosep_aru/my-agent-re-ran-a-tool-it-didnt-like-the-output-of-multi-tool-agents-and-the-thing-the-docs-3a1a</link>
      <guid>https://dev.to/joosep_aru/my-agent-re-ran-a-tool-it-didnt-like-the-output-of-multi-tool-agents-and-the-thing-the-docs-3a1a</guid>
      <description>&lt;p&gt;In the last post I built a minimal Claude agent with one tool, no framework, just the Anthropic SDK. This is the follow-up: three tools, and the discovery that most of the agent's planning behaviour was coming from somewhere I hadn't been paying attention to.&lt;/p&gt;

&lt;p&gt;The setup: three tools, no specified order&lt;br&gt;
Module 1's agent had one tool (save_game_design). Module 2 adds two more:&lt;br&gt;
pythondef search_similar_games(genre: str, mechanics: str) -&amp;gt; str:&lt;br&gt;
    # returns reference games for a genre&lt;br&gt;
    ...&lt;/p&gt;

&lt;p&gt;def estimate_dev_time(features: list, team_size: int) -&amp;gt; str:&lt;br&gt;
    base_weeks = len(features) * 2&lt;br&gt;
    adjusted = base_weeks / max(team_size, 1)&lt;br&gt;
    solo_note = " Note: as a solo dev, budget 3x this estimate." if team_size == 1 else ""&lt;br&gt;
    return f"Estimated development time: {adjusted:.0f}-{adjusted*1.5:.0f} weeks.{solo_note}"&lt;br&gt;
The tool definitions are where it gets interesting. Notice the description fields:&lt;br&gt;
pythontools = [&lt;br&gt;
    {&lt;br&gt;
        "name": "search_similar_games",&lt;br&gt;
        "description": "Search for similar existing games to use as reference "&lt;br&gt;
                       "for scope and mechanics. Call this early to ground the "&lt;br&gt;
                       "design in reality.",&lt;br&gt;
        ...&lt;br&gt;
    },&lt;br&gt;
    {&lt;br&gt;
        "name": "estimate_dev_time",&lt;br&gt;
        "description": "Estimate development time based on planned features and "&lt;br&gt;
                       "team size. Call this before finalizing the design to "&lt;br&gt;
                       "ensure scope is realistic.",&lt;br&gt;
        ...&lt;br&gt;
    },&lt;br&gt;
    {&lt;br&gt;
        "name": "save_game_design",&lt;br&gt;
        "description": "Save the completed game design document to a file. "&lt;br&gt;
                       "Only call this when the full design is ready.",&lt;br&gt;
        ...&lt;br&gt;
    }&lt;br&gt;
]&lt;br&gt;
"Call this early." "Call this before finalizing." "Only call this when the full design is ready." Those phrases are the entire sequencing logic. There is no orchestration code. The system prompt lists the steps as a suggestion but doesn't enforce them. The loop is identical to Module 1 — branch on block.name to dispatch the right function.&lt;br&gt;
What it did with one prompt&lt;br&gt;
Same Krenholm prompt as Module 1. The agent's actual sequence:&lt;br&gt;
[search_similar_games]  → Prison Architect, RimWorld, Dwarf Fortress&lt;br&gt;
[estimate_dev_time]     → 24-36 weeks (3x for solo dev)&lt;br&gt;
[estimate_dev_time]     → 20-30 weeks   ← it ran this one again&lt;br&gt;
[save_game_design]&lt;br&gt;
Between the two estimates, the model's text output:&lt;/p&gt;

&lt;p&gt;"24–36 weeks for the core is workable, but 3x polish is a serious warning. Let me trim scope smartly — dropping procedural maps and day/night cycle — and re-estimate the leaner version."&lt;/p&gt;

&lt;p&gt;It observed a result, evaluated it, cut two features from its own feature list, and re-ran the estimate to verify. The observe → evaluate → adjust loop, with no code telling it to do that. The only thing that makes re-running possible is that the loop keeps going as long as stop_reason == "tool_use" — the agent can call the same tool as many times as it decides to.&lt;br&gt;
The thing the docs underplay: tool descriptions are planning instructions&lt;br&gt;
I came into Module 2 expecting to write orchestration logic — some state machine deciding "research, then design, then estimate, then save." I wrote none. The sequencing came entirely from the natural-language description fields.&lt;br&gt;
This reframes what a tool definition is. It's not just an API contract telling the model what arguments to pass. The description is a planning hint the model reads when deciding whether and when to call the tool relative to the others. "Call this early" and "call this before finalizing" are, functionally, a plan written in prose.&lt;br&gt;
Concretely: in an earlier run with a vaguer save-tool description, the agent saved the document before finishing the design. Tightening the description to "Only call this when the full design is ready" fixed the ordering without touching the system prompt or the loop. If your multi-tool agent calls things in the wrong order, look at the descriptions before you reach for orchestration code.&lt;br&gt;
One implementation gotcha: don't assume one tool_use block per response&lt;br&gt;
At one point the model said two steps were "independent" and it could run them together. In my run the calls still arrived sequentially — but the API permits multiple tool_use blocks in a single response, so the defensive way to write the loop is to iterate over all of them and return all the corresponding tool_result blocks in the next message:&lt;br&gt;
pythonif response.stop_reason == "tool_use":&lt;br&gt;
    tool_results = []&lt;br&gt;
    for block in response.content:&lt;br&gt;
        if block.type == "tool_use":&lt;br&gt;
            result = dispatch(block.name, block.input)  # branch on name&lt;br&gt;
            tool_results.append({&lt;br&gt;
                "type": "tool_result",&lt;br&gt;
                "tool_use_id": block.id,&lt;br&gt;
                "content": result&lt;br&gt;
            })&lt;br&gt;
    conversation_history.append({"role": "user", "content": tool_results})&lt;br&gt;
    continue&lt;br&gt;
Assume one-tool-per-turn and you'll silently drop calls.&lt;br&gt;
The Windows tax, still being paid&lt;br&gt;
Committing Module 2, PowerShell rejected &amp;amp;&amp;amp;:&lt;br&gt;
The token '&amp;amp;&amp;amp;' is not a valid statement separator in this version.&lt;br&gt;
Every git tutorial uses git add . &amp;amp;&amp;amp; git commit &amp;amp;&amp;amp; git push. On PowerShell you run them as three separate commands. Minor, but it's the third Windows-specific paper cut in two modules. If you're following along on Windows, expect these.&lt;br&gt;
What it produced&lt;br&gt;
The design document is meaningfully better than Module 1's — not because the model improved, but because it had tools feeding it reality and it used them on its own output. The doc cites the reference games directly in the art direction, includes a realistic dev-time roadmap, and has an "Intentionally Cut Features" section listing what the agent removed to keep scope shippable.&lt;br&gt;
The mechanic that produced all of it is still just the loop: reason, call a tool, read the result, decide whether to keep going. The leverage turned out to be in the prose around the loop, not the loop itself.&lt;br&gt;
Code and full terminal log: github.com/quietaidev-collab/zero-to-agent&lt;br&gt;
Module 3 next: two of these agents talking to each other.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>claude</category>
      <category>python</category>
    </item>
    <item>
      <title>I built a Claude agent without a framework and something I didn't expect to find weird turned out to be weird</title>
      <dc:creator>Quietai.dev</dc:creator>
      <pubDate>Fri, 22 May 2026 11:10:31 +0000</pubDate>
      <link>https://dev.to/joosep_aru/i-built-a-claude-agent-without-a-framework-and-something-i-didnt-expect-to-find-weird-turned-out-103j</link>
      <guid>https://dev.to/joosep_aru/i-built-a-claude-agent-without-a-framework-and-something-i-didnt-expect-to-find-weird-turned-out-103j</guid>
      <description>&lt;p&gt;I built my first agent last night using the Anthropic Python SDK directly. No LangChain, no CrewAI, no orchestration framework. About 60 lines of code, one tool, one goal. The point was to see the mechanics without abstractions on top of them.&lt;br&gt;
Three things stood out. None of them were what I expected going in.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The conversation history array is the agent
There is no hidden state. The "memory" is a Python list you maintain yourself. Every iteration, you append the model's full response — including any tool_use blocks — and then append any tool results back into the list before the next API call.
pythonconversation_history = []
conversation_history.append({"role": "user", "content": user_input})&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;_while True:&lt;br&gt;
    response = client.messages.create(&lt;br&gt;
        model="claude-sonnet-4-6",&lt;br&gt;
        max_tokens=4096,&lt;br&gt;
        system=system_prompt,&lt;br&gt;
        tools=tools,&lt;br&gt;
        messages=conversation_history,&lt;br&gt;
    )&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;conversation_history.append({
    "role": "assistant",
    "content": response.content
})

if response.stop_reason == "tool_use":
    tool_results = []
    for block in response.content:
        if block.type == "tool_use":
            result = save_game_design(**block.input)
            tool_results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": result
            })

    conversation_history.append({
        "role": "user",
        "content": tool_results
    })
    continue

break_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The non-obvious shape: tool results go back in as a user message, not assistant or system. The content is an array of tool_result blocks keyed by tool_use_id from the previous turn. Get this wrong and the model can't connect its own decision to the result of that decision.&lt;br&gt;
I expected this to feel like an abstraction. It doesn't. It feels like maintaining a transcript. Which is what it is.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;stop_reason == "tool_use" is the entire control flow
The loop terminates when the model's response has any stop_reason other than "tool_use" — usually "end_turn". That's it. There is no explicit "task complete" signal.
This is worth sitting with. The agent stops because the model decides not to call any more tools. Whether the task was actually completed correctly is something you have to verify outside the loop. The model's decision to stop is a judgment, not a status code.
In practice this means your system prompt is doing real work. Mine ended with:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Save the design when you think it's ready. Don't wait for permission.&lt;/p&gt;

&lt;p&gt;Without that last sentence, the model drafts, then asks "should I save this now?" — which kills the agentic behaviour. With it, the model just calls save_game_design() when it judges the work is done.&lt;br&gt;
The line between "obedient chatbot" and "agent that takes initiative" is one sentence in a system prompt. I had not expected the leverage to be that concentrated.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The tool's description field is documentation the model reads
&lt;em&gt;tools = [{
"name": "save_game_design",
"description": "Save the completed game design document to a file. "
               "Call this when you have a complete game design ready.",
"input_schema": {
    "type": "object",
    "properties": {
        "title": {"type": "string", "description": "The game title, used for the filename"},
        "content": {"type": "string", "description": "The full game design document in markdown format"}
    },
    "required": ["title", "content"]
}
}]&lt;/em&gt;
The description field is not for me. It is the only documentation the model has about when to call this tool. My first version had a vaguer description and the agent kept calling save mid-conversation, before the design was actually finished. Tightening it to "Call this when you have a complete game design ready" fixed the behaviour. Same for the per-parameter descriptions — they steer which arguments the model produces.
If your agent calls tools at the wrong times or with weird arguments, the fix is almost always in these strings.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three crashes worth saving you&lt;br&gt;
Outdated model string. claude-sonnet-4-20250514 is gone. Current at time of writing: claude-sonnet-4-6. The model itself will confidently give you the old string. Verify against current docs.&lt;br&gt;
Windows file encoding. If the model puts a Unicode character into a file write on Windows, you'll get UnicodeEncodeError: 'charmap' codec can't encode.... Fix:&lt;br&gt;
pythonopen(filename, 'w', encoding='utf-8')&lt;br&gt;
One parameter.&lt;br&gt;
API key in Git history. Commit .gitignore before .env. If your key ever touches a commit — even one you immediately reverted — rotate it on the Anthropic console. GitHub's secret scanner is good. It is not your security model.&lt;/p&gt;

&lt;p&gt;The actual thing I want to flag&lt;br&gt;
When I ran the working version, I gave the agent a vague prompt about an Estonian factory. It produced a complete game design document for a real 19th century textile mill I had not mentioned — three interlocking game loops, win and lose conditions pulled from historical events, scope estimate, art direction. Then it called save_game_design() on its own. I had been making coffee. When I came back the file was on my hard drive.&lt;br&gt;
What I want to flag is not that this happened. It is that I read what the agent had written and agreed with its judgment that the document was finished.&lt;br&gt;
That's a different sentence than "I was impressed." It is also a sentence I had not expected to be writing about a piece of Python I had just finished debugging.&lt;br&gt;
The frameworks (LangGraph, CrewAI) are elaborations of the loop above with conventions on top. Building the unframeworked version once is the fastest way to understand what those frameworks are actually doing.&lt;br&gt;
Full code and the design document the agent produced: github.com/quietaidev-collab/zero-to-agent&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
