Skip to content

Commit 4be165a

Browse files
authored
Merge pull request #10 from JaimeStill/decoupled-layers
session: flatten agent config structure
2 parents c639ef0 + 3cbd482 commit 4be165a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+3670
-3777
lines changed

ARCHITECTURE.md

Lines changed: 88 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,34 @@ This document describes the current architecture and implementation patterns of
77
```
88
pkg/
99
├── config/ # Configuration management and loading
10-
│ ├── agent.go # Agent configuration structure
11-
│ ├── client.go # Client and retry configuration
10+
│ ├── agent.go # Agent configuration structure (flat: client, provider, model as peers)
11+
│ ├── client.go # Client and retry configuration (HTTP settings only)
1212
│ ├── duration.go # Custom Duration type with human-readable strings
1313
│ ├── model.go # Model configuration with protocol options
1414
│ ├── options.go # Option extraction and validation utilities
1515
│ └── provider.go # Provider configuration structures
16-
├── types/ # Core types, protocols, and message structures
16+
├── protocol/ # Protocol types and message structures
1717
│ ├── protocol.go # Protocol constants and type definitions
18-
│ ├── message.go # Message and Request structures
19-
│ ├── model.go # Model runtime type with option merging
20-
│ ├── chat.go # Chat protocol request/response types
21-
│ ├── vision.go # Vision protocol request/response types
22-
│ ├── tools.go # Tools protocol request/response types
23-
│ └── embeddings.go # Embeddings protocol request/response types
18+
│ └── message.go # Message structures
19+
├── response/ # Response parsing and types
20+
│ ├── chat.go # Chat protocol response types
21+
│ ├── embeddings.go # Embeddings protocol response types
22+
│ ├── streaming.go # Streaming chunk types
23+
│ └── tools.go # Tools protocol response types
24+
├── model/ # Model runtime type
25+
│ └── model.go # Model type bridging config to runtime
2426
├── providers/ # Provider implementations for different LLM services
2527
│ ├── provider.go # Provider interface definition
2628
│ ├── base.go # BaseProvider with common functionality
2729
│ ├── registry.go # Provider registry and initialization
2830
│ ├── azure.go # Azure AI Foundry provider implementation
2931
│ └── ollama.go # Ollama provider implementation
32+
├── request/ # Request interface and protocol-specific request types
33+
│ ├── interface.go # Request interface definition
34+
│ ├── chat.go # ChatRequest implementation
35+
│ ├── vision.go # VisionRequest implementation
36+
│ ├── tools.go # ToolsRequest implementation
37+
│ └── embeddings.go # EmbeddingsRequest implementation
3038
├── client/ # Client layer orchestrating requests across providers
3139
│ ├── client.go # Client interface and implementation
3240
│ └── retry.go # Exponential backoff retry logic with jitter
@@ -75,30 +83,36 @@ type Message struct {
7583
}
7684
```
7785

