Skip to content

reasoning-trace-optimizer

by Murat Can Koylan

Debug and optimize AI agents by analyzing reasoning traces, context degradation, tool confusion, instruction drift, repeated task failures, and performance regressions.

Reaches the networkRuns commands13 scripts4 findings

Labels from its files: reaches the network, runs commands. 63 files, read Oct 21, 2026. Nothing was run.

npx skills add https://github.com/muratcankoylan/Agent-Skills-for-Context-Engineering --skill interleaved-thinking

What it lets an agent do

Each label, and the line in the skill's files that set it.

  • Reaches the network

    Its scripts or instructions fetch from or send to the network.

    • examples/01_basic_capture.py makes network calls.
    • examples/02_tool_usage.py makes network calls.
    • examples/03_full_optimization.py makes network calls.
    • reasoning_trace_optimizer/analyzer.py makes network calls.
    • reasoning_trace_optimizer/capture.py makes network calls.
    • reasoning_trace_optimizer/cli.py makes network calls.
  • Runs commands

    Allows a shell or ships scripts the agent runs.

    • It ships 13 scripts the agent can run.

Findings (4)

Matches of fixed checks, with the text they matched.

  • highexamples/01_basic_capture.py

    examples/01_basic_capture.py mentions a secret on disk and also makes network calls.

    …ath(__file__).parent.parent / ".env" load_dotenv(env_path) def …

    What this check looks for
  • highexamples/02_tool_usage.py

    examples/02_tool_usage.py mentions a secret on disk and also makes network calls.

    …ath(__file__).parent.parent / ".env" load_dotenv(env_path) # De…

    What this check looks for
  • warnexamples/03_full_optimization.py

    examples/03_full_optimization.py runs a command string through a shell, or evaluates code it builds at run time.

    … } result = eval(expression, {"__builtins__": {…

    What this check looks for
  • highexamples/03_full_optimization.py

    examples/03_full_optimization.py mentions a secret on disk and also makes network calls.

    …ath(__file__).parent.parent / ".env" load_dotenv(env_path) # ==…

    What this check looks for

Not a safety rating, and no finding is not a safety guarantee. These are fixed checks of the files as read on Oct 21, 2026. Nothing was run. How skills are checked.

SKILL.md

As the publisher wrote it. Shown as text, never rendered.

Publisher text
---
name: reasoning-trace-optimizer
description: "Debug and optimize AI agents by analyzing reasoning traces, context degradation, tool confusion, instruction drift, repeated task failures, and performance regressions."
---

# Reasoning Trace Optimizer

Debug and optimize AI agents by analyzing their reasoning traces. This skill uses MiniMax M2.1's interleaved thinking to provide deep insight into agent decision-making and generate concrete improvements.

## When to Activate

- Agent reasoning traces need debugging, analysis, or prompt optimization
- Agent task fails and user wants to understand why
- User mentions "context degradation", "tool confusion", or "instruction drift"
- Request to improve agent performance or reduce errors
- User wants to generate shareable learnings from debugging sessions
- After repeated failures on similar tasks

## Core Concepts

### Interleaved Thinking

Unlike standard reasoning models that think once at the start, interleaved thinking allows reasoning BETWEEN each tool interaction. This is critical because:

1. **Long-horizon tasks** require maintaining focus across many turns
2. **External perturbations** (tool outputs, environment changes) need real-time adaptation
3. **Debugging** requires seeing HOW decisions were made, not just WHAT was output

### The Optimization Loop

```
Execute Agent → Capture Traces → Analyze Patterns → Optimize Prompt → Re-run
                                                          ↑____________|
```

Each iteration improves the prompt based on detected patterns until convergence.

### Pattern Detection

Common failure patterns the analyzer detects:

| Pattern | Description |
|---------|-------------|
| `context_degradation` | Model loses track of information over long contexts |
| `tool_confusion` | Model misunderstands tool capabilities or outputs |
| `instruction_drift` | Model gradually deviates from original instructions |
| `goal_abandonment` | Model stops pursuing the original goal |
| `circular_reasoning` | Model repeats similar actions without progress |
| `premature_conclusion` | Model concludes before completing the task |

## Usage Modes

### Mode 1: M2.1 Agent Debugging

Run a task through M2.1 and analyze its reasoning:

```python
from reasoning_trace_optimizer import TraceCapture, TraceAnalyzer

capture = TraceCapture()
trace = capture.run(
    task="Search for Python tutorials and summarize them",
    system_prompt="You are a research assistant.",
    tools=[search_tool],
    tool_executor=execute_search
)

analyzer = TraceAnalyzer()
analysis = analyzer.analyze(trace)

print(f"Score: {analysis.overall_score}/100")
for pattern in analysis.patterns:
    print(f"Found: {pattern.type.value} - {pattern.suggestion}")
```

### Mode 2: Full Optimization Loop

Automatically iterate until the prompt is optimized:

```python
from reasoning_trace_optimizer import OptimizationLoop, LoopConfig

config = LoopConfig(
    max_iterations=5,
    min_score_threshold=80.0,
)

loop = OptimizationLoop(config=config)
result = loop.run(
    task="Analyze this codebase and suggest improvements",
    initial_prompt="You are a code reviewer.",
    tools=[read_file_tool, search_tool],
    tool_executor=execute_tool
)

print(f"Improved: {result.initial_score} → {result.final_score}")
print(f"Final prompt:\n{result.final_prompt}")
```

### Mode 3: Universal Session Analysis

Analyze any agent's previous thinking (works with Claude, GPT, etc.):

When this skill is activated in Claude Code, it can analyze the current session's thinking blocks to identify issues and suggest improvements.

```
/reasoning-trace-optimizer analyze-session
```

### Mode 4: Generate Shareable Skills

Convert optimization learnings into reusable Agent Skills:

```python
from reasoning_trace_optimizer import SkillGenerator

generator = SkillGenerator()
skill_path = generator.generate(
    result=loop_result,
    skill_name="web-search-best-practices",
    output_dir="./skills"
)
```

## CLI Commands

```bash
# Capture reasoning trace
rto capture "Search for Python tutorials" -s "You are a helpful assistant."

# Analyze a task
rto analyze "Debug this code" -o analysis.txt

# Run optimization loop
rto optimize "Research AI papers" --max-iterations 5 --generate-skill

# Generate skill from artifacts
rto generate-skill my-skill-name --artifacts-dir ./optimization_artifacts
```

## Integration with Claude Code

### Auto-trigger on Failure

Add to your hooks to automatically analyze failures:

```json
{
  "hooks": {
    "post_tool_error": {
      "command": "rto analyze-session --last-error"
    }
  }
}
```

### On-demand Analysis

Use the slash command to analyze current session:

```
/reasoning-trace-optimizer
```

This will:
1. Extract thinking blocks from the current session
2. Identify patterns and issues
3. Suggest prompt improvements
4. Optionally update the system prompt

## Guidelines

1. **Preserve full context**: M2.1 requires full response history including thinking blocks for optimal performance
2. **Use appropriate tools**: Define tools clearly with unambiguous descriptions
3. **Set realistic convergence thresholds**: 5-10% improvement per iteration is typical
4. **Review generated skills**: Auto-generated skills should be reviewed before sharing
5. **Monitor token usage**: Each optimization iteration uses significant tokens

## Examples

### Before Optimization

```
System: You are a helpful assistant.

Issue: Agent called wrong tools, lost track of goal after 3 turns
Score: 45/100
Patterns: tool_confusion, goal_abandonment
```

### After Optimization

```
System: You are a research assistant focused on finding accurate information.

IMPORTANT GUIDELINES:
- Always verify search results before summarizing
- If a tool returns an error, try an alternative approach
- Keep track of your original goal throughout the task
- Validate findings against multiple sources when possible

Issue: None
Score: 85/100
Patterns: None detected
```

## References

- MiniMax M2.1 Documentation: https://platform.minimax.io/docs
- Interleaved Thinking Guide: See `docs/interleavedthinking.md`
- Agent Generalization: See `docs/agentthinking.md`

---

## Skill Metadata

**Created**: 2025-01-11
**Author**: Muratcan Koylan
**Version**: 0.1.0
**Powered by**: MiniMax M2.1
**Partnership**: Built in collaboration with MiniMax AI

Scripts (12)

Read as text. anlyon.dev never runs them.

examples/01_basic_capture.pyShow
"""
Example 1: Basic Trace Capture

Demonstrates capturing reasoning traces from M2.1 for a simple task.
This shows how interleaved thinking provides visibility into agent decisions.
"""

import os
from pathlib import Path
from dotenv import load_dotenv

from reasoning_trace_optimizer import TraceCapture
from reasoning_trace_optimizer.capture import format_trace_for_display

# Load environment variables from the project root
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(env_path)


def main():
    """Run a simple task and capture the reasoning trace."""

    # Initialize capture with M2.1
    capture = TraceCapture(
        api_key=os.getenv("ANTHROPIC_API_KEY"),
        base_url="https://api.minimax.io/anthropic",
        model="MiniMax-M2.1",
    )

    # Define a simple task
    task = "Explain what interleaved thinking is and why it matters for AI agents."
    system_prompt = "You are an AI researcher explaining concepts clearly."

    print("=" * 60)
    print("BASIC TRACE CAPTURE EXAMPLE")
    print("=" * 60)
    print(f"\nTask: {task}")
    print(f"System Prompt: {system_prompt}")
    print("\nCapturing reasoning trace...\n")

    # Capture the trace
    trace = capture.run(
        task=task,
        system_prompt=system_prompt,
        max_turns=5,
    )

    # Display the trace
    print(format_trace_for_display(trace))

    # Summary statistics
    print("\n" + "=" * 60)
    print("TRACE STATISTICS")
    print("=" * 60)
    print(f"Session ID: {trace.session_id}")
    print(f"Model: {trace.model}")
    print(f"Success: {trace.success}")
    print(f"Total Turns: {trace.total_turns}")
    print(f"Thinking Blocks: {len(trace.thinking_blocks)}")
    print(f"Tool Calls: {len(trace.tool_calls)}")
    print(f"Total Tokens: {trace.total_tokens}")

    # Show each thinking block summary
    if trace.thinking_blocks:
        print("\n" + "=" * 60)
        print("THINKING BLOCK SUMMARIES")
        print("=" * 60)
        for i, thinking in enumerate(trace.thinking_blocks):
            print(f"\n[Turn {thinking.turn_index}] ({len(thinking.content)} chars)")
            # Show first 200 chars
            preview = thinking.content[:200].replace("\n", " ")
            print(f"  Preview: {preview}...")


if __name__ == "__main__":
    main()
examples/02_tool_usage.pyShow
"""
Example 2: Tool Usage with Trace Capture

Demonstrates how M2.1's interleaved thinking reasons between tool calls.
This is where interleaved thinking really shines - you can see the model
adapting to tool outputs in real-time.
"""

import json
import os
from pathlib import Path
from dotenv import load_dotenv

from reasoning_trace_optimizer import TraceCapture
from reasoning_trace_optimizer.capture import format_trace_for_display

# Load environment variables from the project root
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(env_path)


# Define mock tools
TOOLS = [
    {
        "name": "get_weather",
        "description": "Get current weather for a location. Returns temperature and conditions.",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City name, e.g., 'San Francisco, CA'",
                }
            },
            "required": ["location"],
        },
    },
    {
        "name": "get_forecast",
        "description": "Get 3-day weather forecast for a location.",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City name",
                },
                "days": {
                    "type": "integer",
                    "description": "Number of days (1-3)",
                    "default": 3,
                },
            },
            "required": ["location"],
        },
    },
]


# Mock tool executor
def execute_tool(name: str, input_data: dict) -> str:
    """Execute a mock tool and return results."""
    if name == "get_weather":
        location = input_data.get("location", "Unknown")
        # Simulate different weather for different cities
        if "san francisco" in location.lower():
            return json.dumps({
                "location": location,
                "temperature": "18°C",
                "conditions": "Foggy",
                "humidity": "85%",
            })
        elif "new york" in location.lower():
            return json.dumps({
                "location": location,
                "temperature": "22°C",
                "conditions": "Partly cloudy",
                "humidity": "60%",
            })
        else:
            return json.dumps({
                "location": location,
                "temperature": "20°C",
                "conditions": "Clear",
                "humidity": "50%",
            })

    elif name == "get_forecast":
        location = input_data.get("location", "Unknown")
        days = input_data.get("days", 3)
        forecast = []
        for i in range(days):
            forecast.append({
                "day": i + 1,
                "high": f"{20 + i * 2}°C",
                "low": f"{12 + i}°C",
                "conditions": ["Sunny", "Cloudy", "Rainy"][i % 3],
            })
        return json.dumps({
            "location": location,
            "forecast": forecast,
        })

    return json.dumps({"error": f"Unknown tool: {name}"})


