feat: bot-integrations claude skill (honcho for bot frameworks) (#382)

* feat: add nanobot-honcho claude skill

guided integration skill for adding honcho long-term memory to
HKUDS/nanobot instances. includes SKILL.md with step-by-step
instructions and reference implementations for client, session
manager, and agent tool.

* restructure: nanobot-honcho -> bot-integrations skill

Replace one-off nanobot-honcho skill with general bot-integrations skill
targeting the common architectural pattern shared by conversational bot
frameworks (agent loop, session manager, tool registry, message bus).

Structure:
  .claude/skills/bot-integrations/
    SKILL.md                    # adaptive skill for any bot framework
    references/nanobot/         # concrete nanobot implementations

SKILL.md walks through 4 phases (explore, interview, implement, verify)
with awareness of bot frameworks. When it detects a known framework, it
pulls from the matching reference folder for concrete implementations.

Reference files updated with:
- sync flag moved to after API call success
- cache consistency for aliased sessions
- MEMORY.md/HISTORY.md migration support
- migration transcript formatting with XML context tags

Future framework references (openclaw, picoclaw, etc) drop into
references/<framework>/ as they trend.

* fix: merge skills and repair syntax inconsistencies (#385)

* fix: updating docs based on new clawhub skill (#381)

* fix: use ORM mutation for re-embedded vectors in reconciler (#384)

* feat: implement async workspace deletion with active session checks (#378)

* feat: implement async workspace deletion with active session checks

- Updated the DELETE /workspaces/:id endpoint to return 202 Accepted, indicating that the deletion request is processed in the background.
- Added a check for active sessions before allowing workspace deletion, raising a ConflictException if any exist.
- Updated related tests to ensure proper handling of active sessions during workspace deletion.

* fix: Address review issues

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>

* fix: merge skills and repair syntax inconsistencies

---------

Co-authored-by: ajspig <46900795+ajspig@users.noreply.github.com>
Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>

---------

Co-authored-by: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com>
Co-authored-by: ajspig <46900795+ajspig@users.noreply.github.com>
Co-authored-by: Rajat Ahuja <rahuja445@gmail.com>
Co-authored-by: doria <93405247+dr-frmr@users.noreply.github.com>
This commit is contained in:
Eri Barrett 2026-02-16 17:58:42 -05:00 committed by GitHub
parent a6f029e164
commit e0afc386ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1052 additions and 36 deletions

View File

@ -1,12 +1,18 @@
---
name: honcho-integration
description: Integrate Honcho memory and social cognition into existing Python or TypeScript codebases. Use when adding Honcho SDK, setting up peers, configuring sessions, or implementing the dialectic chat endpoint for AI agents.
description: Integrate Honcho memory and social cognition into existing Python or TypeScript codebases. Use when adding Honcho SDK, setting up peers, configuring sessions, implementing the dialectic chat endpoint for AI agents, or wiring Honcho into bot frameworks (nanobot, openclaw, picoclaw, etc).
allowed-tools: Read, Glob, Grep, Bash(uv:*), Bash(bun:*), Bash(npm:*), Edit, Write, WebFetch, AskUserQuestion
---
# Honcho Integration Guide
This skill helps you integrate Honcho into existing Python or TypeScript applications. Honcho provides AI-native memory for stateful agents—it uses custom reasoning models to learn continually.
## What is Honcho
Honcho is an open source memory library for building stateful agents. It works with any model, framework, or architecture. You send Honcho the messages from your conversations, and custom reasoning models process them in the background — extracting premises, drawing conclusions, and building rich representations of each participant over time. Your agent can then query those representations on-demand ("What does this user care about?", "How technical is this person?") and get grounded, reasoned answers.
The key mental model: **Peers** are any participant — human or AI. Both are represented the same way. Observation settings (`observe_me`, `observe_others`) control which peers Honcho reasons about. Typically you want Honcho to model your users (`observe_me=True`) but not your AI assistant (`observe_me=False`). **Sessions** scope conversations between peers. **Messages** are the raw data you feed in — Honcho reasons about them asynchronously and stores the results as the peer's **representation**. No messages means no reasoning means no memory.
Your agent accesses this memory through `peer.chat(query)` (ask a natural language question, get a reasoned answer), `session.context()` (get formatted conversation history + representations), or both.
## Integration Workflow
@ -28,6 +34,8 @@ Use Glob and Grep to find:
- User/session models or types
- API routes handling chat or conversation endpoints
> **Bot framework detected?** If the codebase is built around an agent loop, tool registry, session manager, and message bus (e.g., nanobot, openclaw, picoclaw), read `{baseDir}/references/bot-frameworks.md` for framework-specific integration guidance and check `{baseDir}/references/bot-frameworks/<framework>/` for concrete reference implementations.
### Phase 2: Interview (REQUIRED)
After exploring the codebase, use the **AskUserQuestion** tool to clarify integration requirements. Ask these questions (adapt based on what you learned in Phase 1):
@ -112,6 +120,31 @@ uv add honcho-ai
bun add @honcho-ai/sdk
```
## Sync vs Async
**TypeScript** — The SDK is async by default. All methods return promises. No separate sync API.
**Python** — The SDK provides both sync and async interfaces:
- **Sync** (default): `from honcho import Honcho` — use in sync frameworks (Flask, Django, CLI scripts)
- **Async**: `from honcho import Honcho` with `.aio` namespace — use in async frameworks (FastAPI, Starlette, async workers)
```python
# Sync usage (Flask, Django, scripts)
from honcho import Honcho
honcho = Honcho(workspace_id="my-app", api_key=os.environ["HONCHO_API_KEY"])
peer = honcho.peer("user-123")
response = peer.chat("What does this user prefer?")
# Async usage (FastAPI, Starlette)
from honcho import Honcho
honcho = Honcho(workspace_id="my-app", api_key=os.environ["HONCHO_API_KEY"])
peer = honcho.aio.peer("user-123")
response = await peer.chat("What does this user prefer?")
```
Match the client to the framework — check whether the codebase uses `async def` handlers or sync `def` handlers and choose accordingly. The rest of this skill shows sync Python examples; swap to `.aio` equivalents for async codebases.
## Core Integration Patterns
### 1. Initialize with a Single Workspace
@ -124,11 +157,15 @@ Use ONE workspace for your entire application. The workspace name should reflect
from honcho import Honcho
import os
# Sync client (Flask, Django, scripts)
honcho = Honcho(
workspace_id="your-app-name",
api_key=os.environ["HONCHO_API_KEY"],
environment="production"
)
# Async client (FastAPI, Starlette) — use honcho.aio for all operations
# honcho.aio.peer(), honcho.aio.session(), etc.
```
**TypeScript:**
@ -136,6 +173,7 @@ honcho = Honcho(
```typescript
import { Honcho } from '@honcho-ai/sdk';
// All methods are async by default
const honcho = new Honcho({
workspaceId: "your-app-name",
apiKey: process.env.HONCHO_API_KEY,
@ -150,12 +188,14 @@ Create peers for **every entity** in your business logic - users AND AI assistan
**Python:**
```python
from honcho import PeerConfig
# Human users
user = honcho.peer("user-123")
# AI assistants - set observe_me=False so Honcho doesn't model the AI
assistant = honcho.peer("assistant", config={"observe_me": False})
support_bot = honcho.peer("support-bot", config={"observe_me": False})
assistant = honcho.peer("assistant", configuration=PeerConfig(observe_me=False))
support_bot = honcho.peer("support-bot", configuration=PeerConfig(observe_me=False))
```
**TypeScript:**
@ -164,9 +204,9 @@ support_bot = honcho.peer("support-bot", config={"observe_me": False})
// Human users
const user = await honcho.peer("user-123");
// AI assistants - set observe_me=False
const assistant = await honcho.peer("assistant", { config: { observe_me: false } });
const supportBot = await honcho.peer("support-bot", { config: { observe_me: false } });
// AI assistants - set observeMe=false so Honcho doesn't model the AI
const assistant = await honcho.peer("assistant", { configuration: { observeMe: false } });
const supportBot = await honcho.peer("support-bot", { configuration: { observeMe: false } });
```
### 3. Multi-Peer Sessions
@ -176,7 +216,7 @@ Sessions can have multiple participants. Configure observation settings per-peer
**Python:**
```python
from honcho import SessionPeerConfig
from honcho.api_types import SessionPeerConfig
session = honcho.session("conversation-123")
@ -380,7 +420,7 @@ import openai
session = honcho.session("conversation-123")
user = honcho.peer("user-123")
assistant = honcho.peer("assistant", config={"observe_me": False})
assistant = honcho.peer("assistant", configuration=PeerConfig(observe_me=False))
# Get context formatted for your LLM
context = session.context(
@ -410,17 +450,65 @@ session.add_messages([
])
```
**TypeScript:**
```typescript
import OpenAI from 'openai';
const session = await honcho.session("conversation-123");
const user = await honcho.peer("user-123");
const assistant = await honcho.peer("assistant", { configuration: { observeMe: false } });
// Get context formatted for your LLM
const context = await session.context({
tokens: 2000,
peerTarget: user.id, // Include representation of this user
summary: true // Include conversation summaries
});
// Convert to OpenAI format
const messages = context.toOpenAI(assistant);
// Or Anthropic format
// const messages = context.toAnthropic(assistant);
// Add the new user message
messages.push({ role: "user", content: "What should I focus on today?" });
const openai = new OpenAI();
const response = await openai.chat.completions.create({
model: "gpt-4",
messages
});
// Store the exchange
await session.addMessages([
user.message("What should I focus on today?"),
assistant.message(response.choices[0].message.content!)
]);
```
## Streaming Responses
**Python:**
```python
response_stream = peer.chat("What do we know about this user?", stream=True)
stream = peer.chat_stream("What do we know about this user?")
for chunk in response_stream.iter_text():
for chunk in stream:
print(chunk, end="", flush=True)
```
**TypeScript:**
```typescript
const stream = await peer.chatStream("What do we know about this user?");
for await (const chunk of stream) {
process.stdout.write(chunk);
}
```
## Integration Checklist
When integrating Honcho into an existing codebase:
@ -443,7 +531,7 @@ When integrating Honcho into an existing codebase:
2. **Forgetting AI peers**: Create peers for AI assistants, not just users
3. **Observing AI peers**: Set `observe_me=False` for AI peers unless you specifically want Honcho to model your AI's behavior
4. **Not storing messages**: Always call `add_messages()` to feed Honcho's reasoning engine
5. **Blocking on processing**: Messages are processed asynchronously; use `get_deriver_status()` if you need to wait
5. **Blocking on processing**: Messages are processed asynchronously — don't poll or wait for reasoning to complete before continuing
## Resources

View File

@ -0,0 +1,205 @@
# Honcho Integration for Bot Frameworks
This reference extends the main honcho-integration skill for **bot frameworks** — applications built around an agent loop, session manager, tool registry, and message bus (e.g., nanobot, openclaw, picoclaw).
## Supported Frameworks
When a known framework is detected, use concrete reference implementations from `{baseDir}/references/bot-frameworks/<framework>/`.
| Framework | Status | Reference Dir |
|-----------|--------|---------------|
| [nanobot](https://github.com/HKUDS/nanobot) | concrete references | `bot-frameworks/nanobot/` |
| openclaw | planned | -- |
| picoclaw | planned | -- |
For unknown frameworks, adapt the general pattern below to the codebase's architecture.
## Phase 1: Explore (bot-specific)
In addition to the main skill's Phase 1, identify these bot-specific components:
1. **Agent loop**: Where messages are processed (look for `while` loops calling an LLM)
2. **Session manager**: How conversation history is stored (JSONL files, database, in-memory)
3. **Tool registry**: How tools/functions are registered for the LLM to call
4. **Message bus**: How inbound/outbound messages are routed between channels and the agent
5. **Config system**: How the bot loads configuration (JSON, YAML, env vars, pydantic models, zod schemas)
6. **CLI entry points**: How the bot is started (commands, gateway, agent modes)
If the framework matches a known one (e.g., nanobot), pull the concrete references from `{baseDir}/references/bot-frameworks/<framework>/` and use them as the implementation target.
## Phase 2: Interview (bot-specific)
In addition to the main skill's interview questions, ask about:
- **Peer model**: Who are the participants? (typically: one user peer per channel:chat_id, one shared assistant peer)
- **Session granularity**: One session per chat? Per user? Per channel?
- **Workspace ID**: What namespace for this bot's Honcho data?
- **Feature flag**: Should Honcho be opt-in (default `false`) or opt-out (default `true`)?
## Phase 3: Implement (bot-specific)
### Step 1: Add dependency
**Python:** Add `honcho-ai>=2.0.1`. If the framework supports optional dependencies, make it optional:
```toml
[project.optional-dependencies]
honcho = ["honcho-ai>=2.0.1"]
```
**TypeScript:** Add `@honcho-ai/sdk`:
```bash
bun add @honcho-ai/sdk
# or npm install @honcho-ai/sdk
```
If the framework supports optional peer dependencies:
```json
{
"peerDependencies": {
"@honcho-ai/sdk": ">=2.0.1"
},
"peerDependenciesMeta": {
"@honcho-ai/sdk": { "optional": true }
}
}
```
### Step 2: Add config schema
Add a Honcho config section to the bot's configuration system:
**Python:**
```python
class HonchoConfig(BaseModel):
"""Honcho AI-native memory integration (optional feature flag)."""
enabled: bool = False # or True for Honcho-first deployments
workspace_id: str = "default"
prefetch: bool = True # inject user context into system prompts
context_tokens: int | None = None
environment: str = "production"
```
**TypeScript:**
```typescript
interface HonchoConfig {
/** Honcho AI-native memory integration (optional feature flag). */
enabled: boolean; // default: false, or true for Honcho-first deployments
workspaceId: string; // default: "default"
prefetch: boolean; // default: true — inject user context into system prompts
contextTokens?: number;
environment: string; // default: "production"
}
const defaultHonchoConfig: HonchoConfig = {
enabled: false,
workspaceId: "default",
prefetch: true,
environment: "production",
};
```
### Step 3: Create the honcho package
Create a honcho integration package with:
- **Client singleton** (`client.py` / `client.ts`): Lazy initialization, deferred imports, `getHonchoClient()` factory
- **Session manager** (`session.py` / `session.ts`): Maps bot sessions to Honcho sessions with peer configuration
- **Agent tool** (`honcho_tool.py` / `honchoTool.ts`): Tool the agent can call to query user context via `peer.chat()`
Key patterns (Python):
- `from __future__ import annotations` + `TYPE_CHECKING` for all honcho imports
- Runtime imports inside functions (never top-level) so the bot doesn't crash without `honcho-ai`
- Wrap in `try/except ImportError` for graceful degradation
Key patterns (TypeScript):
- Use dynamic `import()` for honcho SDK (never top-level `import ... from`) so the bot doesn't crash without `@honcho-ai/sdk`
- Use `import type { ... }` for type-only imports that are erased at runtime
- Wrap in `try/catch` for graceful degradation when the SDK is missing
Key patterns (shared):
- IDs sanitized to `^[a-zA-Z0-9_-]+` (Honcho requirement)
- User peer: `observe_me=True, observe_others=True`
- Assistant peer: `observe_me=False, observe_others=True`
If references exist for this framework, use them directly from `{baseDir}/references/bot-frameworks/<framework>/`.
### Step 4: Wire into the agent loop
Add these integration points to the agent loop:
1. **Tool registration** (at startup): If `honcho.enabled` and `HONCHO_API_KEY` set, initialize client + register Honcho tools.
**Python:** Wrap in `try/except ImportError` for graceful degradation.
**TypeScript:** Use dynamic `import()` inside a `try/catch` block.
2. **Context setup** (per message): Set session context on Honcho tools, ensure Honcho session exists.
3. **Prefetch** (per message): Call `session.context()` to get user representation and inject into system prompt before the LLM call.
4. **Sync** (after response): After saving to local session, sync the user+assistant message pair to Honcho.
**Python:**
```python
session.add_messages([
user_peer.message(user_input),
assistant_peer.message(assistant_response),
])
```
**TypeScript:**
```typescript
await session.addMessages([
userPeer.message(userInput),
assistantPeer.message(assistantResponse),
]);
```
5. **Migration** (on first activation): If Honcho session is empty but local session has history, upload prior messages as a file via `session.upload_file()` (Python) or `session.uploadFile()` (TypeScript). Also upload `MEMORY.md` and `HISTORY.md` if they exist (from frameworks with local memory consolidation). Archive originals after successful upload.
### Step 5: Pass config through CLI
Pass `honcho_config` to every agent loop instantiation in the CLI commands.
### Step 6: Migration support
When Honcho activates on an instance with existing local data, migrate automatically:
- **Session messages** (JSONL files): Format as XML transcript, upload via `session.upload_file()` (Python) or `session.uploadFile()` (TypeScript)
- **Consolidated memory** (MEMORY.md, HISTORY.md): Upload as tagged files with context annotations
- **Archive originals**: Move to `migrated/` subdirectory after successful upload
- **Idempotent**: Skip if Honcho session already has messages
## Phase 4: Verify (bot-specific)
After integration, verify:
- [ ] Bot starts normally without the Honcho SDK installed (no import errors)
- [ ] Bot starts normally with the SDK but without `HONCHO_API_KEY` (graceful skip)
- [ ] With both present and `enabled=true`, logs show "Honcho tools registered"
- [ ] User context is prefetched and visible in system prompts
- [ ] Messages sync to Honcho after each exchange
- [ ] Local session migration works on first Honcho activation
- [ ] Memory file migration works for MEMORY.md/HISTORY.md (if applicable)
## Bot-Specific Patterns
- **Lazy imports everywhere**:
- Python: `from __future__ import annotations` + `TYPE_CHECKING` for type hints, runtime imports inside functions
- TypeScript: `import type { ... }` for type-only imports, dynamic `import()` for runtime access
- **Feature flag gating**: Always check `config.enabled` AND `HONCHO_API_KEY` / `process.env.HONCHO_API_KEY` before touching Honcho
- **Graceful degradation**:
- Python: `try/except ImportError` and generic `Exception` catches with logger warnings, never crash the bot
- TypeScript: `try/catch` around dynamic `import()` with logger warnings, never crash the bot
- **Sanitize IDs**: Honcho requires `^[a-zA-Z0-9_-]+` — replace colons, dots, spaces with dashes
- **Sync after success**: Only mark messages as synced after the API call succeeds, not before
- **Cache consistency**: When creating aliased sessions, store under both original and derived keys

View File

@ -0,0 +1,85 @@
"""Honcho client initialization and configuration."""
from __future__ import annotations
import os
from dataclasses import dataclass
from typing import TYPE_CHECKING
from loguru import logger
if TYPE_CHECKING:
from honcho import Honcho
@dataclass
class HonchoConfig:
"""Configuration for Honcho client."""
workspace_id: str = "nanobot"
api_key: str | None = None
environment: str = "production"
@classmethod
def from_env(cls, workspace_id: str = "nanobot") -> HonchoConfig:
"""Create config from environment variables."""
return cls(
workspace_id=workspace_id,
api_key=os.environ.get("HONCHO_API_KEY"),
environment=os.environ.get("HONCHO_ENVIRONMENT", "production"),
)
_honcho_client: Honcho | None = None
def get_honcho_client(config: HonchoConfig | None = None) -> Honcho:
"""
Get or create the Honcho client singleton.
Args:
config: Optional config. If not provided, uses environment variables.
Returns:
Configured Honcho client.
Raises:
ValueError: If HONCHO_API_KEY is not set.
"""
global _honcho_client
if _honcho_client is not None:
return _honcho_client
if config is None:
config = HonchoConfig.from_env()
if not config.api_key:
raise ValueError(
"HONCHO_API_KEY environment variable is required. "
"Get an API key from https://app.honcho.dev"
)
try:
from honcho import Honcho
except ImportError:
raise ImportError(
"honcho-ai is required for Honcho integration. "
"Install it with: nanobot honcho enable --api-key YOUR_KEY"
)
logger.info(f"Initializing Honcho client (workspace: {config.workspace_id})")
_honcho_client = Honcho(
workspace_id=config.workspace_id,
api_key=config.api_key,
environment=config.environment,
)
return _honcho_client
def reset_honcho_client() -> None:
"""Reset the Honcho client singleton (useful for testing)."""
global _honcho_client
_honcho_client = None

View File

@ -0,0 +1,86 @@
"""Honcho tool for querying user context."""
from typing import Any
from nanobot.agent.tools.base import Tool
class HonchoTool(Tool):
"""
Tool for querying Honcho's AI-native memory.
Allows the agent to retrieve relevant context about users
based on their history and learned preferences.
"""
def __init__(self, session_manager: "HonchoSessionManager"):
"""
Initialize the Honcho tool.
Args:
session_manager: The HonchoSessionManager instance.
"""
self._session_manager = session_manager
self._current_session_key: str | None = None
@property
def name(self) -> str:
return "query_user_context"
@property
def description(self) -> str:
return (
"Query Honcho to retrieve relevant context about the user based on their "
"history and preferences. Use this when you need to understand the user's "
"background, preferences, past interactions, or goals. This helps you "
"personalize your responses and provide more relevant assistance."
)
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": (
"A natural language question about the user. Examples: "
"'What are this user's main goals?', "
"'What communication style does this user prefer?', "
"'What topics has this user discussed recently?', "
"'What is this user's technical expertise level?'"
),
}
},
"required": ["query"],
}
def set_context(self, session_key: str) -> None:
"""
Set the current session context.
Args:
session_key: The session key (channel:chat_id).
"""
self._current_session_key = session_key
async def execute(self, query: str) -> str:
"""
Execute the Honcho context query.
Args:
query: Natural language question about the user.
Returns:
Honcho's response about the user.
"""
if not self._current_session_key:
return "Error: No session context set. Unable to query user information."
try:
result = self._session_manager.get_user_context(
self._current_session_key, query
)
return result
except Exception as e:
return f"Error querying user context: {str(e)}"

View File

@ -0,0 +1,576 @@
"""Honcho-based session management for conversation history."""
from __future__ import annotations
import re
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any, TYPE_CHECKING
from loguru import logger
from nanobot.honcho.client import get_honcho_client
if TYPE_CHECKING:
from honcho import Honcho
from honcho.session import SessionPeerConfig
@dataclass
class HonchoSession:
"""
A conversation session backed by Honcho.
Provides the same interface as the original Session class
but stores messages in Honcho for AI-native memory.
"""
key: str # channel:chat_id
user_peer_id: str # Honcho peer ID for the user
assistant_peer_id: str # Honcho peer ID for the assistant
honcho_session_id: str # Honcho session ID
messages: list[dict[str, Any]] = field(default_factory=list)
created_at: datetime = field(default_factory=datetime.now)
updated_at: datetime = field(default_factory=datetime.now)
metadata: dict[str, Any] = field(default_factory=dict)
def add_message(self, role: str, content: str, **kwargs: Any) -> None:
"""Add a message to the local cache."""
msg = {
"role": role,
"content": content,
"timestamp": datetime.now().isoformat(),
**kwargs,
}
self.messages.append(msg)
self.updated_at = datetime.now()
def get_history(self, max_messages: int = 50) -> list[dict[str, Any]]:
"""
Get message history for LLM context.
Args:
max_messages: Maximum messages to return.
Returns:
List of messages in LLM format.
"""
recent = (
self.messages[-max_messages:]
if len(self.messages) > max_messages
else self.messages
)
return [{"role": m["role"], "content": m["content"]} for m in recent]
def clear(self) -> None:
"""Clear all messages in the session."""
self.messages = []
self.updated_at = datetime.now()
class HonchoSessionManager:
"""
Manages conversation sessions using Honcho.
Replaces the file-based SessionManager with Honcho's
AI-native memory system for user modeling.
"""
def __init__(self, honcho: Honcho | None = None, context_tokens: int | None = None):
"""
Initialize the session manager.
Args:
honcho: Optional Honcho client. If not provided, uses the singleton.
context_tokens: Max tokens for context() calls (None = Honcho default).
"""
self._honcho = honcho
self._context_tokens = context_tokens
self._cache: dict[str, HonchoSession] = {}
self._peers_cache: dict[str, Any] = {}
self._sessions_cache: dict[str, Any] = {}
@property
def honcho(self) -> Honcho:
"""Get the Honcho client, initializing if needed."""
if self._honcho is None:
self._honcho = get_honcho_client()
return self._honcho
def _get_or_create_peer(self, peer_id: str) -> Any:
"""
Get or create a Honcho peer.
Peers are lazy -- no API call until first use.
Observation settings are controlled per-session via SessionPeerConfig.
Args:
peer_id: The peer identifier.
Returns:
The Honcho peer object.
"""
if peer_id in self._peers_cache:
return self._peers_cache[peer_id]
peer = self.honcho.peer(peer_id)
self._peers_cache[peer_id] = peer
return peer
def _get_or_create_honcho_session(
self, session_id: str, user_peer: Any, assistant_peer: Any
) -> Any:
"""
Get or create a Honcho session with peers configured.
Args:
session_id: The session identifier.
user_peer: The user peer object.
assistant_peer: The assistant peer object.
Returns:
The Honcho session object.
"""
if session_id in self._sessions_cache:
logger.debug(f"Honcho session '{session_id}' retrieved from cache")
return self._sessions_cache[session_id], []
session = self.honcho.session(session_id)
# Configure peer observation settings
from honcho.session import SessionPeerConfig
user_config = SessionPeerConfig(observe_me=True, observe_others=True)
ai_config = SessionPeerConfig(observe_me=False, observe_others=True)
session.add_peers([(user_peer, user_config), (assistant_peer, ai_config)])
# Load existing messages via context() - single call for messages + metadata
existing_messages = []
try:
ctx = session.context(summary=True, tokens=self._context_tokens)
existing_messages = ctx.messages or []
# Verify chronological ordering
if existing_messages and len(existing_messages) > 1:
timestamps = [m.created_at for m in existing_messages if m.created_at]
if timestamps and timestamps != sorted(timestamps):
logger.warning(
f"Honcho messages not chronologically ordered for session '{session_id}', sorting"
)
existing_messages = sorted(
existing_messages,
key=lambda m: m.created_at or datetime.min,
)
if existing_messages:
logger.info(f"Honcho session '{session_id}' retrieved ({len(existing_messages)} existing messages)")
else:
logger.info(f"Honcho session '{session_id}' created (new)")
except Exception as e:
logger.warning(f"Honcho session '{session_id}' loaded (failed to fetch context: {e})")
self._sessions_cache[session_id] = session
return session, existing_messages
def _sanitize_id(self, id_str: str) -> str:
"""Sanitize an ID to match Honcho's pattern: ^[a-zA-Z0-9_-]+"""
return re.sub(r'[^a-zA-Z0-9_-]', '-', id_str)
def get_or_create(self, key: str) -> HonchoSession:
"""
Get an existing session or create a new one.
Args:
key: Session key (usually channel:chat_id).
Returns:
The session.
"""
if key in self._cache:
logger.debug(f"Local session cache hit: {key}")
return self._cache[key]
# Parse key to extract user identifier
# Format: channel:chat_id (e.g., "telegram:123456789")
parts = key.split(":", 1)
channel = parts[0] if len(parts) > 1 else "default"
chat_id = parts[1] if len(parts) > 1 else key
# Create peer IDs (sanitized for Honcho's ID pattern)
user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}")
assistant_peer_id = "nanobot-assistant"
# Sanitize session ID for Honcho
honcho_session_id = self._sanitize_id(key)
# Get or create peers
user_peer = self._get_or_create_peer(user_peer_id)
assistant_peer = self._get_or_create_peer(assistant_peer_id)
# Get or create Honcho session
honcho_session, existing_messages = self._get_or_create_honcho_session(
honcho_session_id, user_peer, assistant_peer
)
# Convert Honcho messages to local format
local_messages = []
for msg in existing_messages:
role = "assistant" if msg.peer_id == assistant_peer_id else "user"
local_messages.append({
"role": role,
"content": msg.content,
"timestamp": msg.created_at.isoformat() if msg.created_at else "",
"_synced": True, # Already in Honcho
})
# Create local session wrapper with existing messages
session = HonchoSession(
key=key,
user_peer_id=user_peer_id,
assistant_peer_id=assistant_peer_id,
honcho_session_id=honcho_session_id,
messages=local_messages,
)
self._cache[key] = session
return session
def save(self, session: HonchoSession) -> None:
"""
Save messages to Honcho.
This syncs the local message cache to Honcho's storage.
Args:
session: The session to save.
"""
if not session.messages:
return
# Get the Honcho session and peers
user_peer = self._get_or_create_peer(session.user_peer_id)
assistant_peer = self._get_or_create_peer(session.assistant_peer_id)
honcho_session = self._sessions_cache.get(session.honcho_session_id)
if not honcho_session:
honcho_session, _ = self._get_or_create_honcho_session(
session.honcho_session_id, user_peer, assistant_peer
)
# Convert messages to Honcho format and send
# Only send new messages (those without a 'synced' flag)
new_messages = [m for m in session.messages if not m.get("_synced")]
if not new_messages:
return
honcho_messages = []
for msg in new_messages:
peer = user_peer if msg["role"] == "user" else assistant_peer
honcho_messages.append(peer.message(msg["content"]))
try:
honcho_session.add_messages(honcho_messages)
for msg in new_messages:
msg["_synced"] = True
logger.debug(f"Synced {len(honcho_messages)} messages to Honcho for {session.key}")
except Exception as e:
for msg in new_messages:
msg["_synced"] = False
logger.error(f"Failed to sync messages to Honcho: {e}")
# Update cache
self._cache[session.key] = session
def delete(self, key: str) -> bool:
"""
Delete a session from local cache.
Args:
key: Session key.
Returns:
True if deleted from cache, False if not found.
"""
if key in self._cache:
del self._cache[key]
return True
return False
def new_session(self, key: str) -> HonchoSession:
"""
Create a new session, preserving the old one for user modeling.
This creates a fresh session with a new ID while keeping the old
session's data in Honcho for continued user modeling.
Args:
key: Original session key (e.g., "discord:123456").
Returns:
A fresh HonchoSession with no message history.
"""
import time
# Remove old session from caches (but don't delete from Honcho)
old_session = self._cache.pop(key, None)
if old_session:
self._sessions_cache.pop(old_session.honcho_session_id, None)
# Create new session with timestamp suffix
# This preserves old session in Honcho while starting fresh
timestamp = int(time.time())
new_key = f"{key}:{timestamp}"
# Get or create will create a fresh session
session = self.get_or_create(new_key)
# Cache under both original key (for future lookups) and timestamped
# key (so session.key matches a valid cache entry)
self._cache[key] = session
self._cache[new_key] = session
logger.info(f"Created new session for {key} (honcho: {session.honcho_session_id})")
return session
def get_user_context(self, session_key: str, query: str) -> str:
"""
Query Honcho's dialectic chat for user context.
Args:
session_key: The session key to get context for.
query: Natural language question about the user.
Returns:
Honcho's response about the user.
"""
session = self._cache.get(session_key)
if not session:
return "No session found for this context."
user_peer = self._get_or_create_peer(session.user_peer_id)
try:
return user_peer.chat(query)
except Exception as e:
logger.error(f"Failed to get user context from Honcho: {e}")
return f"Unable to retrieve user context: {e}"
def get_prefetch_context(self, session_key: str, user_message: str | None = None) -> dict[str, str]:
"""
Pre-fetch user context using Honcho's context() method.
This is a single API call that returns the user's representation
and peer card, using semantic search based on the user's message.
Args:
session_key: The session key to get context for.
user_message: The user's message for semantic search.
Returns:
Dictionary with 'representation' and 'card' keys.
"""
session = self._cache.get(session_key)
if not session:
return {}
honcho_session = self._sessions_cache.get(session.honcho_session_id)
if not honcho_session:
return {}
try:
# Single API call to get user representation with semantic search
ctx = honcho_session.context(
summary=False,
tokens=self._context_tokens,
peer_target=session.user_peer_id,
search_query=user_message,
)
# peer_card is list[str] in SDK v2, join for prompt injection
card = ctx.peer_card or []
card_str = "\n".join(card) if isinstance(card, list) else str(card)
return {
"representation": ctx.peer_representation or "",
"card": card_str,
}
except Exception as e:
logger.warning(f"Failed to fetch context from Honcho: {e}")
return {}
def migrate_local_history(self, session_key: str, messages: list[dict[str, Any]]) -> bool:
"""
Upload local session history to Honcho as a file.
Used when Honcho activates mid-conversation to preserve prior context.
Args:
session_key: The session key (e.g., "telegram:123456").
messages: Local messages (dicts with role, content, timestamp).
Returns:
True if upload succeeded, False otherwise.
"""
sanitized = self._sanitize_id(session_key)
honcho_session = self._sessions_cache.get(sanitized)
if not honcho_session:
logger.warning(f"No Honcho session cached for '{session_key}', skipping migration")
return False
# Resolve user peer for attribution
parts = session_key.split(":", 1)
channel = parts[0] if len(parts) > 1 else "default"
chat_id = parts[1] if len(parts) > 1 else session_key
user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}")
user_peer = self._peers_cache.get(user_peer_id)
if not user_peer:
logger.warning(f"No user peer cached for '{user_peer_id}', skipping migration")
return False
content_bytes = self._format_migration_transcript(session_key, messages)
first_ts = messages[0].get("timestamp") if messages else None
try:
honcho_session.upload_file(
file=("prior_history.txt", content_bytes, "text/plain"),
peer=user_peer,
metadata={"source": "local_jsonl", "count": len(messages)},
created_at=first_ts,
)
logger.info(f"Migrated {len(messages)} local messages to Honcho for {session_key}")
return True
except Exception as e:
logger.error(f"Failed to upload local history to Honcho for {session_key}: {e}")
return False
@staticmethod
def _format_migration_transcript(session_key: str, messages: list[dict[str, Any]]) -> bytes:
"""
Format local messages as an XML transcript for Honcho file upload.
Args:
session_key: The session key for metadata.
messages: Local messages (dicts with role, content, timestamp).
Returns:
UTF-8 encoded transcript bytes.
"""
timestamps = [m.get("timestamp", "") for m in messages]
time_range = f"{timestamps[0]} to {timestamps[-1]}" if timestamps else "unknown"
lines = [
"<prior_conversation_history>",
"<context>",
"This conversation history occurred BEFORE the Honcho memory system was activated.",
"These messages are the preceding elements of this conversation session and should",
"be treated as foundational context for all subsequent interactions. The user and",
"assistant have already established rapport through these exchanges.",
"</context>",
"",
f'<transcript session_key="{session_key}" message_count="{len(messages)}"',
f' time_range="{time_range}">',
"",
]
for msg in messages:
ts = msg.get("timestamp", "?")
role = msg.get("role", "unknown")
content = msg.get("content", "")
lines.append(f"[{ts}] {role}: {content}")
lines.append("")
lines.append("</transcript>")
lines.append("</prior_conversation_history>")
return "\n".join(lines).encode("utf-8")
def migrate_memory_files(self, session_key: str, workspace: Any) -> bool:
"""
Upload workspace/memory/MEMORY.md and HISTORY.md to Honcho as files.
Used when Honcho activates on an instance that already has locally
consolidated memory (from upstream's _consolidate_memory). Backwards
compatible -- skips gracefully if files don't exist.
Args:
session_key: The session key to associate files with.
workspace: Path to the workspace directory.
Returns:
True if at least one file was uploaded, False otherwise.
"""
from pathlib import Path
workspace = Path(workspace)
memory_dir = workspace / "memory"
if not memory_dir.exists():
return False
sanitized = self._sanitize_id(session_key)
honcho_session = self._sessions_cache.get(sanitized)
if not honcho_session:
logger.warning(f"No Honcho session cached for '{session_key}', skipping memory migration")
return False
# Resolve user peer for attribution
parts = session_key.split(":", 1)
channel = parts[0] if len(parts) > 1 else "default"
chat_id = parts[1] if len(parts) > 1 else session_key
user_peer_id = self._sanitize_id(f"user-{channel}-{chat_id}")
user_peer = self._peers_cache.get(user_peer_id)
if not user_peer:
logger.warning(f"No user peer cached for '{user_peer_id}', skipping memory migration")
return False
uploaded = False
files = [
("MEMORY.md", "consolidated_memory.md", "Long-term user facts and preferences"),
("HISTORY.md", "conversation_history.md", "Chronological conversation summaries"),
]
for filename, upload_name, description in files:
filepath = memory_dir / filename
if not filepath.exists():
continue
content = filepath.read_text(encoding="utf-8").strip()
if not content:
continue
wrapped = (
f"<prior_memory_file>\n"
f"<context>\n"
f"This file was consolidated from local conversations BEFORE Honcho was activated.\n"
f"{description}. Treat as foundational context for this user.\n"
f"</context>\n"
f"\n"
f"{content}\n"
f"</prior_memory_file>\n"
)
try:
honcho_session.upload_file(
file=(upload_name, wrapped.encode("utf-8"), "text/plain"),
peer=user_peer,
metadata={"source": "local_memory", "original_file": filename},
)
logger.info(f"Uploaded {filename} to Honcho for {session_key}")
uploaded = True
except Exception as e:
logger.error(f"Failed to upload {filename} to Honcho: {e}")
return uploaded
def list_sessions(self) -> list[dict[str, Any]]:
"""
List all cached sessions.
Returns:
List of session info dicts.
"""
return [
{
"key": s.key,
"created_at": s.created_at.isoformat(),
"updated_at": s.updated_at.isoformat(),
"message_count": len(s.messages),
}
for s in self._cache.values()
]

View File

@ -417,18 +417,6 @@
}
]
},
{
"tab": "Changelog",
"groups": [
{
"group": "Overview",
"pages": [
"changelog/introduction",
"changelog/compatibility-guide"
]
}
]
},
{
"tab": "Contributing",
"groups": [
@ -591,18 +579,6 @@
]
}
]
},
{
"tab": "Changelog",
"groups": [
{
"group": "Overview",
"pages": [
"changelog/introduction",
"changelog/compatibility-guide"
]
}
]
}
]
}