AvalaAvala
Start Building — Book a Demo / Become a Coworker
Back to News
EngineeringAgentic AISoftware ArchitecturePhysical AIInfrastructure

Building agent-native software: an engineering guide

A good coding agent is a good general-purpose agent. How we build software around atomic tools, outcome prompts, and agent-native workflows.

By Avala Engineering
12/20/2025
11 min read
0
Share
Building agent-native software: an engineering guide

A good coding agent is a good general-purpose agent. That was the discovery hiding inside Claude Code once it started working reliably: a large language model with access to bash and file tools, operating in a loop until an objective is achieved, can accomplish complex multi-step tasks we never explicitly programmed. The same architecture that refactors a codebase can organize files, manage workflows, or coordinate complex operations. The capability isn't specific to code. It's specific to the pattern.

That pattern changes what a feature is. Features don't have to be code we write. They can be outcomes we describe, achieved by an agent with tools, looping until the outcome is reached. Get this right and judgment moves out of your codebase and into the agent. Get it wrong and you've rebuilt traditional software with an expensive API call in the middle.

We've been exploring these patterns as we build infrastructure for physical AI. What follows is our current thinking on what makes software genuinely agent-native. Some of it is battle-tested; some is still forming. We'll be clear about which is which.

Parity: whatever the user can do, the agent can do

Every other principle depends on this one. Whatever a user can do through your interface, an agent should be able to achieve through tools.

This sounds obvious until you audit your own system. You build an interface for creating, organizing, and tagging items. A user asks the agent: "Create a summary of today's work and tag it as urgent." If the UI can do it but the agent can't, the agent is stuck. It doesn't matter how intelligent the model is; it's constrained by the tools you've given it.

The discipline: when adding any capability, ask whether the agent can achieve the same outcome. If not, add tools or primitives. This isn't about mapping every UI button to a tool; it's about equivalent capability through whatever combination of tools is necessary.

The test is empirical, not theoretical. Pick any action a user can take, describe it to the agent, and see whether it can accomplish the outcome. Every gap you find is work to do.

Granularity: tools are primitives, features are outcomes

Here's where agent-native architecture diverges from traditional design. Traditionally, a feature is a unit of functionality you write, test, and ship. In agent-native development, a tool is a primitive capability, and a feature is an outcome described in a prompt.

Consider two ways to organize files. The less granular approach bundles decision logic into a tool: a classify_and_organize_files function that takes files as input and applies your organization logic. The agent calls your function; your code makes the decisions. To change behavior, you refactor code.

The more granular approach provides primitives (read_file, write_file, move_file, and access to bash) and describes the outcome in the prompt: "Organize the downloads folder by grouping related items, archiving anything older than 30 days, and flagging anything that looks important." The agent makes the decisions. To change behavior, you edit the prompt.

The difference is where judgment lives. In the first approach, you wrote the decision logic. In the second, the agent pursues an outcome with judgment: it can hit unexpected cases, adjust its approach, or ask clarifying questions. The more atomic your tools, the more flexibly the agent can use them. Bundle decision logic into tools and you've moved judgment back into code, which is to say back into traditional software development.

Composability: new features are new prompts

When tools are atomic and parity holds, new features stop requiring code. Want a "weekly review" feature? It's a prompt:

"Review files modified this week. Summarize key changes. Based on incomplete items and approaching deadlines, suggest three priorities for next week."

The agent uses list_files, read_file, and its judgment. You described an outcome; the agent loops until it's achieved.

This works for developers and users alike. Developers ship features by adding prompts. Users customize behavior by modifying prompts or writing their own. The constraint: tools must stay atomic enough to compose in ways you didn't anticipate. If tools encode too much logic, composition breaks down.

Emergence: capability you didn't build

The payoff for atomic tools and parity is that the agent can accomplish things you never built. Imagine a system with tools for reading notes and reading task lists. A user asks: "Cross-reference my meeting notes with my task list and tell me what I've committed to but haven't scheduled."

You didn't build a commitment tracker. But if the agent can read notes and tasks, it can do this anyway. The capability emerged from atomic tools and general intelligence, not from feature development.

This reveals latent demand. Instead of guessing what features users want, you observe what they ask the agent to do. The flywheel: build with atomic tools and parity; users ask for things you didn't anticipate; the agent composes tools to accomplish them, or fails and reveals a gap; you add domain tools or prompts to make common patterns efficient; repeat. You're not imagining every feature upfront. You're building a capable foundation and learning from what emerges.

Add domain tools deliberately, and keep primitives open

Start with pure primitives: bash, file operations, basic storage. This proves the architecture works and reveals what the agent actually needs. As patterns emerge, add domain-specific tools for specific reasons, not because it feels like you should:

  • Vocabulary anchoring. A create_note tool teaches the agent what "note" means in your system. The tool establishes a concept, not just an operation.
  • Guardrails. Some operations need validation that shouldn't be left to agent judgment. A domain tool can enforce invariants while leaving the decision of whether to act to the agent.
  • Efficiency. If the agent frequently combines three primitives the same way, a domain tool that bundles them reduces latency and API calls.

