Introduction: Beyond Static Prompts

So far, you’ve learned how to define multi-stage AI agents using markdown within AIPack. These agents are powerful for sequential tasks, but what happens when your agent needs to make a decision? What if it needs to retry an action or branch its behavior based on an AI model’s output or an external condition? Pure markdown, while excellent for prompt templating, lacks the dynamic control flow needed for truly intelligent and resilient agents.

This is where Lua comes into play. AIPack leverages Lua, a lightweight, powerful, and embeddable scripting language, to introduce sophisticated logic and control flow into your agents. By embedding Lua code, you can empower your AI agents with conditional branching, loops, and custom data manipulation, transforming them from simple sequential processors into dynamic decision-makers.

In this chapter, we’ll dive into the world of Lua scripting within AIPack. We’ll explore why Lua is the perfect fit, learn its fundamental syntax for agent control, and walk through practical examples to build agents that can adapt and respond intelligently to varying circumstances. Get ready to add a new layer of intelligence to your AI Packs!

Core Concepts: Why Lua for Agent Control?

AI agents in real-world production environments often encounter non-deterministic situations. An AI model might respond in unexpected formats, an external API call might fail, or a user’s request might require different processing paths. To handle these scenarios gracefully, agents need explicit programming logic.

Lua’s Role in AIPack

Lua is a scripting language specifically designed to be easily embedded into applications. Its simplicity, small footprint, and high performance make it an ideal choice for adding custom logic without bogging down the core runtime.

Within AIPack, Lua serves as the agent’s “brain” for procedural decisions. It allows you to:

  • Implement Conditional Logic: Use if/else statements to choose different execution paths based on inputs, previous AI outputs, or external checks.
  • Create Loops: Perform retries, iterate over data, or wait for specific conditions to be met.
  • Manipulate Data: Transform, parse, or validate data flowing between agent stages or AI model calls.
  • Manage State: Store and update variables that persist across different parts of your agent’s execution.

This integration allows your markdown agents to delegate complex decision-making to a robust scripting environment, making them far more capable and reliable.

📌 Key Idea: Lua provides the “if this, then that” and “do this until” logic that markdown alone cannot offer, making AIPack agents truly adaptive.

How Lua Integrates with Markdown Agents

AIPack allows you to embed Lua code directly within your .aip markdown files using fenced lua code blocks. These blocks can access and manipulate the agent’s context (inputs, variables, previous outputs) and then set new outputs or variables that subsequent markdown stages can use.

Consider a simplified flow:

flowchart TD A[Start Agent] --> B{Agent Input Analysis} B --> C[Markdown Stage Initial Prompt] C --> D{Need Decision} D -->|Yes| E[Lua Block Conditional Logic] E -->|Path A| F[Markdown Stage Action A] E -->|Path B| G[Markdown Stage Action B] F --> H[End Agent] G --> H

In this diagram, the Lua Block acts as a branching point, directing the agent’s flow based on specific conditions it evaluates.

Fundamental Lua Concepts for AIPack

Before we write some code, let’s quickly review the Lua essentials you’ll use most often:

  • Variables:
    local my_variable = "Hello" -- Local variable
    another_variable = 123      -- Global variable (use 'local' for good practice)
    
  • Data Types: Numbers, strings, booleans (true/false), and nil (null).
  • Tables: Lua’s only data structuring mechanism, serving as arrays, dictionaries, or objects.
    local my_table = {
        name = "Alice",
        age = 30,
        hobbies = {"reading", "coding"}
    }
    print(my_table.name) -- Access by key
    print(my_table.hobbies[1]) -- Access array-like elements (1-indexed!)
    
    🧠 Important: Lua arrays are 1-indexed, meaning the first element is at index 1, not 0 like in many other languages.
  • Conditional Statements:
    local score = 85
    if score >= 90 then
        print("Excellent!")
    elseif score >= 70 then
        print("Good job.")
    else
        print("Keep trying.")
    end
    
  • Loops:
    -- For loop (numeric)
    for i = 1, 3 do
        print("Iteration " .. i)
    end
    -- prints:
    -- Iteration 1
    -- Iteration 2
    -- Iteration 3
    
    -- While loop
    local count = 0
    while count < 2 do
        print("Counting: " .. count)
        count = count + 1
    end
    -- prints:
    -- Counting: 0
    -- Counting: 1
    
  • Functions:
    local function greet(name)
        return "Hello, " .. name .. "!"
    end
    print(greet("World")) -- Prints: Hello, World!
    

