🎸
Movement 1 of 4 Chapter 6 of 42 Ready to Read

The Agent and Its Environment – Designing Fundamental Interactions

An AI agent, no matter how intelligent, is useless if it can't perceive and act on the world around it. Our SpecialistAgent was like a brain in a vat: it could think, but it couldn't read data or write results.

This chapter describes how we built the "arms" and "legs" of our agents: the fundamental interactions with the database, which represented their working environment.

# The Architectural Decision: A Database as Shared "World State"

Our first major decision was to use a database (Supabase, in this case) not just as a simple archive, but as the single source of truth about the "world state". Every relevant information for the project – tasks, objectives, deliverables, memory insights – would be stored there.

This approach, known as "Shared State" or "Shared Blackboard" (*Blackboard Architecture* in the literature), is a well-documented architectural pattern in multi-agent systems. As described by Hayes-Roth in their seminal work on blackboard systems, this architecture allows independent specialists to collaborate by sharing a common knowledge space, without requiring direct communication between agents.

The Customer Support Team Metaphor

Imagine a customer support team where each specialist (technical, sales, billing) works on different tickets. Instead of constantly emailing each other, they use a shared CRM where everyone can see case status, update notes, and pass tickets to the right colleague. The CRM becomes the team's "shared memory" - if a technician goes on break, another can pick up exactly where they left off, because all the history is centrally documented.

In our system, the Supabase database functions exactly like that CRM: it's the shared blackboard where each agent writes its progress and reads that of others. The advantages of this architecture in a multi-agent system are:

# Fundamental Interactions: The "Verbs" of Our Agents

We defined a set of basic interactions, "verbs" that every agent had to be able to perform. For each of these, we created a dedicated function in our database.py, which acted as a Data Access Layer (DAL), another abstraction layer to protect us from Supabase-specific details.

Reference code: backend/database.py

Agent Verb Corresponding DAL Function Strategic Purpose
Read a Task get_task(task_id) Allows an agent to understand what its current assignment is.
Update Task Status update_task_status(...) Communicates to the rest of the system that a task is in progress, completed, or failed.
Create a New Task create_task(...) Allows an agent to delegate or decompose work (essential for planning).
Save an Insight store_insight(...) The fundamental action for learning. Allows an agent to contribute to collective memory.
Read Memory get_relevant_context(...) Allows an agent to learn from past experiences before acting.
Create a Deliverable create_deliverable(...) The final action that produces value for the user.

# "War Story": The Danger of "Race Conditions" and Pessimistic Locking

With multiple agents working in parallel, we encountered a classic distributed systems problem: race conditions.

Disaster Logbook (July 25th):

WARNING: Agent A started task '123', but Agent B had already started it 50ms earlier.
ERROR: Duplicate entry for key 'PRIMARY' on table 'goal_progress_logs'.

What was happening? Two agents, seeing the same "pending" task in the database, tried to take it on simultaneously. Both updated it to "in_progress", and both, once finished, tried to update the progress of the same objective, causing a conflict.

The solution was to implement a form of "Pessimistic Locking" at the application level.

Task Acquisition Flow (Correct):

System Architecture

graph TD A[Free Agent] --> B{Search for 'pending' Tasks} B --> C{Find Task '123'} C --> D[Atomic Action: Try to update status to 'in_progress' CONDITIONALLY] D -- Success: Only 1 agent can win --> E[Start Task Execution] D -- Failure: Another agent was faster --> B

The Code Implementation (Simplified):

Reference code: backend/database.py

def try_claim_task(agent_id: str, task_id: str) -> bool:
    """
    Tries to claim a task atomically. Returns True if successful, False if another agent claimed it first.
    """
    try:
        # This UPDATE query only succeeds if the task is still 'pending'
        result = supabase.table('tasks').update({
            'status': 'in_progress',
            'assigned_agent_id': agent_id,
            'started_at': datetime.utcnow().isoformat()
        }).eq('id', task_id).eq('status', 'pending').execute()
        
        # If no rows were affected, another agent already claimed the task
        return len(result.data) > 0
        
    except Exception as e:
        logger.error(f"Error claiming task {task_id}: {e}")
        return False

This simple conditional update ensured that only one agent could claim a task, eliminating race conditions and duplicate work.

# The Evolution of Database Schema: From Simple to Sophisticated

As our agents became more capable, our database schema had to evolve to support increasingly complex interactions.

War Story: Schema Evolution

Phase 1: Basic Task Management
We started with simple tables: tasks, agents, workspaces. Basic CRUD operations.

Phase 2: Memory Integration
We added memory_insights, context_embeddings tables. Agents could now learn and remember.

Phase 3: Quality Gates
We introduced quality_checks, human_feedback. Every deliverable had to pass validation.

Phase 4: Advanced Orchestration
Finally: goal_progress_logs, agent_handoffs, deliverable_assets. A complete ecosystem.

Each phase required us to maintain backward compatibility while adding new capabilities. The DAL pattern proved invaluable here: changes to the database schema required updates only to our database.py file, not to every agent.

# The Lesson Learned: Treat Your Database as a Communication Protocol

The most important insight from this phase was changing our mental model. We stopped thinking of the database as a mere "storage" and started treating it as a communication protocol between agents.

Every table became a "channel":

This paradigm shift from "storage-centric" to "communication-centric" was fundamental to scaling our system. Instead of requiring complex inter-agent communication protocols, we had a simple, reliable, and auditable message-passing system.

📝 Chapter Key Takeaways:

Design for Concurrency from Day One: Multi-agent systems will have race conditions. Plan for them with atomic operations and proper locking.

Use a Data Access Layer (DAL): Never let your agents talk directly to the database. Abstract all interactions through a dedicated service layer.

Database as Communication Protocol: In a multi-agent system, your database isn't just storage – it's the nervous system enabling coordination.

Plan for Schema Evolution: Your data needs will grow more complex. Design your abstractions to handle schema changes gracefully.

Chapter Conclusion

With a robust database interaction layer, our agents finally had "hands" to manipulate their environment. They could read tasks, update progress, create new work, and share knowledge. We had built the foundation for true collaboration.

But having capable individual agents wasn't enough. We needed someone to conduct the orchestra, to ensure the right agent got the right task at the right time. This brought us to our next challenge: building the Orchestrator, the brain that would coordinate our entire AI team.

🌙 Theme
🔖 Bookmark
📚 My Bookmarks
🔤 Font Size
Bookmark saved!