78-
**Protocol-Specific Request Types**: Each protocol has its own request type implementing the ProtocolRequest interface:
86+
**Protocol-Specific Request Types**: Each protocol has its own request type in `pkg/request` implementing the Request interface:
7987
```go
80-
type ProtocolRequest interface {
81-
GetProtocol() Protocol
82-
GetHeaders() map[string]string
88+
// Request interface in pkg/request
89+
type Request interface {
90+
Protocol() protocol.Protocol
91+
Headers() map[string]string
8392
Marshal() ([]byte, error)
93+
Provider() providers.Provider
94+
Model() *model.Model
8495
}
8596

86-
// Chat protocol request
97+
// ChatRequest encapsulates chat protocol requests
8798
type ChatRequest struct {
88-
Messages []Message
89-
Options map[string]any
99+
messages []protocol.Message
100+
options map[string]any
101+
provider providers.Provider
102+
model *model.Model
90103
}
91104

92-
// Vision protocol request - separates images and vision-specific options from model options
105+
// VisionRequest encapsulates vision protocol requests
93106
type VisionRequest struct {
94-
Messages []Message
95-
Images []string // URLs or data URIs
96-
VisionOptions map[string]any // Vision-specific options (e.g., detail: "high")
97-
Options map[string]any // Model configuration options
107+
messages []protocol.Message
108+
images []string
109+
visionOptions map[string]any
110+
options map[string]any
111+
provider providers.Provider
112+
model *model.Model
98113
}
99114

100-
// Tools protocol request - separates tool definitions from model options
101-
// Tools are marshaled in OpenAI format: {"type": "function", "function": {...}}
115+
// ToolsRequest encapsulates tools protocol requests
102116
type ToolsRequest struct {
103117
Messages []Message
104118
Tools []ToolDefinition // Provider-agnostic tool definitions
@@ -472,25 +486,6 @@ Each agent has a unique identifier assigned at creation time that remains stable
472486
"name": "agent-name",
473487
"system_prompt": "System instructions for the agent",
474488
"client": {
475-
"provider": {
476-
"name": "ollama",
477-
"base_url": "http://localhost:11434",
478-
"model": {
479-
"name": "llama3.2:3b",
480-
"capabilities": {
481-
"chat": {
482-
"max_tokens": 4096,
483-
"temperature": 0.7,
484-
"top_p": 0.95
485-
},
486-
"tools": {
487-
"max_tokens": 4096,
488-
"temperature": 0.7,
489-
"tool_choice": "auto"
490-
}
491-
}
492-
}
493-
},
494489
"timeout": "24s",
495490
"retry": {
496491
"max_retries": 3,
@@ -501,14 +496,33 @@ Each agent has a unique identifier assigned at creation time that remains stable
501496
},
502497
"connection_pool_size": 10,
503498
"connection_timeout": "9s"
499+
},
500+
"provider": {
501+
"name": "ollama",
502+
"base_url": "http://localhost:11434"
503+
},
504+
"model": {
505+
"name": "llama3.2:3b",
506+
"capabilities": {
507+
"chat": {
508+
"max_tokens": 4096,
509+
"temperature": 0.7,
510+
"top_p": 0.95
511+
},
512+
"tools": {
513+
"max_tokens": 4096,
514+
"temperature": 0.7,
515+
"tool_choice": "auto"
516+
}
517+
}
504518
}
505519
}
506520
```
507521

508-
**Configuration Hierarchy**:
509-
- `AgentConfig`: Top-level agent configuration
510-
- `ClientConfig`: Client configuration with retry settings
511-
- `ProviderConfig`: Provider details and model configuration
522+
**Configuration Structure** (Flattened):
523+
- `AgentConfig`: Top-level with `client`, `provider`, and `model` as peers
524+
- `ClientConfig`: HTTP client settings and retry configuration
525+
- `ProviderConfig`: Provider name, base URL, and provider-specific options
512526
- `ModelConfig`: Model name and protocol-specific capabilities
513527
- `RetryConfig`: Retry behavior configuration
514528

@@ -850,15 +864,23 @@ tests/
850864
│ ├── provider_test.go
851865
│ ├── client_test.go
852866
│ └── agent_test.go
853-
├── types/
867+
├── protocol/
854868
│ └── protocol_test.go
869+
├── response/
870+
│ └── response_test.go
871+
├── providers/
872+
│ ├── base_test.go
873+
│ ├── ollama_test.go
874+
│ ├── azure_test.go
875+
│ └── registry_test.go
855876
├── client/
856877
│ └── client_test.go
878+
├── agent/
879+
│ └── agent_test.go
857880
├── mock/
858881
│ ├── agent_test.go
859882
│ ├── client_test.go
860-
│ ├── provider_test.go
861-
│ └── helpers_test.go
883+
│ └── provider_test.go
862884
└── ...
863885
```
864886

@@ -892,11 +914,11 @@ import (
892914
func TestProtocol_IsValid(t *testing.T) {
893915
tests := []struct {
894916
name string
895-
protocol types.Protocol
917+
protocol protocol.Protocol
896918
expected bool
897919
}{
898-
{name: "chat", protocol: types.Chat, expected: true},
899-
{name: "invalid", protocol: types.Protocol("invalid"), expected: false},
920+
{name: "chat", protocol: protocol.Chat, expected: true},
921+
{name: "invalid", protocol: protocol.Protocol("invalid"), expected: false},
900922
}
901923

902924
for _, tt := range tests {
@@ -912,23 +934,21 @@ func TestProtocol_IsValid(t *testing.T) {
912934
**HTTP Mocking**: Use `httptest.Server` for mocking provider responses:
913935

914936
```go
915-
func TestClient_ExecuteProtocol_Chat(t *testing.T) {
937+
func TestClient_Execute_Chat(t *testing.T) {
916938
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
917-
response := types.ChatResponse{
939+
resp := response.ChatResponse{
918940
Model: "test-model",
919-
Choices: []struct {
920-
Index int
921-
Message types.Message
922-
FinishReason string
923-
}{
924-
{
925-
Index: 0,
926-
Message: types.NewMessage("assistant", "Test response"),
927-
},
928-
},
929941
}
942+
resp.Choices = append(resp.Choices, struct {
943+
Index int
944+
Message protocol.Message
945+
FinishReason string
946+
}{
947+
Index: 0,
948+
Message: protocol.NewMessage("assistant", "Test response"),
949+
})
930950
w.Header().Set("Content-Type", "application/json")
931-
json.NewEncoder(w).Encode(response)
951+
json.NewEncoder(w).Encode(resp)
932952
}))
933953
defer server.Close()
934954