def main():
    """Run a task with tools and observe interleaved thinking."""

    capture = TraceCapture(
        api_key=os.getenv("ANTHROPIC_API_KEY"),
        base_url="https://api.minimax.io/anthropic",
        model="MiniMax-M2.1",
    )

    task = """Compare the current weather in San Francisco and New York City.
    Then tell me which city would be better for outdoor activities this weekend."""

    system_prompt = """You are a helpful weather assistant.
    Use the available tools to get accurate weather information.
    Always provide specific data to support your recommendations."""

    print("=" * 60)
    print("TOOL USAGE WITH INTERLEAVED THINKING")
    print("=" * 60)
    print(f"\nTask: {task}")
    print(f"\nTools available: {', '.join(t['name'] for t in TOOLS)}")
    print("\nCapturing trace with tool usage...\n")

    # Capture the trace (using non-streaming for reliability)
    trace = capture.run(
        task=task,
        system_prompt=system_prompt,
        tools=TOOLS,
        tool_executor=execute_tool,
        max_turns=10,
    )

    print("\n\n" + "=" * 60)
    print("TRACE ANALYSIS")
    print("=" * 60)

    print(f"\nSuccess: {trace.success}")
    print(f"Total Turns: {trace.total_turns}")
    print(f"Thinking Blocks: {len(trace.thinking_blocks)}")
    print(f"Tool Calls: {len(trace.tool_calls)}")

    # Show how thinking evolved between tool calls
    print("\n" + "=" * 60)
    print("THINKING EVOLUTION ACROSS TOOL CALLS")
    print("=" * 60)

    for i, thinking in enumerate(trace.thinking_blocks):
        print(f"\n[Turn {thinking.turn_index}] Thinking Block {i + 1}")
        print("-" * 40)

        # Show what tool was called after this thinking
        turn_tools = trace.get_tool_calls_at_turn(thinking.turn_index)
        if turn_tools:
            print(f"Following action: Called {', '.join(t.name for t in turn_tools)}")
        else:
            print("Following action: Generated response")

        # Show key reasoning points (first 300 chars)
        print(f"\nReasoning preview:\n{thinking.content[:300]}...")

    # Show tool call results
    print("\n" + "=" * 60)
    print("TOOL CALL SUMMARY")
    print("=" * 60)

    for tc in trace.tool_calls:
        status = "✅" if tc.success else "❌"
        print(f"\n{status} {tc.name}")
        print(f"   Input: {json.dumps(tc.input)}")
        print(f"   Result: {tc.result[:100]}..." if tc.result and len(tc.result) > 100 else f"   Result: {tc.result}")

    # Final response
    if trace.final_response:
        print("\n" + "=" * 60)
        print("FINAL RESPONSE")
        print("=" * 60)
        print(trace.final_response)


if __name__ == "__main__":
    main()
examples/03_full_optimization.pyShow
"""
Example 3: Full Optimization Loop with Comprehensive Tools

Demonstrates the complete optimization cycle with realistic tools:
- Web search for finding information
- URL reading for fetching content
- File system operations (read, write, list)
- Note-taking for tracking findings

This example uses REAL URLs and realistic content to demonstrate
how the Reasoning Trace Optimizer works in production scenarios.
"""

import json
import os
import random
from datetime import datetime
from pathlib import Path

from dotenv import load_dotenv

from reasoning_trace_optimizer import (
    OptimizationLoop,
    LoopConfig,
    SkillGenerator,
)

# Load environment variables from the project root
env_path = Path(__file__).parent.parent / ".env"
load_dotenv(env_path)


# =============================================================================
# COMPREHENSIVE TOOL DEFINITIONS
# =============================================================================

TOOLS = [
    # Web Search Tool
    {
        "name": "web_search",
        "description": "Search the web for information. Returns a list of results with titles, URLs, and snippets. Use specific queries for better results.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Search query - be specific and use relevant keywords",
                },
                "num_results": {
                    "type": "integer",
                    "description": "Number of results to return (1-10, default 5)",
                    "default": 5,
                },
            },
            "required": ["query"],
        },
    },
    # Read URL Tool
    {
        "name": "read_url",
        "description": "Fetch and read the content of a webpage. Returns the main text content. Use after web_search to get full details from a result.",
        "input_schema": {
            "type": "object",
            "properties": {
                "url": {
                    "type": "string",
                    "description": "The URL to fetch content from",
                },
            },
            "required": ["url"],
        },
    },
    # File Read Tool
    {
        "name": "read_file",
        "description": "Read the contents of a local file. Supports text files, markdown, JSON, etc.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path to the file to read",
                },
            },
            "required": ["path"],
        },
    },
    # File Write Tool
    {
        "name": "write_file",
        "description": "Write content to a local file. Creates the file if it doesn't exist, overwrites if it does.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Path where to write the file",
                },
                "content": {
                    "type": "string",
                    "description": "Content to write to the file",
                },
            },
            "required": ["path", "content"],
        },
    },
    # List Directory Tool
    {
        "name": "list_directory",
        "description": "List files and folders in a directory. Useful for exploring project structure.",
        "input_schema": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "Directory path to list (default: current directory)",
                    "default": ".",
                },
            },
            "required": [],
        },
    },
    # Save Note Tool
    {
        "name": "save_note",
        "description": "Save a research note with title and content. Use to track important findings during research.",
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "Title of the note",
                },
                "content": {
                    "type": "string",
                    "description": "Content of the note",
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Optional tags for categorization",
                },
            },
            "required": ["title", "content"],
        },
    },
    # Calculator Tool
    {
        "name": "calculator",
        "description": "Perform mathematical calculations. Supports basic arithmetic and common functions.",
        "input_schema": {
            "type": "object",
            "properties": {
                "expression": {
                    "type": "string",
                    "description": "Mathematical expression to evaluate (e.g., '2 + 2', 'sqrt(16)', '100 * 0.15')",
                },
            },
            "required": ["expression"],
        },
    },
]


# =============================================================================
# REAL-WORLD SIMULATED DATA
# Based on actual documentation and research from AI companies
# =============================================================================

# Simulated web search results with REAL URLs
SEARCH_DATABASE = {
    "context engineering ai": [
        {
            "title": "Context Engineering for AI Agents - Anthropic",
            "url": "https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching",
            "snippet": "Prompt caching is a feature that optimizes API usage by allowing resuming from specific prefixes in your prompts. Cache the context you want to reuse across requests.",
        },
        {
            "title": "Building Effective AI Agents - Anthropic Research",
            "url": "https://www.anthropic.com/research/building-effective-agents",
            "snippet": "A comprehensive guide to building effective AI agents. Covers tool use, context management, error handling, and best practices for production deployments.",
        },
        {
            "title": "Large Language Models and Context Windows - OpenAI",
            "url": "https://platform.openai.com/docs/guides/text-generation",
            "snippet": "Understanding how context windows work in large language models. Learn about token limits, context management strategies, and optimizing for performance.",
        },
    ],
    "interleaved thinking agents": [
        {
            "title": "MiniMax M2.1 - Interleaved Thinking Model",
            "url": "https://www.minimax.io/platform/docs/M2.1",
            "snippet": "M2.1 introduces interleaved thinking - the ability for models to reason between tool calls, enabling better debugging and adaptability in agentic workflows.",
        },
        {
            "title": "Chain of Thought Prompting - Google Research",
            "url": "https://arxiv.org/abs/2201.11903",
            "snippet": "Chain-of-thought prompting enables complex reasoning in large language models. This paper explores how step-by-step reasoning improves model performance.",
        },
    ],
    "prompt optimization techniques": [
        {
            "title": "Prompt Engineering Guide - DAIR.AI",
            "url": "https://www.promptingguide.ai/techniques",
            "snippet": "Comprehensive guide to prompt engineering techniques including zero-shot, few-shot, chain-of-thought, and advanced methods for optimizing LLM outputs.",
        },
        {
            "title": "Best Practices for Prompt Engineering - OpenAI",
            "url": "https://platform.openai.com/docs/guides/prompt-engineering",
            "snippet": "Official OpenAI guide on prompt engineering best practices. Covers strategies for getting better results, handling edge cases, and iterative refinement.",
        },
    ],
    "agent debugging best practices": [
        {
            "title": "Debugging AI Agents - LangChain Documentation",
            "url": "https://python.langchain.com/docs/how_to/debugging",
            "snippet": "Learn how to debug LangChain agents effectively. Covers tracing, verbose mode, callbacks, and common debugging patterns for complex agent workflows.",
        },
        {
            "title": "LLM Observability and Tracing - Weights & Biases",
            "url": "https://docs.wandb.ai/guides/prompts",
            "snippet": "Track and debug LLM applications with W&B Prompts. Visualize chains, compare outputs, and identify failure patterns in your AI applications.",
        },
    ],
    "context window optimization": [
        {
            "title": "Claude's Context Window - Anthropic Documentation",
            "url": "https://docs.anthropic.com/en/docs/build-with-claude/context-windows",
            "snippet": "Claude supports context windows up to 200K tokens. Learn how to effectively use large context windows and optimize token usage for cost and performance.",
        },
        {
            "title": "Lost in the Middle: How Language Models Use Long Contexts",
            "url": "https://arxiv.org/abs/2307.03172",
            "snippet": "Research on how LLMs utilize information across long contexts. Models perform worse when relevant info is in the middle vs. beginning/end of context.",
        },
    ],
}

