Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ MEMORY_COMPRESSION_THRESHOLD=25000 # Hard limit - compress when exceeded
MEMORY_SHORT_TERM_SIZE=100 # Number of recent messages to keep
MEMORY_COMPRESSION_RATIO=0.3 # Target compression ratio (0.3 = 30% of original)

# Tool Result Processing Configuration
# Model for summarizing large tool results (uses LiteLLM format)
# If not set, falls back to smart truncation (no extra API calls)
# Recommended: use a fast, cheap model like gpt-4o-mini or claude-3-haiku
# TOOL_RESULT_SUMMARY_MODEL=openai/gpt-4o-mini
# TOOL_RESULT_SUMMARY_MODEL=anthropic/claude-3-haiku-20240307

# Logging Configuration
LOG_DIR=logs # Directory for log files
LOG_LEVEL=DEBUG # Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ python main.py --task "Calculate 1+1"
- `agent/`: agent loops (ReAct, Plan-Execute) and orchestration
- `tools/`: tool implementations (file ops, shell, web search, etc.)
- `llm/`: provider adapters + retry logic
- `memory/`: memory manager, compression, persistence
- `memory/`: memory manager, compression, persistence, tool result processing
- `docs/`: user/developer documentation
- `scripts/`: packaging/publishing scripts
- `test/`: tests (some require API keys; memory tests are mostly mocked)
Expand Down Expand Up @@ -113,6 +113,7 @@ Unified entrypoint: `./scripts/dev.sh build`
- Packaging & release checklist: `docs/packaging.md`
- Extending tools/agents: `docs/extending.md`
- Memory system: `docs/memory-management.md`, `docs/memory_persistence.md`
- Tool result processing: `docs/tool_result_processing.md`
- Usage examples: `docs/examples.md`

## Safety & Secrets
Expand All @@ -127,3 +128,4 @@ Unified entrypoint: `./scripts/dev.sh build`
- If you change configuration/env vars: update `docs/configuration.md` and `.env.example`.
- If you change packaging/versioning: update `pyproject.toml` and `docs/packaging.md`.
- If you change memory/compression/persistence: add/adjust tests under `test/memory/` and update `docs/memory-management.md` / `docs/memory_persistence.md`.
- If you change tool result processing: add/adjust tests under `test/memory/test_tool_result_processing.py` and update `docs/tool_result_processing.md`.
132 changes: 131 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1 +1,131 @@
AGENTS.md
# AgenticLoop — Agent Instructions

This file defines the **operational workflow** for making changes in this repo (how to set up, run, test, format, build, and publish). Keep it short, specific, and executable; link to docs for long explanations.

