feat: two-phase execution, think slugs, query enrichment (v1.6.0)
Build and Push / build (release) Successful in 1m14s

Two-Phase Tatlock Execution:
- orchestrate_tool_calls() for Phase 1 coordination
- synthesize_from_results() for Phase 2 butler-toned synthesis
- Guarantees butler personality in all responses

Automatic Think Slugs:
- Deterministic butler-perspective messages during expert delegation
- ActionType enum: RETRIEVE, RESEARCH, CREATE, CONTROL, RECORD
- HOUSEHOLD_THINK_MESSAGES mapping for all experts
- Streaming delegation wrappers with automatic think messages

Steward Query Enrichment:
- Auto-fill user context (location, timezone) when not specified
- _build_enriched_query() with regex word boundary matching
- enriched_query field in StewardRecommendation schema

Documentation:
- ORCHESTRATION_SCENARIOS.md rewritten with Mermaid diagrams
- New Housekeeper and Biographer scenarios
- TESTING_IMPROVEMENTS.md for future LLM testing patterns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 14:00:32 +01:00
co-authored by Claude Opus 4.5
parent a1b8fe46e8
commit 49f0da8068
13 changed files with 1919 additions and 263 deletions
+105 -24
View File
@@ -43,7 +43,7 @@ async def _execute_single_delegation(
Execute a single delegation to an agent.
Args:
agent_name: Name of agent (biographer, librarian)
agent_name: Name of agent (biographer, librarian, housekeeper)
task: Task description
tracker: Tool call tracker
@@ -67,6 +67,13 @@ async def _execute_single_delegation(
await tracker.track_call("delegate_to_librarian", duration)
return (agent_name, result.output)
elif agent_name == "housekeeper":
from src.agents.delegation import delegate_to_housekeeper
result = await delegate_to_housekeeper(task=task)
duration = time.time() - start_time
await tracker.track_call("delegate_to_housekeeper", duration)
return (agent_name, result.output)
else:
return (agent_name, f"Unknown agent: {agent_name}")
@@ -244,6 +251,68 @@ async def _direct_delegation(
return "\n\n".join(results) if results else "I apologize, sir. No delegation results available."
async def _direct_delegation_with_results(
user_message: str,
recommendation: "StewardRecommendation",
tracker: "ToolCallTracker",
conversation_id: str,
) -> dict:
"""
Directly delegate to expert agents and return structured results.
This is the Phase 1 variant of direct delegation that returns results
in the same format as TatlockAgent.orchestrate_tool_calls() for
consistent Phase 2 synthesis.
Args:
user_message: User's request
recommendation: Steward's recommendation
tracker: Tool call tracker
conversation_id: Conversation ID
Returns:
dict: Orchestration results with expert_results, tool_outputs, etc.
"""
logger.info(
"direct_delegation_with_results",
agents=recommendation.recommended_capabilities,
conversation_id=conversation_id,
)
expert_results = {}
tools_called = []
for agent in recommendation.recommended_capabilities:
try:
agent_name, result = await _execute_single_delegation(
agent, user_message, tracker
)
expert_results[agent_name] = result
tools_called.append(f"delegate_to_{agent_name}")
logger.info(
"direct_delegation_result",
agent=agent_name,
result_preview=result[:100] if result else "empty",
conversation_id=conversation_id,
)
except Exception as e:
logger.error(
"direct_delegation_failed",
agent=agent,
error=str(e),
conversation_id=conversation_id,
)
expert_results[agent] = f"Error: {e}"
return {
"tools_called": tools_called,
"expert_results": expert_results,
"tool_outputs": {}, # No tool outputs for direct delegation
"raw_output": "", # No raw output for direct delegation
}
# Global conversation history tracker
# In production, this would be backed by a database or Redis
_conversation_history = ConversationHistory(max_turns=20)
@@ -379,12 +448,13 @@ async def create_response(request: ResponseRequest) -> Response:
async def create_response_with_steward(request: ResponseRequest) -> Response:
"""
Create response using Steward preprocessing (Phase 2 flow).
Create response using Steward preprocessing and two-phase Tatlock execution.
This is the two-tier architecture where:
This is the two-tier architecture with two-phase synthesis:
1. Steward analyzes the request and recommends capabilities
2. Tatlock runs with scoped tools based on recommendations
3. Tool usage is tracked for benchmarking
2. Phase 1: Tatlock orchestrates tool calls and expert delegations
3. Phase 2: Tatlock synthesizes butler-toned response from results
4. Tool usage is tracked for benchmarking
Args:
request: Response request
@@ -420,37 +490,39 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
conversation_id=conversation_id,
)
# Phase 1: Steward preprocessing
# Steward preprocessing
enriched = await preprocess_request(
user_message,
conversation_history=conversation_history,
conversation_id=conversation_id,
)
# Phase 2: Initialize tool tracker
# Initialize tool tracker
tracker = ToolCallTracker(
recommended_capabilities=enriched.recommendation.recommended_capabilities,
conversation_id=conversation_id,
)
# Phase 3: Check if direct delegation is recommended
# If Steward recommends ONLY delegation agents (biographer/librarian),
# skip Tatlock and delegate directly
# Check if direct delegation is recommended
# If Steward recommends ONLY delegation agents (biographer/librarian/housekeeper),
# we still use two-phase but delegate directly in Phase 1
delegation_agents = {"biographer", "librarian", "housekeeper"}
delegation_only = all(
cap in ("biographer", "librarian")
cap in delegation_agents
for cap in enriched.recommendation.recommended_capabilities
) and enriched.recommendation.recommended_capabilities
from src.agents.tatlock import TatlockAgent
tatlock = TatlockAgent()
if delegation_only:
tatlock_response = await _direct_delegation(
# Direct delegation path - collect results then synthesize
orchestration_results = await _direct_delegation_with_results(
user_message, enriched.recommendation, tracker, conversation_id
)
else:
# Phase 3a: Run Tatlock with scoped tools
from src.agents.tatlock import TatlockAgent
tatlock = TatlockAgent()
tatlock_response = await tatlock.run_with_scoped_tools(
# Phase 1: Orchestrate tool calls
orchestration_results = await tatlock.orchestrate_tool_calls(
user_message=user_message,
steward_note=enriched.steward_note,
scoped_tools=enriched.scoped_tools,
@@ -458,14 +530,23 @@ async def create_response_with_steward(request: ResponseRequest) -> Response:
tool_tracker=tracker,
)
# Phase 3b: Check for text-based delegation fallback
# If Tatlock outputs [DELEGATE:...] instead of calling the function,
# we parse and execute it here
tatlock_response = await _handle_text_delegation(
tatlock_response, tracker, conversation_id
)
# Handle text-based delegation fallback if present
if "[DELEGATE:" in orchestration_results.get("raw_output", ""):
text_delegation_results = await _handle_text_delegation(
orchestration_results["raw_output"], tracker, conversation_id
)
# Add text delegation results to expert_results
if text_delegation_results != orchestration_results["raw_output"]:
orchestration_results["expert_results"]["text_delegation"] = text_delegation_results
# Phase 4: Finalize tool tracking
# Phase 2: Synthesize butler-toned response from all results
tatlock_response = await tatlock.synthesize_from_results(
user_message=user_message,
orchestration_results=orchestration_results,
message_history=conversation_history,
)
# Finalize tool tracking
await tracker.finalize()
# Build response output items