# Simulated webpage content based on REAL documentation
PAGE_CONTENT = {
    "https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching": """
# Prompt Caching - Anthropic Documentation

Prompt caching is a feature that optimizes API usage by allowing you to cache frequently used context.

## Overview

Prompt caching allows you to cache the system prompt, examples, and other static content that remains constant across multiple requests. This:

- **Reduces latency** by up to 85% for cached content
- **Lowers costs** by avoiding re-processing of identical context
- **Improves throughput** for high-volume applications

## How It Works

When you enable prompt caching, the API stores a hash of your prompt prefix. On subsequent requests with the same prefix, the cached computation is reused.

### Cache Breakpoints

You can specify cache breakpoints using the `cache_control` parameter:

```python
messages = [
    {
        "role": "user",
        "content": [
            {
                "type": "text",
                "text": "Your static context here...",
                "cache_control": {"type": "ephemeral"}
            }
        ]
    }
]
```

## Best Practices

1. **Cache stable content**: Put instructions and examples that don't change in the cached portion
2. **Place dynamic content last**: User queries and variable data should come after cached content
3. **Monitor cache hits**: Use the response headers to track cache efficiency
4. **Minimum cache size**: Content must be at least 1024 tokens to be cached

## Context Engineering Implications

Effective prompt caching is a key part of context engineering. By understanding what to cache:

- System prompts with role definitions
- Tool descriptions that remain constant
- Few-shot examples for consistent behavior
- Reference documentation the model needs

You reduce both latency and cost while maintaining quality.
""",
    "https://www.anthropic.com/research/building-effective-agents": """
# Building Effective AI Agents - Anthropic Research

This guide covers best practices for building reliable, effective AI agents using Claude.

## Core Principles

### 1. Start Simple, Add Complexity Gradually

Begin with the simplest possible agent architecture:
- Single tool with clear purpose
- Linear workflow without branching
- Explicit success criteria

Only add complexity when you have evidence it's needed.

### 2. Tool Design Matters

Well-designed tools make agents more reliable:

- **Clear descriptions**: Explain what the tool does AND when to use it
- **Typed inputs**: Use JSON Schema to define expected parameters
- **Informative outputs**: Return data the model can interpret and act on
- **Error messages**: Provide actionable guidance when things fail

### 3. Context Management

Context is your most precious resource:

- **Token efficiency**: Every token costs money and attention
- **Structured format**: Use consistent formatting for easier parsing
- **Progressive disclosure**: Load information on-demand
- **Summarization**: Compress long histories while preserving key facts

### 4. Error Handling

Agents will encounter errors. Design for recovery:

- Give the model explicit permission to retry
- Provide diagnostic information in error messages
- Set clear stopping conditions to prevent infinite loops
- Log everything for debugging

## Common Anti-Patterns

1. **Over-engineering**: Building complex multi-agent systems before validating single-agent performance
2. **Vague tools**: Tool descriptions that don't clarify when to use each tool
3. **Context overload**: Stuffing too much information into the prompt
4. **No exit conditions**: Letting agents run indefinitely without progress checks

## Debugging Strategies

### Trace Analysis

The key to debugging agents is understanding their reasoning:

1. Capture the full reasoning trace including thinking blocks
2. Identify where the agent's understanding diverged from reality
3. Look for patterns: tool confusion, goal drift, context loss
4. Iterate on prompts based on specific failure modes

### Interleaved Thinking

Models with interleaved thinking (reasoning between tool calls) provide better debugging insight because you can see:

- How they interpreted each tool result
- What alternatives they considered
- When and why they changed approach
""",
    "https://platform.openai.com/docs/guides/text-generation": """
# Text Generation - OpenAI Documentation

Learn how to generate text with OpenAI's models.

## Context Windows

Each model has a context window that determines the maximum number of tokens it can process:

| Model | Context Window |
|-------|----------------|
| GPT-4o | 128K tokens |
| GPT-4 Turbo | 128K tokens |
| GPT-3.5 Turbo | 16K tokens |

### Managing Context

For long conversations or documents:

1. **Truncation**: Remove oldest messages when approaching the limit
2. **Summarization**: Replace old messages with summaries
3. **Retrieval**: Use RAG to fetch only relevant content

### Token Counting

Use the tiktoken library to count tokens before sending requests:

```python
import tiktoken

encoding = tiktoken.encoding_for_model("gpt-4")
num_tokens = len(encoding.encode("Your text here"))
```

## Best Practices

### Structured Prompts

Organize your prompts with clear sections:
- System message: Role and general instructions
- Context: Background information needed
- Task: Specific request with format requirements
- Examples: Few-shot demonstrations if helpful

### Temperature and Sampling

- **temperature=0**: Deterministic, best for factual tasks
- **temperature=0.7**: Balanced creativity and coherence
- **temperature=1.0+**: More random, for creative tasks
""",
    "https://www.minimax.io/platform/docs/M2.1": """
# MiniMax M2.1 - Interleaved Thinking Model

M2.1 is a next-generation reasoning model that introduces **interleaved thinking** - continuous reasoning throughout task execution.

## What is Interleaved Thinking?

Traditional reasoning models think once at the start, then execute:
```
Think → Act → Act → Act → Done
```

M2.1 thinks between every action:
```
Think → Act → Think → Act → Think → Act → Done
```

## Why This Matters

### 1. Better Debugging

The thinking blocks expose the model's reasoning process. You can see:
- What it understood from tool results
- How it decided what to do next
- Where it might have gone wrong

### 2. Adaptive Behavior

By reasoning after each tool call, M2.1 can:
- React to unexpected outputs
- Recover from errors mid-execution
- Adjust strategy based on new information

### 3. Long-Horizon Tasks

For complex multi-step tasks, maintaining focus is crucial. Interleaved thinking:
- Reinforces the original goal
- Tracks progress toward completion
- Identifies when the task is done

## API Usage

### Anthropic SDK

```python
import anthropic

client = anthropic.Anthropic(
    api_key="your-key",
    base_url="https://api.minimax.io/anthropic"
)

response = client.messages.create(
    model="MiniMax-M2.1",
    max_tokens=4096,
    messages=[{"role": "user", "content": "Your task"}]
)

# Access thinking blocks
for block in response.content:
    if block.type == "thinking":
        print(f"Thinking: {block.thinking}")
    elif block.type == "text":
        print(f"Response: {block.text}")
```

## Best Practices

1. **Preserve full context**: Always include thinking blocks in message history
2. **Clear tool descriptions**: Help the model understand when to use each tool
3. **Explicit success criteria**: Define what "done" looks like
4. **Error guidance**: Give clear instructions for handling failures
""",
    "https://www.promptingguide.ai/techniques": """
# Prompt Engineering Techniques - DAIR.AI

A comprehensive guide to prompt engineering techniques for large language models.

## Basic Techniques

### Zero-Shot Prompting

Ask the model to perform a task without examples:

```
Classify this text as positive, negative, or neutral:
"I really enjoyed the movie but the ending was disappointing."
```

### Few-Shot Prompting

Provide examples to guide the model:

```
Classify sentiment:
"Great product!" → Positive
"Terrible service." → Negative
"It was okay." → Neutral
"I really enjoyed the movie but the ending was disappointing." →
```

## Advanced Techniques

### Chain-of-Thought (CoT)

Encourage step-by-step reasoning:

```
Solve this problem step by step:
If John has 5 apples and gives 2 to Mary, then buys 3 more, how many does he have?

Let's think through this:
1. John starts with 5 apples
2. He gives 2 to Mary: 5 - 2 = 3 apples
3. He buys 3 more: 3 + 3 = 6 apples
Answer: 6 apples
```

### Self-Consistency

Generate multiple reasoning paths and take the majority answer. Improves reliability for complex reasoning tasks.

### Tree of Thoughts

Explore multiple reasoning branches simultaneously, evaluating and pruning paths to find optimal solutions.

## Prompt Optimization

### Iterative Refinement

1. Start with a basic prompt
2. Test on representative examples
3. Analyze failures
4. Refine prompt based on patterns
5. Repeat until convergence

### Common Failure Patterns

| Pattern | Solution |
|---------|----------|
| Goal drift | Add explicit goal reminders |
| Hallucination | Require source citations |
| Incomplete output | Specify format requirements |
| Wrong tool usage | Improve tool descriptions |
""",
    "https://platform.openai.com/docs/guides/prompt-engineering": """
# Prompt Engineering Best Practices - OpenAI

Official guide to getting better results from large language models.

## Six Strategies

### 1. Write Clear Instructions

Be specific about what you want:
- Include details in your query
- Ask the model to adopt a persona
- Use delimiters to mark distinct sections
- Specify desired output format and length

### 2. Provide Reference Text

Reduce hallucinations:
- Instruct the model to answer using provided text
- Ask for citations from the source material
- Use retrieval to inject relevant context

### 3. Split Complex Tasks

Break down hard problems:
- Use intent classification to route queries
- Summarize long documents in chunks
- Break multi-step tasks into sequential prompts

### 4. Give the Model Time to Think

Improve reasoning:
- Ask for a chain of reasoning
- Use inner monologue to hide intermediate steps
- Ask if previous steps were correct

### 5. Use External Tools

Augment model capabilities:
- Use code execution for accurate calculations
- Use retrieval for up-to-date information
- Use … [19936 more characters not shown]
reasoning_trace_optimizer/__init__.pyShow
"""
Reasoning Trace Optimizer

Debug and optimize AI agents by analyzing reasoning traces
using MiniMax M2.1's interleaved thinking capabilities.
"""

from reasoning_trace_optimizer.models import (
    ReasoningTrace,
    ThinkingBlock,
    ToolCall,
    Pattern,
    PatternType,
    Severity,
    AnalysisResult,
    OptimizationResult,
    PromptDiff,
    LoopIteration,
    LoopResult,
)
from reasoning_trace_optimizer.capture import TraceCapture
from reasoning_trace_optimizer.analyzer import TraceAnalyzer
from reasoning_trace_optimizer.optimizer import PromptOptimizer
from reasoning_trace_optimizer.loop import OptimizationLoop, LoopConfig
from reasoning_trace_optimizer.skill_generator import SkillGenerator

__version__ = "0.1.0"

__all__ = [
    # Models
    "ReasoningTrace",
    "ThinkingBlock",
    "ToolCall",
    "Pattern",
    "PatternType",
    "Severity",
    "AnalysisResult",
    "OptimizationResult",
    "PromptDiff",
    "LoopIteration",
    "LoopResult",
    # Capture
    "TraceCapture",
    # Analyzer
    "TraceAnalyzer",
    # Optimizer
    "PromptOptimizer",
    # Loop
    "OptimizationLoop",
    "LoopConfig",
    # Skill Generator
    "SkillGenerator",
]
reasoning_trace_optimizer/analyzer.pyShow
"""
TraceAnalyzer: Analyzes reasoning traces to detect patterns and issues.

Uses M2.1's own interleaved thinking to analyze agent reasoning traces,
detecting patterns like context degradation, tool confusion, and instruction drift.
"""

import json
import os
from typing import Any

import anthropic

from reasoning_trace_optimizer.models import (
    AnalysisResult,
    Pattern,
    PatternType,
    ReasoningTrace,
    Severity,
)


ANALYSIS_SYSTEM_PROMPT = """You are an expert AI agent debugger specializing in analyzing reasoning traces.

Your task is to analyze an agent's interleaved thinking trace and identify:
1. **Patterns of failure** - detect specific failure modes with evidence
2. **Quality scores** - rate the agent's reasoning on multiple dimensions
3. **Actionable recommendations** - specific improvements for prompts/instructions

## Pattern Definitions

Detect these patterns with specific evidence from thinking blocks:

- **context_degradation**: Agent loses or forgets information from earlier in the conversation
  - Look for: Repeated questions, contradicting earlier statements, missing key details
- **tool_confusion**: Agent misunderstands what a tool does or how to use it
  - Look for: Wrong tool selection, incorrect parameters, misinterpreting results
- **instruction_drift**: Agent gradually deviates from original instructions/persona
  - Look for: Changing behavior, ignoring constraints, different tone over time
- **hallucination**: Agent generates information not supported by context or tools
  - Look for: Made-up facts, fabricated tool results, unsourced claims
- **incomplete_reasoning**: Agent reaches conclusions without thorough analysis
  - Look for: Skipped steps, missing validation, superficial exploration
- **tool_misuse**: Agent uses tools incorrectly or inefficiently
  - Look for: Redundant calls, wrong parameters, unused results
- **goal_abandonment**: Agent stops pursuing the original objective
  - Look for: Topic drift, giving up, switching goals without reason
- **circular_reasoning**: Agent repeats similar actions without progress
  - Look for: Same queries repeated, looping behavior, no new information
- **premature_conclusion**: Agent concludes before completing the task
  - Look for: Early stops, incomplete answers, skipped requirements
- **missing_validation**: Agent doesn't verify results or assumptions
  - Look for: No cross-checking, accepting first result, no error handling

## Analysis Focus

You have access to the FULL reasoning trace including all thinking blocks between tool calls.
This gives you unique insight into HOW the agent reasons, not just what it outputs.

For each thinking block, examine:
- What is the agent's current understanding?
- How does it interpret tool results?
- What alternatives does it consider?
- Does it maintain awareness of the original goal?

Provide your analysis in the specified JSON format with concrete evidence."""


ANALYSIS_PROMPT_TEMPLATE = """Analyze the following agent reasoning trace:

## Task
{task}

## System Prompt Given to Agent
{system_prompt}

## Reasoning Trace
{trace}

## Tool Calls Made
{tool_calls}

## Final Outcome
Success: {success}
Final Response: {final_response}
Error (if any): {error}

---

Provide your analysis as JSON with this exact structure:
```json
{{
    "patterns": [
        {{
            "type": "<one of: context_degradation, tool_confusion, instruction_drift, hallucination, incomplete_reasoning, tool_misuse, goal_abandonment, circular_reasoning, premature_conclusion, missing_validation>",
            "severity": "<one of: low, medium, high, critical>",
            "description": "<what the pattern is>",
            "evidence": ["<excerpt from thinking>", "<another excerpt>"],
            "turn_indices": [0, 2],
            "suggestion": "<how to fix this>",
            "confidence": 0.85
        }}
    ],
    "scores": {{
        "reasoning_clarity": 75,
        "goal_adherence": 80,
        "tool_usage_quality": 60,
        "error_recovery": 50,
        "overall": 66
    }},
    "strengths": ["<strength 1>", "<strength 2>"],
    "weaknesses": ["<weakness 1>", "<weakness 2>"],
    "recommendations": [
        "<specific actionable recommendation>",
        "<another recommendation>"
    ]
}}
```

Think carefully about each aspect before providing your analysis."""


class TraceAnalyzer:
    """
    Analyzes reasoning traces using M2.1 to detect patterns and score quality.

    The analyzer uses M2.1's interleaved thinking to deeply understand
    the agent's reasoning process and identify issues that wouldn't be
    visible from outputs alone.

    Example:
        ```python
        analyzer = TraceAnalyzer()
        result = analyzer.analyze(trace)

        print(f"Overall score: {result.overall_score}")
        for pattern in result.patterns:
            print(f"Found: {pattern.type.value} ({pattern.severity.value})")
        ```
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = "https://api.minimax.io/anthropic",
        model: str = "MiniMax-M2.1",
    ):
        """
        Initialize TraceAnalyzer with M2.1 configuration.

        Args:
            api_key: MiniMax API key
            base_url: API endpoint
            model: Model for analysis (M2.1 recommended for best results)
        """
        self.model = model
        self.client = anthropic.Anthropic(
            api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"),
            base_url=base_url,
        )

    def analyze(
        self,
        trace: ReasoningTrace,
        max_tokens: int = 8192,
    ) -> AnalysisResult:
        """
        Analyze a reasoning trace and return detailed analysis.

        Args:
            trace: The reasoning trace to analyze
            max_tokens: Maximum tokens for analysis response

        Returns:
            AnalysisResult with patterns, scores, and recommendations
        """
        # Format trace for analysis
        trace_text = self._format_trace_for_analysis(trace)
        tool_calls_text = self._format_tool_calls(trace)

        prompt = ANALYSIS_PROMPT_TEMPLATE.format(
            task=trace.task,
            system_prompt=trace.system_prompt,
            trace=trace_text,
            tool_calls=tool_calls_text,
            success=trace.success,
            final_response=trace.final_response or "None",
            error=trace.error or "None",
        )

        # Call M2.1 for analysis
        response = self.client.messages.create(
            model=self.model,
            max_tokens=max_tokens,
            system=ANALYSIS_SYSTEM_PROMPT,
            messages=[{"role": "user", "content": prompt}],
        )

        # Extract thinking and text from response
        analyzer_thinking = ""
        analysis_text = ""

        for block in response.content:
            if block.type == "thinking":
                analyzer_thinking = block.thinking
            elif block.type == "text":
                analysis_text = block.text

        # Parse the JSON response
        result = self._parse_analysis_response(analysis_text, trace.session_id)
        result.analyzer_thinking = analyzer_thinking
        result.analyzer_model = self.model

        return result

    def analyze_batch(
        self,
        traces: list[ReasoningTrace],
    ) -> list[AnalysisResult]:
        """Analyze multiple traces and return results."""
        return [self.analyze(trace) for trace in traces]

    def quick_score(
        self,
        trace: ReasoningTrace,
    ) -> float:
        """
        Get a quick overall score without full pattern analysis.

        Useful for optimization loops where you need fast feedback.

        Args:
            trace: The reasoning trace to score

        Returns:
            Overall score from 0-100
        """
        quick_prompt = f"""Rate this agent's performance from 0-100 based on its reasoning trace.

Task: {trace.task}
Success: {trace.success}
Turns: {trace.total_turns}

Thinking excerpts:
{self._get_thinking_excerpts(trace, max_chars=2000)}

Respond with ONLY a number from 0-100."""

        response = self.client.messages.create(
            model=self.model,
            max_tokens=100,
            messages=[{"role": "user", "content": quick_prompt}],
        )

        # Extract score from response
        for block in response.content:
            if block.type == "text":
                try:
                    score = float(block.text.strip())
                    return min(100, max(0, score))
                except ValueError:
                    pass

        return 50.0  # Default middle score if parsing fails

    def _format_trace_for_analysis(self, trace: ReasoningTrace) -> str:
        """Format thinking blocks for analysis."""
        parts = []
        for i, thinking in enumerate(trace.thinking_blocks):
            parts.append(f"[Turn {thinking.turn_index}] Thinking:")
            parts.append(thinking.content)
            parts.append("")

        return "\n".join(parts)

    def _format_tool_calls(self, trace: ReasoningTrace) -> str:
        """Format tool calls for analysis."""
        if not trace.tool_calls:
            return "No tool calls made."

        parts = []
        for tc in trace.tool_calls:
            status = "Success" if tc.success else f"Failed: {tc.error}"
            parts.append(
                f"- {tc.name}({json.dumps(tc.input)}) -> {status}\n"
                f"  Result: {tc.result[:200] if tc.result else 'None'}..."
            )

        return "\n".join(parts)

    def _get_thinking_excerpts(self, trace: ReasoningTrace, max_chars: int = 2000) -> str:
        """Get excerpts from thinking blocks."""
        excerpts = []
        remaining = max_chars

        for thinking in trace.thinking_blocks:
            if remaining <= 0:
                break
            excerpt = thinking.content[:remaining]
            excerpts.append(f"[Turn {thinking.turn_index}]: {excerpt}")
            remaining -= len(excerpt) + 20

        return "\n\n".join(excerpts)

    def _parse_analysis_response(
        self,
        response_text: str,
        trace_id: str,
    ) -> AnalysisResult:
        """Parse the JSON analysis response from M2.1."""
        result = AnalysisResult(trace_id=trace_id)

        try:
            # Extract JSON from response (may have markdown code blocks)
            json_text = response_text
            if "```json" in response_text:
                json_text = response_text.split("```json")[1].split("```")[0]
            elif "```" in response_text:
                json_text = response_text.split("```")[1].split("```")[0]

            data = json.loads(json_text)

            # Parse patterns
            for p in data.get("patterns", []):
                try:
                    pattern = Pattern(
                        type=PatternType(p["type"]),
                        severity=Severity(p["severity"]),
                        description=p["description"],
                        evidence=p.get("evidence", []),
                        turn_indices=p.get("turn_indices", []),
                        suggestion=p.get("suggestion", ""),
                        confidence=p.get("confidence", 0.5),
                    )
                    result.patterns.append(pattern)
                except (KeyError, ValueError):
                    continue

            # Parse scores
            scores = data.get("scores", {})
            result.reasoning_clarity = scores.get("reasoning_clarity", 0)
            result.goal_adherence = scores.get("goal_adherence", 0)
            result.tool_usage_quality = scores.get("tool_usage_quality", 0)
            result.error_recovery = scores.get("error_recovery", 0)
            result.overall_score = scores.get("overall", 0)

            # Parse feedback
            result.strengths = data.get("strengths", [])
            result.weaknesses = data.get("weaknesses", [])
            result.recommendations = data.get("recommendations", [])

        except (json.JSONDecodeError, KeyError) as e:
            # If parsing fails, try fallback extraction and set reasonable defaults
            result = self._fallback_parse_analysis(response_text, trace_id, str(e))

        # Warn if score is suspiciously low (likely parsing failure)
        if result.overall_score == 0 and not result.patterns:
            result.weaknesses.append("WARNING: Analysis may have failed - score is 0 with no patterns detected")
            # Try to extract a score from the response text as fallback
            fallback_score = self._extract_fallback_score(response_text)
            if fallback_score > 0:
                result.overall_score = fallback_score
                result.recommendations.append(f"Score extracted via fallback: {fallback_score}")

        return result

    def _fallback_parse_analysis(
        self,
        response_text: str,
        trace_id: str,
        error_msg: str,
    ) -> AnalysisResult:
        """Fallback parsing when JSON extraction fails."""
        import re

        result = AnalysisResult(trace_id=trace_id)

        # Try to extract score from text patterns like "Overall Score: 75" or "overall": 75
        score_patterns = [
            r'overall["\s:]+(\d+)',
            r'Overall Score[:\s]+(\d+)',
            r'"overall"[:\s]+(\d+)',
            r'Score[:\s]+(\d+)/100',
        ]

        for pattern in score_patterns:
            match = re.search(pattern, response_text, re.IGNORECASE)
            if match:
                result.overall_score = min(100, max(0, int(match.group(1))))
                break

        # If still no score, use a neutral default (not 0)
        if result.overall_score == 0:
            result.overall_score = 50  # Neutral default instead of 0

        result.recommendations = [
            f"Analysis parsing failed ({error_msg}). Using fallback extraction.",
            "Consider re-running analysis if results seem inconsistent."
        ]
        result.weaknesses = ["JSON parsing failed - analysis may be incomplete"]

        return result

    def _extract_fallback_score(self, response_text: str) -> float:
        """Extract a score from response text when JSON parsing fails."""
        import re

        patterns = [
            r'overall["\s:]+(\d+)',
            r'Overall Score[:\s]+(\d+)',
            r'"overall"[:\s]+(\d+)',
            r'(\d+)/100',
            r'score[:\s]+(\d+)',
        ]

        for pattern in patterns:
            match = re.search(pattern, response_text, re.IGNORECASE)
            if match:
                score = int(match.group(1))
                if 0 <= score <= 100:
                    return float(score)

        return 0.0