Step-by-Step Implementation: Building a Conditional Agent

Let’s create an AIPack agent that uses Lua to decide whether to generate a short summary or a detailed report based on the user’s input.

Scenario: Adaptive Summary Agent

Our agent will take a user’s request and a piece of text. If the user explicitly asks for a “short summary” or the text is very brief, it will provide a concise output. Otherwise, it will default to a more detailed report.

Step 1: Create a New AIPack File

Create a file named adaptive_summary.aip in your AIPack project directory.

# Adaptive Summary Agent

This agent dynamically decides between a short summary and a detailed report.

---

Step 2: Define Initial Input and a Lua Block

We’ll start by defining the agent’s inputs and then immediately use a Lua block to analyze them.

Add the following to adaptive_summary.aip:

# Adaptive Summary Agent

This agent dynamically decides between a short summary and a detailed report.

---

## Input

```text
{{.Input}}

Internal Logic - Determine Output Type

-- Access the agent's input from the context
local user_request = aipack.get_input()

-- Define a threshold for "short" text (example: 100 characters)
local TEXT_LENGTH_THRESHOLD = 100

-- Check if the user specifically asked for a short summary
local is_short_request = string.find(string.lower(user_request), "short summary", 1, true) ~= nil

-- Simulate the text content (in a real scenario, this would come from another stage)
-- For now, let's hardcode a sample text for demonstration
local text_to_process = "AIPack is an open-source agentic runtime for building, running, and sharing AI Packs. It uses multi-stage markdown agents and Lua logic for control flow. This makes it highly flexible for various AI-assisted workflows."

-- Check if the text itself is short
local is_text_short = string.len(text_to_process) < TEXT_LENGTH_THRESHOLD

-- Determine the desired output type
local output_type = "detailed_report" -- Default

if is_short_request or is_text_short then
    output_type = "short_summary"
end

-- Store the determined output_type in the agent's context for later stages
aipack.set_output(output_type, "output_type")
aipack.set_output(text_to_process, "text_content")

-- Print for debugging (will show in AIPack console if debugging is enabled)
print("User Request: " .. user_request)
print("Is Short Request: " .. tostring(is_short_request))
print("Is Text Short: " .. tostring(is_text_short))
print("Determined Output Type: " .. output_type)

Explanation of the Lua Block:

  • local user_request = aipack.get_input(): This is how Lua accesses the primary input provided to the AIPack agent. aipack is a global object provided by the AIPack runtime.
  • string.find(...): A Lua function to search for a substring. We convert user_request to lowercase for case-insensitive matching. ~= nil checks if the substring was found.
  • string.len(text_to_process): Returns the length of the string.
  • if is_short_request or is_text_short then ... end: Our core conditional logic. If either condition is true, output_type becomes "short_summary".
  • aipack.set_output(output_type, "output_type"): This is crucial! It stores the output_type variable into the agent’s context under the key "output_type". Subsequent markdown stages can then access this using {{.output_type}}.
  • aipack.set_output(text_to_process, "text_content"): We also store the text to process so later stages can use it.
  • print(...): A standard Lua function for debugging. These messages will appear in your console when running the agent.

Step 3: Define Conditional Markdown Stages

Now, let’s create two different markdown stages, each guarded by a condition based on the output_type set by our Lua block.

Add the following after the Lua block in adaptive_summary.aip:

---

## Short Summary Output
{{ if eq .output_type "short_summary" }}
You asked for a short summary or the text was brief. Here's a concise overview:

Summarize the following text concisely:
```text
{{.text_content}}

{{ end }}


Detailed Report Output

{{ if eq .output_type “detailed_report” }} You requested a detailed report. Here is a comprehensive analysis:

Provide a detailed report on the following text, including key concepts and potential applications:

{{.text_content}}

{{ end }}


**Explanation of Conditional Markdown:**

*   `{{ if eq .output_type "short_summary" }}`: This is a Go template conditional statement (AIPack uses Go templates for markdown processing). It checks if the `output_type` variable in the agent's context is equal to `"short_summary"`.
*   `{{ end }}`: Closes the `if` block.
*   Only one of these two markdown blocks will be processed and sent to an AI model, depending on the value of `.output_type` determined by the Lua script.
*   `{{.text_content}}`: Accesses the `text_content` variable we set in Lua.

### Step 4: Run the Agent

Save `adaptive_summary.aip`. Now, open your terminal and run the agent with different inputs to see the Lua logic in action.