The rule: a domain tool represents one conceptual action from the user's perspective. Mechanical validation belongs in the tool; judgment about what to do, or whether to do it, belongs in the prompt.

And keep primitives available. Domain tools are shortcuts, not gates. Unless there's a specific reason to restrict access (security, data integrity, regulatory requirements), the agent should still be able to reach for the underlying primitives on edge cases. That preserves composability and emergence. The default is open; gating is a conscious decision.

Files are the most battle-tested agent interface

Agents are naturally good at files, and not by accident: language models have seen more examples of file manipulation than almost any other domain. Bash plus filesystem is the most battle-tested agent interface that exists.

Files also have the right properties for agent-native systems. Agents already know the primitives: cat, grep, mv, mkdir. Files are inspectable: users can see what the agent created, edit it, move it, delete it. There's no black box. Files are portable: export and backup are trivial, and users own their data in a way database rows never quite achieve. And files are self-documenting: a path like /projects/acme/notes/ communicates structure in a way that SELECT * FROM notes WHERE project_id = 123 doesn't.

The general principle underneath: design for what agents can reason about. The best proxy is what would make sense to a human examining the system directly.

Improvement without shipping code

Agent-native applications improve through two channels that don't involve pushing builds.

Context accumulates. The agent maintains state across sessions: what exists, what the user has done, what worked. A context file read at the start of each session and updated as state changes becomes portable working memory.

Prompts refine at every level. Developers ship updated prompts for all users. Users customize prompts for their workflow. In advanced cases, agents adjust their own prompts based on feedback, though that requires careful safety rails.

Completion is a signal, not a heuristic

Agents need an explicit way to say "I'm done." This sounds trivial until you try to detect completion through heuristics: checking for consecutive iterations without tool calls, looking for expected output files. These approaches are fragile and fail in edge cases.

The clean solution: tools return a completion signal alongside their result. A tool can succeed and signal continue, fail and signal continue for recovery, or complete and stop the loop. Completion is separate from success or failure.

Richer control flow isn't standard yet. An agent might need to pause for user input, escalate a decision outside its scope, or retry after a transient failure. Today, if the agent needs input, it asks in its text response; there's no formal "blocked waiting for input" state. The industry is still figuring this out.

For multi-step work, track progress at the task level: pending, in progress, completed, failed, or skipped. Partial completion is the norm, not the exception. An agent might hit iteration limits with some tasks done and others pending, or fail on one task while others succeed. Checkpoints should preserve enough state to resume from where things left off.

Design for the context window filling up

Agent sessions can extend indefinitely; context windows can't. Assume context will fill up and design for it from the start rather than treating it as an edge case.

Tools should support iterative refinement rather than all-or-nothing responses: summary first, then detail, then full content if needed, so the agent can drill down without loading everything upfront. And give agents a way to consolidate mid-session: a prompt like "summarize what you've learned so far and continue" compresses working memory while preserving essential state.

Share the workspace; justify the sandbox

Agents and users should work in the same data space, not separate sandboxes. When both can read and write the same directories, users can inspect and modify agent work, agents can build on what users create, no synchronization layer is needed, and transparency is complete.

Sandbox only for a specific reason: security, preventing corruption of critical data, regulatory requirements. The burden of proof belongs on isolation, not on sharing.

Silent agents feel broken

When agents act, the interface should reflect it immediately. Show what the agent is considering and which tool is executing, stream text as it's generated, and update progress in real time. If the agent is working, the user should know it.

Not every tool call deserves surfacing. Internal checks, status queries, and housekeeping operations are noise. The goal is meaningful visibility, not a firehose.

Five patterns that look agent-native but aren't

Agents as routers. The agent figures out what the user wants, then calls the right function. This uses intelligence for dispatch, not action, a fraction of what agents can do.

App-first, agents-later. Building features conventionally and bolting on an agent constrains it to what you already imagined. No emergence.

Request-response thinking. Agent gets input, does one thing, returns output. The power comes from the loop: pursue an outcome, handle surprises, keep going until done.

Over-constrained tools. Strict enums and validation at every layer feel safe but prevent the agent from doing things you didn't anticipate. Gate only for security, data integrity, or compliance.

Happy paths in code. If your code handles all the edge cases, the agent is just an expensive function caller. The point is letting the agent handle edge cases with judgment.

The test: ask for something you never built

Describe an outcome to the agent that's within your application's domain but that you didn't build a specific feature for. Can it figure out how to accomplish it, operating in a loop until it succeeds?

If yes, you've built something agent-native. If no, your architecture is too constrained, and the traps above are the first places to look.

Software that pursues outcomes

We're early in understanding how to build agent-native software well. These patterns come from building annotation infrastructure and internal tooling; some will hold up, some will be superseded.

The core insight seems durable: agents with atomic tools, operating in loops until outcomes are achieved, can accomplish things we didn't explicitly program. That isn't a trick or a shortcut. It's a different model of what software is, one where features are outcomes described in language rather than logic implemented in code.

That's the direction we're building in: not software that executes our logic, but software that pursues our outcomes, with the intelligence to figure out how.


If you're building agent-native systems, we're happy to share what we've learned, and curious what you're finding.