Skip to content

About

Loops AI Case Study

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

Loops Case Study: Anatomy of an AI Agent

<This part is not AI generated>

I want to explain my process before the AI part begins. I studied agentic behaviour here, with the important concepts they require by their nature. Since it's a distributed system with an async nature, they present unique challenges. Idempotency, at-least-once delivery, and handling failures are a few of them.

So the architecture relies on state machines, LLM output validation, and proper orchestration. This project has the fundamental concepts applied as requested by the case study.

I also used GCP Functions Framework since it's a good fit for this kind of system.

AI Usage

I used Claude Code to make the process shorter and aligned to best practices.

I debugged each step as I was simulating my standard dev process. Evolved the source code through incremental steps.

Main usages:

  • Enhance my notes to become guidelines
  • Generate boilerplate code, mocks, tests
  • Find edge cases, code review, optimization

What I Manually Verified

  • Full pipeline via curl: message in → completed with result
  • Duplicate request returns deduplicated: true, no reprocessing
  • Validation gate rejects low-confidence input with reason
  • All 4 tool types complete successfully
  • Docker build and run
  • Cloud Run deployment and live endpoint testing
  • 24 tests passing across 3 test files

</This part is not AI generated>

Event-driven AI agent backend with decoupled services, state-machine idempotency, and a validation gate between reasoning and execution.

Architecture

HTTP Request → API Service → [Pub/Sub] → Reasoning → [Validation Gate] → [Pub/Sub] → Executor
                  ↕                          ↕                                           ↕
              StateStore                 StateStore                                  StateStore

Three services, one process (for now):

  • API — Zod-validated ingress, deduplication via messageId, returns 202 Accepted
  • Reasoning — Mock LLM processing, Zod-validated output before emitting action events
  • Executor — Tool execution with state-based idempotency

Services communicate only through Pub/Sub events and share a StateStore. In production, each becomes its own Cloud Run service backed by real GCP Pub/Sub and Firestore — the consumer code stays identical.

State Machine

received → reasoning → reasoning_complete → executing → completed
                    ↘ failed              ↘ failed

Transitions are enforced by VALID_TRANSITIONS. The updateStatus(id, expectedCurrent, next) method acts as both a state guard and the idempotency mechanism — if the state already moved, the transition returns false and the duplicate is skipped.

Idempotency Strategy

Three levels, each preventing duplicate side effects:

Level Where Mechanism
API POST /messages messageId used as conversationId. Atomic createConversation returns false if already exists.
Event processing Reasoning + Executor consumers State transition guard: updateStatus(id, expected, next) only succeeds if conversation is in the expected state.
Side effects Tool execution Tool only executes if state transitions from reasoning_complete → executing. Second delivery finds state moved on.

Validation Gate

LLM output is treated as unknown and must pass through Zod validation before any action event is emitted. If validation fails, the conversation moves to failed with a reason — no action event is ever emitted.

Failure Scenarios

Scenario Behaviour
Duplicate API request Same messageId → returns existing state, no reprocessing
Duplicate Pub/Sub delivery State transition guard rejects, message acked
Reasoning throws Caught, conversation marked failed, message acked
Validation rejects LLM output Conversation marked failed with reason, no action event emitted
Tool execution throws Caught, conversation marked failed, message acked
Publish fails after state transition Conversation stays in current state — recoverable by reconciliation sweep

Design Notes

Single-pass vs. agent loop: The current system processes one reasoning → execution cycle per message. In a production agent, the conversation record would evolve into an accumulated context carrying message history and intermediate tool results, enabling the LLM to reason over multiple turns. The event-driven architecture supports this naturally — the executor would publish an observation event back to the reasoning consumer, creating a loop. The state machine would gain a reasoning_complete → reasoning transition, and the LLM would decide when to emit a final answer vs. request another tool call.

Setup

npm install

Run

# Build
npm run build

# Local (needs build first)
npm start

# Dev (builds and starts local server)
npm run dev

# Docker
docker build -t loops-ai-agent .
docker run --rm -p 8080:8080 loops-ai-agent

Deploy

gcloud run deploy loops-agent --source . --region europe-west1 --allow-unauthenticated

Test

npm test          # 24 tests across 3 test files
npm run typecheck # tsc --noEmit

API

Send a message:

curl -X POST http://localhost:8080/messages \
  -H "Content-Type: application/json" \
  -d '{"messageId": "msg-1", "content": "show me product details"}'
# → 202 { conversationId: "msg-1" }

Check status:

curl http://localhost:8080/conversations/msg-1

About

Loops AI Case Study

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages