tiny-agent-rs 0.1.0

A lightweight, type-safe Rust agent library for LLM tool calling
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# Agent Module Architecture

**Last Updated**: October 2025  
**Version**: Post-refactoring (commit b8db053)

## Overview

The agent module implements the core agentic loop for the TinyAgent framework. After refactoring, it follows a clean separation of concerns with each module having a single, well-defined responsibility.

**Total Lines**: 2441 (down from 1219 in a single file)  
**Modules**: 9 focused modules (was 1 monolithic file)

---

## Module Structure

```
src/agent/
├── mod.rs                    (122 lines) - Public API & coordination
├── execution.rs              (600 lines) - Core agentic run loops
├── response_handler.rs       (379 lines) - Special tool call handlers
├── schema_validation.rs      (188 lines) - Schema validation & tool definitions
├── tool_call_utils.rs        (42 lines)  - Tool call parsing utilities
├── memory.rs                 (248 lines) - Conversation memory management
├── planning.rs               (129 lines) - Planning prompt generation
├── result.rs                 (388 lines) - Run result types
├── steps.rs                  (111 lines) - Agent step types
└── tool_call.rs              (234 lines) - Tool call execution tracking
```

---

## Module Responsibilities

### 1. `mod.rs` - Public API & Coordinator

**Purpose**: Thin facade providing public API and agent configuration

**Responsibilities**:
- Define `Agent` struct with configuration fields
- Provide builder pattern methods (`.with_model()`, `.with_max_iterations()`, etc.)
- Expose public API methods (`run()`, `run_with_steps()`, `run_with_messages()`)
- Delegate execution to specialized modules
- Re-export public types

**Key Types**:
```rust
pub struct Agent {
    openai_client: OpenAIClient,
    function_factory: FunctionFactory,
    model: String,
    max_iterations: usize,
    max_tokens: Option<u32>,
    timeout: Duration,
    completion_schema: Option<SchemaHandle>,
}
```

**Dependencies**:
- `execution.rs` for run loops
- All submodules for re-exports

---

### 2. `execution.rs` - Core Agentic Run Loops

**Purpose**: Implement the main agentic loops with tool execution

**Responsibilities**:
- Execute `run_with_steps()` - returns detailed `RunResult` with step history
- Execute `run_with_messages()` - returns simple string answer
- Manage iteration loops and max iteration limits
- Coordinate tool calls with function factory
- Handle API requests and timeouts
- Use handlers from `response_handler.rs` for special tools

**Key Functions**:
```rust
impl Agent {
    pub async fn run_with_steps(&self, prompt: &str) -> Result<RunResult>
    pub async fn run_with_messages(&self, mut messages: Vec<Value>) -> Result<String>
}
```

**Design Pattern**: Uses `ErrorSink` trait to abstract over different error reporting mechanisms:
- `MemorySink` for `run_with_steps()` (adds to `AgentMemory`)
- `MessagesSink` for `run_with_messages()` (pushes to message array)

**Dependencies**:
- `response_handler.rs` for final_answer and structured_response handling
- `schema_validation.rs` for tool definitions and schema injection
- `tool_call_utils.rs` for parsing tool call JSON
- `memory.rs` for step tracking

---

### 3. `response_handler.rs` - Special Tool Call Handlers

**Purpose**: Unified handlers for `final_answer` and `structured_response` tools

**Responsibilities**:
- Validate and process `final_answer` tool calls
- Validate and process `structured_response` tool calls
- Abstract error reporting via `ErrorSink` trait
- Return outcomes indicating next action (`Continue`, `ReturnResult`, `ReturnAnswer`)

**Key Design Pattern - ErrorSink Trait**:
```rust
pub(super) trait ErrorSink {
    fn report_error(&mut self, tool_call_id: &str, error_message: String);
    fn report_observation(&mut self, tool_call_id: &str, result: String, is_error: bool);
}
```

