Home / Articles / Building Zero-Idle Agent Loops
AI Architecture • Sep 24, 2026 • 8 min read

Building Zero-Idle Agent Loops in Go with Firestore

How to orchestrate resilient autonomous tool-calling cycles in Go on Google Cloud Run, achieving sub-second cold starts and strict zero-cost idle operations.

Building autonomous coding agents or workflow automations often tricks developers into choosing heavyweight persistent servers or expensive background worker fleets. For solo developers and lean technical teams, paying hundreds of dollars per month for idle compute is an operational anti-pattern.

In this guide, we break down the Zero-Idle Architecture: running single-binary Go services on Google Cloud Run, saving state deterministically to Google Cloud Firestore, and executing autonomous ReAct cycles with strict non-interactive execution gates.

1. The ReAct State Loop Architecture

An autonomous agent operates in a continuous loop consisting of three phases:

"An agent's reliability is bounded by the determinism of its tool interfaces, not the size of its system prompt."

2. Structuring Typed Tool Handlers in Go

Rather than allowing arbitrary shell execution, every tool in our architecture is bound to a strict Go struct with explicit validation. Here is an example of an atomic file edit gateway:

internal/agent/tool_handler.go
package agent

import (
	"context"
	"fmt"
	"os"
	"strings"
)

type EditFileRequest struct {
	FilePath string `json:"file_path"`
	OldChunk string `json:"old_chunk"`
	NewChunk string `json:"new_chunk"`
}

type ToolResult struct {
	Success bool   `json:"success"`
	Output  string `json:"output"`
	Error   string `json:"error,omitempty"`
}

// ExecuteAtomicEdit handles safe, non-destructive file replacements
func ExecuteAtomicEdit(ctx context.Context, req EditFileRequest) (*ToolResult, error) {
	content, err := os.ReadFile(req.FilePath)
	if err != nil {
		return &ToolResult{
			Success: false,
			Error:   fmt.Sprintf("target file not found: %v", err),
		}, nil
	}

	// Ensure atomic diff replacement
	updated := strings.Replace(string(content), req.OldChunk, req.NewChunk, 1)
	if err := os.WriteFile(req.FilePath, []byte(updated), 0644); err != nil {
		return nil, fmt.Errorf("failed writing file: %w", err)
	}

	return &ToolResult{
		Success: true,
		Output:  fmt.Sprintf("Successfully edited %s", req.FilePath),
	}, nil
}

3. Persisting Conversation State in Firestore

When Cloud Run scales to zero after handling an execution step, in-memory state disappears. We store the state machine's conversation transcript in Firestore using a single flat document model:

internal/store/firestore.go
package store

import (
	"context"
	"time"
	"cloud.google.com/go/firestore"
)

type AgentSession struct {
	SessionID string            `firestore:"session_id"`
	Status    string            `firestore:"status"` // RUNNING, WAITING_USER, DONE
	Steps     []SessionStep     `firestore:"steps"`
	CreatedAt time.Time         `firestore:"created_at"`
	UpdatedAt time.Time         `firestore:"updated_at"`
}

func (s *Client) SaveStep(ctx context.Context, sessionID string, step SessionStep) error {
	docRef := s.client.Collection("sessions").Doc(sessionID)
	_, err := docRef.Update(ctx, []firestore.Update{
		{
			Path:  "steps",
			Value: firestore.ArrayUnion(step),
		},
		{
			Path:  "updated_at",
			Value: firestore.ServerTimestamp,
		},
	})
	return err
}

4. Scaling to Zero on Google Cloud Run

Because Go compiles to a single static binary with microsecond initialization, cold starts on Cloud Run typically resolve in under 250 milliseconds. Setting the minimum instances to 0 ensures you pay zero cents when your agents are not actively processing commands.

This is a Preview of Class 3 & 6

Want the complete working Go repository, Dockerfiles, and step-by-step video lessons? Join our private engineer community.

Explore Full Class Curriculum →

Related Architecture Guides

Sep 20, 2026

Client-Side Inverted Index Search for Static Sites

Read Guide →
Sep 16, 2026

Deterministic Structured Outputs with Connect-RPC

Read Guide →
ESC