How to Design a Fully Local Multi-Agent Orchestration System Using TinyLlama for Intelligent Task Decomposition and Autonomous Collaboration

by
0 comments
How to Design a Fully Local Multi-Agent Orchestration System Using TinyLama for Intelligent Task Decomposition and Autonomous Collaboration

Multi-agent AI systems usually mean cloud APIs, per-token bills and opaque orchestration layers. This tutorial takes the opposite route: a team of specialized AI agents coordinated by a manager agent, running entirely on local hardware with TinyLlama, a compact 1.1-billion-parameter open model. Everything runs through the Hugging Face Transformers library — no external APIs — producing an offline, lightweight and fully inspectable multi-agent system covering structured task decomposition, inter-agent collaboration and autonomous logic loops.

How the system fits together

In plain terms, the architecture works like a small project team. A manager agent receives a high-level goal, breaks it into subtasks, and hands each subtask to a specialist agent. The specialists do their work using the local model, the manager respects dependencies between tasks (task C waits for A and B), and a final synthesis step merges all outputs into one coherent answer. Because every component is a few dozen lines of Python, the whole pipeline can be inspected, customized and extended.

Step 1: Imports and core data structures

!pip install transformers torch accelerate bitsandbytes -q


import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
import json
import re
from typing import List, Dict, Any
from dataclasses import dataclass, asdict
from datetime import datetime


@dataclass
class Task:
   id: str
   description: str
   assigned_to: str = None
   status: str = "pending"
   result: Any = None
   dependencies: List(str) = None
  
   def __post_init__(self):
       if self.dependencies is None:
           self.dependencies = ()


@dataclass
class Agent:
   name: str
   role: str
   expertise: str
   system_prompt: str

The first block sets up the key imports and defines the basic data structures for managing tasks and agents. Modeling tasks and agents as structured units gives every later component a consistent, reliable foundation to build on.

Step 2: Registering agents and the local LLM wrapper

AGENT_REGISTRY = {
   "researcher": Agent(
       name="researcher",
       role="Research Specialist",
       expertise="Information gathering, analysis, and synthesis",
       system_prompt="You are a research specialist. Provide thorough research on topics."
   ),
   "coder": Agent(
       name="coder",
       role="Software Engineer",
       expertise="Writing clean, efficient code with best practices",
       system_prompt="You are an expert programmer. Write clean, well-documented code."
   ),
   "writer": Agent(
       name="writer",
       role="Content Writer",
       expertise="Clear communication and documentation",
       system_prompt="You are a professional writer. Create clear, engaging content."
   ),
   "analyst": Agent(
       name="analyst",
       role="Data Analyst",
       expertise="Data interpretation and insights",
       system_prompt="You are a data analyst. Provide clear insights from data."
   )
}