@@ -1045,7 +1065,7 @@ agent := mock.NewSimpleChatAgent("id", "response text")
10451065
agent := mock.NewStreamingChatAgent("id", []string{"chunk1", "chunk2"})
10461066

10471067
// Tools agent
1048-
agent := mock.NewToolsAgent("id", []types.ToolCall{...})
1068+
agent := mock.NewToolsAgent("id", []response.ToolCall{...})
10491069

10501070
// Embeddings agent
10511071
agent := mock.NewEmbeddingsAgent("id", []float64{0.1, 0.2, 0.3})
@@ -1065,8 +1085,8 @@ The option pattern allows precise control over mock behavior:
10651085
// Configure specific behaviors
10661086
mockAgent := mock.NewMockAgent(
10671087
mock.WithID("custom-id"),
1068-
mock.WithChatResponse(&types.ChatResponse{...}, nil),
1069-
mock.WithStreamChunks([]types.StreamingChunk{...}, nil),
1088+
mock.WithChatResponse(&response.ChatResponse{...}, nil),
1089+
mock.WithStreamChunks([]*response.StreamingChunk{...}, nil),
10701090
)
10711091

10721092
// Test error handling

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,45 @@
11
# Changelog
22

3+
## [v0.3.0] - 2025-12-01
4+
5+
**Breaking Changes**:
6+
- Removed `pkg/types` package - split into `pkg/protocol`, `pkg/response`, `pkg/model`, and `pkg/request`
7+
- Flattened `AgentConfig` structure: `Provider` and `Model` are now peer fields with `Client` (not nested under `Client`)
8+
- `ClientConfig` no longer contains `Provider` - HTTP client settings only
9+
- `ProviderConfig` no longer contains `Model` - provider connection settings only
10+
- `Agent.Model()` returns `*model.Model` instead of `*types.Model`
11+
12+
**Added**:
13+
- `pkg/protocol` package for protocol types and message structures
14+
- `Protocol` type with constants: `Chat`, `Vision`, `Tools`, `Embeddings`
15+
- `Message` type and `NewMessage()` constructor
16+
- `IsValid()`, `ValidProtocols()`, `ProtocolStrings()` functions
17+
- `Protocol.SupportsStreaming()` method
18+
- `pkg/response` package for response parsing and types
19+
- `ChatResponse` type with `Content()` method
20+
- `StreamingChunk` type with `Content()` method
21+
- `EmbeddingsResponse` type
22+
- `ToolsResponse` type with `ToolCall` and `ToolCallFunction` types
23+
- `ParseChat()`, `ParseEmbeddings()`, `ParseTools()`, `ParseStream()` functions
24+
- `pkg/model` package for model runtime type
25+
- `Model` type with `Name` and `Options` fields
26+
- `New()` function for creating Model from ModelConfig
27+
- `pkg/request` package for request interface and protocol-specific request types
28+
- `Request` interface with `Protocol()`, `Headers()`, `Marshal()`, `Provider()`, `Model()` methods
29+
- `ChatRequest` type with `NewChat()` constructor
30+
- `VisionRequest` type with `NewVision()` constructor
31+
- `ToolsRequest` type with `NewTools()` constructor
32+
- `EmbeddingsRequest` type with `NewEmbeddings()` constructor
33+
34+
**Changed**:
35+
- `AgentConfig.Provider` moved from `AgentConfig.Client.Provider` to top-level field
36+
- `AgentConfig.Model` moved from `AgentConfig.Client.Provider.Model` to top-level field
37+
- Mock package types updated to use `pkg/protocol` and `pkg/response`
38+
39+
**Removed**:
40+
- `pkg/types` package (replaced by `pkg/protocol`, `pkg/response`, `pkg/model`, `pkg/request`)
41+
- Nested configuration hierarchy (`Client.Provider.Model`)
42+
343
## [v0.2.1] - 2025-11-01
444

545
**Changed**:

