A lightweight bash framework for building LLM-powered workflows and agents.
Not just an LLM CLI tool - PipeGent orchestrates multi-step workflows (data flowing through prompts) and agentic loops (LLM deciding tool calls), with built-in conversation history, cost tracking, and complete observability.
source pipeline-framework.sh
echo "I love this product!" | agent "Classify sentiment as positive, negative, or neutral"
# Output: positiveUse PipeGent for the same reasons you use bash: small, quick, easy, readable for simple stuff.
Just like you choose bash over Python for straightforward automation, choose PipeGent over LangChain for straightforward LLM workflows.
What it does:
- Workflows - Route data through fixed prompt sequences (classify β route β process)
- Agents - Let LLM decide tool calls in agentic loops (multi-turn with conversation history)
- Infrastructure - Handles LLM calls, logging, cost tracking, state management
- You write - The control flow in native bash (if/case/for/while)
Lighter mental model: Just bash + one agent function. No classes, chains, or framework abstractions to learn.
AI-friendly: Tools like Claude Code can read just README.md + pipeline-framework.sh to fully understand the framework - no hidden behavior, no complex abstractions, minimal chance of mistakes.
- Workflow & Agent capable - Fixed pipelines OR agentic loops with tool calling (Example 6)
- MCP Integration - Connect to Model Context Protocol servers for standardized tool/resource access
- Complete transparency - Every LLM call automatically saved to files (prompts, responses, costs)
- Multi-turn conversations - Built-in conversation history with
init_chat/agent_chatfor agentic loops - Built-in observability - Automatic cost tracking, token usage, and execution traces
- Three input modes - Flexible prompt handling: parameter only, parameter + stdin, or stdin only
- Native composability - Works with pipes:
cat file | agent "task1" | agent "task2" - CLI building blocks - Scripts become command-line tools usable by other tools (e.g., Claude Code)
- Model switching - Change models per-call or globally with simple env vars
- Minimal dependencies - Just bash, curl, and jq (already on most systems)
Think: Bash vs Python
Just like bash is for quick scripts and Python is for applications:
PipeGent (like bash):
- Lightweight workflow/agent framework in bash
- Quick to write, easy to read, minimal setup
- Transparent by default (all prompts/responses in files)
- CLI-composable building blocks
- For automation, scripting, simple-to-medium workflows
- ~100 lines you can understand completely
LangChain (like Python):
- Comprehensive framework with rich abstractions
- Production-grade features (RAG, vector stores, sophisticated agents)
- Better for complex applications and web services
- Steeper learning curve, more setup
- For large-scale, team-based LLM applications
Use PipeGent when:
- Building bash automation with LLM capabilities
- Need transparency (see exactly what prompts are sent)
- Want quick prototypes without framework overhead
- Prefer readability over sophistication
- Building CLI tools or simple-to-medium workflows/agents
Use LangChain when:
- Building production web services or APIs
- Need advanced features (RAG, vector search)
- Large codebase with team collaboration
- Complex multi-agent orchestration at scale
Option 1: LiteLLM (Recommended) - Multi-provider with cost tracking
# Install litellm
pip install litellm
# Configure your provider (see https://docs.litellm.ai/docs/)
export AGENT_MODEL="gpt-4" # or claude-3-opus, gemini-pro, etc.
# Set API keys for your chosen provider
export OPENAI_API_KEY="sk-..."
# or ANTHROPIC_API_KEY, GOOGLE_API_KEY, etc.Option 2: OpenRouter - Simple fallback
export OPENROUTER_API_KEY="sk-or-v1-your-key-here"
export AGENT_MODEL="anthropic/claude-3.5-sonnet" # or deepseek/deepseek-v3.2, etc.PipeGent automatically detects and prioritizes litellm if installed, falls back to OpenRouter otherwise.
Option 3: MCP Integration (Optional) - Standardized tool/resource access
Connect to Model Context Protocol servers for filesystem access, GitHub integration, web search, and more.
# Install MCP Python SDK
pip install mcp
# Create config (project-specific or user-level)
cp mcp_config.example.json mcp_config.json
# Edit to configure your MCP servers
# Config locations (checked in order):
# 1. ./mcp_config.json (project-specific)
# 2. ~/.pipegent/mcp_config.json (user-level)Example config:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/directory"],
"transport": "stdio"
}
}
}MCP Functions:
mcp_list_tools- Get formatted list of available tools for LLM promptsmcp_call_tool <json>- Execute MCP tool with{"mcp_server": "...", "tool": "...", "arguments": {...}}mcp_list_resources <server>- List resources from an MCP servermcp_read_resource <server> <uri>- Read a resource
See examples/mcp-assistant.sh for a complete agentic workflow with MCP tools.
No workflow needed for simple one-off LLM calls:
source pipeline-framework.sh
echo "I love this product!" | agent "Classify sentiment as positive, negative, or neutral"
# Output: positiveUse bash case statement for routing based on LLM classification:
#!/bin/bash
source ./pipeline-framework.sh
# Get email text from command line argument, or use default
EMAIL_TEXT="${1:-Hi! You've won \$1,000,000! Click here now!!!}"
workflow "Email Classifier" --show-progress <<'END'
# Framework provides: $INPUT variable with the email text
# Classify the email
CLASSIFICATION=$(echo "$INPUT" | agent "Classify this email as: spam, urgent, or normal. Answer with just one word.")
# Route based on classification using native bash
case "$CLASSIFICATION" in
*spam*)
echo "$INPUT" | agent "Generate spam filter rule for similar emails"
;;
*urgent*)
echo "$INPUT" | agent "Draft immediate response acknowledging urgency"
;;
*)
echo "$INPUT" | agent "Add to normal queue with priority score"
;;
esac
END
# Run workflow with the email text (from argument or default)
echo "$EMAIL_TEXT" | run-workflowUsage:
./examples/simple-classifier.sh # Uses default spam email
./examples/simple-classifier.sh "URGENT: Server is down! Need immediate assistance!"
./examples/simple-classifier.sh "Hello, here's the report you requested..."Output: Spam filter rule (when classifying the default spam email)
Generate structured JSON for database ingestion. Note: LLM generates only fields requiring AI understanding; bash adds bookkeeping fields.
#!/bin/bash
source ./pipeline-framework.sh
workflow "Product Review Analyzer" --show-progress --trace <<'END'
log_progress " π Analyzing review..."
# LLM generates structured data (requires understanding)
ANALYSIS=$(echo "$INPUT" | agent "Analyze this product review. Output JSON with:
- sentiment (positive/negative/mixed)
- rating_implied (1-5)
- positive_aspects (array of strings)
- negative_aspects (array of strings)
- action_required (boolean - true if needs follow-up)
Output ONLY valid JSON, no markdown.")
# Bash adds computable/bookkeeping fields
REVIEW_ID=$(echo "$INPUT" | md5sum | cut -d' ' -f1)
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
WORD_COUNT=$(echo "$INPUT" | wc -w)
# Combine into final JSON using jq
echo "$ANALYSIS" | jq --arg id "$REVIEW_ID" \
--arg ts "$TIMESTAMP" \
--argjson wc "$WORD_COUNT" \
'. + {review_id: $id, processed_at: $ts, word_count: $wc}'
END
echo "The product is amazing! However, the battery life could be better. Overall, I'd give it 4 stars." | run-workflowOutput:
{
"sentiment": "mixed",
"rating_implied": 4,
"positive_aspects": ["product quality", "overall satisfaction"],
"negative_aspects": ["battery life"],
"action_required": true,
"review_id": "a3f5e8c9d1b2...",
"processed_at": "2024-01-08T12:34:56Z",
"word_count": 18
}Run multiple LLM analyses concurrently, then combine into structured output:
#!/bin/bash
source ./pipeline-framework.sh
workflow "Content Analysis" --show-progress <<'END'
log_progress " β‘ Running parallel analysis..."
# Run multiple analyses in parallel using bash background jobs
# Each gets a different aspect of the content
SUMMARY=$(echo "$INPUT" | agent "Summarize in one sentence. Just the summary, no preamble.") &
PID1=$!
KEYWORDS=$(echo "$INPUT" | agent "Extract 3-5 main keywords. Output as JSON array of strings only.") &
PID2=$!
TONE=$(echo "$INPUT" | agent "What is the tone? Answer with ONE word: professional, casual, technical, or marketing.") &
PID3=$!
# Wait for all LLM calls to complete
wait $PID1 $PID2 $PID3
# Bash computes metadata
CHAR_COUNT=$(echo "$INPUT" | wc -c)
WORD_COUNT=$(echo "$INPUT" | wc -w)
READ_TIME=$(( (WORD_COUNT + 199) / 200 )) # Assume 200 words/min, round up
# Combine into structured JSON
# Note: LLM generated content (summary, keywords, tone)
# Bash computed metrics (counts, read time)
jq -n --arg summary "$SUMMARY" \
--argjson keywords "$KEYWORDS" \
--arg tone "$TONE" \
--argjson chars "$CHAR_COUNT" \
--argjson words "$WORD_COUNT" \
--argjson read_min "$READ_TIME" \
'{
content: {
summary: $summary,
keywords: $keywords,
tone: $tone
},
metrics: {
character_count: $chars,
word_count: $words,
reading_time_minutes: $read_min
}
}'
END
echo "Artificial intelligence is transforming industries. Machine learning models can now process vast amounts of data efficiently..." | run-workflowOutput:
{
"content": {
"summary": "AI and machine learning are revolutionizing industry data processing",
"keywords": ["artificial intelligence", "machine learning", "data processing", "industries"],
"tone": "professional"
},
"metrics": {
"character_count": 134,
"word_count": 18,
"reading_time_minutes": 1
}
}Process a list of items:
#!/bin/bash
source ./pipeline-framework.sh
workflow "Batch Translator" --show-progress <<'END'
# Split input into lines and process each
log_progress " π Translating items..."
echo "$INPUT" | while IFS= read -r line; do
if [ -n "$line" ]; then
TRANSLATION=$(echo "$line" | agent "Translate to Spanish")
echo "- $TRANSLATION"
fi
done
END
echo "Hello
Goodbye
Thank you" | run-workflowAn agentic loop where the LLM can call filesystem tools to answer questions. This example demonstrates multi-turn conversation with JSON message format.
#!/bin/bash
source ./pipeline-framework.sh
# Get task from command line argument, or use default
TASK="${1:-How many .sh files are in the current directory?}"
workflow "Filesystem Assistant" --show-progress <<'END'
log_progress " π€ Starting filesystem assistant..."
# User's question
USER_QUESTION="$INPUT"
# Initialize conversation with system prompt (creates JSON message array)
init_chat "You are a filesystem assistant. You can use tools to explore the filesystem and answer questions.
Available tools (call with JSON format):
- {\"tool\": \"ls\", \"args\": [\"-la\", \"path\"]} - list directory contents
- {\"tool\": \"cat\", \"args\": [\"file\"]} - read file contents
- {\"tool\": \"head\", \"args\": [\"-n\", \"20\", \"file\"]} - read first N lines
- {\"tool\": \"grep\", \"args\": [\"-r\", \"pattern\", \"path\"]} - search for pattern
- {\"tool\": \"find\", \"args\": [\"path\", \"-name\", \"pattern\"]} - find files by name
- {\"tool\": \"wc\", \"args\": [\"-l\", \"file1\", \"file2\", ...]} - count lines (accepts multiple files at once)
You have a maximum of 9 tool calls. BE EFFICIENT - pass multiple files to tools like wc instead of calling them one file at a time.
IMPORTANT: When you need to call a tool, respond with ONLY the JSON object - no explanations, no preamble, no additional text.
Example: {\"tool\": \"wc\", \"args\": [\"-l\", \"file1.sh\", \"file2.sh\", \"file3.sh\"]}
When you have the final answer, respond with plain text only (no JSON).
Never mix explanatory text with tool calls."
# Add the user's question as a separate user message
add_to_chat "$USER_QUESTION" "user"
# Agentic loop - continues until LLM provides final answer (no tool call)
MAX_ITERATIONS=10
iteration=0
while [ $iteration -lt $MAX_ITERATIONS ]; do
iteration=$((iteration + 1))
log_progress " π Iteration $iteration..."
# Get LLM response (uses conversation history automatically)
RESPONSE=$(agent_chat)
# Check if response contains a tool call (possibly wrapped in code blocks)
if [[ "$RESPONSE" =~ \"tool\" ]]; then
# Strip markdown code blocks if present (``` or ```json)
TOOL_JSON=$(echo "$RESPONSE" | sed -e 's/^[[:space:]]*```json[[:space:]]*//g' -e 's/^[[:space:]]*```[[:space:]]*//g' -e 's/[[:space:]]*```[[:space:]]*$//g')
log_progress " π§ Tool call: $TOOL_JSON"
# Parse tool call
TOOL_NAME=$(echo "$TOOL_JSON" | jq -r '.tool' 2>/dev/null)
# Execute tool based on name (application-specific logic)
# Parse args into a bash array to handle spaces and special characters properly
mapfile -t ARGS_ARRAY < <(echo "$TOOL_JSON" | jq -r '.args[]' 2>/dev/null)
case "$TOOL_NAME" in
ls)
TOOL_RESULT=$(ls "${ARGS_ARRAY[@]}" 2>&1)
;;
cat)
TOOL_RESULT=$(cat "${ARGS_ARRAY[@]}" 2>&1)
;;
head)
TOOL_RESULT=$(head "${ARGS_ARRAY[@]}" 2>&1)
;;
grep)
TOOL_RESULT=$(grep "${ARGS_ARRAY[@]}" 2>&1)
;;
find)
TOOL_RESULT=$(find "${ARGS_ARRAY[@]}" 2>&1)
;;
wc)
TOOL_RESULT=$(wc "${ARGS_ARRAY[@]}" 2>&1)
;;
*)
TOOL_RESULT="ERROR: Unknown tool '$TOOL_NAME'"
;;
esac
log_progress " π₯ Tool result: ${TOOL_RESULT:0:100}..."
# Add tool result to conversation
add_to_chat "Tool result: $TOOL_RESULT"
else
# LLM provided final answer (no tool call)
log_progress " β
Final answer received"
echo "$RESPONSE"
break
fi
# Safety check - prevent infinite loops
if [ $iteration -eq $MAX_ITERATIONS ]; then
log_progress " β οΈ Max iterations reached"
echo "ERROR: Max iterations ($MAX_ITERATIONS) reached without final answer"
exit 1
fi
done
END
# Run workflow with the task (from argument or default)
echo "$TASK" | run-workflowKey Features:
- JSON Messages Format: Conversation history stored as
[{role: "system", content: "..."}, {role: "user", content: "..."}, ...] - init_chat: Creates conversation with system message
- add_to_chat: Adds messages with optional role parameter (defaults to "user")
- agent_chat: Uses full conversation history automatically, tracks each call separately
- Robust Tool Parsing: Handles LLM responses with leading whitespace, markdown code blocks (
```or```json) - Tool Calling Pattern: LLM returns JSON to invoke tools
- Application-specific Logic: You define what tools do via bash case statement
- Proper Argument Handling: Uses bash arrays to support spaces and special characters
- Agentic Loop: Continues until LLM provides final answer (no tool call)
Initializes a workflow with logging, state management, and observability:
workflow "Workflow Name" [--show-progress] [--trace] <<'END'
# Your bash script here
# Available: $INPUT, agent(), agent_chat(), log_progress(), $WORKFLOW_DIR
ENDMakes an LLM API call with automatic logging. Supports three modes:
Mode 1: Parameter only - Parameter is the complete prompt (no stdin):
# Simple question or task
RESULT=$(agent "Explain quantum computing in one sentence")Mode 2: Parameter + stdin - Parameter is the task, stdin is the input:
# Agent chaining - each agent processes the previous output
cat email.txt | agent "Extract: sender, subject, main request" | agent "Draft a professional reply addressing the main request" | agent "Review for tone and grammar, output final version"In mode 2, the input is wrapped in <input></input> tags automatically:
Your task here
<input>
stdin content here
</input>
Mode 3: Stdin only - Stdin is the complete prompt (for complex/dynamic prompts):
# Build a complex prompt with file contents, context, and instructions
PROMPT=$(cat <<EOF
Review the following bash script for security issues and best practices:
$(cat script.sh)
Specifically check for:
- Command injection vulnerabilities
- Unquoted variables
- Missing error handling
- Unsafe temp file usage
Provide a detailed analysis with line numbers and suggested fixes.
EOF
)
ANALYSIS=$(echo "$PROMPT" | agent)
echo "$ANALYSIS"Each call creates debug files:
agent-N-prompt.txt- The parameter (if provided)agent-N-input.txt- The stdin content (if provided)agent-N-full-prompt.txt- Complete prompt sent to APIagent-N-output.txt- LLM response
Switch models within a script using either approach:
Inline (for single calls):
# Use different models for different tasks
SUMMARY=$(echo "$INPUT" | AGENT_MODEL="anthropic/claude-sonnet-4.5" agent "Summarize")
KEYWORDS=$(echo "$INPUT" | AGENT_MODEL="deepseek/deepseek-chat" agent "Extract keywords")
TRANSLATE=$(echo "$INPUT" | AGENT_MODEL="openai/gpt-5.2" agent "Translate to Spanish")Export (for multiple calls):
# Switch model for all subsequent calls
export AGENT_MODEL="anthropic/claude-sonnet-4.5"
RESULT1=$(echo "$input1" | agent "prompt1")
RESULT2=$(echo "$input2" | agent "prompt2")
# Switch again
export AGENT_MODEL="deepseek/deepseek-chat"
RESULT3=$(echo "$input3" | agent "prompt3")For multi-turn agentic workflows, use these functions to maintain conversation state:
Initialize a new conversation with a system prompt (creates JSON message array):
init_chat "You are a helpful assistant. You can use tools..."This creates conversation-history.json with:
[
{
"role": "system",
"content": "You are a helpful assistant..."
}
]Add a message to the conversation history:
add_to_chat "User's question here" "user" # Add user message
add_to_chat "Tool result: ..." "user" # Add tool result
add_to_chat "Final answer" "assistant" # Manually add assistant message (rarely needed)The second parameter (role) is optional and defaults to "user".
Make an LLM call using the full conversation history:
RESPONSE=$(agent_chat) # Use existing conversation
RESPONSE=$(agent_chat "Additional prompt") # Add user message and get responseEach agent_chat call:
- Sends the entire conversation history as JSON messages to the LLM
- Automatically appends the assistant's response to the conversation
- Creates debug files for observability
- Tracks costs and token usage
Debug files created per call:
agent-chat-N-messages.json- Full messages array sent to APIagent-chat-N-output.txt- LLM response
Conversation state:
conversation-history.json- Full conversation history (updated after each call)agent-chat-counter.txt- Call counter (persists across subprocesses)
Display progress messages (respects --show-progress flag):
log_progress " π Processing step..."--show-progress- Display progress (default: true)--trace- Enable execution trace logs--no-progress- Hide progress output
OPENROUTER_API_KEY- Your OpenRouter API key (required)AGENT_MODEL- Model to use (default:anthropic/claude-3.5-sonnet)PIPELINE_LOG_DIR- Directory for workflow logs and traces (default:/tmp)SHOW_PROGRESS- Enable/disable progress (default:true)TRACE_ENABLED- Enable/disable tracing (default:false)
PipeGent automatically tracks API costs and token usage for all workflows using OpenRouter's usage accounting.
Each workflow run generates:
- costs.csv - Detailed per-call costs and token usage (cost, prompt_tokens, completion_tokens, total_tokens)
- cost-summary.txt - Human-readable summary with totals
- Cost report - Printed at workflow completion (both readable and parseable formats)
Example output:
π° Cost Summary:
API Calls: 2
Total Cost: $0.000468
Tokens: 1139 (prompt: 71, completion: 1068)
COST: workflow=Email Classifier api_calls=2 total_cost=0.000468 prompt_tokens=71 completion_tokens=1068 total_tokens=1139
The parseable COST: line can be easily extracted with grep/awk for aggregation:
./my-workflow.sh 2>&1 | grep '^COST:' | awk -F' ' '{print $3, $5}'All workflow runs create directories with full execution traces (in $PIPELINE_LOG_DIR, defaults to /tmp):
# Check workflow files (replace /tmp with your $PIPELINE_LOG_DIR if customized)
ls -la /tmp/pipeline-wf-*/ # Workflow runs
ls -la /tmp/pipeline-adhoc-*/ # Standalone agent calls
# View last trace
source pipeline-framework.sh
pipeline-trace show-last
# List all traces
pipeline-trace list
# Inspect a specific run
cat /tmp/pipeline-wf-12345-67890/trace.log
cat /tmp/pipeline-wf-12345-67890/workflow-script.sh # Your script
cat /tmp/pipeline-wf-12345-67890/costs.csv # Cost tracking
cat /tmp/pipeline-wf-12345-67890/cost-summary.txt # Cost summary
# For agent() calls
cat /tmp/pipeline-wf-12345-67890/agent-1-prompt.txt # Your prompt
cat /tmp/pipeline-wf-12345-67890/agent-1-input.txt # Input text
cat /tmp/pipeline-wf-12345-67890/agent-1-full-prompt.txt # Full prompt sent to API
cat /tmp/pipeline-wf-12345-67890/agent-1-output.txt # LLM response
# For agent_chat() calls (multi-turn conversation)
cat /tmp/pipeline-wf-12345-67890/conversation-history.json # Full conversation state
cat /tmp/pipeline-wf-12345-67890/agent-chat-1-messages.json # Messages sent in call 1
cat /tmp/pipeline-wf-12345-67890/agent-chat-1-output.txt # Response from call 1
cat /tmp/pipeline-wf-12345-67890/agent-chat-2-messages.json # Messages sent in call 2
cat /tmp/pipeline-wf-12345-67890/agent-chat-2-output.txt # Response from call 2
# ... (one set of files per agent_chat call)Key observability features:
- File-based counters: Each
agent()andagent_chat()call gets a unique number, even across subprocesses - Full conversation history:
conversation-history.jsonshows the complete message thread with roles - Per-call snapshots:
agent-chat-N-messages.jsonshows exactly what was sent in each API call - Cost tracking: Every LLM call logs its cost and token usage to
costs.csv
- Email routing - Classify and route emails based on content
- Content analysis - Sentiment, topics, summarization
- Data extraction - Pull structured data from unstructured text
- Batch processing - Process lists of items with LLM
- Multi-step reasoning - Chain multiple LLM calls
- Parallel analysis - Run multiple analyses concurrently
- Use native bash - Don't fight bash, use it!
if,case,for,whileall work great - Store LLM results in variables -
RESULT=$(echo "$input" | agent "prompt") - Use descriptive prompts - Clear prompts get better results
- Log progress - Help users understand what's happening
- Check the trace logs - When debugging, look at
agent-N-full-prompt.txtand other saved files in$WORKFLOW_DIR
- Bash 4.0+
curl- For API callsjq- For JSON parsing- OpenRouter API key - Get one at openrouter.ai
Optional (for MCP support):
- Python 3.9+
pip install mcp- MCP Python SDK- Node.js/npx - For MCP servers (auto-downloaded with
npx -y)
Try the included examples:
Simple:
examples/simple-classifier.sh- Email classification with bashcaserouting (Example 2)
Medium Complexity:
examples/review-analyzer.sh- Product review analysis with JSON output (Example 3)examples/content-analyzer.sh- Parallel content analysis with structured JSON (Example 4)examples/batch-translator.sh- Iterative batch processing (Example 5)
Complex:
examples/filesystem-assistant.sh- Multi-turn conversation with tool calling (Example 6)examples/mcp-assistant.sh- MCP-enhanced assistant with standardized tool protocol
Run them:
# Email classifier - accepts optional email text argument
./examples/simple-classifier.sh
./examples/simple-classifier.sh "URGENT: Server is down! Need immediate assistance!"
./examples/simple-classifier.sh "Hello, here's the report you requested..."
./examples/review-analyzer.sh
./examples/content-analyzer.sh
./examples/batch-translator.sh
# Filesystem assistant - accepts optional question argument
./examples/filesystem-assistant.sh
./examples/filesystem-assistant.sh "What examples are shown in the README.md file?"
./examples/filesystem-assistant.sh "Find all files containing the word 'workflow' in this directory"
# MCP assistant - requires MCP setup (see Option 3 in Setup)
./examples/mcp-assistant.sh
./examples/mcp-assistant.sh "List the files in the examples directory"TARGET="${1:-default}"
workflow "Name" <<END
log_progress " π― Target: $TARGET"
RESULT=\$(echo "\$INPUT" | agent "Analyze for: $TARGET")
ENDR1=\$(echo "\$CONTEXT" | agent "Initial analysis")
R2=\$(cat <<EOF | agent "Review and improve"
\$CONTEXT
<previous>\$R1</previous>
EOF
)
FINAL=\$(cat <<EOF | agent "Final synthesis"
<round_1>\$R1</round_1>
<round_2>\$R2</round_2>
EOF
)echo "\$RESULT"
OUTPUT_FILE="output_\$(date +%Y%m%d_%H%M%S).md"
echo "\$RESULT" > "\$OUTPUT_FILE"
log_progress " β
Saved: \$OUTPUT_FILE"log_progress " π Loading..."
log_progress " π€ Round 1: Analyzing..."
log_progress " π€ Round 2: Refining..."
log_progress " π Synthesizing..."
log_progress " β
Complete"MIT