def format_analysis_report(analysis: AnalysisResult) -> str:
    """Format an analysis result as a human-readable report."""
    lines = [
        "=" * 60,
        "REASONING TRACE ANALYSIS REPORT",
        "=" * 60,
        "",
        f"Overall Score: {analysis.overall_score}/100",
        "",
        "Scores:",
        f"  - Reasoning Clarity: {analysis.reasoning_clarity}/100",
        f"  - Goal Adherence: {analysis.goal_adherence}/100",
        f"  - Tool Usage Quality: {analysis.tool_usage_quality}/100",
        f"  - Error Recovery: {analysis.error_recovery}/100",
        "",
    ]

    if analysis.patterns:
        lines.append("Detected Patterns:")
        for p in analysis.patterns:
            lines.append(f"\n  [{p.severity.value.upper()}] {p.type.value}")
            lines.append(f"    {p.description}")
            lines.append(f"    Suggestion: {p.suggestion}")

    if analysis.strengths:
        lines.append("\nStrengths:")
        for s in analysis.strengths:
            lines.append(f"  + {s}")

    if analysis.weaknesses:
        lines.append("\nWeaknesses:")
        for w in analysis.weaknesses:
            lines.append(f"  - {w}")

    if analysis.recommendations:
        lines.append("\nRecommendations:")
        for i, r in enumerate(analysis.recommendations, 1):
            lines.append(f"  {i}. {r}")

    return "\n".join(lines)
reasoning_trace_optimizer/capture.pyShow
"""
TraceCapture: Wraps M2.1 API to capture interleaved thinking traces.

This module provides the core functionality for executing agent tasks
through MiniMax M2.1 while capturing all reasoning traces for analysis.
"""

import json
import os
import uuid
from datetime import datetime
from typing import Any, Callable

import anthropic

from reasoning_trace_optimizer.models import (
    ReasoningTrace,
    ThinkingBlock,
    ToolCall,
)


class TraceCapture:
    """
    Captures reasoning traces from MiniMax M2.1's interleaved thinking.

    This class wraps the Anthropic SDK configured for M2.1 and captures
    all thinking blocks, tool calls, and responses during agent execution.

    Example:
        ```python
        capture = TraceCapture()
        trace = capture.run(
            task="What's the weather in San Francisco?",
            tools=[weather_tool],
            tool_executor=execute_tool
        )
        print(f"Captured {len(trace.thinking_blocks)} thinking blocks")
        ```
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = "https://api.minimax.io/anthropic",
        model: str = "MiniMax-M2.1",
    ):
        """
        Initialize TraceCapture with M2.1 configuration.

        Args:
            api_key: MiniMax API key (defaults to ANTHROPIC_API_KEY env var)
            base_url: API base URL (international or China endpoint)
            model: Model to use (MiniMax-M2.1, MiniMax-M2.1-lightning, MiniMax-M2)
        """
        self.model = model
        self.client = anthropic.Anthropic(
            api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"),
            base_url=base_url,
        )

    def run(
        self,
        task: str,
        system_prompt: str = "You are a helpful assistant.",
        tools: list[dict[str, Any]] | None = None,
        tool_executor: Callable[[str, dict], str] | None = None,
        max_turns: int = 10,
        max_tokens: int = 4096,
    ) -> ReasoningTrace:
        """
        Execute a task and capture the full reasoning trace.

        Args:
            task: The user task/query to execute
            system_prompt: System prompt for the agent
            tools: List of tool definitions in Anthropic format
            tool_executor: Function to execute tool calls (name, input) -> result
            max_turns: Maximum conversation turns before stopping
            max_tokens: Maximum tokens per response

        Returns:
            ReasoningTrace containing all thinking blocks, tool calls, and responses
        """
        trace = ReasoningTrace(
            session_id=str(uuid.uuid4()),
            task=task,
            system_prompt=system_prompt,
            model=self.model,
            started_at=datetime.now(),
        )

        messages = [{"role": "user", "content": task}]
        turn = 0

        try:
            while turn < max_turns:
                # Build request parameters
                params = {
                    "model": self.model,
                    "max_tokens": max_tokens,
                    "system": system_prompt,
                    "messages": messages,
                }
                if tools:
                    params["tools"] = tools

                # Make API call
                response = self.client.messages.create(**params)

                # Process response content blocks
                thinking_blocks, text_blocks, tool_use_blocks = self._process_response(
                    response, turn, trace
                )

                # If no tool calls, we're done
                if not tool_use_blocks:
                    trace.final_response = (
                        text_blocks[0].text if text_blocks else None
                    )
                    trace.success = True
                    break

                # Append assistant response to history (CRITICAL for M2.1)
                messages.append({"role": "assistant", "content": response.content})

                # Execute tools and collect results
                tool_results = []
                for tool_block in tool_use_blocks:
                    result = self._execute_tool(
                        tool_block, tool_executor, turn, trace
                    )
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": tool_block.id,
                            "content": result,
                        }
                    )

                # Add tool results to messages
                messages.append({"role": "user", "content": tool_results})

                turn += 1
                trace.total_turns = turn

            # Check if we hit max turns without completion
            if turn >= max_turns and not trace.success:
                trace.success = False
                trace.error = f"Reached maximum turns ({max_turns}) without completion"

        except Exception as e:
            trace.success = False
            trace.error = str(e)

        trace.completed_at = datetime.now()
        return trace

    def _process_response(
        self,
        response: anthropic.types.Message,
        turn: int,
        trace: ReasoningTrace,
    ) -> tuple[list, list, list]:
        """Process response content blocks and update trace."""
        thinking_blocks = []
        text_blocks = []
        tool_use_blocks = []

        for block in response.content:
            if block.type == "thinking":
                thinking = ThinkingBlock(
                    content=block.thinking,
                    turn_index=turn,
                    signature=getattr(block, "signature", None),
                )
                trace.thinking_blocks.append(thinking)
                thinking_blocks.append(block)

            elif block.type == "text":
                text_blocks.append(block)

            elif block.type == "tool_use":
                tool_use_blocks.append(block)

        # Update token count
        trace.total_tokens += response.usage.input_tokens + response.usage.output_tokens

        return thinking_blocks, text_blocks, tool_use_blocks

    def _execute_tool(
        self,
        tool_block: Any,
        executor: Callable[[str, dict], str] | None,
        turn: int,
        trace: ReasoningTrace,
    ) -> str:
        """Execute a tool call and record it in the trace."""
        tool_call = ToolCall(
            id=tool_block.id,
            name=tool_block.name,
            input=tool_block.input,
            turn_index=turn,
        )

        try:
            if executor:
                result = executor(tool_block.name, tool_block.input)
            else:
                result = f"[Mock result for {tool_block.name}]"

            tool_call.result = result
            tool_call.success = True

        except Exception as e:
            result = f"Error: {str(e)}"
            tool_call.result = result
            tool_call.success = False
            tool_call.error = str(e)

        trace.tool_calls.append(tool_call)

        # Link thinking to tool call
        if trace.thinking_blocks:
            last_thinking = trace.thinking_blocks[-1]
            if last_thinking.turn_index == turn:
                last_thinking.following_action = f"tool_use:{tool_block.name}"

        return result

    def run_streaming(
        self,
        task: str,
        system_prompt: str = "You are a helpful assistant.",
        tools: list[dict[str, Any]] | None = None,
        tool_executor: Callable[[str, dict], str] | None = None,
        max_turns: int = 10,
        max_tokens: int = 4096,
        on_thinking: Callable[[str], None] | None = None,
        on_text: Callable[[str], None] | None = None,
        on_tool_call: Callable[[str, dict], None] | None = None,
        on_error: Callable[[str], None] | None = None,
    ) -> ReasoningTrace:
        """
        Execute a task with streaming output and capture reasoning trace.

        Similar to run() but streams thinking and text content in real-time
        via callback functions.

        Note: For multi-turn tool interactions, the non-streaming run() method
        is recommended as it provides more reliable trace capture. Use this
        method when you need real-time display of thinking/text content.

        Args:
            task: The user task/query to execute
            system_prompt: System prompt for the agent
            tools: List of tool definitions
            tool_executor: Function to execute tool calls
            max_turns: Maximum conversation turns
            max_tokens: Maximum tokens per response
            on_thinking: Callback for thinking content chunks
            on_text: Callback for text content chunks
            on_tool_call: Callback when tool is called (name, input)
            on_error: Callback when an error occurs (error message)

        Returns:
            ReasoningTrace containing the full captured trace
        """
        trace = ReasoningTrace(
            session_id=str(uuid.uuid4()),
            task=task,
            system_prompt=system_prompt,
            model=self.model,
            started_at=datetime.now(),
        )

        messages = [{"role": "user", "content": task}]
        turn = 0

        try:
            while turn < max_turns:
                params = {
                    "model": self.model,
                    "max_tokens": max_tokens,
                    "system": system_prompt,
                    "messages": messages,
                    "stream": True,
                }
                if tools:
                    params["tools"] = tools

                # Collect streamed content
                thinking_buffer = ""
                text_buffer = ""
                tool_use_blocks = []
                current_content = []

                with self.client.messages.stream(**params) as stream:
                    for event in stream:
                        if event.type == "content_block_start":
                            if hasattr(event, "content_block"):
                                current_content.append(event.content_block)

                        elif event.type == "content_block_delta":
                            if hasattr(event, "delta"):
                                if event.delta.type == "thinking_delta":
                                    chunk = event.delta.thinking
                                    thinking_buffer += chunk
                                    if on_thinking:
                                        on_thinking(chunk)

                                elif event.delta.type == "text_delta":
                                    chunk = event.delta.text
                                    text_buffer += chunk
                                    if on_text:
                                        on_text(chunk)

                    # Get final message for tool_use blocks
                    final_message = stream.get_final_message()
                    for block in final_message.content:
                        if block.type == "tool_use":
                            tool_use_blocks.append(block)
                            if on_tool_call:
                                on_tool_call(block.name, block.input)

                # Record thinking block
                if thinking_buffer:
                    trace.thinking_blocks.append(
                        ThinkingBlock(
                            content=thinking_buffer,
                            turn_index=turn,
                        )
                    )

                # Update tokens
                trace.total_tokens += (
                    final_message.usage.input_tokens + final_message.usage.output_tokens
                )

                # If no tool calls, we're done
                if not tool_use_blocks:
                    trace.final_response = text_buffer or None
                    trace.success = True
                    break

                # Append to history
                messages.append({"role": "assistant", "content": final_message.content})

                # Execute tools
                tool_results = []
                for tool_block in tool_use_blocks:
                    result = self._execute_tool(tool_block, tool_executor, turn, trace)
                    tool_results.append(
                        {
                            "type": "tool_result",
                            "tool_use_id": tool_block.id,
                            "content": result,
                        }
                    )

                messages.append({"role": "user", "content": tool_results})
                turn += 1
                trace.total_turns = turn

            if turn >= max_turns and not trace.success:
                trace.success = False
                trace.error = f"Reached maximum turns ({max_turns})"

        except Exception as e:
            trace.success = False
            trace.error = str(e)
            if on_error:
                on_error(str(e))

        trace.completed_at = datetime.now()
        return trace


def format_trace_for_display(trace: ReasoningTrace) -> str:
    """Format a reasoning trace for human-readable display."""
    lines = [
        f"Session: {trace.session_id}",
        f"Task: {trace.task}",
        f"Model: {trace.model}",
        f"Status: {'Success' if trace.success else 'Failed'}",
        f"Turns: {trace.total_turns}",
        f"Tokens: {trace.total_tokens}",
        "",
        "=" * 60,
        "REASONING TRACE",
        "=" * 60,
    ]

    for i, thinking in enumerate(trace.thinking_blocks):
        lines.append(f"\n[Turn {thinking.turn_index}] Thinking:")
        lines.append("-" * 40)
        lines.append(thinking.content[:500] + "..." if len(thinking.content) > 500 else thinking.content)

        # Show tool calls at this turn
        turn_tools = trace.get_tool_calls_at_turn(thinking.turn_index)
        for tool in turn_tools:
            lines.append(f"\n  Tool: {tool.name}({json.dumps(tool.input)})")
            lines.append(f"  Result: {tool.result[:100]}..." if tool.result and len(tool.result) > 100 else f"  Result: {tool.result}")

    if trace.final_response:
        lines.append("\n" + "=" * 60)
        lines.append("FINAL RESPONSE")
        lines.append("=" * 60)
        lines.append(trace.final_response)

    if trace.error:
        lines.append("\n" + "=" * 60)
        lines.append("ERROR")
        lines.append("=" * 60)
        lines.append(trace.error)

    return "\n".join(lines)
reasoning_trace_optimizer/cli.pyShow
"""
CLI interface for Reasoning Trace Optimizer.