class LocalLLM:
   def __init__(self, model_name: str = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"):
       self.tokenizer = AutoTokenizer.from_pretrained(model_name)
       quantization_config = BitsAndBytesConfig(
           load_in_4bit=True,
           bnb_4bit_compute_dtype=torch.float16
       ) if torch.cuda.is_available() else None
       self.model = AutoModelForCausalLM.from_pretrained(
           model_name,
           quantization_config=quantization_config,
           device_map="auto",
           low_cpu_mem_usage=True
       )
       if self.tokenizer.pad_token is None:
           self.tokenizer.pad_token = self.tokenizer.eos_token
          
   def generate(self, prompt: str, max_tokens: int = 300) -> str:
       formatted_prompt = f"<|system|>nYou are a helpful AI assistant.n<|user|>n{prompt}n<|assistant|>n"
       inputs = self.tokenizer(
           formatted_prompt,
           return_tensors="pt",
           truncation=True,
           max_length=1024,
           padding=True
       )
       inputs = {k: v.to(self.model.device) for k, v in inputs.items()}
       with torch.no_grad():
           outputs = self.model.generate(
               **inputs,
               max_new_tokens=max_tokens,
               temperature=0.7,
               do_sample=True,
               top_p=0.9,
               pad_token_id=self.tokenizer.pad_token_id,
               eos_token_id=self.tokenizer.eos_token_id,
               use_cache=True
           )
       full_response = self.tokenizer.decode(outputs(0), skip_special_tokens=True)
       if "<|assistant|>" in full_response:
           return full_response.split("<|assistant|>")(-1).strip()
       return full_response(len(formatted_prompt):).strip()

This section registers the specialist agents and implements the local LLM wrapper that powers the system. TinyLlama is loaded in efficient 4-bit mode, so the whole stack runs comfortably in Google Colab or on modest local hardware — a flexible, completely local way to generate responses for each agent.

Step 3: The manager agent and task decomposition

class ManagerAgent:
   def __init__(self, model_name: str = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"):
       self.llm = LocalLLM(model_name)
       self.agents = AGENT_REGISTRY
       self.tasks: Dict(str, Task) = {}
       self.execution_log = ()
      
   def log(self, message: str):
       timestamp = datetime.now().strftime("%H:%M:%S")
       log_entry = f"({timestamp}) {message}"
       self.execution_log.append(log_entry)
       print(log_entry)
  
   def decompose_goal(self, goal: str) -> List(Task):
       self.log(f"🎯 Decomposing goal: {goal}")
       agent_info = "n".join((f"- {name}: {agent.expertise}" for name, agent in self.agents.items()))
       prompt = f"""Break down this goal into 3 specific subtasks. Assign each to the best agent.


Goal: {goal}


Available agents:
{agent_info}


Respond ONLY with a JSON array."""
       response = self.llm.generate(prompt, max_tokens=250)
       try:
           json_match = re.search(r'(s*{.*?}s*)', response, re.DOTALL)
           if json_match:
               tasks_data = json.loads(json_match.group())
           else:
               raise ValueError("No JSON found")
       except:
           tasks_data = self._create_default_tasks(goal)
      
       tasks = ()
       for i, task_data in enumerate(tasks_data(:3)):
           task = Task(
               id=task_data.get('id', f'task_{i+1}'),
               description=task_data.get('description', f'Work on: {goal}'),
               assigned_to=task_data.get('assigned_to', list(self.agents.keys())(i % len(self.agents))),
               dependencies=task_data.get('dependencies', () if i == 0 else (f'task_{i}'))
           )
           self.tasks(task.id) = task
           tasks.append(task)
           self.log(f"  ✓ {task.id}: {task.description(:50)}... → {task.assigned_to}")
      
       return tasks

The ManagerAgent class is where the intelligence lives: it decomposes a high-level goal into well-defined subtasks, generating structured JSON-based tasks and automatically assigning each to the right agent. The system organizes work step by step, much like a human project manager would.

Step 4: Execution flow and fallback logic

 def _create_default_tasks(self, goal: str) -> List(Dict):
       if any(word in goal.lower() for word in ('code', 'program', 'implement', 'algorithm')):
           return (
               {"id": "task_1", "description": f"Research and explain the concept: {goal}", "assigned_to": "researcher", "dependencies": ()},
               {"id": "task_2", "description": f"Write code implementation for: {goal}", "assigned_to": "coder", "dependencies": ("task_1")},
               {"id": "task_3", "description": f"Create documentation and examples", "assigned_to": "writer", "dependencies": ("task_2")}
           )
       return (
           {"id": "task_1", "description": f"Research: {goal}", "assigned_to": "researcher", "dependencies": ()},
           {"id": "task_2", "description": f"Analyze findings and structure content", "assigned_to": "analyst", "dependencies": ("task_1")},
           {"id": "task_3", "description": f"Write comprehensive response", "assigned_to": "writer", "dependencies": ("task_2")}
       )
  
   def execute_task(self, task: Task, context: Dict(str, Any) = None) -> str:
       self.log(f"🤖 Executing {task.id} with {task.assigned_to}")
       task.status = "in_progress"
       agent = self.agents(task.assigned_to)
       context_str = ""
       if context and task.dependencies:
           context_str = "nnContext from previous tasks:n"
           for dep_id in task.dependencies:
               if dep_id in context:
                   context_str += f"- {context(dep_id)(:150)}...n"
      
       prompt = f"""{agent.system_prompt}


Task: {task.description}{context_str}


Provide a clear, concise response:"""
       result = self.llm.generate(prompt, max_tokens=250)
       task.result = result
       task.status = "completed"
       self.log(f"  ✓ Completed {task.id}")
       return result

This block defines the fallback task logic and the complete execution flow for each task. Each agent is guided by its own system prompt and supplied with relevant context, which keeps results consistent while tasks execute in dependency order.

Step 5: Synthesis and the orchestration loop

def synthesize_results(self, goal: str, results: Dict(str, str)) -> str:
       self.log("🔄 Synthesizing final results")
       results_text = "nn".join((f"Task {tid}:n{res(:200)}" for tid, res in results.items()))
       prompt = f"""Combine these task results into one final coherent answer.


Original Goal: {goal}


Task Results:
{results_text}


Final comprehensive answer:"""
       return self.llm.generate(prompt, max_tokens=350)
  
   def execute_goal(self, goal: str) -> Dict(str, Any):
       self.log(f"n{'='*60}n🎬 Starting Manager Agentn{'='*60}")
       tasks = self.decompose_goal(goal)
       results = {}
       completed = set()
       max_iterations = len(tasks) * 2
       iteration = 0
      
       while len(completed) < len(tasks) and iteration < max_iterations:
           iteration += 1
           for task in tasks:
               if task.id in completed:
                   continue
               deps_met = all(dep in completed for dep in task.dependencies)
               if deps_met:
                   result = self.execute_task(task, results)
                   results(task.id) = result
                   completed.add(task.id)
      
       final_output = self.synthesize_results(goal, results)
       self.log(f"n{'='*60}n✅ Execution Complete!n{'='*60}n")
      
       return {
           "goal": goal,
           "tasks": (asdict(task) for task in tasks),
           "final_output": final_output,
           "execution_log": self.execution_log
       }

Outputs from all subtasks are synthesized into an integrated final answer, and an orchestration loop ensures each task runs only after its dependencies are satisfied — bringing everything together into a seamless multi-step pipeline.

Step 6: Demonstration tasks

def demo_basic():
   manager = ManagerAgent()
   goal = "Explain binary search algorithm with a simple example"
   result = manager.execute_goal(goal)
   print("n" + "="*60)
   print("FINAL OUTPUT")
   print("="*60)
   print(result("final_output"))
   return result


def demo_coding():
   manager = ManagerAgent()
   goal = "Implement a function to find the maximum element in a list"
   result = manager.execute_goal(goal)
   print("n" + "="*60)
   print("FINAL OUTPUT")
   print("="*60)
   print(result("final_output"))
   return result


def demo_custom(custom_goal: str):
   manager = ManagerAgent()
   result = manager.execute_goal(custom_goal)
   print("n" + "="*60)
   print("FINAL OUTPUT")
   print("="*60)
   print(result("final_output"))
   return result


if __name__ == "__main__":
   print("🤖 Manager Agent Tutorial - APIless Local Version")
   print("="*60)
   print("Using TinyLlama (1.1B) - Fast & efficient!n")
   result = demo_basic()
   print("nn💡 Try more:")
   print("  - demo_coding()")
   print("  - demo_custom('your goal here')")

Demonstration tasks make it easy to test the system with different goals and watch the manager decompose, execute and synthesize work in real time — an interactive way to understand the workflow before refining it.

Limitations and what to watch

A 1.1B-parameter model is the main constraint: TinyLlama handles structured, well-prompted subtasks but will not match larger models on open-ended reasoning, and JSON output from small models can be brittle — hence the fallback logic above. The design is educational rather than production-grade: there is no persistence, retry budget or safety layer, topics covered in why AI agent projects stall without governance. For heavier local workloads, the same pattern transfers to larger open models, and compression-native approaches like Apple’s CLaRa hint at where efficient local retrieval is heading.

The takeaway

A complete multi-agent orchestration system — manager, specialists, dependency resolution and synthesis — can run locally with minimal dependencies. Built from scratch, the pattern is modular and predictable, and it demystifies what commercial agent frameworks do behind their APIs.

Related Articles