From Single Agents to Orchestrated Intelligence
Imagine you have an AI agent that’s brilliant at writing code, but it struggles with debugging, or another agent that’s fantastic at summarizing documents but can’t generate new content. In the real world, complex problems rarely fit neatly into a single, isolated task. This is where agent composition comes in – the art of combining multiple specialized AI agents to tackle larger, more intricate challenges.
In this chapter, we’ll dive deep into how AIPack empowers you to break down complex problems into manageable, modular AI agents. We’ll learn how to build reusable skills, orchestrate them into sophisticated workflows, and pass context seamlessly between them. By the end, you’ll understand how to design AI solutions that are not only powerful but also maintainable and scalable, mirroring best practices from traditional software engineering.
To get the most out of this chapter, you should be comfortable with creating basic AIPack agents and defining multi-stage markdown agents, as covered in previous chapters.
Core Concepts: Why and How to Compose Agents
Think about how a complex software application is built. It’s not one giant block of code; it’s a collection of smaller, focused modules, functions, and microservices, each handling a specific part of the system. Agent composition applies this same principle to AI agents.
What is Agent Composition?
Agent composition is the strategy of designing and combining multiple, often specialized, AI agents to achieve a larger goal. Instead of trying to build a single “super agent” that does everything, you create a network of agents, where each agent contributes its unique capability to a collaborative workflow.
What Problem Does It Solve?
- Complexity Management: Large problems become easier to manage when broken into smaller, digestible pieces. Each sub-agent can focus on a single responsibility.
- Overcoming Token Limits: Large language models have finite context windows. By chaining agents, you can process information in stages, summarizing or filtering context as it passes from one agent to the next, preventing early token exhaustion.
- Specialization and Expertise: Different agents can be fine-tuned or designed with specific model configurations, prompts, and tools for distinct tasks (e.g., one for code generation, another for security review, a third for documentation).
- Reusability: Once an agent is built to perform a specific, well-defined task, it can be reused across many different composite workflows, saving development time and ensuring consistency.
- Maintainability: Changes to one part of a complex workflow can often be isolated to a single sub-agent, reducing the risk of unintended side effects across the entire system.
📌 Key Idea: Think of agents as modular functions or microservices. Each agent has a clear input, performs a specific operation, and produces a defined output, which can then be fed as input to another agent.
A Simple Composition Flow
Let’s visualize a basic flow where a user request goes through multiple specialized agents. This diagram illustrates how a complex task is broken down into sequential steps, each handled by a dedicated agent.
In this example, Agent A might parse natural language to understand the user’s intent. Then, Agent B could query a database or external API to gather relevant data. Finally, Agent C might perform calculations, generate a summary, or format the output. Each step is handled by a focused agent, contributing to the overall solution.
Designing Reusable Skills with AIPack
A “reusable skill” in AIPack is essentially a self-contained agent (an .aip file) designed to perform a common, atomic task that can be invoked by other agents.
What Makes a Skill “Reusable”?
- Clear Input/Output: It should have well-defined expectations for what kind of input it receives and what format its output will be in.
- Single Responsibility: It focuses on doing one thing well, rather than trying to solve a broad problem.
- Parameterization: It can accept parameters to customize its behavior without requiring code changes.
- Self-Contained: It includes all necessary prompts, tools, and Lua logic within its
.aipfile.
Example: A “Code Linter” Skill
Imagine you want an agent that can lint code snippets according to specific style guides or best practices. This is a perfect candidate for a reusable skill. Any other agent that needs to check code quality can simply invoke this linter agent, passing it the code to analyze. This avoids duplicating linting logic across multiple agents.
Step-by-Step Implementation: Orchestrating Agents with Lua
AIPack provides powerful mechanisms, primarily through its multi-stage markdown agents and integrated Lua logic, to compose agents. Let’s walk through how to build a composite agent step by step. We’ll create a code_reviewer.aip agent that orchestrates a summarize_code.aip agent and a suggest_improvements.aip agent.
Step 1: Create Individual Reusable Agents
First, we’ll create the two specialized agents that our orchestrator will call. These should be placed in a subdirectory, for example, agents/.
1.1. Create summarize_code.aip
This agent will take a code block and return a concise summary of its purpose and functionality.
Create a file agents/summarize_code.aip:
# Summarize Code Agent
This agent summarizes a given code block.
## Stage 1: Summarize
```lua
function run(input)
-- The 'input' table will contain the code to summarize.
-- We expect the input to be a table with a 'code_block' key.
local code_to_summarize = input.code_block or ""
if code_to_summarize == "" then
print("Error: No code block provided for summarization.")
-- Return an error message as part of the output
return { error = "No code block provided for summarization." }
end
-- Construct the prompt for the Large Language Model (LLM)
local prompt = "Summarize the following code block, focusing on its purpose and key functionalities:\n\n```\n" .. code_to_summarize .. "\n```"
-- Call the LLM using the AIPack's configured provider.
-- The 'aipack.llm.generate' function is used to interact with LLMs.
-- As of 2026-05-17, ensure your AIPack environment has a provider like Ollama configured.
local llm_response = aipack.llm.generate({
prompt = prompt,
model = "ollama/llama3", -- Example model, adjust to your configured provider (e.g., 'openai/gpt-4o')
temperature = 0.5 -- A lower temperature makes the output more focused
})
-- Extract the generated summary from the LLM's response
local summary = llm_response.text or "Could not generate summary."
-- Return the summary in a structured table
return { summary = summary }
end
**Explanation:**
* The `run(input)` function is the entry point for an AIPack agent.
* It expects an `input` table containing `code_block`.
* A prompt is constructed to instruct the LLM to summarize the code.
* `aipack.llm.generate` is used to send the prompt to a configured LLM provider (e.g., Ollama's Llama 3).
* The LLM's text response is then extracted and returned in a table.
#### 1.2. Create `suggest_improvements.aip`
This agent will take code and an optional summary, then suggest improvements for it.
Create a file `agents/suggest_improvements.aip`:
```markdown
# Suggest Improvements Agent
This agent suggests improvements for a code block, optionally considering a summary.
## Stage 1: Improve Code
```lua
function run(input)
-- Expects 'code_block' and optionally 'code_summary' from the input table.
local code_block = input.code_block or ""
local code_summary = input.code_summary or ""
if code_block == "" then
print("Error: No code block provided for improvements.")
return { error = "No code block provided for improvements." }
end
-- Build the prompt dynamically based on whether a summary is provided.
local prompt_parts = {
"Review the following code block and suggest specific improvements for readability, performance, security, and best practices.",
"Focus on actionable advice.",
"\n\nCode:\n```\n" .. code_block .. "\n```"
}
if code_summary ~= "" then
-- If a summary exists, add it to the prompt for better context.
table.insert(prompt_parts, "\n\nContextual Summary:\n" .. code_summary)
end
local prompt = table.concat(prompt_parts, "\n")
-- Call the LLM to get improvement suggestions.
local llm_response = aipack.llm.generate({
prompt = prompt,
model = "ollama/llama3", -- Example model
temperature = 0.7 -- Higher temperature for more creative/diverse suggestions
})
local improvements = llm_response.text or "No improvements suggested."
return { improvements = improvements }
end
**Explanation:**
* This agent also expects `code_block` and can optionally use `code_summary`.
* The prompt is crafted to ask for specific, actionable improvements.
* `aipack.llm.generate` is called to get the LLM's suggestions.
* The `improvements` text is returned.
### Step 2: Create the Orchestrator Agent (`code_reviewer.aip`)
Now, we'll create the main agent that orchestrates the execution of the `summarize_code.aip` and `suggest_improvements.aip` agents.
Create a file `code_reviewer.aip`:
```markdown
# Code Reviewer Agent
This agent orchestrates code summarization and improvement suggestions.
## Stage 1: Summarize Code
```lua
function run(input)
-- Input to this orchestrator agent is expected to be a 'code' string.
local code_to_review = input.code or ""
if code_to_review == "" then
print("Error: No code provided for review.")
return { error = "No code provided for review." }
end
print("Running summarization agent...")
-- Call the 'summarize_code.aip' agent using aipack.run()
-- The first argument is the path to the sub-agent, the second is the input table.
local summary_result = aipack.run("agents/summarize_code.aip", { code_block = code_to_review })
if summary_result.error then
print("Error during summarization:", summary_result.error)
return { error = "Summarization failed: " .. summary_result.error }
end
print("Code Summary:\n", summary_result.summary)
-- Return both the original code and the generated summary for the next stage.
return { code_block = code_to_review, code_summary = summary_result.summary }
end
Stage 2: Suggest Improvements
function run(input)
-- 'input' here is the output from Stage 1: { code_block = ..., code_summary = ... }
local code_block = input.code_block
local code_summary = input.code_summary
if not code_block then
print("Error: No code block from previous stage.")
return { error = "No code block to improve." }
end
print("\nRunning improvement suggestion agent...")
-- Call the 'suggest_improvements.aip' agent, passing the relevant context.
local improvement_result = aipack.run("agents/suggest_improvements.aip", {
code_block = code_block,
code_summary = code_summary
})
if improvement_result.error then
print("Error during improvement suggestion:", improvement_result.error)
return { error = "Improvement suggestion failed: " .. improvement_result.error }
end
print("Suggested Improvements:\n", improvement_result.improvements)
-- Return a comprehensive review including summary and improvements.
return {
code_summary = code_summary,
improvements = improvement_result.improvements,
full_review = "Summary:\n" .. code_summary .. "\n\nImprovements:\n" .. improvement_result.improvements
}
end
**Explanation of the Orchestration:**
* **`aipack.run("path/to/agent.aip", {key = value})`**: This is the core AIPack function for invoking sub-agents. It takes the relative path to the `.aip` file and a Lua table as arguments. This table becomes the `input` to the sub-agent's `run` function.
* **Context Passing:** The output of `Stage 1` (a table containing `summary` and the original `code_block`) is explicitly returned. This output then automatically becomes the `input` for `Stage 2`. This demonstrates how data flows seamlessly between stages and, by extension, between composed agents.
* **Error Handling:** Basic `if` checks are included to ensure inputs are present and to propagate errors if a sub-agent fails. This is crucial for robust composite agents.
### Orchestration with Lua Logic
Lua is not just for calling sub-agents; it's also your primary tool for complex control flow within a single agent or across multiple stages. You can use Lua to:
* **Conditional Logic:** Decide which sub-agent to call based on previous outputs or external conditions.
* **Looping:** Iterate over a list of items, applying an agent to each item (e.g., linting multiple files).
* **Data Transformation:** Prepare outputs from one agent to be suitable inputs for another, mapping keys or reformatting data structures.
* **Tool Usage:** Integrate external tools, APIs, or databases as part of the orchestration process, allowing agents to interact with the broader system.
⚡ **Real-world insight:** For highly dynamic workflows, where the path an agent takes depends heavily on the LLM's output or external data, Lua becomes indispensable for building intelligent decision trees and adaptive execution paths. This allows your agents to respond flexibly to varied inputs and conditions.
### Context Management for Composed Agents
Managing context is crucial in composed agent workflows to prevent token limits and ensure relevance.
* **Selective Passing:** Don't pass the entire conversation history or all raw data to every sub-agent. Only pass the information that is strictly necessary for that agent's immediate task.
* **Summarization and Filtering:** If a previous agent generated a large output, consider having an intermediate "summarizer" agent (like our `summarize_code.aip`) that condenses the information before passing it to the next stage.
* **Structured Output:** Encourage agents to produce structured output (e.g., JSON or well-defined Lua tables) which is easier for subsequent agents to parse and selectively extract information from.
🧠 **Important:** Poor context management is a leading cause of expensive, slow, and hallucinating AI agents. Design your information flow carefully, always asking: "Does the next agent *really* need all this information?"
## Mini-Challenge: Building a Multi-Stage Code Review Agent
Let's extend our `code_reviewer.aip` to include a final step: suggesting a commit message based on the identified improvements. This will solidify your understanding of agent composition.
**Your Challenge:**
1. **Create a `generate_commit_message.aip` agent:**
* This agent should be placed in the `agents/` directory.
* It should take `code_summary` and `improvements` as input from the previous stage.
* Its goal is to generate a concise, conventional commit message (e.g., adhering to Conventional Commits specification) that reflects the changes and improvements suggested.
2. **Integrate it into `code_reviewer.aip`:**
* Add a `## Stage 3: Generate Commit Message` to your `code_reviewer.aip` file.
* This new stage should call your `generate_commit_message.aip` agent using `aipack.run()`, passing the relevant context (`code_summary` and `improvements`) from `Stage 2`.
* The final output of `code_reviewer.aip` should include the generated commit message, alongside the summary and improvements.
**Hint:**
* Remember to use `aipack.run()` to invoke your new agent.
* Ensure the `input` table for `generate_commit_message.aip` correctly receives `code_summary` and `improvements`.
* The prompt for the commit message agent should guide the LLM to produce a standard format (e.g., "feat: Add new feature" or "fix: Resolve bug"), perhaps including a scope.
**What to Observe/Learn:**
* How easily you can extend an existing composite agent by adding new stages and integrating new specialized agents.
* The importance of understanding the input/output contracts of each sub-agent to ensure seamless data flow.
* How context truly flows through the entire pipeline to produce a comprehensive, multi-faceted result.
## Common Pitfalls and Troubleshooting
When composing agents, you might encounter some specific challenges. Knowing these common pitfalls can save you significant debugging time.
* **Context Overload:**
* **Problem:** Passing too much raw information between agents, leading to token limit errors, expensive calls, or irrelevant responses from the LLM.
* **Solution:** Implement intermediate summarization stages. Filter inputs to sub-agents, providing only the most critical information. Consider a "context manager" agent specifically designed to prune or condense context.
* **Circular Dependencies:**
* **Problem:** Agent A calls Agent B, which then calls Agent A, leading to an infinite loop or unexpected behavior.
* **Solution:** Carefully design your workflow to ensure a clear, directed flow. Use Lua logic to track execution paths or set limits on recursion depth if dynamic looping is intended.
* **Lack of Clear Interfaces:**
* **Problem:** A sub-agent expects `input.data` but the orchestrator provides `input.payload`, causing runtime errors because the keys don't match.
* **Solution:** Define clear input/output schemas for each reusable agent. Document these schemas explicitly. Use Lua to transform data formats if necessary, ensuring compatibility between agent calls.
* **Debugging Orchestrated Workflows:**
* **Problem:** Tracing errors or unexpected behavior across multiple agents and stages can be complex, as the execution jumps between different `.aip` files.
* **Solution:** Use `print()` statements generously within your Lua scripts to log inputs, outputs, and intermediate states. AIPack's verbose logging (`aipack run --verbose`) can help trace execution. Isolate and test individual sub-agents thoroughly before composing them into a larger workflow.
⚠️ **What can go wrong:** An error in one small sub-agent, such as an incorrect prompt or a malformed output, can cascade and derail the entire composite workflow. Robust error handling and clear logging are your best friends in such scenarios.
## Best Practices for Modular Agent Design
To build robust and scalable composite agents, consider these best practices from traditional software engineering and apply them to your AIPack workflows:
* **Single Responsibility Principle (SRP):** Each agent should have one, and only one, reason to change. This makes agents easier to understand, test, and reuse. For instance, an agent should either summarize *or* suggest improvements, not both.
* **Clear Input/Output Contracts:** Explicitly define what each agent expects as input and what it guarantees as output. Document these contracts (e.g., in comments within the `.aip` file) to facilitate easier composition.
* **Statelessness (where possible):** Design agents to be stateless, meaning their output depends only on their current input, not on previous invocations or internal state. This improves reusability, predictability, and simplifies debugging.
* **Robust Error Handling:** Include `if-else` checks and `print` statements in your Lua logic to gracefully handle missing inputs, unexpected outputs, or LLM failures. Consider retries or fallback mechanisms for critical steps.
* **Versioning Reusable Packs:** As your reusable agents evolve, consider versioning them (e.g., `summarizer_v1.aip`, `summarizer_v2.aip`) to manage compatibility in larger workflows, especially in production environments.
* **Testing:** Test each individual agent in isolation to ensure it performs its specific task correctly. Then, test the composed workflow end-to-end to ensure seamless integration and correct data flow between stages.
🔥 **Optimization / Pro tip:** Treat your `.aip` files like functions in a library. Give them descriptive names, clear parameters, and ensure they do one job well. This mindset will naturally lead to more modular, maintainable, and ultimately, more powerful agent systems.
## Summary: Towards Scalable and Maintainable AI Solutions
Congratulations! You've learned how to move beyond single-task agents to build sophisticated, modular AI solutions using AIPack's composition capabilities. This approach is key to tackling complex, real-world problems with AI.
Here are the key takeaways from this chapter:
* **Agent composition** allows you to break down complex problems into smaller, manageable AI tasks, significantly improving scalability, maintainability, and reusability.
* **Reusable skills** are self-contained `.aip` agents designed for atomic tasks, promoting efficiency and consistency across different projects and workflows.
* AIPack's **`aipack.run()`** function is the fundamental mechanism for invoking sub-agents and passing context seamlessly between them.
* **Lua logic** provides the powerful control flow necessary for orchestrating complex agent workflows, enabling conditional execution, looping, and data transformation.
* **Effective context management** is critical to avoid token limits, reduce costs, and ensure relevant responses in composed agents.
* Adopting **best practices** like the Single Responsibility Principle, clear input/output interfaces, and robust error handling will lead to more reliable and extensible agent systems.
By mastering agent composition and reusable skills, you unlock the true potential of AIPack for tackling real-world, production-grade challenges. You can now design AI solutions that are not just intelligent, but also well-structured, easy to maintain, and ready to evolve.
In the next chapter, we'll shift our focus to advanced debugging techniques and strategies for optimizing your AIPack agents for performance and cost, ensuring your composed agents run efficiently.
## References
* [AIPack GitHub Repository](https://github.com/aipack-ai/aipack)
* [AIPack Documentation (likely within GitHub or a linked site)](https://github.com/aipack-ai/aipack/blob/main/docs/README.md)
* [Lua Programming Language Official Website](https://www.lua.org/)
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.