Provides command-line access to the optimization tools.
"""

import argparse
import json
import sys
from pathlib import Path

from rich.console import Console

from reasoning_trace_optimizer.analyzer import TraceAnalyzer, format_analysis_report
from reasoning_trace_optimizer.capture import TraceCapture, format_trace_for_display
from reasoning_trace_optimizer.loop import OptimizationLoop, LoopConfig
from reasoning_trace_optimizer.skill_generator import SkillGenerator


console = Console()


def cmd_capture(args: argparse.Namespace) -> None:
    """Run a task and capture reasoning trace."""
    capture = TraceCapture(
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
    )

    console.print(f"[cyan]Capturing trace for task: {args.task}[/cyan]")

    trace = capture.run(
        task=args.task,
        system_prompt=args.system_prompt or "You are a helpful assistant.",
        max_turns=args.max_turns,
    )

    # Output trace
    output = format_trace_for_display(trace)
    if args.output:
        Path(args.output).write_text(output)
        console.print(f"[green]Trace saved to: {args.output}[/green]")
    else:
        console.print(output)


def cmd_analyze(args: argparse.Namespace) -> None:
    """Analyze a captured reasoning trace."""
    # For now, run capture + analyze together
    # In future, could load trace from file

    capture = TraceCapture(
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
    )
    analyzer = TraceAnalyzer(
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
    )

    console.print(f"[cyan]Capturing and analyzing: {args.task}[/cyan]")

    trace = capture.run(
        task=args.task,
        system_prompt=args.system_prompt or "You are a helpful assistant.",
    )

    analysis = analyzer.analyze(trace)

    # Output analysis
    output = format_analysis_report(analysis)
    if args.output:
        Path(args.output).write_text(output)
        console.print(f"[green]Analysis saved to: {args.output}[/green]")
    else:
        console.print(output)


def cmd_optimize(args: argparse.Namespace) -> None:
    """Run full optimization loop."""
    config = LoopConfig(
        max_iterations=args.max_iterations,
        convergence_threshold=args.convergence_threshold,
        min_score_threshold=args.min_score,
        save_artifacts=True,
        artifacts_dir=args.artifacts_dir,
        verbose=True,
    )

    loop = OptimizationLoop(
        config=config,
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
    )

    console.print(f"[cyan]Starting optimization for: {args.task}[/cyan]")

    result = loop.run(
        task=args.task,
        initial_prompt=args.system_prompt or "You are a helpful assistant.",
    )

    # Output final prompt
    if args.output:
        Path(args.output).write_text(result.final_prompt)
        console.print(f"[green]Optimized prompt saved to: {args.output}[/green]")

    # Generate skill if requested
    if args.generate_skill:
        generator = SkillGenerator(
            api_key=args.api_key,
            base_url=args.base_url,
            model=args.model,
        )
        skill_path = generator.generate(
            result=result,
            skill_name=args.skill_name or "optimized-agent",
            output_dir=args.skills_dir,
        )
        console.print(f"[green]Generated skill at: {skill_path}[/green]")


def cmd_generate_skill(args: argparse.Namespace) -> None:
    """Generate a skill from optimization artifacts."""
    # Load summary from artifacts
    artifacts_dir = Path(args.artifacts_dir)
    summary_path = artifacts_dir / "summary.json"

    if not summary_path.exists():
        console.print("[red]Error: No optimization summary found. Run optimize first.[/red]")
        sys.exit(1)

    with open(summary_path) as f:
        summary = json.load(f)

    # Create minimal loop result from summary
    from reasoning_trace_optimizer.models import LoopResult, LoopIteration, ReasoningTrace, AnalysisResult

    # Load final prompt
    final_prompt_path = artifacts_dir / "final_prompt.txt"
    final_prompt = final_prompt_path.read_text() if final_prompt_path.exists() else ""

    result = LoopResult(
        task=summary.get("task", "Unknown task"),
        final_prompt=final_prompt,
        total_iterations=summary.get("total_iterations", 0),
        initial_score=summary.get("initial_score", 0),
        final_score=summary.get("final_score", 0),
        improvement_percentage=summary.get("improvement_percentage", 0),
        converged=summary.get("converged", False),
    )

    generator = SkillGenerator(
        api_key=args.api_key,
        base_url=args.base_url,
        model=args.model,
    )

    skill_path = generator.generate(
        result=result,
        skill_name=args.skill_name,
        output_dir=args.output_dir,
    )

    console.print(f"[green]Generated skill at: {skill_path}[/green]")


def main() -> None:
    """Main CLI entry point."""
    parser = argparse.ArgumentParser(
        prog="rto",
        description="Reasoning Trace Optimizer - Debug and optimize AI agents using M2.1's interleaved thinking",
    )

    # Global arguments
    parser.add_argument(
        "--api-key",
        help="MiniMax API key (or set ANTHROPIC_API_KEY env var)",
    )
    parser.add_argument(
        "--base-url",
        default="https://api.minimax.io/anthropic",
        help="API base URL",
    )
    parser.add_argument(
        "--model",
        default="MiniMax-M2.1",
        choices=["MiniMax-M2.1", "MiniMax-M2.1-lightning", "MiniMax-M2"],
        help="Model to use",
    )

    subparsers = parser.add_subparsers(dest="command", required=True)

    # Capture command
    capture_parser = subparsers.add_parser(
        "capture",
        help="Capture reasoning trace for a task",
    )
    capture_parser.add_argument("task", help="Task to execute")
    capture_parser.add_argument("--system-prompt", "-s", help="System prompt")
    capture_parser.add_argument("--max-turns", type=int, default=10)
    capture_parser.add_argument("--output", "-o", help="Output file path")
    capture_parser.set_defaults(func=cmd_capture)

    # Analyze command
    analyze_parser = subparsers.add_parser(
        "analyze",
        help="Capture and analyze reasoning trace",
    )
    analyze_parser.add_argument("task", help="Task to analyze")
    analyze_parser.add_argument("--system-prompt", "-s", help="System prompt")
    analyze_parser.add_argument("--output", "-o", help="Output file path")
    analyze_parser.set_defaults(func=cmd_analyze)

    # Optimize command
    optimize_parser = subparsers.add_parser(
        "optimize",
        help="Run full optimization loop",
    )
    optimize_parser.add_argument("task", help="Task to optimize for")
    optimize_parser.add_argument("--system-prompt", "-s", help="Initial system prompt")
    optimize_parser.add_argument("--max-iterations", type=int, default=5)
    optimize_parser.add_argument("--convergence-threshold", type=float, default=5.0)
    optimize_parser.add_argument("--min-score", type=float, default=80.0)
    optimize_parser.add_argument(
        "--artifacts-dir",
        default="./optimization_artifacts",
        help="Directory for artifacts",
    )
    optimize_parser.add_argument("--output", "-o", help="Output file for final prompt")
    optimize_parser.add_argument(
        "--generate-skill",
        action="store_true",
        help="Generate Agent Skill from results",
    )
    optimize_parser.add_argument("--skill-name", help="Name for generated skill")
    optimize_parser.add_argument(
        "--skills-dir",
        default="./generated_skills",
        help="Directory for generated skills",
    )
    optimize_parser.set_defaults(func=cmd_optimize)

    # Generate skill command
    skill_parser = subparsers.add_parser(
        "generate-skill",
        help="Generate skill from optimization artifacts",
    )
    skill_parser.add_argument("skill_name", help="Name for the skill")
    skill_parser.add_argument(
        "--artifacts-dir",
        default="./optimization_artifacts",
        help="Directory with optimization artifacts",
    )
    skill_parser.add_argument(
        "--output-dir",
        default="./generated_skills",
        help="Output directory for skill",
    )
    skill_parser.set_defaults(func=cmd_generate_skill)

    args = parser.parse_args()
    args.func(args)


if __name__ == "__main__":
    main()
reasoning_trace_optimizer/loop.pyShow
"""
OptimizationLoop: Orchestrates the full capture → analyze → improve → re-run cycle.

