Data Model: Interactive Agent Testing¶
Feature: 007-interactive-chat
Date: 2025-11-22
Status: Complete
Overview¶
This document defines the data models for the interactive chat feature. All models use Pydantic for validation and serialization.
Core Entities¶
1. ChatSession¶
Represents an active conversation between developer and agent.
Purpose: Manages conversation state, history, and lifecycle for a single interactive session.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
session_id |
str |
Yes | Unique session identifier (UUID) |
agent_config |
AgentConfig |
Yes | Loaded agent configuration |
history |
ChatHistory |
Yes | Semantic Kernel ChatHistory instance |
started_at |
datetime |
Yes | Session start timestamp |
message_count |
int |
Yes | Total messages exchanged |
metadata |
dict[str, Any] |
No | Optional session metadata |
State Transitions:
- INITIALIZING → ACTIVE (agent loaded successfully)
- ACTIVE → TERMINATED (user exits or error occurs)
Relationships:
- Contains multiple Message instances (via ChatHistory)
- Executes tools defined in AgentConfig
Validation Rules:
- session_id must be valid UUID format
- message_count must be non-negative
- started_at cannot be in the future
Pydantic Model:
from pydantic import BaseModel, Field, field_validator
from datetime import datetime
from uuid import UUID, uuid4
from semantic_kernel.contents import ChatHistory
from holodeck.models.agent import AgentConfig
class SessionState(str, Enum):
"""Chat session state."""
INITIALIZING = "initializing"
ACTIVE = "active"
TERMINATED = "terminated"
class ChatSession(BaseModel):
"""Interactive chat session model."""
session_id: UUID = Field(default_factory=uuid4)
agent_config: AgentConfig
history: ChatHistory
started_at: datetime = Field(default_factory=datetime.utcnow)
message_count: int = Field(default=0, ge=0)
state: SessionState = Field(default=SessionState.INITIALIZING)
metadata: dict[str, Any] = Field(default_factory=dict)
class Config:
arbitrary_types_allowed = True # Allow ChatHistory
@field_validator('started_at')
@classmethod
def validate_started_at(cls, v: datetime) -> datetime:
if v > datetime.utcnow():
raise ValueError("started_at cannot be in the future")
return v
2. Message¶
Represents a single exchange in the conversation.
Purpose: Encapsulates message content, role, and metadata for display and tracking.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
role |
MessageRole |
Yes | Message role (user, assistant, system) |
content |
str |
Yes | Message content |
timestamp |
datetime |
Yes | Message creation time |
tool_calls |
list[ToolExecution] |
No | Tool calls made during this exchange |
tokens_used |
TokenUsage \| None |
No | Token usage for this message |
Enums:
class MessageRole(str, Enum):
"""Message role in conversation."""
USER = "user"
ASSISTANT = "assistant"
SYSTEM = "system"
Validation Rules:
- content must not be empty (after stripping whitespace)
- content length must be ≤ 10,000 characters for user messages
- role must be valid MessageRole value
- tool_calls can only exist for assistant messages
Pydantic Model:
from pydantic import BaseModel, Field, field_validator
class Message(BaseModel):
"""Chat message model."""
role: MessageRole
content: str = Field(min_length=1)
timestamp: datetime = Field(default_factory=datetime.utcnow)
tool_calls: list[ToolExecution] = Field(default_factory=list)
tokens_used: TokenUsage | None = None
@field_validator('content')
@classmethod
def validate_content(cls, v: str, info) -> str:
# Strip whitespace
v = v.strip()
if not v:
raise ValueError("Message content cannot be empty")
# Enforce size limit for user messages
if info.data.get('role') == MessageRole.USER and len(v) > 10000:
raise ValueError("User message exceeds 10,000 character limit")
return v
@field_validator('tool_calls')
@classmethod
def validate_tool_calls(cls, v: list[ToolExecution], info) -> list[ToolExecution]:
# Tool calls only allowed for assistant messages
if v and info.data.get('role') != MessageRole.ASSISTANT:
raise ValueError("Tool calls only allowed for assistant messages")
return v
3. ToolExecution¶
Metadata about tool calls made during agent processing.
Purpose: Tracks tool invocations for logging, display, and observability.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
tool_name |
str |
Yes | Name of the tool/function called |
parameters |
dict[str, Any] |
Yes | Tool parameters (sanitized) |
result |
str \| None |
No | Tool execution result (sanitized) |
status |
ToolStatus |
Yes | Execution status |
execution_time |
float |
No | Execution time in seconds |
error_message |
str \| None |
No | Error details if status=FAILED |
Enums:
class ToolStatus(str, Enum):
"""Tool execution status."""
PENDING = "pending"
RUNNING = "running"
SUCCESS = "success"
FAILED = "failed"
Validation Rules:
- tool_name must match configured tool in agent
- execution_time must be non-negative if provided
- error_message required if status=FAILED
- result must be sanitized (no terminal escape sequences)
Pydantic Model:
from pydantic import BaseModel, Field, field_validator
class ToolExecution(BaseModel):
"""Tool execution metadata."""
tool_name: str
parameters: dict[str, Any] = Field(default_factory=dict)
result: str | None = None
status: ToolStatus = Field(default=ToolStatus.PENDING)
execution_time: float | None = Field(default=None, ge=0)
error_message: str | None = None
@field_validator('error_message')
@classmethod
def validate_error_message(cls, v: str | None, info) -> str | None:
if info.data.get('status') == ToolStatus.FAILED and not v:
raise ValueError("error_message required when status=FAILED")
return v
@field_validator('result')
@classmethod
def sanitize_result(cls, v: str | None) -> str | None:
if v:
# Sanitize output (implemented in validation.py)
from holodeck.lib.validation import sanitize_tool_output
return sanitize_tool_output(v)
return v
4. TokenUsage¶
Token consumption tracking for LLM calls.
Purpose: Enables cost tracking and observability for interactive sessions.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
prompt_tokens |
int |
Yes | Tokens in the prompt |
completion_tokens |
int |
Yes | Tokens in the completion |
total_tokens |
int |
Yes | Total tokens (prompt + completion) |
Validation Rules:
- All token counts must be non-negative
- total_tokens must equal prompt_tokens + completion_tokens
Pydantic Model:
from pydantic import BaseModel, Field, field_validator
class TokenUsage(BaseModel):
"""Token usage tracking."""
prompt_tokens: int = Field(ge=0)
completion_tokens: int = Field(ge=0)
total_tokens: int = Field(ge=0)
@field_validator('total_tokens')
@classmethod
def validate_total(cls, v: int, info) -> int:
prompt = info.data.get('prompt_tokens', 0)
completion = info.data.get('completion_tokens', 0)
if v != prompt + completion:
raise ValueError(
f"total_tokens ({v}) must equal "
f"prompt_tokens ({prompt}) + completion_tokens ({completion})"
)
return v
5. ChatConfig¶
Runtime configuration for chat sessions.
Purpose: Captures CLI options and runtime settings for a chat session.
Fields:
| Field | Type | Required | Description |
|---|---|---|---|
agent_config_path |
Path |
Yes | Path to agent YAML configuration |
verbose |
bool |
No | Show detailed tool execution (default: False) |
enable_observability |
bool |
No | Enable OpenTelemetry tracing (default: False) |
max_messages |
int |
No | Max conversation messages (default: 50) |
Validation Rules:
- agent_config_path must exist and be readable
- max_messages must be positive
Pydantic Model:
from pathlib import Path
from pydantic import BaseModel, Field, field_validator
class ChatConfig(BaseModel):
"""Chat session runtime configuration."""
agent_config_path: Path
verbose: bool = Field(default=False)
enable_observability: bool = Field(default=False)
max_messages: int = Field(default=50, gt=0)
@field_validator('agent_config_path')
@classmethod
def validate_path(cls, v: Path) -> Path:
if not v.exists():
raise ValueError(f"Agent config not found: {v}")
if not v.is_file():
raise ValueError(f"Path is not a file: {v}")
return v
Entity Relationships¶
ChatSession (1) ───────> (1) AgentConfig [loaded from YAML]
│
│
└─> (1) ChatHistory ───────> (N) Message
│
└─> (N) ToolExecution
└─> (0..1) TokenUsage
Lifecycle Flow:
1. User runs holodeck chat agent.yaml
2. ChatConfig created from CLI arguments
3. AgentConfig loaded from YAML file
4. ChatSession initialized with empty ChatHistory
5. User sends message → Message(role=USER) added to history
6. Agent processes → Message(role=ASSISTANT) with ToolExecution instances
7. Repeat steps 5-6 until user exits
8. Session terminated, history discarded (MVP - no persistence)
File Locations¶
All models will be implemented in:
src/holodeck/models/
├── chat.py # NEW: ChatSession, Message, ChatConfig
├── tool_execution.py # NEW: ToolExecution, ToolStatus
├── token_usage.py # NEW: TokenUsage
├── agent.py # EXISTING: AgentConfig
└── llm.py # EXISTING: LLMConfig
Validation Architecture¶
Input validation happens in layers:
- Pydantic model validation (structure, types, basic constraints)
- Custom validators (business logic via
@field_validator) - ValidationPipeline (runtime validation for user input)
- Sanitization (output cleaning via
sanitize_tool_output())
See research.md Section 4 for detailed validation pipeline design.