Spaces:
Running
Running
Commit ·
7fc669f
1
Parent(s): 00f76b2
minimal impl. of the agent
Browse files- agent/README.md +127 -0
- agent/__init__.py +7 -0
- agent/codex_agent_demo.py +470 -0
- agent/config.py +16 -0
- agent/context_manager/__init__.py +7 -0
- agent/context_manager/manager.py +27 -0
- agent/core/__init__.py +8 -0
- agent/core/agent_loop.py +183 -0
- agent/core/executor.py +21 -0
- agent/core/session.py +53 -0
- agent/tests/__init__.py +3 -0
- agent/tests/unit/test_base.py +10 -0
- agent/tools/__init__.py +8 -0
- agent/utils/__init__.py +7 -0
- agent/utils/logging.py +40 -0
agent/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# HF Agent
|
| 2 |
+
|
| 3 |
+
AI Agent for working with Hugging Face models, datasets, and tools.
|
| 4 |
+
|
| 5 |
+
## Structure
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
agent/
|
| 9 |
+
├── core/ # Core agent logic
|
| 10 |
+
│ ├── agent.py # Main agent implementation
|
| 11 |
+
│ ├── base.py # Base classes and interfaces
|
| 12 |
+
│ ├── planner.py # Task planning and decomposition
|
| 13 |
+
│ └── executor.py # Task execution engine
|
| 14 |
+
│
|
| 15 |
+
├── tools/ # Agent tools and actions
|
| 16 |
+
│ ├── base.py # Tool base class and registry
|
| 17 |
+
│ ├── search/ # Search tools (models, datasets, papers)
|
| 18 |
+
│ ├── generation/ # Generation tools (content, code, data)
|
| 19 |
+
│ ├── analysis/ # Analysis and evaluation tools
|
| 20 |
+
│ └── dataset_ops/ # Dataset operations
|
| 21 |
+
│
|
| 22 |
+
├── prompts/ # Prompt templates
|
| 23 |
+
│ ├── system/ # System prompts
|
| 24 |
+
│ ├── task/ # Task-specific prompts
|
| 25 |
+
│ └── few_shot/ # Few-shot examples
|
| 26 |
+
│
|
| 27 |
+
├── memory/ # Memory systems
|
| 28 |
+
│ ├── short_term/ # Conversational memory
|
| 29 |
+
│ └── long_term/ # Persistent knowledge
|
| 30 |
+
│
|
| 31 |
+
├── config/ # Configuration
|
| 32 |
+
│ ├── settings.py # Settings management
|
| 33 |
+
│ └── default_config.json
|
| 34 |
+
│
|
| 35 |
+
├── utils/ # Utilities
|
| 36 |
+
│ ├── logging.py # Logging setup
|
| 37 |
+
│ └── retry.py # Retry logic
|
| 38 |
+
│
|
| 39 |
+
└── tests/ # Test suite
|
| 40 |
+
├── unit/ # Unit tests
|
| 41 |
+
└── integration/ # Integration tests
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## Key Components
|
| 45 |
+
|
| 46 |
+
### Core
|
| 47 |
+
- **Agent**: Main orchestrator that coordinates planning, execution, and reflection
|
| 48 |
+
- **Planner**: Breaks down complex tasks into actionable steps
|
| 49 |
+
- **Executor**: Executes individual steps using available tools
|
| 50 |
+
|
| 51 |
+
### Tools
|
| 52 |
+
- Modular tool system with base class and registry
|
| 53 |
+
- Tools organized by category (search, generation, analysis, dataset ops)
|
| 54 |
+
- Each tool can be registered and used by the agent
|
| 55 |
+
|
| 56 |
+
### Memory
|
| 57 |
+
- **Short-term**: Manages conversation context and current task state
|
| 58 |
+
- **Long-term**: Persistent storage for learned knowledge and past interactions
|
| 59 |
+
|
| 60 |
+
### Prompts
|
| 61 |
+
- Template-based prompt management
|
| 62 |
+
- System prompts for agent behavior
|
| 63 |
+
- Task-specific prompts for different operations
|
| 64 |
+
- Few-shot examples for learning
|
| 65 |
+
|
| 66 |
+
## Usage
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
from agent import Agent
|
| 70 |
+
from agent.config import load_config
|
| 71 |
+
|
| 72 |
+
# Load configuration
|
| 73 |
+
config = load_config()
|
| 74 |
+
|
| 75 |
+
# Create agent
|
| 76 |
+
agent = Agent(config=config.model_dump())
|
| 77 |
+
|
| 78 |
+
# Run a task
|
| 79 |
+
result = await agent.run("Find the top 5 text generation models")
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
## Development
|
| 83 |
+
|
| 84 |
+
### Adding a New Tool
|
| 85 |
+
|
| 86 |
+
1. Create a new file in the appropriate `tools/` subdirectory
|
| 87 |
+
2. Inherit from `BaseTool`
|
| 88 |
+
3. Implement `execute()` and `_get_parameters()`
|
| 89 |
+
4. Register the tool with the agent
|
| 90 |
+
|
| 91 |
+
```python
|
| 92 |
+
from agent.tools.base import BaseTool
|
| 93 |
+
|
| 94 |
+
class MyTool(BaseTool):
|
| 95 |
+
name = "my_tool"
|
| 96 |
+
description = "Does something useful"
|
| 97 |
+
|
| 98 |
+
async def execute(self, **kwargs):
|
| 99 |
+
# Implementation
|
| 100 |
+
pass
|
| 101 |
+
|
| 102 |
+
def _get_parameters(self):
|
| 103 |
+
return {
|
| 104 |
+
"type": "object",
|
| 105 |
+
"properties": {...}
|
| 106 |
+
}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### Running Tests
|
| 110 |
+
|
| 111 |
+
```bash
|
| 112 |
+
pytest agent/tests/
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
## Configuration
|
| 116 |
+
|
| 117 |
+
Configure the agent via `config/default_config.json` or by passing a config dict:
|
| 118 |
+
|
| 119 |
+
```python
|
| 120 |
+
config = {
|
| 121 |
+
"model_name": "gpt-4",
|
| 122 |
+
"temperature": 0.7,
|
| 123 |
+
"max_iterations": 10,
|
| 124 |
+
"reflection_enabled": True
|
| 125 |
+
}
|
| 126 |
+
agent = Agent(config=config)
|
| 127 |
+
```
|
agent/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
HF Agent - Main agent module
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from agent.core.agent_loop import Agent
|
| 6 |
+
|
| 7 |
+
__all__ = ["Agent"]
|
agent/codex_agent_demo.py
ADDED
|
@@ -0,0 +1,470 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Minimum Viable Implementation of Codex Agent Loop in Python
|
| 3 |
+
|
| 4 |
+
This demonstrates the core architecture patterns from codex-rs:
|
| 5 |
+
- Async submission loop (like submission_loop in codex.rs)
|
| 6 |
+
- Context manager for conversation history
|
| 7 |
+
- Channel-based communication (submissions in, events out)
|
| 8 |
+
- Handler pattern for operations
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import asyncio
|
| 12 |
+
from dataclasses import dataclass, field
|
| 13 |
+
from datetime import datetime
|
| 14 |
+
from enum import Enum
|
| 15 |
+
from typing import Any, Dict, List, Optional
|
| 16 |
+
|
| 17 |
+
# ============================================================================
|
| 18 |
+
# PROTOCOL TYPES (ResponseItem equivalents)
|
| 19 |
+
# ============================================================================
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class MessageRole(Enum):
|
| 23 |
+
SYSTEM = "system"
|
| 24 |
+
USER = "user"
|
| 25 |
+
ASSISTANT = "assistant"
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
@dataclass
|
| 29 |
+
class Message:
|
| 30 |
+
role: MessageRole
|
| 31 |
+
content: str
|
| 32 |
+
timestamp: datetime = field(default_factory=datetime.now)
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class ToolCall:
|
| 37 |
+
call_id: str
|
| 38 |
+
tool_name: str
|
| 39 |
+
arguments: Dict[str, Any]
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
@dataclass
|
| 43 |
+
class ToolOutput:
|
| 44 |
+
call_id: str
|
| 45 |
+
content: str
|
| 46 |
+
success: bool = True
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
# ============================================================================
|
| 50 |
+
# CONTEXT MANAGER (like context_manager/history.rs)
|
| 51 |
+
# ============================================================================
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ContextManager:
|
| 55 |
+
"""
|
| 56 |
+
Manages conversation history with normalization and truncation.
|
| 57 |
+
Based on codex-rs/core/src/context_manager/history.rs
|
| 58 |
+
"""
|
| 59 |
+
|
| 60 |
+
def __init__(self, max_history_length: int = 1000):
|
| 61 |
+
self.items: List[Any] = [] # Oldest → Newest
|
| 62 |
+
self.token_count: int = 0
|
| 63 |
+
self.max_history_length = max_history_length
|
| 64 |
+
|
| 65 |
+
def record_items(self, items: List[Any]) -> None:
|
| 66 |
+
"""Record new items to history (like record_items in history.rs:41)"""
|
| 67 |
+
for item in items:
|
| 68 |
+
# Filter and process items
|
| 69 |
+
if self._is_api_message(item):
|
| 70 |
+
processed = self._process_item(item)
|
| 71 |
+
self.items.append(processed)
|
| 72 |
+
|
| 73 |
+
def _is_api_message(self, item: Any) -> bool:
|
| 74 |
+
"""Filter out system messages (like is_api_message in history.rs:157)"""
|
| 75 |
+
if isinstance(item, Message):
|
| 76 |
+
return item.role != MessageRole.SYSTEM
|
| 77 |
+
return isinstance(item, (ToolCall, ToolOutput))
|
| 78 |
+
|
| 79 |
+
def _process_item(self, item: Any) -> Any:
|
| 80 |
+
"""Process item before adding (like process_item in history.rs:119)"""
|
| 81 |
+
# Truncate long outputs
|
| 82 |
+
if isinstance(item, ToolOutput):
|
| 83 |
+
if len(item.content) > 2000:
|
| 84 |
+
item.content = item.content[:2000] + "...[truncated]"
|
| 85 |
+
return item
|
| 86 |
+
|
| 87 |
+
def get_history_for_prompt(self) -> List[Any]:
|
| 88 |
+
"""
|
| 89 |
+
Get normalized history ready for model
|
| 90 |
+
(like get_history_for_prompt in history.rs:65)
|
| 91 |
+
"""
|
| 92 |
+
self._normalize_history()
|
| 93 |
+
return self.items.copy()
|
| 94 |
+
|
| 95 |
+
def _normalize_history(self) -> None:
|
| 96 |
+
"""
|
| 97 |
+
Enforce invariants (like normalize_history in history.rs:102):
|
| 98 |
+
1. Every tool call has corresponding output
|
| 99 |
+
2. Every output has corresponding call
|
| 100 |
+
"""
|
| 101 |
+
# Build mapping of call_id → call
|
| 102 |
+
calls = {}
|
| 103 |
+
outputs = {}
|
| 104 |
+
|
| 105 |
+
for item in self.items:
|
| 106 |
+
if isinstance(item, ToolCall):
|
| 107 |
+
calls[item.call_id] = item
|
| 108 |
+
elif isinstance(item, ToolOutput):
|
| 109 |
+
outputs[item.call_id] = item
|
| 110 |
+
|
| 111 |
+
# Remove orphan outputs (no matching call)
|
| 112 |
+
self.items = [
|
| 113 |
+
item
|
| 114 |
+
for item in self.items
|
| 115 |
+
if not isinstance(item, ToolOutput) or item.call_id in calls
|
| 116 |
+
]
|
| 117 |
+
|
| 118 |
+
# Add missing outputs for calls (create synthetic outputs)
|
| 119 |
+
for call_id, call in calls.items():
|
| 120 |
+
if call_id not in outputs:
|
| 121 |
+
self.items.append(
|
| 122 |
+
ToolOutput(
|
| 123 |
+
call_id=call_id, content="[No output recorded]", success=False
|
| 124 |
+
)
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
def remove_first_item(self) -> None:
|
| 128 |
+
"""Remove oldest item for compaction (like remove_first_item in history.rs:71)"""
|
| 129 |
+
if self.items:
|
| 130 |
+
removed = self.items.pop(0)
|
| 131 |
+
# Also remove corresponding pair if needed
|
| 132 |
+
if isinstance(removed, ToolCall):
|
| 133 |
+
self.items = [
|
| 134 |
+
item
|
| 135 |
+
for item in self.items
|
| 136 |
+
if not (
|
| 137 |
+
isinstance(item, ToolOutput) and item.call_id == removed.call_id
|
| 138 |
+
)
|
| 139 |
+
]
|
| 140 |
+
elif isinstance(removed, ToolOutput):
|
| 141 |
+
self.items = [
|
| 142 |
+
item
|
| 143 |
+
for item in self.items
|
| 144 |
+
if not (
|
| 145 |
+
isinstance(item, ToolCall) and item.call_id == removed.call_id
|
| 146 |
+
)
|
| 147 |
+
]
|
| 148 |
+
|
| 149 |
+
def compact(self, target_size: int) -> None:
|
| 150 |
+
"""Remove old items until we're under target size"""
|
| 151 |
+
while len(self.items) > target_size:
|
| 152 |
+
self.remove_first_item()
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
# ============================================================================
|
| 156 |
+
# OPERATIONS (like Op enum in codex.rs)
|
| 157 |
+
# ============================================================================
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class OpType(Enum):
|
| 161 |
+
USER_INPUT = "user_input"
|
| 162 |
+
EXEC_APPROVAL = "exec_approval"
|
| 163 |
+
INTERRUPT = "interrupt"
|
| 164 |
+
UNDO = "undo"
|
| 165 |
+
COMPACT = "compact"
|
| 166 |
+
SHUTDOWN = "shutdown"
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
@dataclass
|
| 170 |
+
class Operation:
|
| 171 |
+
op_type: OpType
|
| 172 |
+
data: Optional[Dict[str, Any]] = None
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@dataclass
|
| 176 |
+
class Submission:
|
| 177 |
+
id: str
|
| 178 |
+
operation: Operation
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
# ============================================================================
|
| 182 |
+
# EVENTS (like Event in codex-rs)
|
| 183 |
+
# ============================================================================
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
@dataclass
|
| 187 |
+
class Event:
|
| 188 |
+
event_type: str
|
| 189 |
+
data: Optional[Dict[str, Any]] = None
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
# ============================================================================
|
| 193 |
+
# SESSION STATE (like Session in codex.rs)
|
| 194 |
+
# ============================================================================
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
class Session:
|
| 198 |
+
"""
|
| 199 |
+
Maintains agent session state
|
| 200 |
+
Similar to Session in codex-rs/core/src/codex.rs
|
| 201 |
+
"""
|
| 202 |
+
|
| 203 |
+
def __init__(self, event_queue: asyncio.Queue):
|
| 204 |
+
self.context_manager = ContextManager()
|
| 205 |
+
self.event_queue = event_queue
|
| 206 |
+
self.is_running = True
|
| 207 |
+
self.current_task: Optional[asyncio.Task] = None
|
| 208 |
+
|
| 209 |
+
async def send_event(self, event: Event) -> None:
|
| 210 |
+
"""Send event back to client"""
|
| 211 |
+
await self.event_queue.put(event)
|
| 212 |
+
|
| 213 |
+
def interrupt(self) -> None:
|
| 214 |
+
"""Interrupt current running task"""
|
| 215 |
+
if self.current_task and not self.current_task.done():
|
| 216 |
+
self.current_task.cancel()
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
# ============================================================================
|
| 220 |
+
# OPERATION HANDLERS (like handlers module in codex.rs:1343)
|
| 221 |
+
# ============================================================================
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
class Handlers:
|
| 225 |
+
"""Handler functions for each operation type"""
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
async def user_input(session: Session, text: str) -> None:
|
| 229 |
+
"""Handle user input (like user_input_or_turn in codex.rs:1291)"""
|
| 230 |
+
# Add user message to history
|
| 231 |
+
user_msg = Message(role=MessageRole.USER, content=text)
|
| 232 |
+
session.context_manager.record_items([user_msg])
|
| 233 |
+
|
| 234 |
+
# Send event that we're processing
|
| 235 |
+
await session.send_event(
|
| 236 |
+
Event(event_type="processing", data={"message": "Processing user input"})
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
# Simulate agent processing
|
| 240 |
+
await asyncio.sleep(0.1)
|
| 241 |
+
|
| 242 |
+
# Generate mock assistant response
|
| 243 |
+
assistant_msg = Message(
|
| 244 |
+
role=MessageRole.ASSISTANT, content=f"I received: {text}"
|
| 245 |
+
)
|
| 246 |
+
session.context_manager.record_items([assistant_msg])
|
| 247 |
+
|
| 248 |
+
# Simulate tool call
|
| 249 |
+
tool_call = ToolCall(
|
| 250 |
+
call_id="call_123", tool_name="bash", arguments={"command": "echo 'hello'"}
|
| 251 |
+
)
|
| 252 |
+
session.context_manager.record_items([tool_call])
|
| 253 |
+
|
| 254 |
+
# Simulate tool execution
|
| 255 |
+
await asyncio.sleep(0.1)
|
| 256 |
+
|
| 257 |
+
tool_output = ToolOutput(call_id="call_123", content="hello\n", success=True)
|
| 258 |
+
session.context_manager.record_items([tool_output])
|
| 259 |
+
|
| 260 |
+
# Send completion event
|
| 261 |
+
await session.send_event(
|
| 262 |
+
Event(
|
| 263 |
+
event_type="turn_complete",
|
| 264 |
+
data={"history_size": len(session.context_manager.items)},
|
| 265 |
+
)
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
@staticmethod
|
| 269 |
+
async def interrupt(session: Session) -> None:
|
| 270 |
+
"""Handle interrupt (like interrupt in codex.rs:1266)"""
|
| 271 |
+
session.interrupt()
|
| 272 |
+
await session.send_event(Event(event_type="interrupted"))
|
| 273 |
+
|
| 274 |
+
@staticmethod
|
| 275 |
+
async def compact(session: Session) -> None:
|
| 276 |
+
"""Handle compact (like compact in codex.rs:1317)"""
|
| 277 |
+
old_size = len(session.context_manager.items)
|
| 278 |
+
session.context_manager.compact(target_size=10)
|
| 279 |
+
new_size = len(session.context_manager.items)
|
| 280 |
+
|
| 281 |
+
await session.send_event(
|
| 282 |
+
Event(
|
| 283 |
+
event_type="compacted",
|
| 284 |
+
data={"removed": old_size - new_size, "remaining": new_size},
|
| 285 |
+
)
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
@staticmethod
|
| 289 |
+
async def undo(session: Session) -> None:
|
| 290 |
+
"""Handle undo (like undo in codex.rs:1314)"""
|
| 291 |
+
# Remove last user turn and all following items
|
| 292 |
+
# Simplified: just remove last 2 items
|
| 293 |
+
for _ in range(min(2, len(session.context_manager.items))):
|
| 294 |
+
session.context_manager.items.pop()
|
| 295 |
+
|
| 296 |
+
await session.send_event(Event(event_type="undo_complete"))
|
| 297 |
+
|
| 298 |
+
@staticmethod
|
| 299 |
+
async def shutdown(session: Session) -> bool:
|
| 300 |
+
"""Handle shutdown (like shutdown in codex.rs:1329)"""
|
| 301 |
+
session.is_running = False
|
| 302 |
+
await session.send_event(Event(event_type="shutdown"))
|
| 303 |
+
return True
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
# ============================================================================
|
| 307 |
+
# MAIN AGENT LOOP (like submission_loop in codex.rs:1259)
|
| 308 |
+
# ============================================================================
|
| 309 |
+
|
| 310 |
+
|
| 311 |
+
async def submission_loop(
|
| 312 |
+
submission_queue: asyncio.Queue, event_queue: asyncio.Queue
|
| 313 |
+
) -> None:
|
| 314 |
+
"""
|
| 315 |
+
Main agent loop - processes submissions and dispatches to handlers.
|
| 316 |
+
This is the core of the agent (like submission_loop in codex.rs:1259-1340)
|
| 317 |
+
"""
|
| 318 |
+
session = Session(event_queue)
|
| 319 |
+
|
| 320 |
+
print("🤖 Agent loop started")
|
| 321 |
+
|
| 322 |
+
# Main processing loop
|
| 323 |
+
while session.is_running:
|
| 324 |
+
try:
|
| 325 |
+
# Wait for next submission (like rx_sub.recv() in codex.rs:1262)
|
| 326 |
+
submission = await submission_queue.get()
|
| 327 |
+
|
| 328 |
+
print(f"📨 Received: {submission.operation.op_type.value}")
|
| 329 |
+
|
| 330 |
+
# Dispatch to handler based on operation type
|
| 331 |
+
# (like match in codex.rs:1264-1337)
|
| 332 |
+
op = submission.operation
|
| 333 |
+
|
| 334 |
+
if op.op_type == OpType.USER_INPUT:
|
| 335 |
+
text = op.data.get("text", "") if op.data else ""
|
| 336 |
+
await Handlers.user_input(session, text)
|
| 337 |
+
|
| 338 |
+
elif op.op_type == OpType.INTERRUPT:
|
| 339 |
+
await Handlers.interrupt(session)
|
| 340 |
+
|
| 341 |
+
elif op.op_type == OpType.COMPACT:
|
| 342 |
+
await Handlers.compact(session)
|
| 343 |
+
|
| 344 |
+
elif op.op_type == OpType.UNDO:
|
| 345 |
+
await Handlers.undo(session)
|
| 346 |
+
|
| 347 |
+
elif op.op_type == OpType.SHUTDOWN:
|
| 348 |
+
if await Handlers.shutdown(session):
|
| 349 |
+
break
|
| 350 |
+
|
| 351 |
+
else:
|
| 352 |
+
print(f"⚠️ Unknown operation: {op.op_type}")
|
| 353 |
+
|
| 354 |
+
except asyncio.CancelledError:
|
| 355 |
+
break
|
| 356 |
+
except Exception as e:
|
| 357 |
+
print(f"❌ Error in agent loop: {e}")
|
| 358 |
+
await session.send_event(Event(event_type="error", data={"error": str(e)}))
|
| 359 |
+
|
| 360 |
+
print("🛑 Agent loop exited")
|
| 361 |
+
|
| 362 |
+
|
| 363 |
+
# ============================================================================
|
| 364 |
+
# CODEX INTERFACE (like Codex struct in codex.rs:154)
|
| 365 |
+
# ============================================================================
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
class Codex:
|
| 369 |
+
"""
|
| 370 |
+
Main interface to the agent (like Codex in codex.rs:154-246)
|
| 371 |
+
Provides submit() and next_event() methods
|
| 372 |
+
"""
|
| 373 |
+
|
| 374 |
+
def __init__(self):
|
| 375 |
+
self.submission_queue = asyncio.Queue()
|
| 376 |
+
self.event_queue = asyncio.Queue()
|
| 377 |
+
self.agent_task: Optional[asyncio.Task] = None
|
| 378 |
+
self.submission_counter = 0
|
| 379 |
+
|
| 380 |
+
async def spawn(self) -> None:
|
| 381 |
+
"""Spawn the agent loop (like Codex::spawn in codex.rs:156)"""
|
| 382 |
+
self.agent_task = asyncio.create_task(
|
| 383 |
+
submission_loop(self.submission_queue, self.event_queue)
|
| 384 |
+
)
|
| 385 |
+
|
| 386 |
+
async def submit(self, operation: Operation) -> str:
|
| 387 |
+
"""Submit operation to agent (like Codex::submit in codex.rs:218)"""
|
| 388 |
+
self.submission_counter += 1
|
| 389 |
+
submission = Submission(
|
| 390 |
+
id=f"sub_{self.submission_counter}", operation=operation
|
| 391 |
+
)
|
| 392 |
+
await self.submission_queue.put(submission)
|
| 393 |
+
return submission.id
|
| 394 |
+
|
| 395 |
+
async def next_event(self) -> Optional[Event]:
|
| 396 |
+
"""Get next event from agent (like Codex::next_event in codex.rs:238)"""
|
| 397 |
+
try:
|
| 398 |
+
return await asyncio.wait_for(self.event_queue.get(), timeout=1.0)
|
| 399 |
+
except asyncio.TimeoutError:
|
| 400 |
+
return None
|
| 401 |
+
|
| 402 |
+
async def shutdown(self) -> None:
|
| 403 |
+
"""Shutdown the agent"""
|
| 404 |
+
await self.submit(Operation(op_type=OpType.SHUTDOWN))
|
| 405 |
+
if self.agent_task:
|
| 406 |
+
await self.agent_task
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
# ============================================================================
|
| 410 |
+
# DEMO / EXAMPLE USAGE
|
| 411 |
+
# ============================================================================
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
async def main():
|
| 415 |
+
"""Demo of the agent system"""
|
| 416 |
+
print("=" * 60)
|
| 417 |
+
print("Codex Agent Loop Demo (Python MVP)")
|
| 418 |
+
print("=" * 60)
|
| 419 |
+
|
| 420 |
+
# Create and spawn agent
|
| 421 |
+
codex = Codex()
|
| 422 |
+
await codex.spawn()
|
| 423 |
+
|
| 424 |
+
# Submit some operations
|
| 425 |
+
print("\n1️⃣ Submitting user input...")
|
| 426 |
+
await codex.submit(
|
| 427 |
+
Operation(op_type=OpType.USER_INPUT, data={"text": "Hello, agent!"})
|
| 428 |
+
)
|
| 429 |
+
|
| 430 |
+
# Receive events
|
| 431 |
+
for _ in range(3):
|
| 432 |
+
event = await codex.next_event()
|
| 433 |
+
if event:
|
| 434 |
+
print(f" ✅ Event: {event.event_type} - {event.data}")
|
| 435 |
+
|
| 436 |
+
print("\n2️⃣ Submitting another input...")
|
| 437 |
+
await codex.submit(
|
| 438 |
+
Operation(op_type=OpType.USER_INPUT, data={"text": "What's the weather?"})
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
for _ in range(3):
|
| 442 |
+
event = await codex.next_event()
|
| 443 |
+
if event:
|
| 444 |
+
print(f" ✅ Event: {event.event_type} - {event.data}")
|
| 445 |
+
|
| 446 |
+
print("\n3️⃣ Compacting history...")
|
| 447 |
+
await codex.submit(Operation(op_type=OpType.COMPACT))
|
| 448 |
+
|
| 449 |
+
event = await codex.next_event()
|
| 450 |
+
if event:
|
| 451 |
+
print(f" ✅ Event: {event.event_type} - {event.data}")
|
| 452 |
+
|
| 453 |
+
print("\n4️⃣ Undoing last turn...")
|
| 454 |
+
await codex.submit(Operation(op_type=OpType.UNDO))
|
| 455 |
+
|
| 456 |
+
event = await codex.next_event()
|
| 457 |
+
if event:
|
| 458 |
+
print(f" ✅ Event: {event.event_type}")
|
| 459 |
+
|
| 460 |
+
# Shutdown
|
| 461 |
+
print("\n5️⃣ Shutting down...")
|
| 462 |
+
await codex.shutdown()
|
| 463 |
+
|
| 464 |
+
print("\n" + "=" * 60)
|
| 465 |
+
print("Demo complete!")
|
| 466 |
+
print("=" * 60)
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
if __name__ == "__main__":
|
| 470 |
+
asyncio.run(main())
|
agent/config.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from litellm import Tool
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Config(BaseModel):
|
| 6 |
+
"""Configuration manager"""
|
| 7 |
+
|
| 8 |
+
model_name: str
|
| 9 |
+
tools: list[Tool]
|
| 10 |
+
system_prompt_path: str
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def load_config(config_path: str = "config.json") -> Config:
|
| 14 |
+
"""Load configuration from file"""
|
| 15 |
+
with open(config_path, "r") as f:
|
| 16 |
+
return Config.model_validate_json(f.read())
|
agent/context_manager/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt templates and management
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from agent.prompts.manager import PromptManager
|
| 6 |
+
|
| 7 |
+
__all__ = ["PromptManager"]
|
agent/context_manager/manager.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Prompt template management
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from litellm import Message
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class ContextManager:
|
| 9 |
+
"""Manages context templates for the agent"""
|
| 10 |
+
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.system_prompt = self._load_system_prompt()
|
| 13 |
+
self.messages: list[Message] = [
|
| 14 |
+
Message(role="system", content=self.system_prompt)
|
| 15 |
+
]
|
| 16 |
+
|
| 17 |
+
def _load_system_prompt(self):
|
| 18 |
+
"""Load the system prompt"""
|
| 19 |
+
|
| 20 |
+
# TODO: get system prompt from jinja template
|
| 21 |
+
return "You are a helpful assistant."
|
| 22 |
+
|
| 23 |
+
def add_message(self, message: Message) -> None:
|
| 24 |
+
self.messages.append(message)
|
| 25 |
+
|
| 26 |
+
def get_messages(self) -> list[Message]:
|
| 27 |
+
return self.messages
|
agent/core/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core agent implementation
|
| 3 |
+
Contains the main agent logic, decision-making, and orchestration
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from agent.core.executor import ToolExecutor
|
| 7 |
+
|
| 8 |
+
__all__ = ["ToolExecutor"]
|
agent/core/agent_loop.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main agent implementation
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
|
| 7 |
+
from litellm import (
|
| 8 |
+
ChatCompletionMessageToolCall,
|
| 9 |
+
Message,
|
| 10 |
+
ModelResponse,
|
| 11 |
+
acompletion,
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
from agent.core.session import Event, OpType, Session
|
| 15 |
+
|
| 16 |
+
ToolCall = ChatCompletionMessageToolCall
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class Handlers:
|
| 20 |
+
"""Handler functions for each operation type"""
|
| 21 |
+
|
| 22 |
+
@staticmethod
|
| 23 |
+
async def run_agent(session: Session, text: str, max_iterations: int = 10) -> None:
|
| 24 |
+
"""Handle user input (like user_input_or_turn in codex.rs:1291)"""
|
| 25 |
+
# Add user message to history
|
| 26 |
+
user_msg = Message(role="user", content=text)
|
| 27 |
+
session.context_manager.add_message(user_msg)
|
| 28 |
+
|
| 29 |
+
# Send event that we're processing
|
| 30 |
+
await session.send_event(
|
| 31 |
+
Event(event_type="processing", data={"message": "Processing user input"})
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# Agentic loop - continue until model doesn't call tools or max iterations is reached
|
| 35 |
+
iteration = 0
|
| 36 |
+
while iteration < max_iterations:
|
| 37 |
+
messages = session.context_manager.get_messages()
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
response: ModelResponse = await acompletion(
|
| 41 |
+
model=session.config.model_name,
|
| 42 |
+
messages=messages,
|
| 43 |
+
tools=session.config.tools,
|
| 44 |
+
)
|
| 45 |
+
message = response.choices[0].message
|
| 46 |
+
|
| 47 |
+
# Extract content and tool calls
|
| 48 |
+
content = message.content
|
| 49 |
+
tool_calls: list[ToolCall] = message.get("tool_calls", [])
|
| 50 |
+
|
| 51 |
+
# Record assistant message if there's content
|
| 52 |
+
if content:
|
| 53 |
+
assistant_msg = Message(role="assistant", content=content)
|
| 54 |
+
session.context_manager.add_message(assistant_msg)
|
| 55 |
+
|
| 56 |
+
await session.send_event(
|
| 57 |
+
Event(
|
| 58 |
+
event_type="assistant_message",
|
| 59 |
+
data={"message": assistant_msg},
|
| 60 |
+
)
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# If no tool calls, we're done
|
| 64 |
+
if not tool_calls:
|
| 65 |
+
break
|
| 66 |
+
|
| 67 |
+
for tool_call in tool_calls:
|
| 68 |
+
result = await session.tool_executor.execute_tool(tool_call)
|
| 69 |
+
|
| 70 |
+
tool_output = Message(role="tool", content=result.output)
|
| 71 |
+
session.context_manager.add_message(tool_output)
|
| 72 |
+
|
| 73 |
+
await session.send_event(
|
| 74 |
+
Event(
|
| 75 |
+
event_type="tool_output",
|
| 76 |
+
data={"message": tool_output},
|
| 77 |
+
)
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
iteration += 1
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
await session.send_event(
|
| 84 |
+
Event(event_type="error", data={"error": str(e)})
|
| 85 |
+
)
|
| 86 |
+
break
|
| 87 |
+
|
| 88 |
+
# Send completion event
|
| 89 |
+
await session.send_event(
|
| 90 |
+
Event(
|
| 91 |
+
event_type="turn_complete",
|
| 92 |
+
data={"history_size": len(session.context_manager.items)},
|
| 93 |
+
)
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
async def interrupt(session: Session) -> None:
|
| 98 |
+
"""Handle interrupt (like interrupt in codex.rs:1266)"""
|
| 99 |
+
session.interrupt()
|
| 100 |
+
await session.send_event(Event(event_type="interrupted"))
|
| 101 |
+
|
| 102 |
+
@staticmethod
|
| 103 |
+
async def compact(session: Session) -> None:
|
| 104 |
+
"""Handle compact (like compact in codex.rs:1317)"""
|
| 105 |
+
old_size = len(session.context_manager.items)
|
| 106 |
+
session.context_manager.compact(target_size=10)
|
| 107 |
+
new_size = len(session.context_manager.items)
|
| 108 |
+
|
| 109 |
+
await session.send_event(
|
| 110 |
+
Event(
|
| 111 |
+
event_type="compacted",
|
| 112 |
+
data={"removed": old_size - new_size, "remaining": new_size},
|
| 113 |
+
)
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
@staticmethod
|
| 117 |
+
async def undo(session: Session) -> None:
|
| 118 |
+
"""Handle undo (like undo in codex.rs:1314)"""
|
| 119 |
+
# Remove last user turn and all following items
|
| 120 |
+
# Simplified: just remove last 2 items
|
| 121 |
+
for _ in range(min(2, len(session.context_manager.items))):
|
| 122 |
+
session.context_manager.items.pop()
|
| 123 |
+
|
| 124 |
+
await session.send_event(Event(event_type="undo_complete"))
|
| 125 |
+
|
| 126 |
+
@staticmethod
|
| 127 |
+
async def shutdown(session: Session) -> bool:
|
| 128 |
+
"""Handle shutdown (like shutdown in codex.rs:1329)"""
|
| 129 |
+
session.is_running = False
|
| 130 |
+
await session.send_event(Event(event_type="shutdown"))
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
async def submission_loop(
|
| 135 |
+
submission_queue: asyncio.Queue, event_queue: asyncio.Queue
|
| 136 |
+
) -> None:
|
| 137 |
+
"""
|
| 138 |
+
Main agent loop - processes submissions and dispatches to handlers.
|
| 139 |
+
This is the core of the agent (like submission_loop in codex.rs:1259-1340)
|
| 140 |
+
"""
|
| 141 |
+
session = Session(event_queue)
|
| 142 |
+
|
| 143 |
+
print("🤖 Agent loop started")
|
| 144 |
+
|
| 145 |
+
# Main processing loop
|
| 146 |
+
while session.is_running:
|
| 147 |
+
try:
|
| 148 |
+
# Wait for next submission (like rx_sub.recv() in codex.rs:1262)
|
| 149 |
+
submission = await submission_queue.get()
|
| 150 |
+
|
| 151 |
+
print(f"📨 Received: {submission.operation.op_type.value}")
|
| 152 |
+
|
| 153 |
+
# Dispatch to handler based on operation type
|
| 154 |
+
op = submission.operation
|
| 155 |
+
|
| 156 |
+
if op.op_type == OpType.USER_INPUT:
|
| 157 |
+
text = op.data.get("text", "") if op.data else ""
|
| 158 |
+
await Handlers.run_agent(session, text, max_iterations=10)
|
| 159 |
+
|
| 160 |
+
elif op.op_type == OpType.INTERRUPT:
|
| 161 |
+
# im not currently sure what this does lol
|
| 162 |
+
await Handlers.interrupt(session)
|
| 163 |
+
|
| 164 |
+
elif op.op_type == OpType.COMPACT:
|
| 165 |
+
await Handlers.compact(session)
|
| 166 |
+
|
| 167 |
+
elif op.op_type == OpType.UNDO:
|
| 168 |
+
await Handlers.undo(session)
|
| 169 |
+
|
| 170 |
+
elif op.op_type == OpType.SHUTDOWN:
|
| 171 |
+
if await Handlers.shutdown(session):
|
| 172 |
+
break
|
| 173 |
+
|
| 174 |
+
else:
|
| 175 |
+
print(f"⚠️ Unknown operation: {op.op_type}")
|
| 176 |
+
|
| 177 |
+
except asyncio.CancelledError:
|
| 178 |
+
break
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"❌ Error in agent loop: {e}")
|
| 181 |
+
await session.send_event(Event(event_type="error", data={"error": str(e)}))
|
| 182 |
+
|
| 183 |
+
print("🛑 Agent loop exited")
|
agent/core/executor.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Task execution engine
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
from litellm import ChatCompletionMessageToolCall
|
| 8 |
+
|
| 9 |
+
ToolCall = ChatCompletionMessageToolCall
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class ToolExecutor:
|
| 13 |
+
"""Executes planned tasks using available tools"""
|
| 14 |
+
|
| 15 |
+
def __init__(self, tools: List[Any] = None):
|
| 16 |
+
self.tools = tools or []
|
| 17 |
+
|
| 18 |
+
async def execute_tool(self, tool_call: ToolCall) -> Dict[str, Any]:
|
| 19 |
+
"""Execute a single step in the plan"""
|
| 20 |
+
# TODO: Implement step execution
|
| 21 |
+
return {"status": "success", "result": None}
|
agent/core/session.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
from enum import Enum
|
| 3 |
+
from typing import Any, Literal
|
| 4 |
+
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
from agent.context_manager.manager import ContextManager
|
| 8 |
+
from agent.core import ToolExecutor
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class OpType(Enum):
|
| 12 |
+
USER_INPUT = "user_input"
|
| 13 |
+
EXEC_APPROVAL = "exec_approval"
|
| 14 |
+
INTERRUPT = "interrupt"
|
| 15 |
+
UNDO = "undo"
|
| 16 |
+
COMPACT = "compact"
|
| 17 |
+
SHUTDOWN = "shutdown"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class Event(BaseModel):
|
| 21 |
+
event_type: Literal[
|
| 22 |
+
"processing",
|
| 23 |
+
"turn_complete",
|
| 24 |
+
"compacted",
|
| 25 |
+
"undo_complete",
|
| 26 |
+
"shutdown",
|
| 27 |
+
"error",
|
| 28 |
+
"interrupted",
|
| 29 |
+
] # Dummy events for now
|
| 30 |
+
data: dict[str, Any] | None = None
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Session:
|
| 34 |
+
"""
|
| 35 |
+
Maintains agent session state
|
| 36 |
+
Similar to Session in codex-rs/core/src/codex.rs
|
| 37 |
+
"""
|
| 38 |
+
|
| 39 |
+
def __init__(self, event_queue: asyncio.Queue):
|
| 40 |
+
self.context_manager = ContextManager()
|
| 41 |
+
self.tool_executor = ToolExecutor()
|
| 42 |
+
self.event_queue = event_queue
|
| 43 |
+
self.is_running = True
|
| 44 |
+
self.current_task: asyncio.Task | None = None
|
| 45 |
+
|
| 46 |
+
async def send_event(self, event: Event) -> None:
|
| 47 |
+
"""Send event back to client"""
|
| 48 |
+
await self.event_queue.put(event)
|
| 49 |
+
|
| 50 |
+
def interrupt(self) -> None:
|
| 51 |
+
"""Interrupt current running task"""
|
| 52 |
+
if self.current_task and not self.current_task.done():
|
| 53 |
+
self.current_task.cancel()
|
agent/tests/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Test suite for HF Agent
|
| 3 |
+
"""
|
agent/tests/unit/test_base.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Unit tests for base agent components
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def test_base_agent_initialization():
|
| 7 |
+
"""Test that BaseAgent can be initialized with config"""
|
| 8 |
+
# This will fail because BaseAgent is abstract
|
| 9 |
+
# Subclasses should implement this
|
| 10 |
+
pass
|
agent/tools/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Agent tools and actions
|
| 3 |
+
Tools are the actions the agent can take to interact with the environment
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from agent.tools.base import BaseTool, ToolRegistry
|
| 7 |
+
|
| 8 |
+
__all__ = ["BaseTool", "ToolRegistry"]
|
agent/utils/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Utility functions and helpers
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from agent.utils.logging import setup_logger
|
| 6 |
+
|
| 7 |
+
__all__ = ["setup_logger"]
|
agent/utils/logging.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Logging utilities
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def setup_logger(
|
| 12 |
+
name: str = "hf_agent", level: int = logging.INFO, log_file: Optional[Path] = None
|
| 13 |
+
) -> logging.Logger:
|
| 14 |
+
"""Setup and configure logger"""
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(name)
|
| 17 |
+
logger.setLevel(level)
|
| 18 |
+
|
| 19 |
+
# Remove existing handlers
|
| 20 |
+
logger.handlers = []
|
| 21 |
+
|
| 22 |
+
# Console handler
|
| 23 |
+
console_handler = logging.StreamHandler(sys.stdout)
|
| 24 |
+
console_handler.setLevel(level)
|
| 25 |
+
console_format = logging.Formatter(
|
| 26 |
+
"%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
| 27 |
+
datefmt="%Y-%m-%d %H:%M:%S",
|
| 28 |
+
)
|
| 29 |
+
console_handler.setFormatter(console_format)
|
| 30 |
+
logger.addHandler(console_handler)
|
| 31 |
+
|
| 32 |
+
# File handler if log_file specified
|
| 33 |
+
if log_file:
|
| 34 |
+
log_file.parent.mkdir(parents=True, exist_ok=True)
|
| 35 |
+
file_handler = logging.FileHandler(log_file)
|
| 36 |
+
file_handler.setLevel(level)
|
| 37 |
+
file_handler.setFormatter(console_format)
|
| 38 |
+
logger.addHandler(file_handler)
|
| 39 |
+
|
| 40 |
+
return logger
|