This is the main entry point for automated prompt optimization,
running iterative improvements until convergence or max iterations.
"""

import json
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Callable

from rich.console import Console
from rich.panel import Panel
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table

from reasoning_trace_optimizer.analyzer import TraceAnalyzer, format_analysis_report
from reasoning_trace_optimizer.capture import TraceCapture, format_trace_for_display
from reasoning_trace_optimizer.models import (
    AnalysisResult,
    LoopIteration,
    LoopResult,
    OptimizationResult,
    ReasoningTrace,
)
from reasoning_trace_optimizer.optimizer import PromptOptimizer, format_optimization_report


console = Console()


@dataclass
class LoopConfig:
    """Configuration for the optimization loop."""

    max_iterations: int = 5
    convergence_threshold: float = 3.0  # Stop if improvement < this %
    min_score_threshold: float = 75.0  # Stop if score >= this (realistic for complex tasks)
    regression_threshold: float = 8.0  # Rollback if score drops by this much

    # Scoring weights
    success_weight: float = 0.4
    score_weight: float = 0.4
    error_weight: float = 0.2

    # Optimization behavior
    use_best_prompt: bool = True  # Use best performing prompt, not final
    max_prompt_growth: float = 5.0  # Max ratio of new prompt length to original

    # Output options
    save_artifacts: bool = True
    artifacts_dir: str = "./optimization_artifacts"
    verbose: bool = True


class OptimizationLoop:
    """
    Orchestrates the full optimization cycle.

    Runs iterative loops of:
    1. Execute agent with current prompt
    2. Capture reasoning trace
    3. Analyze trace for issues
    4. Generate optimized prompt
    5. Repeat until convergence

    Example:
        ```python
        loop = OptimizationLoop()
        result = loop.run(
            task="Search for Python tutorials and summarize them",
            initial_prompt="You are a helpful research assistant.",
            tools=[search_tool],
            tool_executor=execute_search
        )

        print(f"Improved from {result.initial_score} to {result.final_score}")
        print(f"Final prompt:\\n{result.final_prompt}")
        ```
    """

    def __init__(
        self,
        config: LoopConfig | None = None,
        api_key: str | None = None,
        base_url: str = "https://api.minimax.io/anthropic",
        model: str = "MiniMax-M2.1",
    ):
        """
        Initialize the optimization loop.

        Args:
            config: Loop configuration
            api_key: MiniMax API key
            base_url: API endpoint
            model: Model to use for all components
        """
        self.config = config or LoopConfig()

        # Initialize components with same configuration
        self.capture = TraceCapture(api_key=api_key, base_url=base_url, model=model)
        self.analyzer = TraceAnalyzer(api_key=api_key, base_url=base_url, model=model)
        self.optimizer = PromptOptimizer(api_key=api_key, base_url=base_url, model=model)

        # Create artifacts directory
        if self.config.save_artifacts:
            Path(self.config.artifacts_dir).mkdir(parents=True, exist_ok=True)

    def run(
        self,
        task: str,
        initial_prompt: str,
        tools: list[dict[str, Any]] | None = None,
        tool_executor: Callable[[str, dict], str] | None = None,
        on_iteration: Callable[[LoopIteration], None] | None = None,
    ) -> LoopResult:
        """
        Run the full optimization loop.

        Args:
            task: The task to optimize for
            initial_prompt: Starting system prompt
            tools: Tool definitions for the agent
            tool_executor: Function to execute tool calls
            on_iteration: Optional callback after each iteration

        Returns:
            LoopResult with all iterations and final optimized prompt
        """
        result = LoopResult(task=task, final_prompt=initial_prompt)
        current_prompt = initial_prompt

        # Track best performing iteration
        best_score = 0.0
        best_prompt = initial_prompt
        best_iteration = 0
        consecutive_regressions = 0

        if self.config.verbose:
            console.print(Panel(
                f"[bold]Starting Optimization Loop[/bold]\n\n"
                f"Task: {task}\n"
                f"Max Iterations: {self.config.max_iterations}\n"
                f"Convergence Threshold: {self.config.convergence_threshold}%",
                title="Reasoning Trace Optimizer"
            ))

        with Progress(
            SpinnerColumn(),
            TextColumn("[progress.description]{task.description}"),
            console=console,
            disable=not self.config.verbose,
        ) as progress:

            for i in range(self.config.max_iterations):
                task_id = progress.add_task(f"Iteration {i + 1}/{self.config.max_iterations}", total=4)

                # Step 1: Capture trace
                progress.update(task_id, description=f"[cyan]Iteration {i + 1}: Capturing trace...")
                trace = self.capture.run(
                    task=task,
                    system_prompt=current_prompt,
                    tools=tools,
                    tool_executor=tool_executor,
                )
                progress.advance(task_id)

                # Step 2: Analyze trace
                progress.update(task_id, description=f"[cyan]Iteration {i + 1}: Analyzing trace...")
                analysis = self.analyzer.analyze(trace)
                progress.advance(task_id)

                # Calculate iteration score
                iteration_score = self._calculate_score(trace, analysis)

                # Record initial score
                if i == 0:
                    result.initial_score = iteration_score
                    best_score = iteration_score
                    best_prompt = current_prompt

                # Step 3: Check convergence
                should_continue, reason = self._check_convergence(
                    iteration=i,
                    score=iteration_score,
                    prev_score=result.iterations[-1].analysis.overall_score if result.iterations else 0,
                    best_score=best_score,
                    consecutive_regressions=consecutive_regressions,
                )

                # Step 4: Optimize if continuing
                optimization = None
                if should_continue:
                    progress.update(task_id, description=f"[cyan]Iteration {i + 1}: Optimizing prompt...")
                    optimization = self.optimizer.optimize(
                        original_prompt=current_prompt,
                        analysis=analysis,
                        trace=trace,
                    )

                    # Check for excessive prompt growth
                    new_prompt = optimization.optimized_prompt
                    if len(new_prompt) > len(initial_prompt) * self.config.max_prompt_growth:
                        if self.config.verbose:
                            console.print(f"[yellow]Warning: Prompt grew too large ({len(new_prompt)} chars), limiting growth[/yellow]")
                        # Keep the current prompt instead of the bloated one
                        new_prompt = current_prompt

                    current_prompt = new_prompt
                    progress.advance(task_id)

                # Track best performing iteration AFTER optimization
                # This ensures we capture the optimized prompt, not the input prompt
                if iteration_score > best_score:
                    best_score = iteration_score
                    # Use the optimized prompt if available, otherwise the current prompt
                    if optimization and optimization.optimized_prompt != initial_prompt:
                        best_prompt = optimization.optimized_prompt
                    else:
                        best_prompt = current_prompt
                    best_iteration = i + 1
                    consecutive_regressions = 0
                elif iteration_score < best_score - self.config.regression_threshold:
                    consecutive_regressions += 1
                    if self.config.verbose:
                        console.print(f"[yellow]Warning: Score regressed from {best_score:.1f} to {iteration_score:.1f}[/yellow]")

                # Record iteration
                iteration = LoopIteration(
                    iteration=i + 1,
                    trace=trace,
                    analysis=analysis,
                    optimization=optimization,
                    task_completed=trace.success or False,
                    error_count=len([tc for tc in trace.tool_calls if not tc.success]),
                    token_usage=trace.total_tokens,
                )
                result.iterations.append(iteration)

                # Callback
                if on_iteration:
                    on_iteration(iteration)

                # Print iteration summary
                if self.config.verbose:
                    self._print_iteration_summary(iteration)

                # Save artifacts
                if self.config.save_artifacts:
                    self._save_iteration_artifacts(iteration, i + 1)

                # Check if we should stop
                if not should_continue:
                    if self.config.verbose:
                        console.print(f"\n[green]Stopping: {reason}[/green]")
                    result.converged = True
                    break

                progress.remove_task(task_id)

        # Finalize result - use best prompt if configured
        if self.config.use_best_prompt and best_score > result.iterations[-1].analysis.overall_score:
            result.final_prompt = best_prompt
            result.final_score = best_score
            if self.config.verbose:
                console.print(f"[green]Using best prompt from iteration {best_iteration} (score: {best_score:.1f})[/green]")
        else:
            result.final_prompt = current_prompt
            result.final_score = result.iterations[-1].analysis.overall_score if result.iterations else 0

        result.total_iterations = len(result.iterations)
        result.improvement_percentage = (
            (result.final_score - result.initial_score) / max(result.initial_score, 1) * 100
        )

        # Warn if prompt was never successfully optimized
        if result.final_prompt == initial_prompt:
            if self.config.verbose:
                console.print(
                    "[yellow]Warning: Final prompt unchanged from initial. "
                    "Optimization may have failed to parse model responses.[/yellow]"
                )
            # Check if any iteration actually produced a different prompt
            any_optimized = any(
                i.optimization and i.optimization.optimized_prompt != initial_prompt
                for i in result.iterations
                if i.optimization
            )
            if not any_optimized:
                console.print(
                    "[yellow]No successful prompt optimizations were extracted. "
                    "Check artifacts for raw optimizer responses.[/yellow]"
                )

        # Print final summary
        if self.config.verbose:
            self._print_final_summary(result)

        # Save final artifacts
        if self.config.save_artifacts:
            self._save_final_artifacts(result)

        return result

    def run_single(
        self,
        task: str,
        prompt: str,
        tools: list[dict[str, Any]] | None = None,
        tool_executor: Callable[[str, dict], str] | None = None,
    ) -> tuple[ReasoningTrace, AnalysisResult]:
        """
        Run a single capture + analysis cycle (no optimization).

        Useful for debugging or when you just want analysis without
        automatic optimization.

        Returns:
            Tuple of (trace, analysis)
        """
        trace = self.capture.run(
            task=task,
            system_prompt=prompt,
            tools=tools,
            tool_executor=tool_executor,
        )
        analysis = self.analyzer.analyze(trace)
        return trace, analysis

    def _calculate_score(
        self,
        trace: ReasoningTrace,
        analysis: AnalysisResult,
    ) -> float:
        """Calculate weighted score from trace and analysis."""
        success_score = 100 if trace.success else 0
        error_penalty = len([tc for tc in trace.tool_calls if not tc.success]) * 10

        weighted = (
            success_score * self.config.success_weight
            + analysis.overall_score * self.config.score_weight
            - error_penalty * self.config.error_weight
        )

        return max(0, min(100, weighted))

    def _check_convergence(
        self,
        iteration: int,
        score: float,
        prev_score: float,
        best_score: float = 0.0,
        consecutive_regressions: int = 0,
    ) -> tuple[bool, str]:
        """Check if optimization should continue."""
        # Check score threshold
        if score >= self.config.min_score_threshold:
            return False, f"Score {score:.1f} >= threshold {self.config.min_score_threshold}"

        # Check for consecutive regressions (stop if we've regressed twice in a row)
        if consecutive_regressions >= 2:
            return False, f"Consecutive regressions detected (best was {best_score:.1f})"

        # Check improvement threshold (after first iteration)
        if iteration > 0:
            improvement = score - prev_score
            if abs(improvement) < self.config.convergence_threshold and score >= prev_score:
                return False, f"Converged (improvement {improvement:.1f}% < threshold)"

        # Check max iterations
        if iteration >= self.config.max_iterations - 1:
            return False, f"Reached max iterations ({self.config.max_iterations})"

        return True, ""

    def _print_iteration_summary(self, iteration: LoopIteration) -> None:
        """Print summary of an iteration."""
        table = Table(title=f"Iteration {iteration.iteration} Summary")
        table.add_column("Metric", style="cyan")
        table.add_column("Value", style="green")

        table.add_row("Task Completed", "Yes" if iteration.task_completed else "No")
        table.add_row("Overall Score", f"{iteration.analysis.overall_score:.1f}/100")
        table.add_row("Patterns Found", str(len(iteration.analysis.patterns)))
        table.add_row("Tool Errors", str(iteration.error_count))
        table.add_row("Token Usage", str(iteration.token_usage))

        if iteration.optimization:
            table.add_row(
                "Predicted Improvement",
                f"{iteration.optimization.predicted_improvement}%"
            )

        console.print(table)

    def _print_final_summary(self, result: LoopResult) -> None:
        """Print final optimization summary."""
        console.print("\n")
        panel_content = (
            f"[bold]Iterations:[/bold] {result.total_iterations}\n"
            f"[bold]Converged:[/bold] {'Yes' if result.converged else 'No'}\n"
            f"[bold]Initial Score:[/bold] {result.initial_score:.1f}\n"
            f"[bold]Final Score:[/bold] {result.final_score:.1f}\n"
            f"[bold]Improvement:[/bold] {result.improvement_percentage:+.1f}%"
        )
        console.print(Panel(panel_content, title="[green]Optimization Complete[/green]"))

    def _save_iteration_artifacts(self, iteration: LoopIteration, num: int) -> None:
        """Save iteration artifacts to disk."""
        base_path = Path(self.config.artifacts_dir) / f"iteration_{num}"
        base_path.mkdir(exist_ok=True)

        # Save trace
        with open(base_path / "trace.txt", "w") as f:
            f.write(format_trace_for_display(iteration.trace))

        # Save analysis
        with open(base_path / "analysis.txt", "w") as f:
            f.write(format_analysis_report(iteration.analysis))

        # Save optimization if present
        if iteration.optimization:
            with open(base_path / "optimization.txt", "w") as f:
                f.write(format_optimization_report(iteration.optimization))

            with open(base_path / "optimized_prompt.txt", "w") as f:
                f.write(iteration.optimization.optimized_prompt)

    def _save_final_artifacts(self, result: LoopResult) -> None:
        """Save final optimization artifacts."""
        base_path = Path(self.config.artifacts_dir)

        # Save final prompt
        with open(base_path / "final_prompt.txt", "w") as f:
            f.write(result.final_prompt)

        # Save summary JSON
        summary = {
            "task": result.task,
            "total_iterations": result.total_iterations,
            "converged": result.converged,
            "initial_score": result.initial_score,
            "final_score": result.final_score,
            "improvement_percentage": result.improvement_percentage,
            "timestamp": datetime.now().isoformat(),
        }
        with open(base_path / "summary.json", "w") as f:
            json.dump(summary, f, indent=2)


def run_quick_optimization(
    task: str,
    initial_prompt: str,
    tools: list[dict[str, Any]] | None = None,
    tool_executor: Callable[[str, dict], str] | None = None,
    max_iterations: int = 3,
) -> str:
    """
    Quick helper function for one-shot optimization.

    Returns the optimized prompt directly.
    """
    config = LoopConfig(max_iterations=max_iterations, verbose=False)
    loop = OptimizationLoop(config=config)
    result = loop.run(task, initial_prompt, tools, tool_executor)
    return result.final_prompt
reasoning_trace_optimizer/models.pyShow
"""
Core data models for reasoning trace optimization.
"""

from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any


class PatternType(Enum):
    """Types of patterns detected in reasoning traces."""

    CONTEXT_DEGRADATION = "context_degradation"
    TOOL_CONFUSION = "tool_confusion"
    INSTRUCTION_DRIFT = "instruction_drift"
    HALLUCINATION = "hallucination"
    INCOMPLETE_REASONING = "incomplete_reasoning"
    TOOL_MISUSE = "tool_misuse"
    GOAL_ABANDONMENT = "goal_abandonment"
    CIRCULAR_REASONING = "circular_reasoning"
    PREMATURE_CONCLUSION = "premature_conclusion"
    MISSING_VALIDATION = "missing_validation"


class Severity(Enum):
    """Severity levels for detected patterns."""

    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


@dataclass
class ThinkingBlock:
    """A single thinking/reasoning block from the model."""

    content: str
    turn_index: int
    timestamp: datetime = field(default_factory=datetime.now)
    token_count: int = 0
    signature: str | None = None  # M2.1 thinking signature

    # Context at time of thinking
    preceding_tool_call: str | None = None
    preceding_tool_result: str | None = None
    following_action: str | None = None  # tool_use, text, or end_turn


@dataclass
class ToolCall:
    """A tool call made by the agent."""

    id: str
    name: str
    input: dict[str, Any]
    turn_index: int
    result: str | None = None
    success: bool | None = None
    error: str | None = None


@dataclass
class ReasoningTrace:
    """Complete reasoning trace for an agent session."""

    session_id: str
    task: str
    system_prompt: str
    thinking_blocks: list[ThinkingBlock] = field(default_factory=list)
    tool_calls: list[ToolCall] = field(default_factory=list)
    final_response: str | None = None

    # Metadata
    model: str = "MiniMax-M2.1"
    total_turns: int = 0
    total_tokens: int = 0
    success: bool | None = None
    error: str | None = None
    started_at: datetime = field(default_factory=datetime.now)
    completed_at: datetime | None = None

    def get_thinking_at_turn(self, turn: int) -> ThinkingBlock | None:
        """Get thinking block at specific turn."""
        for block in self.thinking_blocks:
            if block.turn_index == turn:
                return block
        return None

    def get_tool_calls_at_turn(self, turn: int) -> list[ToolCall]:
        """Get all tool calls at specific turn."""
        return [tc for tc in self.tool_calls if tc.turn_index == turn]


@dataclass
class Pattern:
    """A detected pattern in reasoning traces."""

    type: PatternType
    severity: Severity
    description: str
    evidence: list[str]  # Excerpts from thinking blocks
    turn_indices: list[int]
    suggestion: str
    confidence: float  # 0.0 to 1.0


@dataclass
class AnalysisResult:
    """Result of analyzing a reasoning trace."""

    trace_id: str
    patterns: list[Pattern] = field(default_factory=list)

    # Scores (0-100)
    reasoning_clarity: float = 0.0
    goal_adherence: float = 0.0
    tool_usage_quality: float = 0.0
    error_recovery: float = 0.0
    overall_score: float = 0.0

    # Feedback
    strengths: list[str] = field(default_factory=list)
    weaknesses: list[str] = field(default_factory=list)
    recommendations: list[str] = field(default_factory=list)

    # Analysis metadata
    analyzer_model: str = "MiniMax-M2.1"
    analyzer_thinking: str = ""  # The analyzer's own reasoning


@dataclass
class PromptDiff:
    """Difference between original and optimized prompt."""

    section: str  # e.g., "system_prompt", "tool_description", "instruction"
    original: str
    optimized: str
    reason: str


@dataclass
class OptimizationResult:
    """Result of prompt optimization."""

    original_prompt: str
    optimized_prompt: str
    diffs: list[PromptDiff] = field(default_factory=list)

    # Improvement predictions
    predicted_improvement: float = 0.0  # Percentage
    confidence: float = 0.0

    # Optimizer reasoning
    optimizer_thinking: str = ""
    key_changes: list[str] = field(default_factory=list)


@dataclass
class LoopIteration:
    """Single iteration of the optimization loop."""

    iteration: int
    trace: ReasoningTrace
    analysis: AnalysisResult
    optimization: OptimizationResult | None

    # Metrics
    task_completed: bool = False
    error_count: int = 0
    token_usage: int = 0


@dataclass
class LoopResult:
    """Result of running the full optimization loop."""

    task: str
    iterations: list[LoopIteration] = field(default_factory=list)

    # Final state
    final_prompt: str = ""
    converged: bool = False
    total_iterations: int = 0

    # Improvement metrics
    initial_score: float = 0.0
    final_score: float = 0.0
    improvement_percentage: float = 0.0

    # Generated artifacts
    generated_skill_path: str | None = None
reasoning_trace_optimizer/optimizer.pyShow
"""
PromptOptimizer: Generates improved prompts based on trace analysis.

