TOON Optimizations

This commit is contained in:
Renn F
2025-12-12 18:08:06 +01:00
parent d570334e04
commit 316325625c
17 changed files with 780 additions and 324 deletions
+14 -1
View File
@@ -36,6 +36,7 @@ dependencies = [
"anthropic",
"openai", # For embeddings
"tiktoken", # Token counting
"python-toon", # Token-efficient LLM serialization
# MCP (Model Context Protocol)
"mcp",
@@ -120,11 +121,23 @@ select = [
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib
"ERA", # eradicate (commented code)
"PL", # Pylint
"RUF", # Ruff-specific
]
# MCP servers: Tool functions require explicit parameters for schema generation.
# PLR0913 - MCP tools need many params (no dataclass workaround)
# PLR0915 - Factory functions define all tools (unavoidable nesting)
# PLC0415 - Lazy imports for circular import avoidance
# E501 - Docstrings and guidance messages
#
# Services: Internal methods often need multiple related parameters for DB ops.
# PLR0913 - Service methods wrapping DB/external calls need multiple params
# PLC0415 - Lazy imports for circular import avoidance
[tool.ruff.lint.per-file-ignores]
"roboco/mcp/*.py" = ["PLR0913", "PLR0915", "PLC0415", "E501"]
"roboco/services/*.py" = ["PLC0415", "PLR0913"]
# =============================================================================
# MyPy Configuration
# =============================================================================
+48
View File
@@ -21,6 +21,7 @@ from pydantic import BaseModel, Field
from roboco.api.websocket import broadcast_agent_chunk
from roboco.config import settings
from roboco.llm import ToonAdapter
from roboco.models import AgentRole, AgentStatus, Team
logger = structlog.get_logger()
@@ -113,6 +114,7 @@ class Agent(ABC):
self._running = False
self._task: asyncio.Task | None = None
self._llm_client: AsyncAnthropic | None = None
self._toon = ToonAdapter()
self.log = logger.bind(
agent_id=str(config.id),
@@ -157,6 +159,52 @@ class Agent(ABC):
self._llm_client = AsyncAnthropic(api_key=settings.anthropic_api_key)
return self._llm_client
# =========================================================================
# TOON SERIALIZATION (for token-efficient LLM communication)
# =========================================================================
def format_context(self, data: dict[str, Any]) -> str:
"""
Format context data for LLM using TOON.
TOON (Token-Oriented Object Notation) reduces token consumption
by 30-60% compared to JSON while maintaining semantic clarity.
Args:
data: Dictionary to encode for LLM prompt.
Returns:
TOON-formatted string.
"""
return self._toon.encode(data)
def format_context_labeled(self, label: str, data: dict[str, Any]) -> str:
"""
Format labeled context data for embedding in prompts.
Args:
label: Section label (e.g., "Task Context").
data: Dictionary to encode.
Returns:
Labeled TOON-formatted string.
"""
return self._toon.format_for_prompt(label, data)
def parse_llm_response(self, response: str) -> dict[str, Any] | list[Any]:
"""
Parse structured data from LLM response.
Attempts TOON parsing first, falls back to JSON.
Args:
response: Raw LLM response text.
Returns:
Parsed Python dict or list.
"""
return self._toon.decode(response)
# =========================================================================
# LIFECYCLE METHODS
# =========================================================================
+23 -21
View File
@@ -153,17 +153,20 @@ class ProductOwnerAgent(Agent):
result = await self._api_call("GET", f"/tasks/{task_id}")
acceptance_criteria = result.get("acceptance_criteria", [])
# Use LLM to check if criteria are met
prompt = f"""
Review this completed feature against its acceptance criteria:
# Use TOON for token-efficient context encoding
task_context = self.format_context_labeled(
"Feature Review",
{
"title": result.get("title", "Unknown"),
"description": result.get("description", "No description"),
"acceptance_criteria": acceptance_criteria,
"dev_notes": result.get("dev_notes", "None"),
},
)
Task: {result.get("title", "Unknown")}
Description: {result.get("description", "No description")}
prompt = f"""Review this completed feature against its acceptance criteria:
Acceptance Criteria:
{chr(10).join(f"- {c}" for c in acceptance_criteria)}
Dev Notes: {result.get("dev_notes", "None")}
{task_context}
Determine if all criteria are met. Respond with:
ACCEPTED: [reason] or NEEDS_CHANGES: [what's missing]
@@ -465,11 +468,15 @@ class AuditorAgent(Agent):
if not self._observations:
return
prompt = f"""
Analyze these observations for quality and efficiency issues:
# Use TOON for token-efficient context encoding
observations_context = self.format_context_labeled(
"Observations",
{"recent": self._observations[-50:]},
)
Observations:
{chr(10).join(str(o) for o in self._observations[-50:])}
prompt = f"""Analyze these observations for quality and efficiency issues:
{observations_context}
Look for:
1. Efficiency issues - wasted effort, unclear processes
@@ -478,14 +485,9 @@ Look for:
4. Process violations - skipping QA, missing documentation
5. Team health - frustration, conflicts
For each issue found, provide:
- Category
- Severity (info/warning/concern/critical)
- Description
- Evidence
- Recommendation
Be thorough but fair.
Format response as TOON tabular:
[N,]{{category,severity,description,evidence,recommendation}}:
efficiency,warning,Unclear handoff process,3 tasks delayed,Document handoff steps
"""
analysis = await self.think(prompt)
self.log.info("Analysis complete", analysis_length=len(analysis))
+57 -24
View File
@@ -249,13 +249,16 @@ class DeveloperAgent(Agent):
# Read task requirements
requirements = await self._read_task_requirements(ctx.task_id)
# Use LLM to understand and identify gaps
prompt = f"""
You are analyzing a task before beginning work.
# Format context using TOON for token efficiency
task_context = self.format_context_labeled(
"Task Context",
{"title": ctx.title, "requirements": requirements},
)
Task: {ctx.title}
Requirements:
{requirements}
# Use LLM to understand and identify gaps
prompt = f"""You are analyzing a task before beginning work.
{task_context}
Analyze:
1. What exactly needs to be done?
@@ -293,28 +296,48 @@ If clarification needed, respond with: "QUESTION: [your question]"
"""
self.log.info("PLAN phase", task_id=str(ctx.task_id))
# Use LLM to create plan
prompt = f"""
Create an implementation plan for this task:
# Format context using TOON
plan_context = self.format_context_labeled(
"Task",
{
"title": ctx.title,
"understanding": ctx.journal_entries[-1]
if ctx.journal_entries
else "No context",
},
)
Task: {ctx.title}
Understanding: {ctx.journal_entries[-1] if ctx.journal_entries else "No context"}
# Use LLM to create plan - request TOON tabular response
prompt = f"""Create an implementation plan for this task:
Break this into ordered subtasks. For each subtask:
{plan_context}
Break this into ordered subtasks. For each subtask provide:
- Clear description
- Files to modify
- Estimated complexity (small/medium/large)
Format as JSON array:
[
{{"description": "...", "files": ["..."], "complexity": "small|medium|large"}},
...
]
Format response as TOON tabular:
[N,]{{description,files,complexity}}:
Implement the main logic,src/main.py|src/utils.py,medium
Add unit tests,tests/test_main.py,small
"""
response = await self.think(prompt)
# Parse subtasks (simplified - would use proper JSON parsing)
ctx.subtasks = [{"description": response, "files": [], "complexity": "medium"}]
# Parse subtasks using TOON (falls back to JSON)
try:
subtasks = self.parse_llm_response(response)
if isinstance(subtasks, list):
ctx.subtasks = subtasks
else:
ctx.subtasks = [
{"description": response, "files": [], "complexity": "medium"}
]
except ValueError:
# Fallback if parsing fails
ctx.subtasks = [
{"description": response, "files": [], "complexity": "medium"}
]
# Journal entry
ts = datetime.now(UTC).isoformat()
@@ -349,12 +372,22 @@ Format as JSON array:
subtask = ctx.subtasks[ctx.current_subtask]
# Use LLM to work on subtask
prompt = f"""
Execute this subtask:
# Format context using TOON
execute_context = self.format_context_labeled(
"Execution Context",
{
"task": ctx.title,
"subtask_number": ctx.current_subtask + 1,
"total_subtasks": len(ctx.subtasks),
"description": subtask.get("description", ""),
"files": subtask.get("files", []),
},
)
Task: {ctx.title}
Subtask {ctx.current_subtask + 1}/{len(ctx.subtasks)}: {subtask.get("description", "")}
# Use LLM to work on subtask
prompt = f"""Execute this subtask:
{execute_context}
Provide:
1. Code changes needed
+27 -18
View File
@@ -321,19 +321,21 @@ Respond with structured analysis.
doc_spec = ctx.documents_needed[ctx.current_doc]
# Use LLM to write documentation
prompt = f"""
Write documentation for this task.
# Use TOON for token-efficient context encoding
doc_context = self.format_context_labeled(
"Documentation Task",
{
"title": ctx.title,
"doc_type": doc_spec.doc_type.value,
"target_path": doc_spec.path,
"summary": ctx.summary,
"dev_notes": ctx.dev_notes or "None",
},
)
Task: {ctx.title}
Document Type: {doc_spec.doc_type.value}
Target Path: {doc_spec.path}
prompt = f"""Write documentation for this task.
Summary:
{ctx.summary}
Developer Notes:
{ctx.dev_notes or "None"}
{doc_context}
Write professional, clear documentation following best practices.
Include:
@@ -373,14 +375,19 @@ Format appropriately for the document type.
if not doc_spec.content:
continue
prompt = f"""
Review this documentation for quality:
# Use TOON for token-efficient context encoding
review_context = self.format_context_labeled(
"Document Review",
{
"title": doc_spec.title,
"doc_type": doc_spec.doc_type.value,
"content": doc_spec.content,
},
)
Document: {doc_spec.title}
Type: {doc_spec.doc_type.value}
prompt = f"""Review this documentation for quality:
Content:
{doc_spec.content}
{review_context}
Check:
1. Accuracy - Does it correctly describe the feature?
@@ -388,7 +395,9 @@ Check:
3. Clarity - Is it easy to understand?
4. Examples - Are examples helpful and correct?
If issues found, provide suggestions.
Format response as TOON:
{{accuracy,completeness,clarity,examples,suggestions}}:
good,complete,clear,helpful,None
"""
review = await self.think(prompt)
ts = datetime.now(UTC).isoformat()
+51 -28
View File
@@ -199,12 +199,15 @@ class CellPMAgent(Agent):
new_tasks = await self._get_unassigned_tasks()
for task_id in new_tasks:
# Assess complexity and priority
prompt = f"""
Assess this task for prioritization:
# Use TOON for token-efficient context encoding
triage_context = self.format_context_labeled(
"Task Triage",
{"task_id": str(task_id), "cell": self.cell_name},
)
Task ID: {task_id}
Cell: {self.cell_name}
prompt = f"""Assess this task for prioritization:
{triage_context}
Consider:
1. Complexity (low/medium/high)
@@ -212,7 +215,9 @@ Consider:
3. Priority (P0-P3)
4. Best dev fit based on skills
Provide assessment.
Format response as TOON:
{{complexity,dependencies,priority,dev_fit}}:
medium,TASK-abc123,P1,backend-dev-1
"""
assessment = await self.think(prompt)
self.log.info(
@@ -249,11 +254,15 @@ Provide assessment.
questions = await self._get_pending_questions()
for question in questions:
# Try to answer or route appropriately
prompt = f"""
A cell member has a question:
# Use TOON for token-efficient context encoding
question_context = self.format_context_labeled(
"Cell Question",
{"question": question, "cell": self.cell_name},
)
{question}
prompt = f"""A cell member needs help:
{question_context}
As the Cell PM, provide:
1. Answer if you can
@@ -610,23 +619,29 @@ class MainPMAgent(Agent):
self.log.debug("PRIORITIZE phase")
if self._board_directives:
directives = chr(10).join(f"- {d}" for d in self._board_directives)
status_lines = []
for k, v in self._cell_statuses.items():
active = v.active_tasks
blocked = v.blocked_tasks
status_lines.append(f"- {k}: {active} active, {blocked} blocked")
cell_status = chr(10).join(status_lines)
prompt = f"""
Translate these Board directives into cell priorities:
# Build status data for TOON encoding
cell_status_data = {
name: {"active": s.active_tasks, "blocked": s.blocked_tasks}
for name, s in self._cell_statuses.items()
}
Directives:
{directives}
# Use TOON for token-efficient context encoding
priority_context = self.format_context_labeled(
"Prioritization Context",
{
"directives": self._board_directives,
"cell_status": cell_status_data,
},
)
Current Cell Status:
{cell_status}
prompt = f"""Translate these Board directives into cell priorities:
Provide prioritized task list for each cell.
{priority_context}
Format response as TOON tabular:
[N,]{{cell,priority,task_description}}:
backend-cell,P0,Implement critical auth fix
frontend-cell,P1,Update dashboard layout
"""
priorities = await self.think(prompt)
self.log.info("Priorities set", priorities=priorities[:200])
@@ -636,11 +651,19 @@ Provide prioritized task list for each cell.
self.log.debug("COORDINATE phase")
for issue in self._cross_cell_issues:
prompt = f"""
Resolve this cross-cell issue:
# Use TOON for token-efficient context encoding
issue_context = self.format_context_labeled(
"Cross-Cell Issue",
{
"description": issue.get("description"),
"cells": issue.get("cells"),
"task_id": issue.get("task_id"),
},
)
Issue: {issue.get("description")}
Cells Involved: {issue.get("cells")}
prompt = f"""Resolve this cross-cell issue:
{issue_context}
Propose a resolution that unblocks all parties.
"""
+33 -34
View File
@@ -211,27 +211,22 @@ class QAAgent(Agent):
dev_notes = await self._read_dev_notes(ctx.task_id)
commits = await self._get_task_commits(ctx.task_id)
# Use LLM to understand and create test plan
prompt = f"""
You are a QA engineer reviewing a completed task.
# Use TOON for token-efficient context encoding
task_context = self.format_context_labeled(
"QA Review Context",
{
"title": ctx.title,
"requirements": requirements,
"dev_notes": dev_notes,
"commits": commits,
},
)
Task: {ctx.title}
prompt = f"""You are a QA engineer reviewing a completed task.
Requirements:
{requirements}
Developer Notes:
{dev_notes}
Commits:
{commits}
{task_context}
Based on this, create test cases to verify the implementation.
For each test case provide:
1. Name
2. What to test
3. Steps to execute
4. Expected outcome
Focus on:
- Acceptance criteria verification
@@ -239,8 +234,10 @@ Focus on:
- Integration points
- Error handling
Format as JSON array.
"""
Format response as TOON tabular:
[N,]{{name,description,steps,expected}}:
Acceptance Criteria,Verify all criteria met,Review implementation|Check each criterion,All criteria satisfied
""" # noqa: E501
_response = await self.think(prompt) # Response informs test case structure
# Create test cases (simplified parsing)
@@ -289,24 +286,26 @@ Format as JSON array.
test_case = ctx.test_cases[ctx.current_test]
# Use LLM to execute test
prompt = f"""
Execute this test case:
# Use TOON for token-efficient context encoding
test_context = self.format_context_labeled(
"Test Case",
{
"name": test_case.name,
"description": test_case.description,
"steps": test_case.steps,
"expected": test_case.expected,
},
)
Test: {test_case.name}
Description: {test_case.description}
Steps: {", ".join(test_case.steps)}
Expected: {test_case.expected}
prompt = f"""Execute this test case:
Simulate executing this test and provide:
1. RESULT: PASS or FAIL
2. ACTUAL: What was observed
3. NOTES: Any additional findings
{test_context}
Format:
RESULT: [PASS|FAIL]
ACTUAL: [observation]
NOTES: [notes]
Simulate executing this test and provide results.
Format response as TOON:
{{result,actual,notes}}:
PASS,All criteria verified successfully,No issues found
"""
response = await self.think(prompt)
+15
View File
@@ -0,0 +1,15 @@
"""
LLM Communication Layer
Provides utilities for efficient communication with Large Language Models,
including TOON serialization for token-efficient data transfer.
"""
from roboco.llm.metrics import ToonMetrics
from roboco.llm.toon_adapter import ToonAdapter, ToonConfig
__all__ = [
"ToonAdapter",
"ToonConfig",
"ToonMetrics",
]
+86
View File
@@ -0,0 +1,86 @@
"""
TOON Metrics
Tracks token savings and usage statistics for TOON vs JSON serialization.
"""
from dataclasses import dataclass, field
from datetime import UTC, datetime
@dataclass
class ToonMetrics:
"""
Metrics for tracking TOON serialization efficiency.
Tracks character counts (as proxy for tokens) for JSON vs TOON
to measure actual savings in production.
"""
json_chars: int = 0
toon_chars: int = 0
encode_count: int = 0
decode_count: int = 0
decode_fallback_count: int = 0
started_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@property
def savings_percent(self) -> float:
"""Calculate percentage of characters saved using TOON."""
if self.json_chars == 0:
return 0.0
return (1 - self.toon_chars / self.json_chars) * 100
@property
def fallback_rate(self) -> float:
"""Calculate rate of fallback to JSON decoding."""
if self.decode_count == 0:
return 0.0
return (self.decode_fallback_count / self.decode_count) * 100
def record_encode(self, json_chars: int, toon_chars: int) -> None:
"""Record an encode operation with character counts."""
self.json_chars += json_chars
self.toon_chars += toon_chars
self.encode_count += 1
def record_decode(self, used_fallback: bool = False) -> None:
"""Record a decode operation."""
self.decode_count += 1
if used_fallback:
self.decode_fallback_count += 1
def to_dict(self) -> dict:
"""Convert metrics to dictionary for logging/reporting."""
return {
"json_chars": self.json_chars,
"toon_chars": self.toon_chars,
"savings_percent": round(self.savings_percent, 2),
"encode_count": self.encode_count,
"decode_count": self.decode_count,
"fallback_rate": round(self.fallback_rate, 2),
"started_at": self.started_at.isoformat(),
}
def reset(self) -> None:
"""Reset all metrics."""
self.json_chars = 0
self.toon_chars = 0
self.encode_count = 0
self.decode_count = 0
self.decode_fallback_count = 0
self.started_at = datetime.now(UTC)
# Global metrics holder
class _MetricsHolder:
"""Holder for singleton ToonMetrics instance."""
instance: ToonMetrics | None = None
def get_toon_metrics() -> ToonMetrics:
"""Get the global TOON metrics instance."""
if _MetricsHolder.instance is None:
_MetricsHolder.instance = ToonMetrics()
return _MetricsHolder.instance
+191
View File
@@ -0,0 +1,191 @@
"""
TOON Adapter
Provides serialization/deserialization between Python objects and TOON format
for token-efficient LLM communication. TOON (Token-Oriented Object Notation)
achieves 30-60% fewer tokens than JSON while maintaining semantic clarity.
Usage:
adapter = ToonAdapter()
# Encode data for LLM prompt
toon_str = adapter.encode({"name": "Alice", "age": 30})
# Decode LLM response (falls back to JSON if TOON fails)
data = adapter.decode(response_text)
# Format for embedding in prompt
prompt_section = adapter.format_for_prompt("Task Context", task_data)
"""
import json
from dataclasses import dataclass
from typing import Any
import structlog
import toon
from pydantic import BaseModel
logger = structlog.get_logger()
@dataclass
class ToonConfig:
"""Configuration for TOON encoding."""
delimiter: str = ","
indent: int = 2
include_length: bool = True
class ToonAdapter:
"""
Adapter for TOON serialization at LLM boundaries.
Converts Python dicts/Pydantic models to TOON for sending to LLMs,
and parses TOON responses back to Python objects. Falls back to
JSON parsing if TOON decode fails.
"""
def __init__(self, config: ToonConfig | None = None) -> None:
"""
Initialize the TOON adapter.
Args:
config: Optional configuration for TOON encoding.
"""
self.config = config or ToonConfig()
self.log = logger.bind(component="toon_adapter")
def encode(self, data: dict[str, Any] | list[Any] | BaseModel) -> str:
"""
Convert Python object to TOON for LLM consumption.
Args:
data: Dictionary, list, or Pydantic model to encode.
Returns:
TOON-formatted string.
"""
if isinstance(data, BaseModel):
data = data.model_dump()
return toon.encode(data, indent=self.config.indent)
def decode(self, toon_str: str) -> dict[str, Any] | list[Any]:
"""
Parse TOON response from LLM.
Falls back to JSON parsing if TOON decode fails, logging a warning.
Args:
toon_str: TOON-formatted string from LLM response.
Returns:
Parsed Python dict or list.
Raises:
ValueError: If neither TOON nor JSON parsing succeeds.
"""
# Try TOON first
toon_err_msg = ""
try:
return toon.decode(toon_str)
except Exception as toon_error:
toon_err_msg = str(toon_error)
self.log.warning(
"TOON decode failed, trying JSON fallback",
error=toon_err_msg,
)
# Fallback to JSON
try:
return json.loads(toon_str)
except json.JSONDecodeError as json_error:
self.log.error(
"Both TOON and JSON decode failed",
toon_error=toon_err_msg,
json_error=str(json_error),
)
raise ValueError(
f"Failed to decode response as TOON or JSON: {toon_str[:100]}..."
) from json_error
def format_for_prompt(self, label: str, data: dict[str, Any]) -> str:
"""
Format data with label for embedding in LLM prompt.
Args:
label: Section label (e.g., "Task Context", "Requirements").
data: Data to encode.
Returns:
Formatted string suitable for prompt inclusion.
"""
encoded = self.encode(data)
return f"{label}:\n{encoded}"
def format_tabular_request(
self,
fields: list[str],
description: str,
example_rows: list[list[str]] | None = None,
) -> str:
"""
Format a request for tabular TOON response.
Args:
fields: Column names for the table.
description: What the LLM should return.
example_rows: Optional example data rows.
Returns:
Formatted instruction for LLM to return TOON tabular data.
"""
fields_str = ",".join(fields)
header = f"[N,]{{{fields_str}}}:"
instruction = f"{description}\n\nFormat response as TOON tabular:\n{header}"
if example_rows:
instruction += "\n"
for row in example_rows:
instruction += f"{self.config.delimiter.join(row)}\n"
return instruction
def estimate_token_savings(
self,
data: dict[str, Any] | list[Any],
) -> tuple[int, int, float]:
"""
Estimate token savings of TOON vs JSON for given data.
Args:
data: Data to compare.
Returns:
Tuple of (json_chars, toon_chars, savings_percent).
"""
json_str = json.dumps(data, separators=(",", ":"))
toon_str = self.encode(data)
json_chars = len(json_str)
toon_chars = len(toon_str)
savings = (1 - toon_chars / json_chars) * 100 if json_chars > 0 else 0.0
return json_chars, toon_chars, savings
# Module-level singleton holder
class _AdapterHolder:
"""Holder for singleton ToonAdapter instance."""
instance: ToonAdapter | None = None
def get_toon_adapter() -> ToonAdapter:
"""Get the default TOON adapter singleton."""
if _AdapterHolder.instance is None:
_AdapterHolder.instance = ToonAdapter()
return _AdapterHolder.instance
+5
View File
@@ -21,6 +21,10 @@ from fastapi import status
from mcp.server.fastmcp import FastMCP
from roboco.config import settings
from roboco.llm import ToonAdapter
# Global TOON adapter for encoding journal data
_toon = ToonAdapter()
# =============================================================================
# HELPER FUNCTIONS
@@ -141,6 +145,7 @@ def create_journal_mcp_server(agent_id: str) -> FastMCP:
return {
"status": "created",
"entry": entry,
"entry_toon": _toon.encode(entry), # TOON-encoded for LLM token efficiency
"guidance": "Journal entry saved. Use roboco_journal_search to find past entries.",
}
+4
View File
@@ -21,6 +21,10 @@ from mcp.server.fastmcp import FastMCP
from roboco.agents_config import CHANNEL_ACCESS
from roboco.config import settings
from roboco.llm import ToonAdapter
# Global TOON adapter for encoding message data
_toon = ToonAdapter()
def _check_channel_access(agent_id: str, channel_slug: str, action: str) -> bool:
+15 -1
View File
@@ -27,6 +27,10 @@ from fastapi import status
from mcp.server.fastmcp import FastMCP
from roboco.config import settings
from roboco.llm import ToonAdapter
# Global TOON adapter for encoding task data
_toon = ToonAdapter()
# =============================================================================
# VALID STATE TRANSITIONS
@@ -63,15 +67,25 @@ def _format_task_response(
guidance: str,
project: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Format a standardized task response with guidance."""
"""
Format a standardized task response with guidance.
Includes both JSON task data and TOON-encoded version for
token-efficient LLM consumption.
"""
# Encode task data as TOON for token efficiency when LLM processes response
task_toon = _toon.encode(task)
response = {
"status": task.get("status"),
"task": task,
"task_toon": task_toon, # TOON-encoded for LLM token efficiency
"next_step": next_step,
"guidance": guidance,
}
if project:
response["project"] = project
response["project_toon"] = _toon.encode(project)
return response
+22 -16
View File
@@ -5,6 +5,7 @@ Documenter handoffs contain all the information needed for
a Documenter to create production documentation from developer work.
"""
from dataclasses import dataclass, field
from datetime import UTC, datetime
from uuid import UUID, uuid4
@@ -225,24 +226,29 @@ class DocumenterHandoff(TimestampMixin):
# =============================================================================
def create_handoff(
task_id: UUID,
summary: str,
commits: list[dict[str, str]],
dev_notes_location: str,
new_functionality: list[str] | None = None,
modified_behavior: list[str] | None = None,
breaking_changes: list[str] | None = None,
) -> DocumenterHandoff:
@dataclass
class HandoffParams:
"""Parameters for creating a handoff document."""
task_id: UUID
summary: str
commits: list[dict[str, str]]
dev_notes_location: str
new_functionality: list[str] = field(default_factory=list)
modified_behavior: list[str] = field(default_factory=list)
breaking_changes: list[str] = field(default_factory=list)
def create_handoff(params: HandoffParams) -> DocumenterHandoff:
"""Create a basic handoff document."""
handoff = DocumenterHandoff(
task_id=task_id,
summary=summary,
commits=commits,
dev_notes_location=dev_notes_location,
new_functionality=new_functionality or [],
modified_behavior=modified_behavior or [],
breaking_changes=breaking_changes or [],
task_id=params.task_id,
summary=params.summary,
commits=params.commits,
dev_notes_location=params.dev_notes_location,
new_functionality=params.new_functionality,
modified_behavior=params.modified_behavior,
breaking_changes=params.breaking_changes,
)
# Always add changelog as required
+117 -91
View File
@@ -5,6 +5,7 @@ Personal agent journals for reflection, growth tracking, and debugging.
Each agent maintains their own journal with entries tied to tasks and sessions.
"""
from dataclasses import dataclass, field
from datetime import UTC, datetime
from uuid import UUID, uuid4
@@ -114,170 +115,195 @@ class Journal(TimestampMixin):
# =============================================================================
def create_task_reflection(
journal_id: UUID,
task_id: UUID,
title: str,
what_done: str,
what_learned: str,
what_struggled: str,
next_steps: list[str],
tags: list[str] | None = None,
) -> JournalEntry:
@dataclass
class TaskReflectionParams:
"""Parameters for creating a task reflection entry."""
journal_id: UUID
task_id: UUID
title: str
what_done: str
what_learned: str
what_struggled: str
next_steps: list[str]
tags: list[str] = field(default_factory=list)
@dataclass
class DecisionLogParams:
"""Parameters for creating a decision log entry."""
journal_id: UUID
title: str
context: str
options: list[dict[str, str]]
chosen: str
rationale: str
consequences: list[str]
task_id: UUID | None = None
tags: list[str] = field(default_factory=list)
@dataclass
class LearningEntryParams:
"""Parameters for creating a learning entry."""
journal_id: UUID
title: str
what_learned: str
how_applied: str | None = None
source: str | None = None
task_id: UUID | None = None
tags: list[str] = field(default_factory=list)
@dataclass
class StruggleEntryParams:
"""Parameters for creating a struggle entry."""
journal_id: UUID
title: str
what_struggled: str
attempted_solutions: list[str]
resolution: str | None = None
help_needed: str | None = None
task_id: UUID | None = None
tags: list[str] = field(default_factory=list)
@dataclass
class GeneralEntryParams:
"""Parameters for creating a general journal entry."""
journal_id: UUID
title: str
content: str
task_id: UUID | None = None
session_id: UUID | None = None
tags: list[str] = field(default_factory=list)
is_private: bool = False
def create_task_reflection(params: TaskReflectionParams) -> JournalEntry:
"""Create a task reflection entry."""
content = f"""## What I Did
{what_done}
{params.what_done}
## What I Learned
{what_learned}
{params.what_learned}
## What I Struggled With
{what_struggled}
{params.what_struggled}
## Next Steps
{chr(10).join(f"- [ ] {step}" for step in next_steps)}
{chr(10).join(f"- [ ] {step}" for step in params.next_steps)}
"""
return JournalEntry(
journal_id=journal_id,
journal_id=params.journal_id,
type=JournalEntryType.TASK_REFLECTION,
title=title,
title=params.title,
content=content,
task_id=task_id,
tags=tags or [],
task_id=params.task_id,
tags=params.tags,
)
def create_decision_log(
journal_id: UUID,
title: str,
context: str,
options: list[dict[str, str]],
chosen: str,
rationale: str,
consequences: list[str],
task_id: UUID | None = None,
tags: list[str] | None = None,
) -> JournalEntry:
def create_decision_log(params: DecisionLogParams) -> JournalEntry:
"""Create a decision log entry."""
options_text = ""
for i, opt in enumerate(options, 1):
for i, opt in enumerate(params.options, 1):
options_text += f"\n**Option {i}: {opt.get('name', f'Option {i}')}**\n"
options_text += f"- Pros: {opt.get('pros', 'N/A')}\n"
options_text += f"- Cons: {opt.get('cons', 'N/A')}\n"
content = f"""## Context
{context}
{params.context}
## Options Considered
{options_text}
## Decision
Chose **{chosen}** because {rationale}
Chose **{params.chosen}** because {params.rationale}
## Consequences
{chr(10).join(f"- {c}" for c in consequences)}
{chr(10).join(f"- {c}" for c in params.consequences)}
"""
return JournalEntry(
journal_id=journal_id,
journal_id=params.journal_id,
type=JournalEntryType.DECISION_LOG,
title=title,
title=params.title,
content=content,
task_id=task_id,
tags=tags or [],
task_id=params.task_id,
tags=params.tags,
)
def create_learning_entry(
journal_id: UUID,
title: str,
what_learned: str,
how_applied: str | None = None,
source: str | None = None,
task_id: UUID | None = None,
tags: list[str] | None = None,
) -> JournalEntry:
def create_learning_entry(params: LearningEntryParams) -> JournalEntry:
"""Create a learning entry."""
content = f"""## What I Learned
{what_learned}
{params.what_learned}
"""
if how_applied:
if params.how_applied:
content += f"""
## How I Applied It
{how_applied}
{params.how_applied}
"""
if source:
if params.source:
content += f"""
## Source
{source}
{params.source}
"""
return JournalEntry(
journal_id=journal_id,
journal_id=params.journal_id,
type=JournalEntryType.LEARNING,
title=title,
title=params.title,
content=content,
task_id=task_id,
tags=tags or [],
task_id=params.task_id,
tags=params.tags,
sentiment="positive",
)
def create_struggle_entry(
journal_id: UUID,
title: str,
what_struggled: str,
attempted_solutions: list[str],
resolution: str | None = None,
help_needed: str | None = None,
task_id: UUID | None = None,
tags: list[str] | None = None,
) -> JournalEntry:
def create_struggle_entry(params: StruggleEntryParams) -> JournalEntry:
"""Create a struggle/difficulty entry."""
content = f"""## What I Struggled With
{what_struggled}
{params.what_struggled}
## What I Tried
{chr(10).join(f"- {s}" for s in attempted_solutions)}
{chr(10).join(f"- {s}" for s in params.attempted_solutions)}
"""
if resolution:
if params.resolution:
content += f"""
## Resolution
{resolution}
{params.resolution}
"""
if help_needed:
if params.help_needed:
content += f"""
## Help Needed
{help_needed}
{params.help_needed}
"""
return JournalEntry(
journal_id=journal_id,
journal_id=params.journal_id,
type=JournalEntryType.STRUGGLE,
title=title,
title=params.title,
content=content,
task_id=task_id,
tags=tags or [],
task_id=params.task_id,
tags=params.tags,
sentiment="frustrated",
)
def create_general_entry(
journal_id: UUID,
title: str,
content: str,
task_id: UUID | None = None,
session_id: UUID | None = None,
tags: list[str] | None = None,
is_private: bool = False,
) -> JournalEntry:
def create_general_entry(params: GeneralEntryParams) -> JournalEntry:
"""Create a general journal entry."""
return JournalEntry(
journal_id=journal_id,
journal_id=params.journal_id,
type=JournalEntryType.GENERAL,
title=title,
content=content,
task_id=task_id,
session_id=session_id,
tags=tags or [],
is_private=is_private,
title=params.title,
content=params.content,
task_id=params.task_id,
session_id=params.session_id,
tags=params.tags,
is_private=params.is_private,
)
+54 -83
View File
@@ -29,6 +29,19 @@ logger = structlog.get_logger()
# Maximum length for raw excerpt storage
MAX_EXCERPT_LENGTH = 200
@dataclass
class ExtractionContext:
"""Context for message extraction."""
content: str
agent_id: UUID
channel_id: UUID
session_id: UUID
group_id: UUID
task_id: UUID | None = None
# =============================================================================
# EXTRACTION PATTERNS
# =============================================================================
@@ -202,40 +215,27 @@ class ExtractionService:
# Mention pattern
self._mention_pattern = re.compile(r"@(\w+)")
async def extract(
self,
content: str,
agent_id: UUID,
channel_id: UUID,
session_id: UUID,
group_id: UUID,
task_id: UUID | None = None,
) -> ExtractionResult:
async def extract(self, ctx: ExtractionContext) -> ExtractionResult:
"""
Extract messages from raw content.
Args:
content: Raw LLM output text
agent_id: Agent who produced the content
channel_id: Target channel
session_id: Current session
group_id: Group within channel
task_id: Optional related task
ctx: Extraction context with content and metadata
Returns:
ExtractionResult with extracted messages
"""
if len(content) < self.config.min_content_length:
if len(ctx.content) < self.config.min_content_length:
return ExtractionResult(
messages=[],
raw_content=content,
agent_id=agent_id,
channel_id=channel_id,
session_id=session_id,
raw_content=ctx.content,
agent_id=ctx.agent_id,
channel_id=ctx.channel_id,
session_id=ctx.session_id,
)
# Segment the content
segments = self._segment_content(content)
segments = self._segment_content(ctx.content)
messages: list[ExtractedMessage] = []
pattern_matches: dict[str, list[str]] = {}
@@ -264,15 +264,15 @@ class ExtractionService:
# Create message
message = ExtractedMessage(
id=uuid4(),
agent_id=agent_id,
channel_id=channel_id,
group_id=group_id,
session_id=session_id,
agent_id=ctx.agent_id,
channel_id=ctx.channel_id,
group_id=ctx.group_id,
session_id=ctx.session_id,
type=msg_type,
content=segment.strip(),
content_length=len(segment.strip()),
mentions=mentions,
task_id=task_id,
task_id=ctx.task_id,
confidence=confidence,
raw_excerpt=segment[:MAX_EXCERPT_LENGTH]
if len(segment) > MAX_EXCERPT_LENGTH
@@ -284,17 +284,17 @@ class ExtractionService:
result = ExtractionResult(
messages=messages,
raw_content=content,
agent_id=agent_id,
channel_id=channel_id,
session_id=session_id,
raw_content=ctx.content,
agent_id=ctx.agent_id,
channel_id=ctx.channel_id,
session_id=ctx.session_id,
pattern_matches=pattern_matches,
confidence_scores=confidence_scores,
)
self.log.info(
"Extraction complete",
agent_id=str(agent_id),
agent_id=str(ctx.agent_id),
message_count=result.message_count,
types=result.types_extracted,
)
@@ -365,46 +365,41 @@ class ExtractionService:
return best_type, confidence, matches
async def extract_with_llm(
self,
content: str,
agent_id: UUID,
channel_id: UUID,
session_id: UUID,
group_id: UUID,
task_id: UUID | None = None,
) -> ExtractionResult:
async def extract_with_llm(self, ctx: ExtractionContext) -> ExtractionResult:
"""
Extract messages using LLM classification.
This is more accurate but slower and more expensive.
Falls back to pattern matching if LLM unavailable.
Uses TOON format for token-efficient communication.
"""
from anthropic import AsyncAnthropic
from roboco.config import settings
from roboco.llm import ToonAdapter
toon = ToonAdapter()
try:
client = AsyncAnthropic(api_key=settings.anthropic_api_key)
# Build prompt for LLM classification
# Build prompt for LLM classification using TOON
prompt = f"""Analyze this agent output and classify each distinct segment.
Agent output:
{content}
{ctx.content}
For each segment, identify:
- type: one of [reasoning, dialogue, decision, action, blocker, technical]
- content: the segment text
- confidence: 0.0 to 1.0
Return as JSON array of objects. Example:
[
{{"type": "reasoning", "content": "Analyzing the problem...", "confidence": 0.9}},
{{"type": "action", "content": "Creating file utils.py", "confidence": 0.95}}
]
Return as TOON tabular format:
[N,]{{type,content,confidence}}:
reasoning,Analyzing the problem...,0.9
action,Creating file utils.py,0.95
Output only valid JSON, no other text."""
Output only valid TOON, no other text."""
response = await client.messages.create(
model="claude-3-haiku-20240307", # Fast, cheap for classification
@@ -412,11 +407,9 @@ Output only valid JSON, no other text."""
messages=[{"role": "user", "content": prompt}],
)
# Parse response
import json
# Parse response using TOON (falls back to JSON)
response_text = response.content[0].text
segments = json.loads(response_text)
segments = toon.decode(response_text)
messages: list[ExtractedMessage] = []
for segment in segments:
@@ -430,11 +423,11 @@ Output only valid JSON, no other text."""
id=uuid4(),
content=msg_content,
message_type=msg_type,
agent_id=agent_id,
channel_id=channel_id,
session_id=session_id,
group_id=group_id,
task_id=task_id,
agent_id=ctx.agent_id,
channel_id=ctx.channel_id,
session_id=ctx.session_id,
group_id=ctx.group_id,
task_id=ctx.task_id,
confidence=confidence,
metadata={"extraction_method": "llm"},
)
@@ -442,7 +435,7 @@ Output only valid JSON, no other text."""
return ExtractionResult(
messages=messages,
raw_content=content,
raw_content=ctx.content,
extraction_time=0.0, # Could measure actual time
confidence=sum(m.confidence for m in messages) / max(1, len(messages)),
)
@@ -450,14 +443,7 @@ Output only valid JSON, no other text."""
except Exception as e:
# Fall back to pattern matching
self.log.warning("LLM extraction failed, using patterns", error=str(e))
return await self.extract(
content=content,
agent_id=agent_id,
channel_id=channel_id,
session_id=session_id,
group_id=group_id,
task_id=task_id,
)
return await self.extract(ctx)
# =============================================================================
@@ -496,26 +482,11 @@ class ExtractionPipeline:
"""Register a callback for extracted messages."""
self._message_callbacks.append(callback)
async def process_buffer(
self,
content: str,
agent_id: UUID,
channel_id: UUID,
session_id: UUID,
group_id: UUID,
task_id: UUID | None = None,
) -> ExtractionResult:
async def process_buffer(self, ctx: ExtractionContext) -> ExtractionResult:
"""
Process a buffer and invoke callbacks for each message.
"""
result = await self.extraction.extract(
content=content,
agent_id=agent_id,
channel_id=channel_id,
session_id=session_id,
group_id=group_id,
task_id=task_id,
)
result = await self.extraction.extract(ctx)
# Invoke callbacks for each message
for message in result.messages:
Generated
+18 -7
View File
@@ -1796,7 +1796,7 @@ name = "nvidia-cudnn-cu12"
version = "9.10.2.21"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
{ name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
@@ -1807,7 +1807,7 @@ name = "nvidia-cufft-cu12"
version = "11.3.3.83"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
@@ -1834,9 +1834,9 @@ name = "nvidia-cusolver-cu12"
version = "11.7.3.90"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
dependencies = [
{ name = "nvidia-cublas-cu12" },
{ name = "nvidia-cusparse-cu12" },
{ name = "nvidia-nvjitlink-cu12" },
{ name = "nvidia-cublas-cu12", marker = "sys_platform != 'win32'" },
{ name = "nvidia-cusparse-cu12", marker = "sys_platform != 'win32'" },
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
@@ -1847,7 +1847,7 @@ name = "nvidia-cusparse-cu12"
version = "12.5.8.93"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
dependencies = [
{ name = "nvidia-nvjitlink-cu12" },
{ name = "nvidia-nvjitlink-cu12", marker = "sys_platform != 'win32'" },
]
wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
@@ -2062,7 +2062,7 @@ name = "pexpect"
version = "4.9.0"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
dependencies = [
{ name = "ptyprocess" },
{ name = "ptyprocess", marker = "sys_platform != 'win32'" },
]
sdist = { url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
wheels = [
@@ -2577,6 +2577,15 @@ wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/d9/4f/00be2196329ebbff56ce564aa94efb0fbc828d00de250b1980de1a34ab49/python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba", size = 472788, upload-time = "2024-08-07T17:33:28.192Z" },
]
[[package]]
name = "python-toon"
version = "0.1.3"
source = { registry = "https://pkgs.safetycli.com/repository/renzof/pypi/simple/" }
sdist = { url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/4e/92/640c83ca46d5fe9c49895449a8932f55252537dd13dd22186cbac3a1ce59/python_toon-0.1.3.tar.gz", hash = "sha256:ca348b214c4f1cdad3579fd83dd60032d9eb87eb349c2d430ad9eb6371f174bf", size = 31280, upload-time = "2025-11-04T09:12:20.949Z" }
wheels = [
{ url = "https://pkgs.safetycli.com/package/renzof/pypi/packages/26/a4/2f2def0378b44f913d2d6cb3bc5b1a15267b363937ab1cb9afb07ce2313c/python_toon-0.1.3-py3-none-any.whl", hash = "sha256:a27b0ee4a729e730d1037d0a63eb8b344b3e5a26e3dc9a173067b6c31a868ee6", size = 21797, upload-time = "2025-11-04T09:12:19.443Z" },
]
[[package]]
name = "pytz"
version = "2025.2"
@@ -2781,6 +2790,7 @@ dependencies = [
{ name = "pydantic-settings" },
{ name = "python-jose", extra = ["cryptography"] },
{ name = "python-multipart" },
{ name = "python-toon" },
{ name = "redis" },
{ name = "sqlalchemy", extra = ["asyncio"] },
{ name = "structlog" },
@@ -2842,6 +2852,7 @@ requires-dist = [
{ name = "pytest-xdist", marker = "extra == 'dev'" },
{ name = "python-jose", extras = ["cryptography"] },
{ name = "python-multipart" },
{ name = "python-toon" },
{ name = "redis" },
{ name = "rich", marker = "extra == 'dev'" },
{ name = "ruff", marker = "extra == 'dev'" },