diff --git a/.gitignore b/.gitignore
index 8b377089..c3374209 100644
--- a/.gitignore
+++ b/.gitignore
@@ -181,3 +181,6 @@ timing_logs.csv
config.toml
.aider*
+
+CRUSH.md
+.crush/
diff --git a/CLAUDE.md b/CLAUDE.md
index 803c285f..4adce6ac 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,3 +1,7 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
# Honcho Overview
## What is Honcho?
@@ -100,28 +104,64 @@ All API routes follow the pattern: `/v1/{resource}/{id}/{action}`
```
src/
-├── main.py # FastAPI app setup with middleware and exception handlers
-├── models.py # SQLAlchemy ORM models with proper type annotations
-├── schemas.py # Pydantic validation schemas for API
-├── crud.py # Database operations
-├── dependencies.py # Dependency injection (DB sessions)
-├── exceptions.py # Custom exception types
-├── security.py # JWT authentication
-├── agent.py # Dialectic API implementation
-├── routers/ # API endpoints
+├── main.py # FastAPI app setup with middleware and exception handlers
+├── models.py # SQLAlchemy ORM models with proper type annotations
+├── schemas.py # Pydantic validation schemas for API
+├── config.py # Configuration management
+├── db.py # Database connection and session management
+├── dependencies.py # Dependency injection (DB sessions)
+├── exceptions.py # Custom exception types
+├── security.py # JWT authentication
+├── embedding_client.py # Embedding service client
+├── crud/ # Database operations
+│ ├── __init__.py
+│ ├── collection.py # Collection CRUD operations
+│ ├── deriver.py # Deriver-related CRUD operations
+│ ├── document.py # Document CRUD operations
+│ ├── message.py # Message CRUD operations
+│ ├── peer.py # Peer CRUD operations
+│ ├── representation.py # Representation CRUD operations
+│ ├── session.py # Session CRUD operations
+│ ├── webhook.py # Webhook CRUD operations
+│ └── workspace.py # Workspace CRUD operations
+├── dialectic/ # Dialectic API implementation
+│ ├── __init__.py
+│ ├── chat.py # Chat functionality
+│ ├── prompts.py # Prompt templates
+│ └── utils.py # Dialectic utilities
+├── routers/ # API endpoints
│ ├── workspaces.py
│ ├── peers.py
│ ├── sessions.py
│ ├── messages.py
-│ └── keys.py
-├── deriver/ # Background processing system
-│ ├── consumer.py # Message processing logic
-│ ├── queue.py # Queue management
-│ └── tom/ # Theory of Mind implementations
-└── utils/ # Utilities
- ├── history.py # Session history management
- ├── cache.py # Caching utilities
- └── model_client.py # LLM client abstraction
+│ ├── keys.py
+│ └── webhooks.py # Webhook endpoints
+├── deriver/ # Background processing system
+│ ├── __init__.py
+│ ├── __main__.py # Deriver entry point
+│ ├── consumer.py # Message consumer
+│ ├── deriver.py # Main deriver logic
+│ ├── enqueue.py # Queue operations
+│ ├── prompts.py # Deriver prompts
+│ ├── queue_manager.py # Queue management
+│ ├── queue_payload.py # Queue payload schemas
+│ └── utils.py # Deriver utilities
+├── utils/ # Utilities
+│ ├── __init__.py
+│ ├── clients.py # LLM client abstraction
+│ ├── embedding_store.py # Vector storage management
+│ ├── files.py # File handling utilities
+│ ├── filter.py # Query filtering utilities
+│ ├── formatting.py # Message formatting utilities
+│ ├── logging.py # Logging configuration
+│ ├── search.py # Search functionality
+│ ├── shared_models.py # Shared data models
+│ ├── summarizer.py # Session summarization
+│ └── types.py # Type definitions
+└── webhooks/ # Webhook system
+ ├── events.py # Webhook event definitions
+ ├── webhook_delivery.py # Webhook delivery logic
+ └── README.md # Webhook documentation
```
- Tests in pytest with fixtures in tests/conftest.py
diff --git a/src/config.py b/src/config.py
index 3f7ea425..24b64763 100644
--- a/src/config.py
+++ b/src/config.py
@@ -194,6 +194,11 @@ class DeriverSettings(HonchoSettings):
# Thinking budget tokens are only applied when using Anthropic as provider
THINKING_BUDGET_TOKENS: Annotated[int, Field(default=1024, gt=0, le=5000)] = 1024
+ # Context token limit for get_context method
+ CONTEXT_TOKEN_LIMIT: Annotated[int, Field(default=30_000, gt=1000, le=100_000)] = (
+ 30_000
+ )
+
class DialecticSettings(HonchoSettings):
model_config = SettingsConfigDict(env_prefix="DIALECTIC_", extra="ignore") # pyright: ignore
diff --git a/src/deriver/deriver.py b/src/deriver/deriver.py
index 48d92ae5..73597457 100644
--- a/src/deriver/deriver.py
+++ b/src/deriver/deriver.py
@@ -169,12 +169,14 @@ class Deriver:
# created_at is now always a datetime object from Pydantic validation
message_dt_obj = created_at
- formatted_history = await summarizer.get_summarized_history(
+ # Use get_session_context_formatted with configurable token limit
+ formatted_history = await summarizer.get_session_context_formatted(
db,
workspace_name,
session_name,
+ token_limit=settings.DERIVER.CONTEXT_TOKEN_LIMIT,
cutoff=message_id,
- summary_type=summarizer.SummaryType.SHORT,
+ include_summary=True,
)
# instantiate embedding store from collection
diff --git a/src/routers/sessions.py b/src/routers/sessions.py
index f93b7f04..e8fbbf29 100644
--- a/src/routers/sessions.py
+++ b/src/routers/sessions.py
@@ -388,56 +388,13 @@ async def get_session_context(
"""
token_limit = tokens or config.settings.GET_CONTEXT_MAX_TOKENS
- summary_content = ""
- messages_tokens = token_limit
- messages_start_id = 0
-
- if summary:
- summary_tokens_limit = token_limit * 0.4
-
- latest_short_summary, latest_long_summary = await summarizer.get_both_summaries(
- db,
- workspace_name=workspace_id,
- session_name=session_id,
- )
-
- long_len = latest_long_summary["token_count"] if latest_long_summary else 0
- short_len = latest_short_summary["token_count"] if latest_short_summary else 0
-
- # The goal is to return the longest summary that fits within the token limit
- # Sometimes (rarely) the short summary can be longer than the long summary,
- # so we need to check for that and return the longer one.
-
- if (
- latest_long_summary
- and long_len <= summary_tokens_limit
- and long_len > short_len
- ):
- summary_content = latest_long_summary["content"]
- messages_tokens = token_limit - latest_long_summary["token_count"]
- messages_start_id = latest_long_summary["message_id"]
- elif (
- latest_short_summary and short_len <= summary_tokens_limit and short_len > 0
- ):
- summary_content = latest_short_summary["content"]
- messages_tokens = token_limit - latest_short_summary["token_count"]
- messages_start_id = latest_short_summary["message_id"]
- else:
- logger.info(
- "No summary available for get_context call with token limit %s, returning empty string. long_summary_len: %s, short_summary_len: %s",
- token_limit,
- long_len,
- short_len,
- )
- summary_content = ""
-
- # Get the recent messages after summary to return verbatim
- messages = await crud.get_messages_id_range(
+ # Use the shared get_session_context function from summarizer
+ summary_content, messages = await summarizer.get_session_context(
db,
workspace_name=workspace_id,
session_name=session_id,
- start_id=messages_start_id,
- token_limit=messages_tokens,
+ token_limit=token_limit,
+ include_summary=summary,
)
return schemas.SessionContext(
diff --git a/src/utils/summarizer.py b/src/utils/summarizer.py
index e90547ae..352dec23 100644
--- a/src/utils/summarizer.py
+++ b/src/utils/summarizer.py
@@ -45,6 +45,8 @@ __all__ = [
"get_summary",
"get_both_summaries",
"get_summarized_history",
+ "get_session_context",
+ "get_session_context_formatted",
"SummaryType",
"Summary",
]
@@ -554,6 +556,126 @@ async def get_both_summaries(
return summaries.get(SummaryType.SHORT.value), summaries.get(SummaryType.LONG.value)
+async def get_session_context(
+ db: AsyncSession,
+ workspace_name: str,
+ session_name: str,
+ token_limit: int,
+ *,
+ cutoff: int | None = None,
+ include_summary: bool = True,
+) -> tuple[str, list[models.Message]]:
+ """
+ Get session context similar to the API endpoint but for internal use.
+
+ Args:
+ db: Database session
+ workspace_name: The workspace name
+ session_name: The session name
+ token_limit: Maximum tokens for the context
+ cutoff: Optional message ID to stop at (exclusive)
+ include_summary: Whether to include summary if available
+
+ Returns:
+ Tuple of (summary_content, messages) where summary_content is the summary text (or empty string)
+ and messages is the list of message objects
+ """
+ summary_content = ""
+ messages_tokens = token_limit
+ messages_start_id = 0
+
+ if include_summary:
+ # Allocate 40% of tokens to summary, 60% to messages
+ summary_tokens_limit = int(token_limit * 0.4)
+
+ latest_short_summary, latest_long_summary = await get_both_summaries(
+ db, workspace_name, session_name
+ )
+
+ long_len = latest_long_summary["token_count"] if latest_long_summary else 0
+ short_len = latest_short_summary["token_count"] if latest_short_summary else 0
+
+ # Return the longest summary that fits within the token limit
+ if (
+ latest_long_summary
+ and long_len <= summary_tokens_limit
+ and long_len > short_len
+ ):
+ summary_content = latest_long_summary["content"]
+ messages_tokens = token_limit - latest_long_summary["token_count"]
+ messages_start_id = latest_long_summary["message_id"]
+ elif (
+ latest_short_summary and short_len <= summary_tokens_limit and short_len > 0
+ ):
+ summary_content = latest_short_summary["content"]
+ messages_tokens = token_limit - latest_short_summary["token_count"]
+ messages_start_id = latest_short_summary["message_id"]
+ else:
+ logger.warning(
+ "No summary available for get_context call with token limit %s, returning empty string. long_summary_len: %s, short_summary_len: %s",
+ token_limit,
+ long_len,
+ short_len,
+ )
+
+ # Get recent messages after summary
+ messages = await crud.get_messages_id_range(
+ db,
+ workspace_name,
+ session_name,
+ start_id=messages_start_id,
+ end_id=cutoff,
+ token_limit=messages_tokens,
+ )
+
+ return summary_content, messages
+
+
+async def get_session_context_formatted(
+ db: AsyncSession,
+ workspace_name: str,
+ session_name: str,
+ token_limit: int,
+ *,
+ cutoff: int | None = None,
+ include_summary: bool = True,
+) -> str:
+ """
+ Get formatted session context as a string for internal use (e.g., deriver).
+
+ This is a convenience wrapper around get_session_context that formats
+ the output as a string.
+ """
+ summary_content, messages = await get_session_context(
+ db,
+ workspace_name,
+ session_name,
+ token_limit,
+ cutoff=cutoff,
+ include_summary=include_summary,
+ )
+
+ # Format the messages
+ messages_text = _format_messages(messages)
+
+ if summary_content and messages_text:
+ return f"""
+{summary_content}
+
+
+
+{messages_text}
+"""
+ elif summary_content:
+ return f"""
+{summary_content}
+"""
+ elif messages_text:
+ return messages_text
+ else:
+ return ""
+
+
def _format_messages(messages: list[models.Message]) -> str:
"""
Format a list of messages into a string by concatenating their content and