Research - Issue #311: core → types Layer Violation
Date: 2026-01-27
Owner: claude-agent
Phase: Research
Issue: https://github.com/alchemiststudiosDOTai/tunacode/issues/311
Goal
Understand the scope and implications of the core → types layer violation reported in issue #311, and determine the correct remediation approach.
Findings
The Reported Problem
Issue #311 proposes a strict 5-layer hierarchy:
ui (Layer 0) → core (Layer 1) → tools/indexing/lsp (Layer 2) → utils (Layer 3) → types/configuration (Layer 4)
Under this model, core should NOT directly import from types (skips Layer 2 and 3).
Current Architecture (DEPENDENCY_MAP.md)
The frozen baseline (2026-01-26) shows a different model:
ui → outer layer (TUI)
core → business logic
tools → agent tools
indexing → code indexing infrastructure
lsp → language server protocol
─────────────────────────────────
utils ┐
types │ utils-level (importable from anywhere)
configuration│
constants ┘
Key difference: Types is classified as "utils-level" - a horizontal layer that ANY vertical layer can import from. This is consistent with CLAUDE.md Gate 2.
Architectural Contradiction
| Document |
Position |
Status |
| Issue #311 |
types is Layer 4, core cannot import |
Proposed change |
| DEPENDENCY_MAP.md |
types is utils-level, importable anywhere |
Frozen baseline |
| CLAUDE.md Gate 2 |
types/ is utils-level module |
Authoritative |
The frozen baseline marks core → types: 26 imports as "✅ valid".
Import Inventory (17 files, 26 imports)
State Management (5 imports)
| File |
Types Imported |
core/state.py:17 |
ConversationState, InputSessions, ModelName, RuntimeState, SessionId, UserConfig |
core/state.py:27 |
UsageMetrics |
core/user_configuration.py:5-6 |
UserConfig, StateManagerProtocol |
core/lsp_status.py:3 |
UserConfig |
Core Types Re-export (2 imports)
| File |
Types Imported |
core/types.py:5 |
ModelName, ToolArgs, ToolCallback, ToolName, ToolResultCallback, ToolStartCallback |
core/types.py:14 |
UsageMetrics |
Configuration (1 import)
| File |
Types Imported |
core/configuration.py:31 |
ModelPricing |
Logging (1 import)
| File |
Types Imported |
core/logging/manager.py:10 |
StateManagerProtocol |
Agent Core (1 import)
| File |
Types Imported |
core/agents/main.py:22 |
AgentRun, ModelName, NoticeCallback, StateManagerProtocol, StreamingCallback, ToolCallback, ToolResultCallback, ToolStartCallback |
Agent Components (8 imports)
| File |
Types Imported |
core/agents/agent_components/agent_config.py:23 |
ModelName, PydanticAgent, SessionStateProtocol |
core/agents/agent_components/agent_helpers.py:5 |
CanonicalToolCall |
core/agents/agent_components/response_state.py:6 |
AgentState |
core/agents/agent_components/state_transition.py:7 |
AgentState |
core/agents/agent_components/streaming.py:16 |
StreamingCallback |
Orchestrator (4 imports)
| File |
Types Imported |
core/agents/agent_components/orchestrator/orchestrator.py:5-6 |
AgentState, StreamingCallback, ToolCallback, ToolResultCallback, ToolStartCallback |
core/agents/agent_components/orchestrator/tool_dispatcher.py:12-13 |
AgentState, ToolArgs, ToolCallId, ToolCallback, ToolResultCallback, ToolStartCallback |
core/agents/agent_components/orchestrator/usage_tracker.py:6-7 |
UsageMetrics, normalize_request_usage |
Resume/Sanitization (4 imports)
| File |
Types Imported |
core/agents/resume/sanitize.py:18-20 |
ToolCallId, CanonicalMessage, MessageRole, SystemPromptPart, ToolCallRegistry |
core/agents/resume/sanitize_debug.py:10 |
ToolCallId |
Types Layer Analysis
The src/tunacode/types/ directory contains 9 files with types in three categories:
Category A: Valid Utils-Level (Should Stay)
These types are used across multiple layers and are correctly placed:
| File |
Types |
Used By |
base.py |
42 primitive aliases (ModelName, ToolArgs, etc.) |
core, tools, utils |
pydantic_ai.py |
PydanticAgent, MessageHistory, NormalizedUsage |
core, utils |
canonical.py |
CanonicalMessage, UsageMetrics, MessageRole, etc. |
core, utils/messaging/adapter |
callbacks.py |
ToolCallback, StreamingCallback, etc. |
core (dependency injection) |
Category B: Core-Specific (Candidates for Move)
These types are ONLY used by core and should not be in utils-level:
| File |
Types |
Reason |
dataclasses.py |
ResponseState |
Only used in core/agents/agent_components/response_state.py |
dataclasses.py |
AgentState |
Only used in core agent state transitions |
state.py |
SessionStateProtocol |
Protocol for core's SessionState |
state.py |
StateManagerProtocol |
Protocol for core's StateManager |
state_structures.py |
ConversationState, TaskState, RuntimeState, UsageState |
Only compose core's SessionState |
tool_registry.py |
ToolCallRegistry |
Stateful business logic, only used in RuntimeState and sanitize.py |
Category C: Dead Code (Delete)
| File |
Types |
Reason |
dataclasses.py |
CommandContext |
Never imported anywhere |
callbacks.py |
ProcessRequestCallback |
Only referenced in dead CommandContext |
Key Patterns Found
Pattern 1: Core Facade (commit 10b48c30)
- When UI needed LSP functionality, a facade was created in core
- Pattern:
ui → core/facade.py → tools/implementation.py → lsp/
Pattern 2: Move to Utils (commit be735754)
- Shared constants moved from tools to
utils/system/ignore_patterns.py
- Both tools and indexing import from there
Pattern 3: Central Re-export (current)
types/__init__.py re-exports everything, hiding internal structure
core/types.py already re-exports a subset for core's use
Knowledge Gaps
Architectural Intent Unclear: Is issue #311's strict layering the desired target state, or is the current "types as utils-level" the correct model?
Migration Strategy: If we adopt strict layering, do we:
- Move core-specific types to
core/types/?
- Create re-exports in utils layer?
- Use dependency injection?
Backward Compatibility: How do we handle the 26 existing imports without breaking changes?
Recommendations
Option A: Keep Current Architecture (Minimal Change)
If types/ remains "utils-level":
- Close issue #311 as "works as designed"
- Delete dead code (CommandContext, ProcessRequestCallback)
- Update documentation to clarify the model
Option B: Strict Layer Enforcement (Issue #311's Proposal)
If we adopt strict layering:
Phase 1: Move Core-Specific Types
- Create
src/tunacode/core/types/ directory
- Move: ResponseState, AgentState, StateManagerProtocol, SessionStateProtocol
- Move: ConversationState, TaskState, RuntimeState, UsageState
- Move: ToolCallRegistry
- Update 17 files in core to import from
core/types/
Phase 2: Cleanup
- Delete dead code: CommandContext, ProcessRequestCallback
- Update
types/__init__.py to only export utils-level types
Phase 3: Verify
- Run grimp to confirm zero violations
- Update DEPENDENCY_MAP.md
My Recommendation
Option B is the correct path forward, but with nuance:
The current architecture has types/ as utils-level because it evolved organically. Issue #311 correctly identifies that core-specific types (protocols, state structures) should live in core, not in a shared layer.
However, the issue's proposed fix (re-exports through utils/tools) adds indirection. The cleaner fix is:
- Move core-specific types to
core/types/ - types that describe core's internal structure belong in core
- Keep shared types in
types/ - primitives, callbacks, canonical message types remain utils-level
- Result: core imports its own types directly (no indirection), and shared types from utils-level (valid)
This maintains the "utils-level is importable anywhere" principle while moving core-specific implementation details out of the shared layer.
References
1---2name: 512-2026-01-27-00-10-38-issue-311-core-types-layer-violation3description: Research - Issue #311: core → types Layer Violation4---5# Research - Issue #311: core → types Layer Violation67**Date:** 2026-01-278**Owner:** claude-agent9**Phase:** Research10**Issue:** https://github.com/alchemiststudiosDOTai/tunacode/issues/3111112## Goal1314Understand the scope and implications of the `core → types` layer violation reported in issue #311, and determine the correct remediation approach.1516## Findings1718### The Reported Problem1920Issue #311 proposes a strict 5-layer hierarchy:2122```23ui (Layer 0) → core (Layer 1) → tools/indexing/lsp (Layer 2) → utils (Layer 3) → types/configuration (Layer 4)24```2526Under this model, `core` should NOT directly import from `types` (skips Layer 2 and 3).2728### Current Architecture (DEPENDENCY_MAP.md)2930The frozen baseline (2026-01-26) shows a different model:3132```33ui → outer layer (TUI)34core → business logic35tools → agent tools36indexing → code indexing infrastructure37lsp → language server protocol38─────────────────────────────────39utils ┐40types │ utils-level (importable from anywhere)41configuration│42constants ┘43```4445**Key difference:** Types is classified as "utils-level" - a horizontal layer that ANY vertical layer can import from. This is consistent with CLAUDE.md Gate 2.4647### Architectural Contradiction4849| Document | Position | Status |50|----------|----------|--------|51| Issue #311 | types is Layer 4, core cannot import | Proposed change |52| DEPENDENCY_MAP.md | types is utils-level, importable anywhere | Frozen baseline |53| CLAUDE.md Gate 2 | types/ is utils-level module | Authoritative |5455The frozen baseline marks `core → types: 26 imports` as "✅ valid".5657### Import Inventory (17 files, 26 imports)5859#### State Management (5 imports)60| File | Types Imported |61|------|----------------|62| `core/state.py:17` | ConversationState, InputSessions, ModelName, RuntimeState, SessionId, UserConfig |63| `core/state.py:27` | UsageMetrics |64| `core/user_configuration.py:5-6` | UserConfig, StateManagerProtocol |65| `core/lsp_status.py:3` | UserConfig |6667#### Core Types Re-export (2 imports)68| File | Types Imported |69|------|----------------|70| `core/types.py:5` | ModelName, ToolArgs, ToolCallback, ToolName, ToolResultCallback, ToolStartCallback |71| `core/types.py:14` | UsageMetrics |7273#### Configuration (1 import)74| File | Types Imported |75|------|----------------|76| `core/configuration.py:31` | ModelPricing |7778#### Logging (1 import)79| File | Types Imported |80|------|----------------|81| `core/logging/manager.py:10` | StateManagerProtocol |8283#### Agent Core (1 import)84| File | Types Imported |85|------|----------------|86| `core/agents/main.py:22` | AgentRun, ModelName, NoticeCallback, StateManagerProtocol, StreamingCallback, ToolCallback, ToolResultCallback, ToolStartCallback |8788#### Agent Components (8 imports)89| File | Types Imported |90|------|----------------|91| `core/agents/agent_components/agent_config.py:23` | ModelName, PydanticAgent, SessionStateProtocol |92| `core/agents/agent_components/agent_helpers.py:5` | CanonicalToolCall |93| `core/agents/agent_components/response_state.py:6` | AgentState |94| `core/agents/agent_components/state_transition.py:7` | AgentState |95| `core/agents/agent_components/streaming.py:16` | StreamingCallback |9697#### Orchestrator (4 imports)98| File | Types Imported |99|------|----------------|100| `core/agents/agent_components/orchestrator/orchestrator.py:5-6` | AgentState, StreamingCallback, ToolCallback, ToolResultCallback, ToolStartCallback |101| `core/agents/agent_components/orchestrator/tool_dispatcher.py:12-13` | AgentState, ToolArgs, ToolCallId, ToolCallback, ToolResultCallback, ToolStartCallback |102| `core/agents/agent_components/orchestrator/usage_tracker.py:6-7` | UsageMetrics, normalize_request_usage |103104#### Resume/Sanitization (4 imports)105| File | Types Imported |106|------|----------------|107| `core/agents/resume/sanitize.py:18-20` | ToolCallId, CanonicalMessage, MessageRole, SystemPromptPart, ToolCallRegistry |108| `core/agents/resume/sanitize_debug.py:10` | ToolCallId |109110### Types Layer Analysis111112The `src/tunacode/types/` directory contains 9 files with types in three categories:113114#### Category A: Valid Utils-Level (Should Stay)115116These types are used across multiple layers and are correctly placed:117118| File | Types | Used By |119|------|-------|---------|120| `base.py` | 42 primitive aliases (ModelName, ToolArgs, etc.) | core, tools, utils |121| `pydantic_ai.py` | PydanticAgent, MessageHistory, NormalizedUsage | core, utils |122| `canonical.py` | CanonicalMessage, UsageMetrics, MessageRole, etc. | core, utils/messaging/adapter |123| `callbacks.py` | ToolCallback, StreamingCallback, etc. | core (dependency injection) |124125#### Category B: Core-Specific (Candidates for Move)126127These types are ONLY used by core and should not be in utils-level:128129| File | Types | Reason |130|------|-------|--------|131| `dataclasses.py` | ResponseState | Only used in core/agents/agent_components/response_state.py |132| `dataclasses.py` | AgentState | Only used in core agent state transitions |133| `state.py` | SessionStateProtocol | Protocol for core's SessionState |134| `state.py` | StateManagerProtocol | Protocol for core's StateManager |135| `state_structures.py` | ConversationState, TaskState, RuntimeState, UsageState | Only compose core's SessionState |136| `tool_registry.py` | ToolCallRegistry | Stateful business logic, only used in RuntimeState and sanitize.py |137138#### Category C: Dead Code (Delete)139140| File | Types | Reason |141|------|-------|--------|142| `dataclasses.py` | CommandContext | Never imported anywhere |143| `callbacks.py` | ProcessRequestCallback | Only referenced in dead CommandContext |144145### Key Patterns Found146147**Pattern 1: Core Facade** (commit 10b48c30)148- When UI needed LSP functionality, a facade was created in core149- Pattern: `ui → core/facade.py → tools/implementation.py → lsp/`150151**Pattern 2: Move to Utils** (commit be735754)152- Shared constants moved from tools to `utils/system/ignore_patterns.py`153- Both tools and indexing import from there154155**Pattern 3: Central Re-export** (current)156- `types/__init__.py` re-exports everything, hiding internal structure157- `core/types.py` already re-exports a subset for core's use158159## Knowledge Gaps1601611. **Architectural Intent Unclear**: Is issue #311's strict layering the desired target state, or is the current "types as utils-level" the correct model?1621632. **Migration Strategy**: If we adopt strict layering, do we:164 - Move core-specific types to `core/types/`?165 - Create re-exports in utils layer?166 - Use dependency injection?1671683. **Backward Compatibility**: How do we handle the 26 existing imports without breaking changes?169170## Recommendations171172### Option A: Keep Current Architecture (Minimal Change)173174If types/ remains "utils-level":175- Close issue #311 as "works as designed"176- Delete dead code (CommandContext, ProcessRequestCallback)177- Update documentation to clarify the model178179### Option B: Strict Layer Enforcement (Issue #311's Proposal)180181If we adopt strict layering:182183**Phase 1: Move Core-Specific Types**1841. Create `src/tunacode/core/types/` directory1852. Move: ResponseState, AgentState, StateManagerProtocol, SessionStateProtocol1863. Move: ConversationState, TaskState, RuntimeState, UsageState1874. Move: ToolCallRegistry1885. Update 17 files in core to import from `core/types/`189190**Phase 2: Cleanup**1911. Delete dead code: CommandContext, ProcessRequestCallback1922. Update `types/__init__.py` to only export utils-level types193194**Phase 3: Verify**1951. Run grimp to confirm zero violations1962. Update DEPENDENCY_MAP.md197198### My Recommendation199200**Option B is the correct path forward**, but with nuance:201202The current architecture has types/ as utils-level because it evolved organically. Issue #311 correctly identifies that core-specific types (protocols, state structures) should live in core, not in a shared layer.203204However, the issue's proposed fix (re-exports through utils/tools) adds indirection. The cleaner fix is:2052061. **Move core-specific types to `core/types/`** - types that describe core's internal structure belong in core2072. **Keep shared types in `types/`** - primitives, callbacks, canonical message types remain utils-level2083. **Result**: core imports its own types directly (no indirection), and shared types from utils-level (valid)209210This maintains the "utils-level is importable anywhere" principle while moving core-specific implementation details out of the shared layer.211212## References213214- Issue: https://github.com/alchemiststudiosDOTai/tunacode/issues/311215- `docs/architecture/DEPENDENCY_MAP.md` - Current baseline216- `CLAUDE.md` Gate 2 - Dependency direction rules217- `src/tunacode/types/` - Types layer (9 files)218- `src/tunacode/core/types.py` - Existing core re-export pattern