Reasoning: Document Protection and Agent Configuration

This document covers two complementary aspects of reasoning in Thalabus:

  1. Reasoning Document Context Protection — how reasoning documents maintain data consistency with Neo4j
  2. 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())

2. Read Protection (_check_read_allowed())

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:

  1. Write Operations Log ERRORs when attempted outside write_context()
  2. Read Operations Log WARNINGs when attempted outside read_context()
  3. Changes Persist to Neo4j only when made inside write_context()
  4. Nested Writes Work (division → task → status changes all persist)
  5. Concurrent Access doesn't cause data loss (lock mechanism works)
  6. 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:

  1. Read Path: Check cache → check Neo4j timestamp → load from Neo4j if stale
  2. Write Path: Save to Neo4j → update cache (write-through policy)
  3. 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:

  1. Identify the setter calls without write_context()
  2. Locate the document instance
  3. 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

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

  1. Global Active Reasoning (User Setting)
  2. Location: LlmManager._active_reasoning (Python)
  3. Default: AgentReasoningEnum.COMPLEX
  4. Purpose: System-wide cap on reasoning effort set by the user
  5. Setter: LlmManager.set_active_reasoning(reasoning_str)
  6. Getter: LlmManager.get_active_reasoning()

  7. Agent Minimum Reasoning (Agent Property)

  8. Location: Agent.agent_minimum_reasoning (Python)
  9. Default: AgentReasoningEnum.FAST
  10. Purpose: Minimum global reasoning level required for this agent to run
  11. Check: LlmManager.is_reasoning_enabled_for_agent(agent)

  12. Maximum Reasoning Required (Per LLM Call)

  13. Location: Parameter passed to LlmManager.chat_completions(maximum_reasoning_required=...)
  14. Default: None (uses active_reasoning as-is)
  15. Purpose: Agent-specific cap for a specific LLM call

  16. Effective Reasoning Computation

  17. Location: LlmInterface._compute_reasoning_effort(active_reasoning, maximum_reasoning_required)
  18. Logic: Returns minimum of active_reasoning and maximum_reasoning_required
  19. 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

  1. Where to store per-agent reasoning in Python?
  2. Option A: Add agent_maximum_reasoning property to Agent class (similar to agent_minimum_reasoning)
  3. Option B: Store in LlmManager as a lookup dictionary by agent name

  4. AgentAssistantFiller Configuration ✅

  5. RESOLVED: Now uses configuration-driven MEDIUM reasoning
  6. Replaced conditional logic (NONE if active_reasoning==FAST else None) with static MEDIUM cap
  7. User's active_reasoning is capped at MEDIUM: min(active_reasoning, MEDIUM)

  8. Where to inject the configuration value?

  9. Option A: Load during Agent instantiation
  10. Option B: Look up dynamically in chat_completions() call
  11. 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:

  1. Add reasoning property to C# AgentSetting: csharp [JsonProperty("reasoning", NullValueHandling = NullValueHandling.Ignore)] public string? Reasoning { get; set; } = null;

  2. 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 ```

  3. 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... ```

  4. Remove hardcoded values from agent files:

  5. AgentFocusser.py: Remove maximum_reasoning_required=AgentReasoningEnum.MINIMAL
  6. AgentCustom.py: Remove maximum_reasoning_required=AgentReasoningEnum.FAST
  7. etc.

  8. 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