This trait eliminates 400+ lines of duplication by abstracting the difference between:
- `memory.add_step(AgentStep::Observation {...})` in `run_with_steps()`
- `messages.push(json!({...}))` in `run_with_messages()`

**Handler Outcome**:
```rust
pub(super) enum HandlerOutcome {
    Continue,                    // Continue loop iteration
    ReturnResult(RunResult),     // Return complete result (run_with_steps)
    ReturnAnswer(String),        // Return simple answer (run_with_messages)
}
```

**Handler Functions**:
```rust
// For run_with_steps
pub(super) fn handle_final_answer_steps(
    ctx: FinalAnswerStepsContext<'_>,
    sink: &mut dyn ErrorSink
) -> Result<HandlerOutcome>

pub(super) fn handle_structured_response_steps(
    ctx: StructuredResponseStepsContext<'_>,
    sink: &mut dyn ErrorSink
) -> Result<HandlerOutcome>

// For run_with_messages
pub(super) fn handle_final_answer_messages(
    ctx: FinalAnswerContext<'_>,
    sink: &mut dyn ErrorSink
) -> Result<HandlerOutcome>

pub(super) fn handle_structured_response_messages(
    ctx: StructuredResponseContext<'_>,
    sink: &mut dyn ErrorSink
) -> Result<HandlerOutcome>
```

**Dependencies**:
- `schema_validation.rs` for validation logic
- `result.rs` for `RunResult` type
- `steps.rs` for `AgentStep` type

---

### 4. `schema_validation.rs` - Schema Validation & Tool Definitions

**Purpose**: Validate structured responses against JSON schemas and define tool specifications

**Responsibilities**:
- Validate structured payloads against `SchemaHandle` using `jsonschema` crate
- Generate `final_answer` tool definition for OpenAI API
- Generate `structured_response` tool definition with embedded schema
- Inject schema instructions into system messages
- Provide detailed validation error messages

**Key Functions**:
```rust
pub(super) fn validate_structured_payload(
    schema: &SchemaHandle,
    payload: &Value,
) -> std::result::Result<(), AgentError>

pub(super) fn final_answer_tool_definition() -> Value

pub(super) fn structured_response_tool_definition(schema: &SchemaHandle) -> Value

pub(super) fn inject_schema_instructions(messages: &mut [Value], schema: &SchemaHandle)
```

**Helper Types**:
```rust
#[derive(Deserialize)]
pub(super) struct FinalAnswerArguments {
    pub answer: String,
    pub structured: Option<Value>,
    pub _meta: Option<Value>,
}

#[derive(Deserialize)]
pub(super) struct StructuredResponseArguments {
    pub structured: Value,
    pub _meta: Option<Value>,
}
```

**Validation Features**:
- JSON Schema Draft 7 validation
- Clear error messages with field paths (e.g., `/itinerary/0: "estimated_cost" is a required property`)
- Error truncation (max 3 errors shown to avoid overwhelming the LLM)

**Dependencies**:
- `jsonschema` crate for validation
- `schema.rs` for `SchemaHandle` type

---

### 5. `tool_call_utils.rs` - Tool Call Parsing Utilities

**Purpose**: Extract and parse tool call information from OpenAI API responses

**Responsibilities**:
- Extract `tool_call_id` from tool call JSON
- Extract function name and object from tool call JSON
- Parse function arguments from JSON string
- Extract arguments string from function object

**Key Functions**:
```rust
pub(super) fn extract_tool_call_id(tool_call: &Value) -> &str

pub(super) fn extract_function_info(tool_call: &Value) -> Option<(Value, Option<String>)>

pub(super) fn parse_function_arguments(
    arguments_str: &str,
    function_name: &str,
) -> Result<Value, AgentError>

pub(super) fn extract_arguments_str(function: &Value) -> &str
```

**Design Note**: These utilities eliminate repeated JSON navigation code and provide consistent error messages when tool calls are malformed.

**Dependencies**: None (pure utility functions)

---

### 6. `memory.rs` - Conversation Memory Management