Uses M2.1 to synthesize analysis results into concrete prompt improvements,
with full reasoning transparency via interleaved thinking.
"""

import json
import os
from typing import Any

import anthropic

from reasoning_trace_optimizer.models import (
    AnalysisResult,
    OptimizationResult,
    PromptDiff,
    ReasoningTrace,
)


OPTIMIZER_SYSTEM_PROMPT = """You are an expert prompt engineer specializing in AI agent optimization.

Your task is to improve agent prompts based on reasoning trace analysis.
You have access to:
1. The original prompt that was used
2. Analysis of how the agent reasoned (its thinking trace)
3. Detected patterns and issues
4. Specific recommendations

Your goal is to create an IMPROVED prompt that:
- Addresses identified weaknesses
- Maintains existing strengths
- Prevents detected failure patterns
- Improves clarity and specificity

When optimizing, consider:
- Adding explicit guardrails for common failure modes
- Clarifying ambiguous instructions
- Adding examples for complex behaviors
- Restructuring for better context positioning
- Adding validation steps where missing

Provide the optimized prompt with clear explanations of changes."""


OPTIMIZATION_PROMPT_TEMPLATE = """Optimize the following agent prompt based on trace analysis:

## Original Task
{task}

## Original System Prompt
```
{original_prompt}
```

## Analysis Results

### Overall Score: {overall_score}/100

### Detected Patterns
{patterns}

### Weaknesses
{weaknesses}

### Recommendations
{recommendations}

### Analyzer's Reasoning
{analyzer_thinking}

---

Provide your optimization as JSON:
```json
{{
    "optimized_prompt": "<the full improved prompt>",
    "diffs": [
        {{
            "section": "<which part changed, e.g., 'instructions', 'guardrails', 'examples'>",
            "original": "<original text or 'N/A' if new>",
            "optimized": "<new/changed text>",
            "reason": "<why this change helps>"
        }}
    ],
    "key_changes": [
        "<summary of major change 1>",
        "<summary of major change 2>"
    ],
    "predicted_improvement": 15,
    "confidence": 0.75
}}
```

Think carefully about what changes will have the biggest impact on agent performance."""


class PromptOptimizer:
    """
    Optimizes agent prompts based on reasoning trace analysis.

    Uses M2.1's interleaved thinking to generate thoughtful improvements
    with full transparency into the optimization reasoning.

    Example:
        ```python
        optimizer = PromptOptimizer()
        result = optimizer.optimize(
            original_prompt=system_prompt,
            analysis=analysis_result,
            trace=reasoning_trace
        )

        print(f"Predicted improvement: {result.predicted_improvement}%")
        print(f"New prompt:\\n{result.optimized_prompt}")
        ```
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = "https://api.minimax.io/anthropic",
        model: str = "MiniMax-M2.1",
    ):
        """
        Initialize PromptOptimizer with M2.1 configuration.

        Args:
            api_key: MiniMax API key
            base_url: API endpoint
            model: Model for optimization
        """
        self.model = model
        self.client = anthropic.Anthropic(
            api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"),
            base_url=base_url,
        )

    def optimize(
        self,
        original_prompt: str,
        analysis: AnalysisResult,
        trace: ReasoningTrace | None = None,
        max_tokens: int = 8192,
    ) -> OptimizationResult:
        """
        Generate an optimized prompt based on analysis.

        Args:
            original_prompt: The original system prompt to improve
            analysis: Analysis results from TraceAnalyzer
            trace: Optional original trace for additional context
            max_tokens: Maximum tokens for response

        Returns:
            OptimizationResult with new prompt and change details
        """
        # Format analysis for prompt
        patterns_text = self._format_patterns(analysis)
        weaknesses_text = "\n".join(f"- {w}" for w in analysis.weaknesses)
        recommendations_text = "\n".join(f"- {r}" for r in analysis.recommendations)

        prompt = OPTIMIZATION_PROMPT_TEMPLATE.format(
            task=trace.task if trace else "Unknown task",
            original_prompt=original_prompt,
            overall_score=analysis.overall_score,
            patterns=patterns_text,
            weaknesses=weaknesses_text or "None identified",
            recommendations=recommendations_text or "None provided",
            analyzer_thinking=analysis.analyzer_thinking[:2000] if analysis.analyzer_thinking else "Not available",
        )

        # Call M2.1 for optimization
        response = self.client.messages.create(
            model=self.model,
            max_tokens=max_tokens,
            system=OPTIMIZER_SYSTEM_PROMPT,
            messages=[{"role": "user", "content": prompt}],
        )

        # Extract thinking and response
        optimizer_thinking = ""
        optimization_text = ""

        for block in response.content:
            if block.type == "thinking":
                optimizer_thinking = block.thinking
            elif block.type == "text":
                optimization_text = block.text

        # Parse the response
        result = self._parse_optimization_response(optimization_text, original_prompt)
        result.optimizer_thinking = optimizer_thinking

        return result

    def optimize_iterative(
        self,
        original_prompt: str,
        analyses: list[AnalysisResult],
        traces: list[ReasoningTrace],
    ) -> OptimizationResult:
        """
        Optimize based on multiple analysis iterations.

        Synthesizes patterns across multiple runs for more robust improvements.

        Args:
            original_prompt: The original system prompt
            analyses: List of analysis results from multiple runs
            traces: Corresponding reasoning traces

        Returns:
            OptimizationResult incorporating learnings from all iterations
        """
        # Aggregate patterns across all analyses
        all_patterns = []
        all_weaknesses = []
        all_recommendations = []
        avg_score = 0

        for analysis in analyses:
            all_patterns.extend(analysis.patterns)
            all_weaknesses.extend(analysis.weaknesses)
            all_recommendations.extend(analysis.recommendations)
            avg_score += analysis.overall_score

        avg_score /= len(analyses) if analyses else 1

        # Create aggregated analysis
        aggregated = AnalysisResult(
            trace_id="aggregated",
            patterns=all_patterns,
            overall_score=avg_score,
            weaknesses=list(set(all_weaknesses)),  # Deduplicate
            recommendations=list(set(all_recommendations)),
        )

        # Optimize based on aggregated analysis
        return self.optimize(
            original_prompt=original_prompt,
            analysis=aggregated,
            trace=traces[0] if traces else None,
        )

    def suggest_tool_improvements(
        self,
        tools: list[dict[str, Any]],
        analysis: AnalysisResult,
        trace: ReasoningTrace,
    ) -> dict[str, str]:
        """
        Suggest improvements for tool definitions based on analysis.

        Args:
            tools: Original tool definitions
            analysis: Analysis results
            trace: Original reasoning trace

        Returns:
            Dict mapping tool names to suggested description improvements
        """
        tool_issues = [
            p for p in analysis.patterns
            if p.type.value in ("tool_confusion", "tool_misuse")
        ]

        if not tool_issues:
            return {}

        prompt = f"""Based on these tool usage issues:

{self._format_patterns_for_tools(tool_issues)}

And the original tool definitions:
{json.dumps(tools, indent=2)}

Suggest improved tool descriptions. Respond as JSON:
```json
{{
    "tool_name": "improved description that addresses the confusion"
}}
```"""

        response = self.client.messages.create(
            model=self.model,
            max_tokens=2048,
            messages=[{"role": "user", "content": prompt}],
        )

        for block in response.content:
            if block.type == "text":
                try:
                    text = block.text
                    if "```json" in text:
                        text = text.split("```json")[1].split("```")[0]
                    return json.loads(text)
                except json.JSONDecodeError:
                    pass

        return {}

    def _format_patterns(self, analysis: AnalysisResult) -> str:
        """Format patterns for optimization prompt."""
        if not analysis.patterns:
            return "No significant patterns detected."

        parts = []
        for p in analysis.patterns:
            parts.append(
                f"[{p.severity.value.upper()}] {p.type.value}\n"
                f"  Description: {p.description}\n"
                f"  Evidence: {', '.join(p.evidence[:2])}\n"
                f"  Suggestion: {p.suggestion}"
            )
        return "\n\n".join(parts)

    def _format_patterns_for_tools(self, patterns: list) -> str:
        """Format tool-related patterns."""
        return "\n".join(
            f"- {p.type.value}: {p.description}" for p in patterns
        )

    def _parse_optimization_response(
        self,
        response_text: str,
        original_prompt: str,
    ) -> OptimizationResult:
        """Parse the JSON optimization response with fallback extraction."""
        result = OptimizationResult(
            original_prompt=original_prompt,
            optimized_prompt=original_prompt,  # Default to original if parsing fails
        )

        try:
            json_text = response_text
            if "```json" in response_text:
                json_text = response_text.split("```json")[1].split("```")[0]
            elif "```" in response_text:
                json_text = response_text.split("```")[1].split("```")[0]

            data = json.loads(json_text)

            result.optimized_prompt = data.get("optimized_prompt", original_prompt)
            result.predicted_improvement = data.get("predicted_improvement", 0)
            result.confidence = data.get("confidence", 0.5)
            result.key_changes = data.get("key_changes", [])

            # Parse diffs
            for d in data.get("diffs", []):
                diff = PromptDiff(
                    section=d.get("section", "unknown"),
                    original=d.get("original", ""),
                    optimized=d.get("optimized", ""),
                    reason=d.get("reason", ""),
                )
                result.diffs.append(diff)

        except (json.JSONDecodeError, KeyError) as e:
            # Fallback: try to extract optimized_prompt directly from response
            extracted_prompt = self._fallback_extract_prompt(response_text)
            if extracted_prompt and extracted_prompt != original_prompt:
                result.optimized_prompt = extracted_prompt
                result.key_changes = [f"JSON parsing failed ({type(e).__name__}), extracted prompt via fallback"]
                result.confidence = 0.3  # Lower confidence for fallback extraction
            else:
                result.key_changes = [f"Optimization parsing failed ({type(e).__name__}) - using original prompt"]

        return result

    def _fallback_extract_prompt(self, response_text: str) -> str | None:
        """
        Fallback method to extract optimized prompt when JSON parsing fails.

        Tries multiple strategies to find the prompt content.
        """
        import re

        # Strategy 1: Look for "optimized_prompt": "..." pattern
        match = re.search(r'"optimized_prompt"\s*:\s*"([^"]+)"', response_text, re.DOTALL)
        if match:
            # Unescape the string
            return match.group(1).replace('\\n', '\n').replace('\\"', '"')

        # Strategy 2: Look for content between specific markers
        markers = [
            ('## Optimized Prompt', '##'),
            ('**Optimized Prompt**', '**'),
            ('OPTIMIZED PROMPT:', '\n\n'),
            ('Here is the improved prompt:', '\n\n---'),
        ]

        for start_marker, end_marker in markers:
            if start_marker in response_text:
                start_idx = response_text.find(start_marker) + len(start_marker)
                remaining = response_text[start_idx:].strip()
                if end_marker in remaining:
                    end_idx = remaining.find(end_marker)
                    extracted = remaining[:end_idx].strip()
                    if len(extracted) > 50:  # Minimum length check
                        return extracted

        # Strategy 3: Look for a substantial code block that might be the prompt
        code_blocks = re.findall(r'```(?:text|markdown)?\n(.*?)```', response_text, re.DOTALL)
        for block in code_blocks:
            # Skip JSON blocks, look for prose blocks that could be prompts
            if not block.strip().startswith('{') and len(block) > 100:
                return block.strip()

        return None


def format_optimization_report(result: OptimizationResult) -> str:
    """Format an optimization result as a human-readable report."""
    lines = [
        "=" * 60,
        "PROMPT OPTIMIZATION REPORT",
        "=" * 60,
        "",
        f"Predicted Improvement: {result.predicted_improvement}%",
        f"Confidence: {result.confidence * 100:.0f}%",
        "",
    ]

    if result.key_changes:
        lines.append("Key Changes:")
        for change in result.key_changes:
            lines.append(f"  - {change}")
        lines.append("")

    if result.diffs:
        lines.append("Detailed Changes:")
        for diff in result.diffs:
            lines.append(f"\n  [{diff.section}]")
            if diff.original and diff.original != "N/A":
                lines.append(f"    Before: {diff.original[:100]}...")
            lines.append(f"    After: {diff.optimized[:100]}...")
            lines.append(f"    Reason: {diff.reason}")

    lines.extend([
        "",
        "=" * 60,
        "OPTIMIZED PROMPT",
        "=" * 60,
        result.optimized_prompt,
    ])

    return "\n".join(lines)
