akseljoonas HF Staff commited on
Commit
7fc669f
·
1 Parent(s): 00f76b2

minimal impl. of the agent

Browse files
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