**Run 1: Requesting a short summary**

```bash
aipack run adaptive_summary.aip "Please provide a short summary."

You should see output indicating the “Short Summary Output” stage was activated. The prompt sent to the AI model will be for a concise summary.

Run 2: Requesting a detailed report

aipack run adaptive_summary.aip "I need a detailed analysis of this topic."

This time, the “Detailed Report Output” stage should be active, prompting for a comprehensive analysis.

Run 3: Short text (without explicit request)

Modify the text_to_process in your Lua block to be very short (e.g., "AIPack is cool."). Then run again:

aipack run adaptive_summary.aip "Analyze this."

Even without explicitly asking for a “short summary,” the Lua logic should detect the short text and activate the “Short Summary Output” stage.

Real-world insight: This pattern is incredibly useful for creating flexible agents. Imagine an agent that checks if a user’s query contains sensitive information before sending it to a cloud AI model, or an agent that retries a code generation task if the initial output fails a linter check.

Step-by-Step Implementation: Looping with Lua for Retries

Another common need in agentic workflows is the ability to retry actions, especially when dealing with external services or non-deterministic AI outputs. Lua’s looping constructs are perfect for this.

Scenario: Robust API Call Agent

Let’s imagine an agent that needs to call an external API. This API might occasionally fail or return an invalid response. We want our agent to retry the call up to a maximum number of times before giving up.

Step 1: Create a New AIPack File for Retries

Create retry_api_agent.aip.

# Robust API Call Agent

This agent attempts an API call multiple times if it fails.

---

Step 2: Implement Lua Logic with a Loop

Add a Lua block that simulates an API call and retries it.

# Robust API Call Agent

This agent attempts an API call multiple times if it fails.

---

## Internal Logic - API Call with Retries

```lua
local max_retries = 3
local current_retry = 0
local api_success = false
local api_response = "Initial state"

print("Attempting API call with retries...")

while current_retry < max_retries and not api_success do
    current_retry = current_retry + 1
    print("--- Attempt " .. current_retry .. " ---")

    -- Simulate an API call
    -- In a real scenario, this would be `aipack.call_tool("my_api_tool", { param = "value" })`
    -- For demonstration, let's make it "fail" twice, then "succeed"
    if current_retry < 3 then
        print("Simulating API failure...")
        api_response = "Error: Service Unavailable"
        api_success = false
        aipack.sleep(1) -- Wait for 1 second before retrying
    else
        print("Simulating API success!")
        api_response = "Data received: { 'status': 'ok', 'data': 'Hello from API!' }"
        api_success = true
    end
end

-- After loop, set the final status and response
aipack.set_output(tostring(api_success), "api_call_success")
aipack.set_output(api_response, "api_final_response")

print("Final API Call Status: " .. tostring(api_success))
print("Final API Response: " .. api_response)

Explanation of the Retry Lua Block:

  • while current_retry < max_retries and not api_success do ... end: This while loop continues as long as we haven’t exceeded max_retries AND the api_success flag is false.
  • current_retry = current_retry + 1: Increments the retry counter.
  • aipack.sleep(1): A utility function provided by AIPack to pause execution for a specified number of seconds. This is crucial for real-world retries to avoid overwhelming the external service.
  • The if current_retry < 3 block simulates a failure for the first two attempts and then a success on the third.
  • tostring(api_success): Converts the boolean api_success to a string for aipack.set_output, as set_output generally expects string values or values that can be easily serialized.

Step 3: Define Output Stages Based on Retry Result

Add markdown stages to show the outcome.

---

## API Call Result

{{ if eq .api_call_success "true" }}
The API call was successful! Here's the response:
```json
{{.api_final_response}}

{{ else }} The API call failed after multiple retries. Last response:

{{.api_final_response}}

{{ end }}


### Step 4: Run the Agent

Save `retry_api_agent.aip` and run it:

```bash
aipack run retry_api_agent.aip

Observe the console output. You’ll see the agent retrying twice, pausing, and then succeeding on the third attempt. If you change max_retries to 2 and keep the simulation logic, you’d see it fail after two attempts.

⚠️ What can go wrong: Without proper retry logic, an agent might fail on transient network issues or temporary service outages, leading to brittle workflows. Lua’s loops make your agents much more robust.

Mini-Challenge: Text Processing Decision Agent

Let’s combine what you’ve learned to build an agent that makes a more complex decision.