reasoning_trace_optimizer/skill_generator.pyShow
"""
SkillGenerator: Converts optimization insights into shareable Agent Skills.

Transforms the learnings from optimization loops into reusable skills
following the Agent Skills template format.
"""

import json
import os
from datetime import datetime
from pathlib import Path
from typing import Any

import anthropic

from reasoning_trace_optimizer.models import (
    AnalysisResult,
    LoopResult,
    Pattern,
    PatternType,
)


SKILL_TEMPLATE = '''---
name: {skill_name}
description: "{description}"
---

# {title}

{intro}

## When to Activate

{activation}

## Core Concepts

{concepts}

## Patterns to Avoid

{anti_patterns}

## Recommended Practices

{practices}

## Guidelines

{guidelines}

## Examples

{examples}

---

## Skill Metadata

**Generated**: {date}
**Source**: Reasoning Trace Optimizer
**Optimization Iterations**: {iterations}
**Score Improvement**: {initial_score:.1f} → {final_score:.1f} (+{improvement:.1f}%)
'''


GENERATOR_SYSTEM_PROMPT = """You are an expert at converting agent optimization insights into reusable skills.

Your task is to analyze optimization results and generate a shareable Agent Skill that
captures the learnings so other developers can benefit.

The skill should:
1. Describe WHEN to use these learnings (activation triggers)
2. Explain the PATTERNS to avoid (anti-patterns found)
3. Provide CONCRETE practices that improved performance
4. Give VERIFIABLE guidelines (things that can be checked)
5. Include EXAMPLES showing before/after improvements

Write in a clear, direct style. Focus on actionable guidance, not theory."""


def _format_list_to_markdown(items: list | str) -> str:
    """Convert a list to markdown bullet points."""
    if isinstance(items, str):
        return items
    if not items:
        return ""

    import re
    formatted = []
    for item in items:
        # Strip any existing leading bullet points/dashes to avoid duplication
        cleaned = re.sub(r'^[-*•]\s*', '', str(item).strip())
        formatted.append(f"- {cleaned}")
    return "\n".join(formatted)


def _format_numbered_list_to_markdown(items: list | str) -> str:
    """Convert a list to markdown numbered list."""
    if isinstance(items, str):
        return items
    if not items:
        return ""

    import re
    formatted = []
    for i, item in enumerate(items):
        # Strip any existing leading numbers (e.g., "1. ", "2. ") to avoid duplication
        cleaned = re.sub(r'^\d+\.\s*', '', str(item).strip())
        formatted.append(f"{i+1}. {cleaned}")
    return "\n".join(formatted)


def _format_examples_to_markdown(examples: list | str) -> str:
    """Convert example dicts to markdown format."""
    if isinstance(examples, str):
        return examples
    if not examples:
        return ""

    parts = []
    for i, ex in enumerate(examples):
        if isinstance(ex, dict):
            parts.append(f"### Example {i+1}: {ex.get('context', 'Scenario')}")
            if ex.get('before'):
                parts.append(f"\n**Before:**\n```\n{ex['before']}\n```")
            if ex.get('after'):
                parts.append(f"\n**After:**\n```\n{ex['after']}\n```")
            if ex.get('improvement'):
                parts.append(f"\n**Improvement:** {ex['improvement']}")
            parts.append("")
        else:
            parts.append(f"- {ex}")
    return "\n".join(parts)


class SkillGenerator:
    """
    Generates shareable Agent Skills from optimization results.

    Converts the learnings from optimization loops into the standard
    Agent Skills format for sharing with other developers.

    Example:
        ```python
        generator = SkillGenerator()
        skill_path = generator.generate(
            result=loop_result,
            skill_name="web-search-agent",
            output_dir="./generated_skills"
        )
        print(f"Generated skill at: {skill_path}")
        ```
    """

    def __init__(
        self,
        api_key: str | None = None,
        base_url: str = "https://api.minimax.io/anthropic",
        model: str = "MiniMax-M2.1",
    ):
        """
        Initialize SkillGenerator.

        Args:
            api_key: MiniMax API key
            base_url: API endpoint
            model: Model for skill generation
        """
        self.model = model
        self.client = anthropic.Anthropic(
            api_key=api_key or os.environ.get("ANTHROPIC_API_KEY"),
            base_url=base_url,
        )

    def generate(
        self,
        result: LoopResult,
        skill_name: str,
        output_dir: str = "./generated_skills",
        title: str | None = None,
    ) -> str:
        """
        Generate an Agent Skill from optimization results.

        Args:
            result: The optimization loop result
            skill_name: Name for the skill (lowercase-with-hyphens)
            output_dir: Directory to save the skill
            title: Optional human-readable title

        Returns:
            Path to the generated SKILL.md file
        """
        # Extract insights from all iterations
        all_patterns = self._collect_patterns(result)
        all_recommendations = self._collect_recommendations(result)
        key_changes = self._collect_key_changes(result)

        # Generate skill content using M2.1
        content = self._generate_skill_content(
            task=result.task,
            patterns=all_patterns,
            recommendations=all_recommendations,
            key_changes=key_changes,
            initial_prompt=result.iterations[0].trace.system_prompt if result.iterations else "",
            final_prompt=result.final_prompt,
        )

        # Format content - convert lists to markdown
        formatted_content = {
            "activation": _format_list_to_markdown(content.get("activation", "")),
            "concepts": _format_list_to_markdown(content.get("concepts", "")),
            "anti_patterns": _format_list_to_markdown(content.get("anti_patterns", "")),
            "practices": _format_list_to_markdown(content.get("practices", "")),
            "guidelines": _format_numbered_list_to_markdown(content.get("guidelines", "")),
            "examples": _format_examples_to_markdown(content.get("examples", "")),
        }

        # Format using template
        skill_content = SKILL_TEMPLATE.format(
            skill_name=skill_name,
            description=content.get("description", f"Optimized practices for {skill_name}"),
            title=title or content.get("title", skill_name.replace("-", " ").title()),
            intro=content.get("intro", ""),
            activation=formatted_content["activation"],
            concepts=formatted_content["concepts"],
            anti_patterns=formatted_content["anti_patterns"],
            practices=formatted_content["practices"],
            guidelines=formatted_content["guidelines"],
            examples=formatted_content["examples"],
            date=datetime.now().strftime("%Y-%m-%d"),
            iterations=result.total_iterations,
            initial_score=result.initial_score,
            final_score=result.final_score,
            improvement=result.improvement_percentage,
        )

        # Save skill
        skill_dir = Path(output_dir) / skill_name
        skill_dir.mkdir(parents=True, exist_ok=True)

        skill_path = skill_dir / "SKILL.md"
        with open(skill_path, "w") as f:
            f.write(skill_content)

        # Save optimization data as reference
        self._save_references(skill_dir, result, content)

        return str(skill_path)

    def generate_from_analysis(
        self,
        analyses: list[AnalysisResult],
        skill_name: str,
        task_description: str,
        output_dir: str = "./generated_skills",
    ) -> str:
        """
        Generate a skill from multiple analysis results (without full loop).

        Useful when you have analysis data but didn't run the full optimization loop.

        Args:
            analyses: List of analysis results
            skill_name: Name for the skill
            task_description: Description of the task context
            output_dir: Output directory

        Returns:
            Path to generated skill
        """
        # Aggregate patterns and recommendations
        all_patterns = []
        all_recommendations = []

        for analysis in analyses:
            all_patterns.extend(analysis.patterns)
            all_recommendations.extend(analysis.recommendations)

        content = self._generate_skill_content(
            task=task_description,
            patterns=all_patterns,
            recommendations=list(set(all_recommendations)),
            key_changes=[],
            initial_prompt="",
            final_prompt="",
        )

        # Calculate average score
        avg_score = sum(a.overall_score for a in analyses) / len(analyses) if analyses else 0

        skill_content = SKILL_TEMPLATE.format(
            skill_name=skill_name,
            description=content.get("description", f"Learnings for {skill_name}"),
            title=content.get("title", skill_name.replace("-", " ").title()),
            intro=content.get("intro", ""),
            activation=content.get("activation", ""),
            concepts=content.get("concepts", ""),
            anti_patterns=content.get("anti_patterns", ""),
            practices=content.get("practices", ""),
            guidelines=content.get("guidelines", ""),
            examples=content.get("examples", ""),
            date=datetime.now().strftime("%Y-%m-%d"),
            iterations=len(analyses),
            initial_score=avg_score,
            final_score=avg_score,
            improvement=0,
        )

        skill_dir = Path(output_dir) / skill_name
        skill_dir.mkdir(parents=True, exist_ok=True)

        skill_path = skill_dir / "SKILL.md"
        with open(skill_path, "w") as f:
            f.write(skill_content)

        return str(skill_path)

    def _collect_patterns(self, result: LoopResult) -> list[Pattern]:
        """Collect all unique patterns from iterations."""
        patterns = []
        seen = set()

        for iteration in result.iterations:
            for pattern in iteration.analysis.patterns:
                key = (pattern.type, pattern.description[:50])
                if key not in seen:
                    patterns.append(pattern)
                    seen.add(key)

        return patterns

    def _collect_recommendations(self, result: LoopResult) -> list[str]:
        """Collect all unique recommendations."""
        recommendations = []
        seen = set()

        for iteration in result.iterations:
            for rec in iteration.analysis.recommendations:
                if rec not in seen:
                    recommendations.append(rec)
                    seen.add(rec)

        return recommendations

    def _collect_key_changes(self, result: LoopResult) -> list[str]:
        """Collect all key changes from optimizations."""
        changes = []

        for iteration in result.iterations:
            if iteration.optimization:
                changes.extend(iteration.optimization.key_changes)

        return changes

    def _generate_skill_content(
        self,
        task: str,
        patterns: list[Pattern],
        recommendations: list[str],
        key_changes: list[str],
        initial_prompt: str,
        final_prompt: str,
    ) -> dict[str, str]:
        """Use M2.1 to generate skill content sections."""
        patterns_text = "\n".join(
            f"- [{p.severity.value}] {p.type.value}: {p.description}"
            for p in patterns
        )

        recommendations_text = "\n".join(f"- {r}" for r in recommendations)
        changes_text = "\n".join(f"- {c}" for c in key_changes)

        prompt = f"""Generate an Agent Skill based on these optimization insights:

## Task Context
{task}

## Patterns Detected (Anti-patterns to avoid)
{patterns_text or "No significant patterns detected"}

## Recommendations from Analysis
{recommendations_text or "No specific recommendations"}

## Key Changes That Improved Performance
{changes_text or "No recorded changes"}

## Prompt Evolution
Initial: {initial_prompt[:500] if initial_prompt else "N/A"}...
Final: {final_prompt[:500] if final_prompt else "N/A"}...

---

Generate skill content as JSON:
```json
{{
    "title": "Human-readable skill title",
    "description": "One-line description for skill discovery (what triggers this skill)",
    "intro": "2-3 sentence introduction explaining what this skill teaches",
    "activation": "Bullet points of when to activate this skill (specific keywords, task types)",
    "concepts": "Core concepts this skill covers (3-5 key ideas)",
    "anti_patterns": "Patterns to AVOID - formatted as markdown list with descriptions",
    "practices": "Recommended practices - formatted as markdown list",
    "guidelines": "Numbered verifiable guidelines (things that can be checked)",
    "examples": "1-2 concrete before/after examples showing improvement"
}}
```"""

        response = self.client.messages.create(
            model=self.model,
            max_tokens=4096,
            system=GENERATOR_SYSTEM_PROMPT,
            messages=[{"role": "user", "content": prompt}],
        )

        # Parse response
        for block in response.content:
            if block.type == "text":
                try:
                    text = block.text
                    if "```json" in text:
                        text = text.split("```json")[1].split("```")[0]
                    return json.loads(text)
                except json.JSONDecodeError:
                    pass

        # Return defaults if parsing fails
        return {
            "title": "Generated Agent Skill",
            "description": f"Optimized practices for {task}",
            "intro": "This skill contains learnings from automated prompt optimization.",
            "activation": "- When working on similar tasks\n- When debugging agent failures",
            "concepts": "See recommendations section.",
            "anti_patterns": patterns_text or "No patterns identified.",
            "practices": recommendations_text or "No specific practices.",
            "guidelines": "1. Review the anti-patterns before implementation\n2. Apply recommended practices",
            "examples": "See optimization artifacts for detailed examples.",
        }

    def _save_references(
        self,
        skill_dir: Path,
        result: LoopResult,
        content: dict[str, str],
    ) -> None:
        """Save reference materials alongside the skill."""
        refs_dir = skill_dir / "references"
        refs_dir.mkdir(exist_ok=True)

        # Save optimization summary
        summary = {
            "task": result.task,
            "iterations": result.total_iterations,
            "initial_score": result.initial_score,
            "final_score": result.final_score,
            "improvement": result.improvement_percentage,
            "converged": result.converged,
            "generated_at": datetime.now().isoformat(),
        }
        with open(refs_dir / "optimization_summary.json", "w") as f:
            json.dump(summary, f, indent=2)

        # Save final optimized prompt
        with open(refs_dir / "optimized_prompt.txt", "w") as f:
            f.write(result.final_prompt)

        # Save all patterns found
        patterns_data = []
        for iteration in result.iterations:
            for p in iteration.analysis.patterns:
                patterns_data.append({
                    "type": p.type.value,
                    "severity": p.severity.value,
                    "description": p.description,
                    "suggestion": p.suggestion,
                    "iteration": iteration.iteration,
                })

        with open(refs_dir / "patterns_found.json", "w") as f:
            json.dump(patterns_data, f, indent=2)


def generate_skill_from_loop(
    result: LoopResult,
    skill_name: str,
    output_dir: str = "./generated_skills",
) -> str:
    """
    Quick helper to generate a skill from optimization results.

    Args:
        result: Optimization loop result
        skill_name: Name for the skill
        output_dir: Output directory

    Returns:
        Path to generated skill
    """
    generator = SkillGenerator()
    return generator.generate(result, skill_name, output_dir)
tests/__init__.pyShow
"""Tests for Reasoning Trace Optimizer."""