Prerequisites: Python 3.12+ and `uv` (https://github.com/astral-sh/uv).

## Quickstart (Local Dev)

```bash
./scripts/bootstrap.sh
source .venv/bin/activate
./scripts/dev.sh test
```

Optional (recommended): enable git hooks:

```bash
pre-commit install
```

## CI

GitHub Actions runs `./scripts/dev.sh precommit`, `./scripts/dev.sh test -q`, and strict typecheck on PRs.

## Review Checklist

Run these before concluding a change:

```bash
./scripts/dev.sh precommit
TYPECHECK_STRICT=1 ./scripts/dev.sh typecheck
./scripts/dev.sh test -q
```

Manual doc/workflow checks:
- README/AGENTS/docs: avoid legacy/removed commands (`LLM_PROVIDER`, `pip install -e`, `requirements.txt`, `setup.py`)
- Docker examples use `--mode`/`--task`
- Python 3.12+ + uv-only prerequisites documented consistently

Change impact reminders:
- CLI changes → update `README.md`, `docs/examples.md`
- Config changes → update `.env.example`, `docs/configuration.md`
- Workflow scripts → update `AGENTS.md`, `docs/packaging.md`

Run a quick smoke task (requires a configured provider in `.env`):

```bash
python main.py --task "Calculate 1+1"
```

## Repo Map

- `main.py`, `cli.py`, `interactive.py`: CLI entry points and UX
- `agent/`: agent loops (ReAct, Plan-Execute) and orchestration
- `tools/`: tool implementations (file ops, shell, web search, etc.)
- `llm/`: provider adapters + retry logic
- `memory/`: memory manager, compression, persistence, tool result processing
- `docs/`: user/developer documentation
- `scripts/`: packaging/publishing scripts
- `test/`: tests (some require API keys; memory tests are mostly mocked)

## Commands (Golden Path)

### Install

- Use `./scripts/bootstrap.sh` to create `.venv` and install dependencies.
- Use `./scripts/dev.sh install` to reinstall dev deps into an existing `.venv`.

### Tests

- All tests: `python -m pytest test/`
- Memory suite: `python -m pytest test/memory/ -v`
- Script: `./scripts/test.sh`
- Unified entrypoint: `./scripts/dev.sh test`
- Integration tests: set `RUN_INTEGRATION_TESTS=1` (live LLM; may incur cost)

### Format

This repo uses `black` + `isort` (see `pyproject.toml`).

```bash
python -m black .
python -m isort .
```

Script: `./scripts/format.sh`
Unified entrypoint: `./scripts/dev.sh format`

### Lint / Typecheck

- Lint (format check): `./scripts/dev.sh lint`
- Pre-commit (recommended): `./scripts/dev.sh precommit`
- Typecheck (best-effort): `./scripts/dev.sh typecheck` (set `TYPECHECK_STRICT=1` to fail on errors)

### Build (Packaging)

```bash
./scripts/build.sh
```
Unified entrypoint: `./scripts/dev.sh build`

### Publish (Manual / Interactive)

`./scripts/publish.sh` defaults to an interactive confirmation and refuses to run without a TTY unless you pass `--yes`.

- TestPyPI: `./scripts/publish.sh --test`
- PyPI (manual): `./scripts/publish.sh`
- Unified entrypoint: `./scripts/dev.sh publish`

## Docs Pointers

- Configuration & `.env`: `docs/configuration.md`
- Packaging & release checklist: `docs/packaging.md`
- Extending tools/agents: `docs/extending.md`
- Memory system: `docs/memory-management.md`, `docs/memory_persistence.md`
- Tool result processing: `docs/tool_result_processing.md`
- Usage examples: `docs/examples.md`

## Safety & Secrets

- Never commit `.env` or API keys.
- Avoid running destructive shell commands; keep file edits scoped and reversible.
- Publishing/releasing steps require explicit human intent (see `docs/packaging.md`).

## When Changing Key Areas

- If you change CLI flags / behavior: update `README.md` and `docs/examples.md`.
- If you change configuration/env vars: update `docs/configuration.md` and `.env.example`.
- If you change packaging/versioning: update `pyproject.toml` and `docs/packaging.md`.
- If you change memory/compression/persistence: add/adjust tests under `test/memory/` and update `docs/memory-management.md` / `docs/memory_persistence.md`.
- If you change tool result processing: add/adjust tests under `test/memory/test_tool_result_processing.py` and update `docs/tool_result_processing.md`.
46 changes: 27 additions & 19 deletions agent/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import TYPE_CHECKING, List, Optional

from llm import LLMMessage, LLMResponse, ToolResult
from memory import MemoryConfig, MemoryManager
from memory import MemoryManager
from tools.base import BaseTool
from tools.todo import TodoTool
from utils import get_logger, terminal_ui
Expand All @@ -26,15 +26,13 @@ def __init__(
llm: "LiteLLMLLM",
tools: List[BaseTool],
max_iterations: int = 10,
memory_config: Optional[MemoryConfig] = None,
):
"""Initialize the agent.

Args:
llm: LLM instance to use
max_iterations: Maximum number of agent loop iterations
tools: List of tools available to the agent
memory_config: Optional memory configuration (None = use defaults)
"""
self.llm = llm
self.max_iterations = max_iterations
Expand All @@ -53,10 +51,8 @@ def __init__(

self.tool_executor = ToolExecutor(tools)

# Initialize memory manager
if memory_config is None:
memory_config = MemoryConfig()
self.memory = MemoryManager(memory_config, llm)
# Initialize memory manager (uses Config directly)
self.memory = MemoryManager(llm)

@abstractmethod
def run(self, task: str) -> str:
Expand Down Expand Up @@ -97,6 +93,7 @@ def _react_loop(
use_memory: bool = True,
save_to_memory: bool = True,
verbose: bool = True,
task: str = "",
) -> str:
"""Execute a ReAct (Reasoning + Acting) loop.

Expand All @@ -111,6 +108,7 @@ def _react_loop(
use_memory: If True, use self.memory for context; if False, use local messages list
save_to_memory: If True, save messages to self.memory (only when use_memory=True)
verbose: If True, print iteration and tool call information
task: Optional task description for context in tool result processing

Returns:
Final answer as a string
Expand Down Expand Up @@ -181,19 +179,29 @@ def _react_loop(

result = self.tool_executor.execute_tool_call(tc.name, tc.arguments)

# Truncate overly large results to prevent context overflow
MAX_TOOL_RESULT_LENGTH = 8000 # characters
if len(result) > MAX_TOOL_RESULT_LENGTH:
truncated_length = MAX_TOOL_RESULT_LENGTH
result = (
result[:truncated_length]
+ f"\n\n[... Output truncated. Showing first {truncated_length} characters of {len(result)} total. "
f"Use grep_content or glob_files for more targeted searches instead of reading large files.]"
# Process tool result with intelligent summarization if memory is enabled
if use_memory and self.memory:
result = self.memory.process_tool_result(
tool_name=tc.name,
tool_call_id=tc.id,
result=result,
context=task, # Pass task as context for intelligent summarization
)
if verbose:
terminal_ui.print_tool_result(result, truncated=True)
elif verbose:
terminal_ui.print_tool_result(result, truncated=False)
else:
# Fallback: simple truncation for non-memory mode
MAX_TOOL_RESULT_LENGTH = 8000 # characters
if len(result) > MAX_TOOL_RESULT_LENGTH:
truncated_length = MAX_TOOL_RESULT_LENGTH
result = (
result[:truncated_length]
+ f"\n\n[... Output truncated. Showing first {truncated_length} characters of {len(result)} total. "
f"Use grep_content or glob_files for more targeted searches instead of reading large files.]"
)

if verbose:
# Check if result was truncated/processed
truncated = "[... " in result or "[Tool Result #" in result
terminal_ui.print_tool_result(result, truncated=truncated)

# Log result (truncated)
logger.debug(f"Tool result: {result[:200]}{'...' if len(result) > 200 else ''}")
Expand Down
1 change: 1 addition & 0 deletions agent/react_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def run(self, task: str) -> str:
use_memory=True,
save_to_memory=True,
verbose=True,
task=task,
)

self._print_memory_stats()
Expand Down
59 changes: 31 additions & 28 deletions config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
"""Configuration management for the agentic system."""

import os
import random

from dotenv import load_dotenv

load_dotenv()


class Config:
"""Configuration for the agentic system."""
"""Configuration for the agentic system.

All configuration is centralized here. Access config values directly via Config.XXX.
"""

# LiteLLM Model Configuration
# Format: provider/model_name (e.g. "anthropic/claude-3-5-sonnet-20241022")
Expand All @@ -31,14 +35,26 @@ class Config:
RETRY_MAX_ATTEMPTS = int(os.getenv("RETRY_MAX_ATTEMPTS", "3"))
RETRY_INITIAL_DELAY = float(os.getenv("RETRY_INITIAL_DELAY", "1.0"))
RETRY_MAX_DELAY = float(os.getenv("RETRY_MAX_DELAY", "60.0"))
RETRY_EXPONENTIAL_BASE = 2.0
RETRY_JITTER = True

# Memory Management Configuration
MEMORY_ENABLED = os.getenv("MEMORY_ENABLED", "true").lower() == "true"
MEMORY_MAX_CONTEXT_TOKENS = int(os.getenv("MEMORY_MAX_CONTEXT_TOKENS", "100000"))
MEMORY_TARGET_TOKENS = int(os.getenv("MEMORY_TARGET_TOKENS", "50000"))
MEMORY_COMPRESSION_THRESHOLD = int(os.getenv("MEMORY_COMPRESSION_THRESHOLD", "40000"))
MEMORY_SHORT_TERM_SIZE = int(os.getenv("MEMORY_SHORT_TERM_SIZE", "100"))
MEMORY_SHORT_TERM_MIN_SIZE = int(os.getenv("MEMORY_SHORT_TERM_MIN_SIZE", "5"))
MEMORY_COMPRESSION_RATIO = float(os.getenv("MEMORY_COMPRESSION_RATIO", "0.3"))
MEMORY_PRESERVE_TOOL_CALLS = True
MEMORY_PRESERVE_SYSTEM_PROMPTS = True

# Tool Result Processing Configuration
TOOL_RESULT_STORAGE_THRESHOLD = int(os.getenv("TOOL_RESULT_STORAGE_THRESHOLD", "10000"))
TOOL_RESULT_STORAGE_PATH = os.getenv("TOOL_RESULT_STORAGE_PATH")
# Model for summarizing large tool results (e.g., "openai/gpt-4o-mini", "anthropic/claude-3-haiku-20240307")
# If not set, LLM summarization is disabled and falls back to smart truncation
TOOL_RESULT_SUMMARY_MODEL = os.getenv("TOOL_RESULT_SUMMARY_MODEL")

# Logging Configuration
LOG_DIR = os.getenv("LOG_DIR", "logs")
Expand All @@ -47,39 +63,26 @@ class Config:
LOG_TO_CONSOLE = os.getenv("LOG_TO_CONSOLE", "false").lower() == "true"

@classmethod
def get_retry_config(cls):
"""Get retry configuration.
def get_retry_delay(cls, attempt: int) -> float:
"""Calculate delay for a given retry attempt using exponential backoff.

Args:
attempt: Current attempt number (0-indexed)

Returns:
RetryConfig instance with settings from environment variables
Delay in seconds
"""
from llm.retry import RetryConfig

return RetryConfig(
max_retries=cls.RETRY_MAX_ATTEMPTS,
initial_delay=cls.RETRY_INITIAL_DELAY,
max_delay=cls.RETRY_MAX_DELAY,
exponential_base=2.0,
jitter=True,
# Calculate exponential backoff
delay = min(
cls.RETRY_INITIAL_DELAY * (cls.RETRY_EXPONENTIAL_BASE**attempt),
cls.RETRY_MAX_DELAY,
)

@classmethod
def get_memory_config(cls):
"""Get memory configuration.
# Add jitter to avoid thundering herd
if cls.RETRY_JITTER:
delay = delay * (0.5 + random.random())

Returns:
MemoryConfig instance with settings from environment variables
"""
from memory import MemoryConfig

return MemoryConfig(
max_context_tokens=cls.MEMORY_MAX_CONTEXT_TOKENS,
target_working_memory_tokens=cls.MEMORY_TARGET_TOKENS,
compression_threshold=cls.MEMORY_COMPRESSION_THRESHOLD,
short_term_message_count=cls.MEMORY_SHORT_TERM_SIZE,
compression_ratio=cls.MEMORY_COMPRESSION_RATIO,
enable_compression=cls.MEMORY_ENABLED,
)
return delay

@classmethod
def validate(cls):
Expand Down
Loading
Loading