Orchestrating Offline Multi-Agent Workflows: A Hands-On Guide to LangGraph and Goose

Air-gapping your engineering workflow usually means forfeiting modern AI tooling. Enterprise security policies strictly prohibit routing proprietary codebases through external cloud endpoints, leaving systems administrators and developers stuck between compliance mandates and manual productivity bottlenecks. While single-agent scripts offer quick automation hacks, complex engineering tasks—like full-stack code refactoring, automated security patching, and multi-file architecture audits—demand multiple specialized agents working in tandem.

Stitching local models into multi-agent systems often triggers execution loops, state corruption, and context window blowouts. By pairing LangGraph as a cyclic state machine with Goose—Block’s open-source autonomous developer agent—backed by a local inference daemon like Ollama, you build a deterministic, fully air-gapped multi-agent workforce on consumer-grade hardware.

Architectural Anatomy: Graph Stateful Control Meets Autonomous Execution

Running multiple autonomous agents on an offline workstation introduces a hard operational truth: local open-weights models (such as Qwen 2.5 14B or Llama 3.1 8B) lack the massive context capacity and forgiving instruction-following heuristics of cloud-hosted 70B+ models. If you grant an unconstrained local model free reign over a multi-step project, it rapidly drifts off course or gets trapped in tool-calling failure loops.

To keep offline agents reliable, you must separate workflow orchestration from task execution.

+-----------------------------------------------------------------------+
|                          OFFLINE WORKSTATION                          |
|                                                                       |
|  +-----------------------------------------------------------------+  |
|  |                  LangGraph State Machine                        |  |
|  |  (Manages state, graph edges, checkpointers & human oversight)   |  |
|  +-----------------------------------------------------------------+  |
|               |                                       ^               |
|       Sub-Task Assignment                     Execution Results       |
|               v                                       |               |
|  +---------------------------+       +-----------------------------+  |
|  |   Architect Agent Node    |       |   Goose Developer Worker    |  |
|  |  (Analyzes repo / schema) | ----> | (Edits code / runs builds)  |  |
|  +---------------------------+       +-----------------------------+  |
|               |                                       |               |
|               +-------------------+-------------------+               |
|                                   v                                   |
|                      +--------------------------+                     |
|                      | Local Ollama LLM Daemon  |                     |
|                      +--------------------------+                     |
+-----------------------------------------------------------------------+

LangGraph: The Deterministic Orchestrator

LangGraph models multi-agent interactions as directed, cyclic state graphs. Instead of relying on an LLM manager to guess what step comes next, LangGraph enforces explicit state transitions, conditional branching, and memory persistence in code.

  • State Centralization: System state is defined as a typed dictionary passed between nodes, allowing you to explicitly prune message histories before invoking local models.
  • Cyclic Flow Control: Graph edges define loop conditions, enabling agents to attempt code fixes, evaluate test outputs, and retry failures deterministically.
  • Checkpointing: Native support for local SQLite checkpointers lets you pause execution, inspect modified state, and approve agent actions before they write to disk.

Goose: The On-Metal Execution Worker

Goose operates as a specialized, tool-equipped developer worker inside your nodes. Designed specifically for software engineering automation, Goose manages file reading, shell command execution, and local file patching through structured tool calls.

Delegating hands-on file modifications to Goose while keeping high-level planning inside LangGraph prevents local models from burning context tokens on low-level environment management.

Setting Up Your Offline Stack

To execute this architecture locally, configure an offline LLM inference daemon, install the framework dependencies, and initialize Goose.

Prerequisites & Local Daemon Configuration

  1. Install Ollama and pull a tool-calling optimized model:
    ollama pull qwen2.5:14b
  2. Optimize Ollama environment variables for parallel request handling and memory persistence:
    export OLLAMA_NUM_PARALLEL=4
    export OLLAMA_KEEP_ALIVE="24h"
  3. Install the required Python packages:
    pip install langgraph langchain-ollama pydantic goose-ai

Step-by-Step Guide: Building a Local Audit and Remediation Graph

This practical implementation creates a two-node local graph: an Architect Node that analyzes system logs or code snippets for security vulnerabilities, and a Goose Execution Node that writes and validates the necessary patches offline.

Step 1: Define the Shared Graph State

Create a script named local_orchestrator.py. Start by defining the explicit shape of your system state.

import sys
import logging
from typing import Annotated, TypedDict, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_ollama import ChatOllama

# Configure logging to monitor offline execution state
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")