CLAUDE.md

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,19 @@ All documentation should be written in a clear, objective, and factual manner wi
189189
**Implementation**:
190190
- Package dependency layers (from low to high):
191191
- `pkg/config` (foundation-level, serves all layers)
192-
- `pkg/protocols` (protocol types and request/response structures)
193-
- `pkg/capabilities` (capability abstraction and registry)
194-
- `pkg/models` (model definitions and format handling)
192+
- `pkg/protocol` (protocol types and message structures)
193+
- `pkg/response` (response parsing and types)
195194
- `pkg/providers` (provider-specific implementations)
196-
- `pkg/transport` (client abstraction and HTTP orchestration)
195+
- `pkg/model` (model runtime type, depends on protocol)
196+
- `pkg/request` (request interface and types, depends on model, protocol, providers)
197+
- `pkg/client` (client abstraction and HTTP orchestration)
197198
- `pkg/agent` (high-level agent functionality)
199+
- `pkg/mock` (mock implementations for testing)
198200
- Lower layers must not import higher layers
199201
- Shared types should be defined in the lowest layer that needs them
200202
- Use interfaces to invert dependencies when needed
201203

202-
**Example**: `pkg/providers` can import from `pkg/models`, `pkg/capabilities`, and `pkg/config`, but not from `pkg/transport` or `pkg/agent`. The `pkg/transport` layer orchestrates providers through interfaces.
204+
**Example**: `pkg/request` can import from `pkg/model`, `pkg/protocol`, `pkg/providers`, and `pkg/config`, but not from `pkg/client` or `pkg/agent`. The `pkg/client` layer executes requests through the `request.Request` interface.
203205

204206
### Implementation Guide Refactoring Order
205207
**Principle**: When creating implementation guides for refactoring, always structure changes to proceed from lowest-level packages to highest-level packages following the dependency hierarchy.
@@ -212,14 +214,16 @@ All documentation should be written in a clear, objective, and factual manner wi
212214
- Each step should result in a compilable state
213215
- Higher-level packages should only be refactored after all their dependencies are complete
214216

215-
**Example**: When refactoring to a protocol-based architecture, update in this order:
217+
**Example**: When refactoring architecture, update in this order:
216218
1. `pkg/config` (configuration structures if needed)
217-
2. `pkg/protocols` (foundational protocol types)
218-
3. `pkg/capabilities` (capability system updates)
219-
4. `pkg/models` (model and format handling)
220-
5. `pkg/providers` (provider implementations)
221-
6. `pkg/transport` (client orchestration)
222-
7. `pkg/agent` (high-level interface)
219+
2. `pkg/protocol` (foundational protocol types)
220+
3. `pkg/response` (response parsing types)
221+
4. `pkg/providers` (provider implementations)
222+
5. `pkg/model` (model runtime type)
223+
6. `pkg/request` (request interface and types)
224+
7. `pkg/client` (client orchestration)
225+
8. `pkg/agent` (high-level interface)
226+
9. `pkg/mock` (mock implementations)
223227

224228
### Parameter Encapsulation
225229
**Principle**: If more than two parameters are needed for a function or method, encapsulate the parameters into a structure.
@@ -370,10 +374,10 @@ tests/
370374
│ ├── duration_test.go # Tests for pkg/config/duration.go
371375
│ ├── options_test.go # Tests for pkg/config/options.go
372376
│ └── agent_test.go # Tests for pkg/config/agent.go
373-
├── protocols/
377+
├── protocol/
374378
│ └── protocol_test.go
375-
└── capabilities/
376-
└── chat_test.go
379+
└── response/
380+
└── response_test.go
377381
```
378382

379383
### Black-Box Testing Approach
@@ -465,20 +469,20 @@ func TestExtractOption(t *testing.T) {
465469
```go
466470
func TestOllama_Request(t *testing.T) {
467471
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
468-
// Verify request
469472
if r.Method != "POST" {
470473
t.Errorf("expected POST, got %s", r.Method)
471474
}
472475

473-
// Send mock response
474-
response := protocols.ChatResponse{
475-
Choices: []struct{
476-
Message protocols.Message
477-
}{
478-
{Message: protocols.NewMessage("assistant", "Test response")},
479-
},
480-
}
481-
json.NewEncoder(w).Encode(response)
476+
resp := response.ChatResponse{Model: "test-model"}
477+
resp.Choices = append(resp.Choices, struct {
478+
Index int
479+
Message protocol.Message
480+
FinishReason string
481+
}{
482+
Index: 0,
483+
Message: protocol.NewMessage("assistant", "Test response"),
484+
})
485+
json.NewEncoder(w).Encode(resp)
482486
}))
483487
defer server.Close()
484488

0 commit comments

Comments
 (0)