Reasoning: Document Protection and Agent Configuration
This document covers two complementary aspects of reasoning in Thalabus:
- Reasoning Document Context Protection — how reasoning documents maintain data consistency with Neo4j
- Agent Reasoning Level Configuration — how agents configure and use reasoning effort levels
Part 1: Reasoning Document Context Protection
Overview
All ReasoningDocument classes (ReasoningDocument, ReasoningDivision, ReasoningTask, ReasoningIssue, ReasoningResult) implement read/write protection to ensure data consistency with Neo4j.
Protection Mechanisms
1. Write Protection (_check_write_allowed())
- Purpose: Prevents modifications outside of
write_context() - Enforced by: All setter methods
- Error Behavior: Logs ERROR with stack trace, operation is silently ignored
2. Read Protection (_check_read_allowed())
- Purpose: Ensures reading fresh data from Neo4j/cache
- Enforced by: Getter methods for mutable properties
- Warning Behavior: Logs WARNING (non-blocking)
Class Hierarchy & Protection
ReasoningDocument (root)
└─ _check_write_allowed() - checks _rdoc_lock_depth > 0
└─ _check_read_allowed() - checks _rdoc_read_depth > 0 or _rdoc_lock_depth > 0
ReasoningDivision
└─ _check_write_allowed() - delegates to parent_document._check_write_allowed()
└─ _check_read_allowed() - delegates to parent_document._check_read_allowed()
ReasoningTask / ReasoningIssue / ReasoningResult
└─ _check_write_allowed() - delegates via parent_division → parent_document
└─ _check_read_allowed() - delegates via parent_division → parent_document
Usage Patterns
✅ CORRECT: Write Operations
document_lock = get_lock("REASONING_DOCUMENT", doc_id)
with reasoning_document.write_context(document_lock):
# Fresh document loaded from Neo4j/cache automatically
div = reasoning_document.get_division_by_id(division_id)
div.set_status(ReasoningStatus.RS_IN_PROGRESS)
# Nested object modifications
task = div.get_task_by_id(task_id)
task.set_status(TaskStatus.TS_COMPLETE)
issue = div.add_issue("i-001", "Description", IssueSeverity.HIGH)
issue.set_description("Updated description")
# Document automatically saved to Neo4j on context exit
✅ CORRECT: Read Operations
with reasoning_document.read_context():
# Fresh document loaded from Neo4j/cache if stale
div = reasoning_document.get_division_by_id(division_id)
status = div.status() # Protected read
tasks = div.tasks()
for task in tasks:
task_status = task.status() # Protected read
❌ INCORRECT: Unprotected Writes
# NO LOCK - Changes will be lost!
div = reasoning_document.get_division_by_id(division_id)
div.set_status(ReasoningStatus.RS_COMPLETE) # ⚠️ ERROR logged, change ignored
task = div.get_task_by_id(task_id)
task.set_status(TaskStatus.TS_COMPLETE) # ⚠️ ERROR logged, change ignored
⚠️ WARNING: Unprotected Reads
# May read stale data
div = reasoning_document.get_division_by_id(division_id)
status = div.status() # ⚠️ WARNING logged, may be stale
Error Messages
Write Protection Violation
ERROR: Write operation attempted without lock context in ReasoningDivision.set_status for division 'div-1'.
Operation: set status. All write operations must be performed within write_context().
Stack trace: ...
Read Protection Warning
WARNING: Read operation performed without read_context in ReasoningDivision.status for division 'div-1'.
Data may be stale. Consider using read_context() to ensure fresh data from Neo4j.
Testing Checklist
When testing, verify:
- Write Operations Log ERRORs when attempted outside
write_context() - Read Operations Log WARNINGs when attempted outside
read_context() - Changes Persist to Neo4j only when made inside
write_context() - Nested Writes Work (division → task → status changes all persist)
- Concurrent Access doesn't cause data loss (lock mechanism works)
- Cache Integration reduces Neo4j queries (check debug logs)
Common Patterns in Codebase
AgentDeepReasoning.py Pattern
# Always acquire lock before writes
document_lock = get_lock("REASONING_DOCUMENT", reasoning_document.id())
with reasoning_document.write_context(document_lock):
div = reasoning_document.get_division_by_id(division.id())
div.set_status(next_status)
# Multiple nested writes in same context
for task in div.tasks():
if task.status() == TaskStatus.TS_READY:
task.set_status(TaskStatus.TS_COMPLETE)
Lazy Read Context Pattern
# Used in ReasoningDocument methods
if self._check_read_allowed():
# Already in protected context, proceed directly
return [division for division in self._rdoc_divisions if not division.is_complete()]
else:
# Not protected, wrap in read_context
with self.read_context():
return [division for division in self._rdoc_divisions if not division.is_complete()]
Initialization Phase Exception
Objects under construction (before first Neo4j save) bypass protection:
# During XML parsing or new document creation
doc = ReasoningDocument()
doc._rdoc_lock_depth = 1 # Enable writes during construction
div = ReasoningDivision()
div.set_title("New Division") # Allowed during construction
doc._rdoc_lock_depth = 0 # Restore protection after construction
Cache Integration
As of recent updates, write_context() and read_context() use reasoning_cache_service:
- Read Path: Check cache → check Neo4j timestamp → load from Neo4j if stale
- Write Path: Save to Neo4j → update cache (write-through policy)
- Performance: Eliminates redundant Neo4j queries for cached documents
Debugging Tips
Enable Verbose Logging
from src.utils.log import log, DEBUG
# Watch for:
# - "Reloaded fresh document state..."
# - "Using cached document state..."
# - "Cache miss for document..."
# - "ERROR: Write operation attempted without lock..."
# - "WARNING: Read operation performed without read_context..."
Find Unprotected Access
# Search for setter calls without write_context
grep -n "\.set_status\|\.set_description\|\.set_result" AgentDeepReasoning.py | \
grep -v "write_context"
Verify Cache Effectiveness
# Check debug logs for cache hits vs Neo4j queries
# Cache hit: "Using cached document state..."
# Cache miss: "Cache miss for document..., loading from Neo4j"
Migration Guide
If you find unprotected writes:
- Identify the setter calls without
write_context() - Locate the document instance
- Wrap in write_context with proper lock:
# Before
div.set_status(ReasoningStatus.RS_COMPLETE)
# After
document_lock = get_lock("REASONING_DOCUMENT", reasoning_document.id())
with reasoning_document.write_context(document_lock):
div = reasoning_document.get_division_by_id(div.id())
div.set_status(ReasoningStatus.RS_COMPLETE)
Performance Considerations
- Lock Acquisition: External locks prevent race conditions
- Reload Cost: Mitigated by cache (only loads from Neo4j when cache miss/stale)
- Nested Contexts: Supported (reload only on outermost entry)
- Write Batching: Multiple writes in same context = single Neo4j save
Future Enhancements
Potential improvements: - Automatic write_context detection via decorators - Transaction rollback on exceptions - Audit trail for all modifications - Optimistic locking with version numbers
Part 2: Agent Reasoning Level Configuration
Current Implementation Overview
Key Components
- Global Active Reasoning (User Setting)
- Location:
LlmManager._active_reasoning(Python) - Default:
AgentReasoningEnum.COMPLEX - Purpose: System-wide cap on reasoning effort set by the user
- Setter:
LlmManager.set_active_reasoning(reasoning_str) -
Getter:
LlmManager.get_active_reasoning() -
Agent Minimum Reasoning (Agent Property)
- Location:
Agent.agent_minimum_reasoning(Python) - Default:
AgentReasoningEnum.FAST - Purpose: Minimum global reasoning level required for this agent to run
-
Check:
LlmManager.is_reasoning_enabled_for_agent(agent) -
Maximum Reasoning Required (Per LLM Call)
- Location: Parameter passed to
LlmManager.chat_completions(maximum_reasoning_required=...) - Default:
None(uses active_reasoning as-is) -
Purpose: Agent-specific cap for a specific LLM call
-
Effective Reasoning Computation
- Location:
LlmInterface._compute_reasoning_effort(active_reasoning, maximum_reasoning_required) - Logic: Returns minimum of
active_reasoningandmaximum_reasoning_required - Result:
(effort_string, effective_reasoning_enum)
AgentReasoningEnum Levels (from lowest to highest)
NONE = -1 # no reasoning at all
MINIMAL = 0 # no internal_thoughts in most circumstances
FAST = 1 # no internal_thoughts overall
SIMPLE = 2 # internal_thoughts enabled for AgentPlanner
MEDIUM = 3 # internal_thoughts for AgentPlanner and AgentExecutor
COMPLEX = 4 # internal_thoughts enabled overall
Current Hardcoded Reasoning Levels by Agent
| Agent | maximum_reasoning_required |
Location | Notes |
|---|---|---|---|
| AgentFocusser | AgentReasoningEnum.MINIMAL |
AgentFocusser.py:64 |
Hardcoded in run_llm() |
| AgentCustom | AgentReasoningEnum.FAST |
AgentCustom.py:109 |
Hardcoded in run() |
| AgentOntology | AgentReasoningEnum.FAST |
AgentOntology.py:221 |
Hardcoded in run() |
| AgentPromptAugmenter | AgentReasoningEnum.FAST |
AgentPromptAugmenter.py:137 |
Hardcoded in run() |
| AgentAssistantFiller | None (reads from config: MEDIUM) |
LlmSettings.json |
Configuration-driven: capped at MEDIUM |
| AgentAssistant | Not specified | AgentAssistant.py:257, 363 |
Uses None (defaults to active_reasoning) |
| AgentPlanner | Not specified | AgentPlanner.py:698, 857 |
Uses None (defaults to active_reasoning) |
| AgentDeepReasoning | Not specified | AgentDeepReasoning.py:572 |
Uses None (defaults to active_reasoning) |
| AgentImager | Not specified | AgentImager.py:59 |
Uses None (defaults to active_reasoning) |
| AgentMicroplanner | Not specified | AgentMicroplanner.py:101 |
Uses None (defaults to active_reasoning) |
| AgentReviewer | Not specified | AgentReviewer.py:90 |
Uses None (defaults to active_reasoning) |
| AgentStrategist | Not specified | AgentStrategist.py:66, 90 |
Uses None (defaults to active_reasoning) |
| AgentScanner | Not specified | Multiple locations | Uses None (defaults to active_reasoning) |
Data Flow
User sets global reasoning
↓
LlmManager._active_reasoning (COMPLEX/MEDIUM/SIMPLE/FAST/MINIMAL/NONE)
↓
Agent.run() → LlmManager.chat_completions(maximum_reasoning_required=HARDCODED_OR_NONE)
↓
LlmInterface._prepare_reasoning_effort()
↓
LlmInterface._compute_reasoning_effort(active_reasoning, maximum_reasoning_required)
↓
effective_reasoning = min(active_reasoning, maximum_reasoning_required OR active_reasoning)
↓
Maps to effort string: "minimal", "low", "medium", "high", or None
↓
Sent to LLM endpoint (gpt-5/gpt-oss specific)
Current C# Data Structure
File: ThalabusInterfaces/Data/Llm/LlmSettings.cs
public class AgentSetting {
[JsonProperty("customagent")]
public bool IsCustomAgent { get; set; } = false;
[JsonProperty("name")]
public string? Name { get; set; }
[JsonProperty("purpose")]
public string? Purpose { get; set; }
[JsonProperty("description")]
public string? Description { get; set; }
[JsonProperty("requires_no_input")]
public bool RequiresNoInput { get; set; } = false;
[JsonProperty("promptengineering")]
public Dictionary<string, PromptEngineering>? PromptEngineering { get; set; }
[JsonProperty("structured_output")]
public string? StructuredOutputJson { get; set; } = string.Empty;
// ⚠️ NO REASONING PROPERTY YET
}
Current Blazor Settings Page
File: ThalabusWeb/Pages/SettingsPages/SettingsLlm3.razor
This page allows editing agent prompts but does NOT have a reasoning dropdown yet.
Proposed Changes
STATUS: ✅ FULLY IMPLEMENTED
1. ✅ Extend C# Data Structure
Added to AgentSetting class in ThalabusInterfaces/Data/Llm/LlmSettings.cs:
[JsonProperty("reasoning", NullValueHandling = NullValueHandling.Ignore)]
public string? Reasoning { get; set; } = null; // null, or "NONE", "MINIMAL", "FAST", "SIMPLE", "MEDIUM", "COMPLEX"
2. ⏳ Update Blazor UI (SettingsLlm3.razor)
PENDING: Add a dropdown for reasoning configuration alongside the existing agent settings.
3. ✅ Update Python Agent Loading
Modified LlmManager in PythonServer/src/core/llm/manager.py:
- ✅ Added get_agent_reasoning(agent_name, purpose) method to read reasoning property from agent configuration
- ✅ Injected configured reasoning in chat_completions() when not explicitly provided
4. ✅ Migrate Hardcoded Values to Configuration
COMPLETED: Added reasoning values to ThalabusWeb/Configuration/LlmSettings.json:
- ✅ AgentFocusser: "MINIMAL"
- ✅ AgentOntology: "FAST"
- ✅ AgentPromptAugmenter: "FAST"
- ✅ AgentAssistantFiller: "MEDIUM"
- ⚠️ AgentCustom: Uses dynamic configuration per custom agent instance (no static config)
- ℹ️ All other agents: Use null (defaults to active_reasoning)
5. ✅ Update Agent Implementation
COMPLETED: Removed hardcoded maximum_reasoning_required values from agent files:
- ✅ AgentFocusser.py: Removed maximum_reasoning_required=AgentReasoningEnum.MINIMAL
- ✅ AgentCustom.py: Removed maximum_reasoning_required=AgentReasoningEnum.FAST
- ✅ AgentOntology.py: Removed maximum_reasoning_required=AgentReasoningEnum.FAST
- ✅ AgentPromptAugmenter.py: Removed maximum_reasoning_required=AgentReasoningEnum.FAST
- ✅ AgentAssistantFiller.py: Removed conditional logic, now uses configuration-driven MEDIUM
Key Questions to Resolve
- Where to store per-agent reasoning in Python?
- Option A: Add
agent_maximum_reasoningproperty toAgentclass (similar toagent_minimum_reasoning) -
Option B: Store in
LlmManageras a lookup dictionary by agent name -
AgentAssistantFiller Configuration ✅
- RESOLVED: Now uses configuration-driven
MEDIUMreasoning - Replaced conditional logic (
NONE if active_reasoning==FAST else None) with static MEDIUM cap -
User's active_reasoning is capped at MEDIUM:
min(active_reasoning, MEDIUM) -
Where to inject the configuration value?
- Option A: Load during Agent instantiation
- Option B: Look up dynamically in
chat_completions()call - Option C: Pass through from caller (requires changing all agent implementations)
Recommended Approach
Option B (recommended - least invasive):
Instead of modifying each agent's __init__() and all their chat_completions() calls, we can centralize the lookup in LlmManager.chat_completions() itself:
-
Add reasoning property to C# AgentSetting:
csharp [JsonProperty("reasoning", NullValueHandling = NullValueHandling.Ignore)] public string? Reasoning { get; set; } = null; -
Add helper method in LlmManager: ```python def get_agent_reasoning(self, agent_name: str, purpose: Optional[str] = None) -> Optional[AgentReasoningEnum]: """Get configured maximum reasoning for an agent from settings.""" if self._llm_configuration is None: return None
agents = self._llm_configuration.get("agents", {}) config_code = self.get_agent_config_name(agent_name, purpose) agent_config = agents.get(config_code, agents.get("DEFAULT", {}))
reasoning_str = agent_config.get("reasoning") if reasoning_str is None or reasoning_str == "": return None
try: return AgentReasoningEnum[reasoning_str.upper()] except (KeyError, AttributeError): log(WARNING, f"Invalid reasoning value '{reasoning_str}' for agent '{config_code}'") return None ```
-
Modify
LlmManager.chat_completions()to use configured value when not explicitly provided: ```python def chat_completions( self, messages: List[Dict], agent: Optional['Agent'] = None, # ... other params ... maximum_reasoning_required: Optional[AgentReasoningEnum] = None, # ... other params ... ): # NEW: If no explicit reasoning provided, check agent configuration if maximum_reasoning_required is None and agent is not None: maximum_reasoning_required = self.get_agent_reasoning(agent.agent_name, purpose)# Continue with existing logic... ```
-
Remove hardcoded values from agent files:
- AgentFocusser.py: Remove
maximum_reasoning_required=AgentReasoningEnum.MINIMAL - AgentCustom.py: Remove
maximum_reasoning_required=AgentReasoningEnum.FAST -
etc.
-
Populate initial configuration with current hardcoded values during migration script or manual update of LlmSettings.json
Advantages of this approach: - ✅ Minimal code changes (only one central location + config) - ✅ Backward compatible (None = use active_reasoning) - ✅ Agent code doesn't need to know about configuration - ✅ Easy to test and debug - ✅ Follows existing pattern for system prompts and model mappings
Migration strategy:
1. Add C# property and Blazor UI
2. Add Python helper method
3. Inject into chat_completions()
4. Update LlmSettings.json with current hardcoded values
5. Remove hardcoded values from agent files (one by one, with testing)
Summary: Where Does active_reasoning Come From?
Answer: active_reasoning comes from LlmManager._active_reasoning, which is:
- Set by: User via Blazor settings UI → HTTP API → LlmManager.set_active_reasoning(reasoning_str)
- Default: AgentReasoningEnum.COMPLEX (initialized in LlmManager.__init__())
- Purpose: Global system-wide cap on reasoning effort (user preference)
- Used in: _compute_reasoning_effort() where it's compared against maximum_reasoning_required
The missing piece (what you're implementing):
- maximum_reasoning_required is currently HARDCODED in agent files
- Should be CONFIGURABLE per agent in LlmSettings.json
- Should be loaded from configuration when chat_completions() is called