# Define system state
class AgentWorkflowState(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    target_file: str
    audit_findings: str
    patch_status: str
    iteration_count: int

# Initialize local LLM backend
local_llm = ChatOllama(
    model="qwen2.5:14b",
    base_url="http://localhost:11434",
    temperature=0.0
)

Step 2: Build the Architect and Goose Worker Nodes

The Architect Node inspects input files and writes diagnostic reports. The Goose Worker Node interprets those findings and executes code modifications.

def architect_node(state: AgentWorkflowState) -> dict:
    """Analyzes the targeted file for security defects or architectural issues."""
    logging.info("Executing Architect Node...")
    
    file_path = state["target_file"]
    prompt = f"""You are a Senior Systems Architect. Analyze the requirements for target file: {file_path}.
Identify security vulnerabilities, unhandled exceptions, or performance bugs.
Provide a concise, numbered list of required code fixes."""

    response = local_llm.invoke([HumanMessage(content=prompt)])
    
    return {
        "audit_findings": response.content,
        "messages": [AIMessage(content=f"Architect Analysis Complete:\n{response.content}")],
        "iteration_count": state.get("iteration_count", 0) + 1
    }

def goose_worker_node(state: AgentWorkflowState) -> dict:
    """Simulates Goose executing code changes and running local validation tests."""
    logging.info("Executing Goose Developer Worker Node...")
    
    findings = state["audit_findings"]
    target_file = state["target_file"]
    
    # Instruct Goose worker to fix issues
    remediation_prompt = f"""You are Goose, an autonomous developer agent.
Apply the following architectural fixes to '{target_file}':
{findings}

Output a status report indicating whether all fixes were successfully applied."""

    response = local_llm.invoke([HumanMessage(content=remediation_prompt)])
    
    # Parse mock execution success criteria
    status = "SUCCESS" if "successfully applied" in response.content.lower() else "FAILED"
    
    return {
        "patch_status": status,
        "messages": [AIMessage(content=f"Goose Execution Status: {status}\n{response.content}")]
    }

Step 3: Wire the Graph and Define Conditional Logic

Connect your nodes using stateful edges and add a safety check to prevent infinite loops when local models fail to resolve errors.

def evaluate_next_step(state: AgentWorkflowState) -> str:
    """Routes execution based on patch outcome and strict iteration limits."""
    if state["patch_status"] == "SUCCESS":
        logging.info("Patch verified. Terminating workflow.")
        return END
    
    if state["iteration_count"] >= 3:
        logging.warning("Maximum retry threshold reached. Aborting to prevent context loop.")
        return END
        
    logging.info("Patch unverified. Re-routing to Architect for re-evaluation.")
    return "architect"

# Assemble the State Graph
builder = StateGraph(AgentWorkflowState)

# Add Nodes
builder.add_node("architect", architect_node)
builder.add_node("goose_worker", goose_worker_node)

# Add Edges
builder.add_edge(START, "architect")
builder.add_edge("architect", "goose_worker")
builder.add_conditional_edges("goose_worker", evaluate_next_step)

# Compile Graph
graph = builder.compile()

Step 4: Run the Air-Gapped Workflow

Trigger your compiled pipeline with initial state inputs.

if __name__ == "__main__":
    initial_input = {
        "messages": [HumanMessage(content="Start local code review pipeline.")],
        "target_file": "/var/www/internal_api/auth.py",
        "audit_findings": "",
        "patch_status": "PENDING",
        "iteration_count": 0
    }

    logging.info("Starting local multi-agent execution pipeline...")
    final_state = graph.invoke(initial_input)
    
    print("\n=== FINAL PIPELINE OUTPUT ===")
    print(final_state["messages"][-1].content)

Troubleshooting & Pro-Tips for Air-Gapped Deployments

Running multi-agent systems without cloud infrastructure exposes hardware constraints quickly. Apply these operational adjustments to stabilize your local node pipeline.

1. Fix Context Window Slowdowns with Active Message Trimming

As agents loop between analysis and execution, message arrays expand rapidly. Local LLMs slow from 40 tokens per second down to single digits when forced to re-process bloated context buffers.

  • The Fix: Inject message trimming directly inside your node definitions before passing state to ChatOllama:
    from langchain_core.messages import trim_messages
    
    trimmed_history = trim_messages(
        state["messages"],
        max_tokens=2048,
        strategy="last",
        token_counter=local_llm
    )

2. Prevent Tool-Calling Failures on 8B and 14B Models

Smaller open-weight models frequently fail to parse multi-nested JSON schemas, throwing raw parsing errors when attempting complex tool interactions.

  • The Fix: Flatten tool parameters. Replace deeply nested objects with flat string arguments, and explicitly specify temperature=0.0 on your model initializer to maximize deterministic output formatting.

3. Prevent VRAM OOM Swapping During Concurrent Runs

If LangGraph attempts to invoke multiple nodes concurrently while Ollama is set to process parallel streams, GPU VRAM can spill over into slow system RAM (swap memory).

  • The Fix: Bound model memory allocations by setting num_gpu explicitly or restricting Ollama context sizes in a Modelfile:
    FROM qwen2.5:14b
    PARAMETER num_ctx 4096

Taking Your Local Multi-Agent Stack to Production

By decoupling high-level workflow orchestration in LangGraph from low-level execution in Goose, you create a resilient, privacy-compliant multi-agent stack on local hardware. System state remains transparent, execution paths stay deterministic, and sensitive operational code never leaves your local workstation.

To expand this local setup:

  1. Attach local SQLite checkpointers (SqliteSaver) to your LangGraph compilation step to enable manual pause-and-resume capabilities for human code reviews.
  2. Mount isolated Docker containers around your Goose execution worker to safely test code patches before committing changes to host disk repositories.
  3. Register custom local MCP servers to expose internal monitoring tools and database metrics directly to your offline agents without opening external network ports.

Leave a Reply

Your email address will not be published. Required fields are marked *