**Purpose**: Track agent reasoning steps and convert to OpenAI message format

**Key Type**:
```rust
pub struct AgentMemory {
    steps: Vec<AgentStep>,
    system_prompt: Option<String>,
}
```

**Responsibilities**:
- Store agent steps (Task, Planning, Action, Observation, FinalAnswer)
- Convert steps to OpenAI message format
- Provide filtering and counting utilities
- Update structured response in final answer step

---

### 7. `planning.rs` - Planning Prompt Generation

**Purpose**: Generate planning prompts for ReAct-style reasoning

**Responsibilities**:
- Generate initial planning prompts
- Generate tool planning prompts
- Check if response is a planning response

---

### 8. `result.rs` - Run Result Types

**Purpose**: Define result types returned from agent execution

**Key Types**:
```rust
pub struct RunResult {
    pub output: String,
    pub structured: Option<Value>,
    pub schema: Option<SchemaHandle>,
    pub steps: Vec<AgentStep>,
    pub tokens: Option<TokenUsage>,
    pub duration: Duration,
    pub iterations: usize,
}

pub struct TokenUsage {
    pub prompt_tokens: u32,
    pub completion_tokens: u32,
    pub total_tokens: u32,
}
```

**Responsibilities**:
- Aggregate execution results
- Provide replay/explain methods for debugging
- Deserialize structured responses with type safety

---

### 9. `steps.rs` - Agent Step Types

**Purpose**: Define the types of steps an agent can take

**Key Type**:
```rust
pub enum AgentStep {
    Task { content: String },
    Planning { plan: String },
    Action { tool_name: String, tool_call_id: String, arguments: Value },
    Observation { tool_call_id: String, result: String, is_error: bool },
    FinalAnswer { answer: String, structured: Option<Value> },
}
```

**Responsibilities**:
- Define step variants
- Convert steps to OpenAI message format
- Provide human-readable descriptions

---

## Data Flow

### Typical Execution Flow (run_with_steps)

```
User calls agent.run_with_steps(prompt)
    ↓
execution.rs creates AgentMemory
    ↓
Loop: while iteration < max_iterations
    ↓
execution.rs calls OpenAI API with tools
    ↓
Receive tool calls from API
    ↓
For each tool call:
    ↓
    tool_call_utils.rs parses tool call JSON
    ↓
    If "final_answer" or "structured_response":
        ↓
        response_handler.rs handles special tool
        ↓
        schema_validation.rs validates if needed
        ↓
        ErrorSink reports result to AgentMemory
        ↓
        HandlerOutcome determines if done or continue
    Else:
        ↓
        execution.rs calls function_factory.execute_function()
        ↓
        Result added to AgentMemory
    ↓
Loop continues or returns RunResult
```

### Key Abstraction: ErrorSink Pattern

The `ErrorSink` trait enables code reuse between two similar but different execution paths:

**run_with_steps** (uses `AgentMemory`):
```rust
struct MemorySink<'a> {
    memory: &'a mut AgentMemory,
}

impl ErrorSink for MemorySink<'_> {
    fn report_error(&mut self, tool_call_id: &str, error_message: String) {
        self.memory.add_step(AgentStep::Observation {
            tool_call_id: tool_call_id.to_string(),
            result: error_message,
            is_error: true,
        });
    }
}
```

**run_with_messages** (uses `Vec<Value>`):
```rust
struct MessagesSink<'a> {
    messages: &'a mut Vec<Value>,
}

impl ErrorSink for MessagesSink<'_> {
    fn report_error(&mut self, tool_call_id: &str, error_message: String) {
        self.messages.push(json!({
            "role": "tool",
            "tool_call_id": tool_call_id,
            "content": error_message
        }));
    }
}
```

This pattern eliminated over 400 lines of duplicated validation and error handling code.

---

## Benefits of Refactored Architecture

### 1. DRY (Don't Repeat Yourself)
- Eliminated 400+ lines of duplication via `ErrorSink` trait
- Tool call parsing extracted to reusable utilities
- Schema validation centralized