Challenge: Create an AIPack agent (text_processor.aip) that takes a user’s input request and a piece of text_content. Your Lua logic should:

  1. Check if the text_content is empty or very short (e.g., less than 20 characters). If so, set an action_type to "ask_for_more_text".
  2. If the text_content is substantial, check if the user’s request contains keywords like “translate” or “summarize”.
    • If “translate” is found, set action_type to "translate".
    • If “summarize” is found, set action_type to "summarize".
    • If neither is found, default action_type to "analyze".
  3. Have three distinct markdown stages, each guarded by {{ if eq .action_type "..." }}:
    • One to prompt the user for more text.
    • One to translate the text_content.
    • One to summarize the text_content.
    • One to analyze the text_content.

Hint:

  • Use aipack.get_input() for the user’s request.
  • Define text_content as a hardcoded string in Lua for now, or pass it as a second input to aipack.get_input("text_content") if you define it as a named input in a previous markdown stage. For simplicity, hardcode it in Lua first.
  • Use string.find for keyword detection and string.len for text length.
  • Remember Lua’s elseif for multiple conditions.

What to observe/learn: This challenge will solidify your understanding of combining multiple conditional checks, string manipulation, and branching execution paths in AIPack using Lua. It demonstrates how a single agent can perform diverse tasks based on dynamic input.

Common Pitfalls & Troubleshooting

Working with Lua in AIPack is powerful, but like any scripting, it has its quirks.

  1. Lua Syntax Errors:

    • Missing end: Every if, for, while, and function block in Lua must be closed with an end. This is a very common mistake.
    • Incorrect comparison: In Lua, == is for equality comparison, = is for assignment. Using = in an if condition is a syntax error.
    • Case sensitivity: Lua is case-sensitive. myVar is different from myvar.
    • 1-indexing vs. 0-indexing: Remember Lua tables (arrays) start at index 1.
    • Debugging: Use print("My variable value: " .. tostring(my_var)) statements generously within your Lua blocks. They will show up in your terminal when you run the agent.
  2. Incorrect Variable Access:

    • aipack.get_input() and aipack.set_output(): Ensure you’re using these functions correctly to pass data between the AIPack runtime and your Lua script, and then between Lua and subsequent markdown stages.
    • Named inputs: If you define named inputs in markdown (e.g., {{.text_content}}), you can access them in Lua using aipack.get_input("text_content").
    • Go template variables: Remember that variables set via aipack.set_output() are accessed in markdown with {{.variable_name}}.
  3. Infinite Loops:

    • If your while loop condition never becomes false (e.g., you forget to increment a counter or modify a flag), your agent will hang indefinitely.
    • Debugging: If your agent hangs, stop it (Ctrl+C). Add print() statements inside your loop to check if the loop variable or condition is changing as expected.
  4. Security Concerns with Arbitrary Lua:

    • While convenient, allowing arbitrary Lua execution means your agent has the power to do whatever Lua can do (e.g., read/write files, make network requests if the runtime allows).
    • Best Practice: When sharing or deploying AIPacks, ensure you understand the Lua code’s implications. AIPack’s runtime might sandbox Lua, but it’s always good to be aware.

Summary: Mastering Agent Control

You’ve now taken a significant step in building more intelligent and robust AI agents with AIPack. By integrating Lua scripting, you can move beyond simple sequential prompts and into the realm of dynamic, decision-making workflows.

Here are the key takeaways from this chapter:

  • Lua’s Purpose: It provides essential control flow (conditionals, loops), data manipulation, and state management capabilities to AIPack agents, complementing markdown’s templating power.
  • Integration: Lua code is embedded directly into .aip files using fenced lua blocks.
  • AIPack Lua API: Use aipack.get_input() to read data into Lua and aipack.set_output() to pass data from Lua back to the AIPack context for markdown stages. aipack.sleep() is useful for pauses.
  • Conditional Logic: if/elseif/else statements in Lua allow agents to branch execution paths based on various conditions.
  • Looping for Robustness: while and for loops enable agents to retry operations, iterate over data, and build more resilient workflows.
  • Debugging: print() statements are your best friend for understanding Lua’s execution flow.

With Lua in your toolkit, your AIPack agents are no longer just prompt wrappers; they are capable, adaptive programs that can handle complexity, errors, and diverse user intentions.

In the next chapter, we’ll explore how to connect your AIPack agents to the outside world by diving into various AI model provider integrations, from local models like Ollama to powerful cloud APIs.

References

This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.