### 2. Single Responsibility Principle
- Each module has one clear purpose
- Easy to locate where changes should be made
- Reduced cognitive load when reading code

### 3. Testability
- Utilities can be unit tested in isolation
- Handlers can be tested with mock `ErrorSink` implementations
- Validation logic separated from execution logic

### 4. Maintainability
- Changes to tool handling happen in one place
- Adding new special tools requires minimal code
- Clear module boundaries prevent coupling

### 5. Readability
- All files under 600 lines (meeting project guideline)
- Clear module names indicate purpose
- Self-documenting structure

### 6. Extensibility
- Easy to add new tool types by extending handlers
- Easy to add new sink types for different output formats
- Schema validation can be extended without touching execution logic

---

## Design Patterns Used

### 1. Strategy Pattern
`ErrorSink` trait allows swapping error reporting strategies without changing handler code.

### 2. Builder Pattern
`Agent` struct uses builder methods for configuration.

### 3. Facade Pattern
`mod.rs` provides simplified public API hiding internal complexity.

### 4. Template Method Pattern
Handler functions define algorithm structure, with hooks for different contexts (steps vs messages).

### 5. Adapter Pattern
`MemorySink` and `MessagesSink` adapt different data structures to common `ErrorSink` interface.

---

## Future Improvements

### Potential Enhancements
1. **Streaming Support**: Add streaming execution for real-time step updates
2. **Retry Logic**: Implement automatic retry for specific error types
3. **Parallel Tool Execution**: Execute independent tool calls in parallel
4. **Handler Registry**: Dynamic registration of custom tool handlers
5. **Middleware Pipeline**: Add hooks for logging, metrics, or custom processing

### Monitoring Line Counts
- `execution.rs` is at 600 lines (the guideline limit)
- If it grows further, consider splitting into:
  - `execution/run_with_steps.rs`
  - `execution/run_with_messages.rs`
  - `execution/common.rs`

---

## Testing Strategy

### Unit Tests
- `tool_call_utils.rs`: Test each parsing function with valid/invalid inputs
- `schema_validation.rs`: Test validation with valid/invalid schemas
- `response_handler.rs`: Test handlers with mock `ErrorSink`

### Integration Tests
- Full agent execution with mock API responses
- Schema validation end-to-end
- Error recovery scenarios

### Current Test Coverage
- ✅ 34 tests passing (20 unit + 14 integration)
- ✅ All modules compile without warnings
- ✅ Clippy passes with `-D warnings`

---

## Migration Notes

### For Developers

**Before Refactoring**:
```rust
// Everything in src/agent/mod.rs (1219 lines)
impl Agent {
    pub async fn run_with_steps(...) { /* 440 lines */ }
    pub async fn run_with_messages(...) { /* 390 lines */ }
    fn validate_structured_payload(...) { /* ... */ }
    // ... everything else
}
```

**After Refactoring**:
```rust
// src/agent/mod.rs (122 lines) - clean public API
// src/agent/execution.rs - run loops
// src/agent/response_handler.rs - special tool handlers
// src/agent/schema_validation.rs - validation logic
// src/agent/tool_call_utils.rs - parsing utilities
```

**No Breaking Changes**: Public API remains identical. Only internal organization changed.

---

## References

- **Refactoring Commit**: `b8db053` - "Refactor Agent Module to Address Code Smells and Reduce Complexity"
- **Plan Document**: `refactor-agent-module.plan.md`
- **Repository Guidelines**: `README.md` and repo rules

---

## Glossary

- **ErrorSink**: Trait abstracting error reporting to memory or messages
- **HandlerOutcome**: Enum indicating what execution loop should do next
- **SchemaHandle**: Cached JSON schema with validation capabilities
- **AgentMemory**: Structured conversation history with reasoning steps
- **RunResult**: Complete execution result with steps, timing, and structured output

---

**Document Status**: ✅ Complete  
**Maintenance**: Update when major architectural changes occur