Reviewed Honcho's ORM and agent pipeline.
PlanMapped database tables to inference invocation points.
PlanDescribed core pairwise memory system and key models.
PlanEnvironment Context
cwd: /Users/me/c/vendor/honcho
shell: zsh
current date: 2026-05-17
timezone: America/TorontoExplain Honcho's data model from the ground up. I notice that it makes inference requests for a number of different facets. What does it need inference for?
I’ll trace this from the actual ORM and agent pipeline rather than only the overview text, then map the tables to where inference is invoked.
>rg "class .*\(.*Base|class .*\(Base|__tablename__|Mapped\[" src/models.py src -g'*.py'Chunk ID: b31992
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 5594
Output:
src/models.py:class Workspace(Base):
src/models.py: __tablename__: str = "workspaces"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: name: Mapped[str] = mapped_column(TEXT, unique=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Peer(Base):
src/models.py: __tablename__: str = "peers"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: name: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Session(Base):
src/models.py: __tablename__: str = "sessions"
src/models.py: id: Mapped[str] = mapped_column(TEXT, primary_key=[REDACTED] default=generate_nanoid)
src/models.py: name: Mapped[str] = mapped_column(TEXT)
src/models.py: is_active: Mapped[bool] = mapped_column(default=True, server_default=text("true"))
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Message(Base):
src/models.py: __tablename__: str = "messages"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: public_id: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: token_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
src/models.py: seq_in_session: Mapped[int] = mapped_column(BigInteger, nullable=False)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: peer_name: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: workspace_name: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py:class MessageEmbedding(Base):
src/models.py: __tablename__: str = "message_embeddings"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: message_id: Mapped[str] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
src/models.py: peer_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: sync_state: Mapped[VectorSyncState] = mapped_column(
src/models.py: last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_attempts: Mapped[int] = mapped_column(
src/models.py:class Collection(Base):
src/models.py: __tablename__: str = "collections"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: observer: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: observed: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py:class Document(Base):
src/models.py: __tablename__: str = "documents"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: level: Mapped[DocumentLevel] = mapped_column(
src/models.py: times_derived: Mapped[int] = mapped_column(
src/models.py: source_ids: Mapped[list[str] | None] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: observer: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: observed: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str | None] = mapped_column(TEXT, nullable=True, index=True)
src/models.py: deleted_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_state: Mapped[VectorSyncState] = mapped_column(
src/models.py: last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_attempts: Mapped[int] = mapped_column(
src/models.py:class QueueItem(Base):
src/models.py: __tablename__: str = "queue"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: session_id: Mapped[str | None] = mapped_column(
src/models.py: work_unit_key: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: task_type: Mapped[TaskType] = mapped_column(TEXT, nullable=False)
src/models.py: payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
src/models.py: processed: Mapped[bool] = mapped_column(
src/models.py: error: Mapped[str | None] = mapped_column(TEXT, nullable=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str | None] = mapped_column(
src/models.py: message_id: Mapped[int | None] = mapped_column(
src/models.py:class ActiveQueueSession(Base):
src/models.py: __tablename__: str = "active_queue_sessions"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: work_unit_key: Mapped[str] = mapped_column(TEXT, unique=True)
src/models.py: last_updated: Mapped[datetime.datetime] = mapped_column(
src/models.py:class WebhookEndpoint(Base):
src/models.py: __tablename__: str = "webhook_endpoints"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: url: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py:class SessionPeer(Base):
src/models.py: workspace_name: Mapped[str]
src/models.py: session_name: Mapped[str]
src/models.py: peer_name: Mapped[str]
src/models.py: configuration: Mapped[dict[str, Any]]
src/models.py: internal_metadata: Mapped[dict[str, Any]]
src/models.py: joined_at: Mapped[datetime.datetime]
src/models.py: left_at: Mapped[datetime.datetime | None]
src/webhooks/events.py:class BaseWebhookEvent(BaseModel):
src/webhooks/events.py:class QueueEmptyEvent(BaseWebhookEvent):
src/webhooks/events.py:class TestEvent(BaseWebhookEvent):
src/reconciler/scheduler.py:class ReconcilerTask(BaseModel):
src/utils/queue_payload.py:class BasePayload(BaseModel):
src/utils/queue_payload.py:class RepresentationPayload(BasePayload):
src/utils/queue_payload.py:class RepresentationPayloads(BasePayload):
src/utils/queue_payload.py:class SummaryPayload(BasePayload):
src/utils/queue_payload.py:class WebhookPayload(BasePayload):
src/utils/queue_payload.py:class DreamPayload(BasePayload):
src/utils/queue_payload.py:class DeletionPayload(BasePayload):
src/utils/queue_payload.py:class ReconcilerPayload(BasePayload):
src/vector_store/__init__.py:class VectorRecord(BaseModel):
src/vector_store/__init__.py:class VectorQueryResult(BaseModel):
src/dreamer/specialists.py:class DeductionSpecialist(BaseSpecialist):
src/dreamer/specialists.py:class InductionSpecialist(BaseSpecialist):
src/security.py:class JWTParams(BaseModel):
src/schemas/api.py:class WorkspaceBase(BaseModel):
src/schemas/api.py:class WorkspaceCreate(WorkspaceBase):
src/schemas/api.py:class WorkspaceGet(WorkspaceBase):
src/schemas/api.py:class WorkspaceUpdate(WorkspaceBase):
src/schemas/api.py:class Workspace(WorkspaceBase):
src/schemas/api.py:class PeerBase(BaseModel):
src/schemas/api.py:class PeerCreate(PeerBase):
src/schemas/api.py:class PeerGet(PeerBase):
src/schemas/api.py:class PeerUpdate(PeerBase):
src/schemas/api.py:class Peer(PeerBase):
src/schemas/api.py:class PeerRepresentationGet(BaseModel):
src/schemas/api.py:class RepresentationResponse(BaseModel):
src/schemas/api.py:class PeerCardResponse(BaseModel):
src/schemas/api.py:class PeerCardSet(BaseModel):
src/schemas/api.py:class MessageBase(BaseModel):
src/schemas/api.py:class MessageCreate(MessageBase):
src/schemas/api.py:class MessageGet(MessageBase):
src/schemas/api.py:class MessageUpdate(MessageBase):
src/schemas/api.py:class Message(MessageBase):
src/schemas/api.py:class MessageBatchCreate(BaseModel):
src/schemas/api.py:class MessageUploadCreate(BaseModel):
src/schemas/api.py:class SessionBase(BaseModel):
src/schemas/api.py:class SessionCreate(SessionBase):
src/schemas/api.py:class SessionGet(SessionBase):
src/schemas/api.py:class SessionUpdate(SessionBase):
src/schemas/api.py:class Session(SessionBase):
src/schemas/api.py:class Summary(BaseModel):
src/schemas/api.py:class SessionContext(SessionBase):
src/schemas/api.py:class PeerContext(BaseModel):
src/schemas/api.py:class SessionSummaries(SessionBase):
src/schemas/api.py:class ConclusionGet(BaseModel):
src/schemas/api.py:class Conclusion(BaseModel):
src/schemas/api.py:class ConclusionQuery(BaseModel):
src/schemas/api.py:class ConclusionCreate(BaseModel):
src/schemas/api.py:class ConclusionBatchCreate(BaseModel):
src/schemas/api.py:class MessageSearchOptions(BaseModel):
src/schemas/api.py:class DialecticOptions(BaseModel):
src/schemas/api.py:class DialecticResponse(BaseModel):
src/schemas/api.py:class DialecticStreamDelta(BaseModel):
src/schemas/api.py:class DialecticStreamChunk(BaseModel):
src/schemas/api.py:class SessionQueueStatus(BaseModel):
src/schemas/api.py:class QueueStatus(BaseModel):
src/schemas/api.py:class ScheduleDreamRequest(BaseModel):
src/schemas/api.py:class WebhookEndpointBase(BaseModel):
src/schemas/api.py:class WebhookEndpointCreate(WebhookEndpointBase):
src/schemas/api.py:class WebhookEndpoint(WebhookEndpointBase):
src/utils/representation.py:class ObservationMetadata(BaseModel):
src/utils/representation.py:class ExplicitObservationBase(BaseModel):
src/utils/representation.py:class DeductiveObservationBase(BaseModel):
src/utils/representation.py:class InductiveObservationBase(BaseModel):
src/utils/representation.py:class ContradictionObservationBase(BaseModel):
src/utils/representation.py:class PromptRepresentation(BaseModel):
src/utils/representation.py:class ExplicitObservation(ExplicitObservationBase, ObservationMetadata):
src/utils/representation.py:class DeductiveObservation(DeductiveObservationBase, ObservationMetadata):
src/utils/representation.py:class InductiveObservation(InductiveObservationBase, ObservationMetadata):
src/utils/representation.py:class ContradictionObservation(ContradictionObservationBase, ObservationMetadata):
src/utils/representation.py:class Representation(BaseModel):
src/models.py:class Workspace(Base):
src/models.py: __tablename__: str = "workspaces"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: name: Mapped[str] = mapped_column(TEXT, unique=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Peer(Base):
src/models.py: __tablename__: str = "peers"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: name: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Session(Base):
src/models.py: __tablename__: str = "sessions"
src/models.py: id: Mapped[str] = mapped_column(TEXT, primary_key=[REDACTED] default=generate_nanoid)
src/models.py: name: Mapped[str] = mapped_column(TEXT)
src/models.py: is_active: Mapped[bool] = mapped_column(default=True, server_default=text("true"))
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: configuration: Mapped[dict[str, Any]] = mapped_column(
src/models.py:class Message(Base):
src/models.py: __tablename__: str = "messages"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: public_id: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: token_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
src/models.py: seq_in_session: Mapped[int] = mapped_column(BigInteger, nullable=False)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: peer_name: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: workspace_name: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py:class MessageEmbedding(Base):
src/models.py: __tablename__: str = "message_embeddings"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: message_id: Mapped[str] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
src/models.py: peer_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: sync_state: Mapped[VectorSyncState] = mapped_column(
src/models.py: last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_attempts: Mapped[int] = mapped_column(
src/models.py:class Collection(Base):
src/models.py: __tablename__: str = "collections"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: observer: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: observed: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: h_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py:class Document(Base):
src/models.py: __tablename__: str = "documents"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: internal_metadata: Mapped[dict[str, Any]] = mapped_column(
src/models.py: content: Mapped[str] = mapped_column(TEXT)
src/models.py: level: Mapped[DocumentLevel] = mapped_column(
src/models.py: times_derived: Mapped[int] = mapped_column(
src/models.py: source_ids: Mapped[list[str] | None] = mapped_column(
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: observer: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: observed: Mapped[str] = mapped_column(TEXT, index=True)
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: session_name: Mapped[str | None] = mapped_column(TEXT, nullable=True, index=True)
src/models.py: deleted_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_state: Mapped[VectorSyncState] = mapped_column(
src/models.py: last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
src/models.py: sync_attempts: Mapped[int] = mapped_column(
src/models.py:class QueueItem(Base):
src/models.py: __tablename__: str = "queue"
src/models.py: id: Mapped[int] = mapped_column(
src/models.py: session_id: Mapped[str | None] = mapped_column(
src/models.py: work_unit_key: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: task_type: Mapped[TaskType] = mapped_column(TEXT, nullable=False)
src/models.py: payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
src/models.py: processed: Mapped[bool] = mapped_column(
src/models.py: error: Mapped[str | None] = mapped_column(TEXT, nullable=True)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py: workspace_name: Mapped[str | None] = mapped_column(
src/models.py: message_id: Mapped[int | None] = mapped_column(
src/models.py:class ActiveQueueSession(Base):
src/models.py: __tablename__: str = "active_queue_sessions"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: work_unit_key: Mapped[str] = mapped_column(TEXT, unique=True)
src/models.py: last_updated: Mapped[datetime.datetime] = mapped_column(
src/models.py:class WebhookEndpoint(Base):
src/models.py: __tablename__: str = "webhook_endpoints"
src/models.py: id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
src/models.py: workspace_name: Mapped[str] = mapped_column(
src/models.py: url: Mapped[str] = mapped_column(TEXT, nullable=False)
src/models.py: created_at: Mapped[datetime.datetime] = mapped_column(
src/models.py:class SessionPeer(Base):
src/models.py: workspace_name: Mapped[str]
src/models.py: session_name: Mapped[str]
src/models.py: peer_name: Mapped[str]
src/models.py: configuration: Mapped[dict[str, Any]]
src/models.py: internal_metadata: Mapped[dict[str, Any]]
src/models.py: joined_at: Mapped[datetime.datetime]
src/models.py: left_at: Mapped[datetime.datetime | None]
src/schemas/internal.py:class DocumentBase(BaseModel):
src/schemas/internal.py:class DocumentMetadata(BaseModel):
src/schemas/internal.py:class DocumentCreate(DocumentBase):
src/schemas/internal.py:class ObservationInput(BaseModel):
src/schemas/internal.py:class SessionCounts(BaseModel):
src/schemas/internal.py:class QueueCounts(BaseModel):
src/schemas/internal.py:class QueueStatusRow(BaseModel):
src/schemas/internal.py:class SessionPeerData(BaseModel):
src/schemas/internal.py:class MessageBulkData(BaseModel):
src/utils/work_unit.py:class ParsedWorkUnit(BaseModel):
src/schemas/configuration.py:class ReasoningConfiguration(BaseModel):
src/schemas/configuration.py:class PeerCardConfiguration(BaseModel):
src/schemas/configuration.py:class SummaryConfiguration(BaseModel):
src/schemas/configuration.py:class DreamConfiguration(BaseModel):
src/schemas/configuration.py:class WorkspaceConfiguration(BaseModel):
src/schemas/configuration.py:class MessageConfiguration(BaseModel):
src/schemas/configuration.py:class ResolvedReasoningConfiguration(BaseModel):
src/schemas/configuration.py:class ResolvedPeerCardConfiguration(BaseModel):
src/schemas/configuration.py:class ResolvedSummaryConfiguration(BaseModel):
src/schemas/configuration.py:class ResolvedDreamConfiguration(BaseModel):
src/schemas/configuration.py:class ResolvedConfiguration(BaseModel):
src/schemas/configuration.py:class PeerConfig(BaseModel):
src/telemetry/events/base.py:class BaseEvent(BaseModel):
src/telemetry/events/dream.py:class DreamRunEvent(BaseEvent):
src/telemetry/events/dream.py:class DreamSpecialistEvent(BaseEvent):
src/telemetry/events/representation.py:class RepresentationCompletedEvent(BaseEvent):
src/telemetry/events/agent.py:class AgentIterationEvent(BaseEvent):
src/telemetry/events/agent.py:class AgentToolConclusionsCreatedEvent(BaseEvent):
src/telemetry/events/agent.py:class AgentToolConclusionsDeletedEvent(BaseEvent):
src/telemetry/events/agent.py:class AgentToolPeerCardUpdatedEvent(BaseEvent):
src/telemetry/events/agent.py:class AgentToolSummaryCreatedEvent(BaseEvent):
src/telemetry/events/reconciliation.py:class SyncVectorsCompletedEvent(BaseEvent):
src/telemetry/events/reconciliation.py:class CleanupStaleItemsCompletedEvent(BaseEvent):
src/telemetry/events/dialectic.py:class DialecticCompletedEvent(BaseEvent):
src/telemetry/events/deletion.py:class DeletionCompletedEvent(BaseEvent):
src/config.py:class ModelOverrideSettings(BaseModel):
src/config.py:class PromptCachePolicy(BaseModel):
src/config.py:class FallbackModelSettings(BaseModel):
src/config.py:class ConfiguredModelSettings(BaseModel):
src/config.py:class ResolvedFallbackConfig(BaseModel):
src/config.py:class ModelConfig(BaseModel):
src/config.py:class ConfiguredEmbeddingModelSettings(BaseModel):
src/config.py:class EmbeddingModelConfig(BaseModel):
src/config.py:class TomlConfigSettingsSource(PydanticBaseSettingsSource):
src/config.py:class HonchoSettings(BaseSettings):
src/config.py:class DialecticLevelSettings(BaseModel):
src/config.py:class SurprisalSettings(BaseModel):
src/llm/types.py:class HonchoLLMCallResponse(BaseModel, Generic[T]):
src/llm/types.py:class HonchoLLMCallStreamChunk(BaseModel):
src/llm/caching.py:class GeminiCacheHandle(BaseModel):
>rg "inference|facet|representation|summary|dream|derive|observe|observations|peer_card|queue|honcho_llm_call|embedding" src tests docs -g'*.py' -g'*.md' -g'*.mdx'Chunk ID: bbdcf3
Wall time: 0.0003 seconds
Process exited with code 0
Original token count: 200138
Output:
Total output lines: 8245
src/webhooks/README.md:* **Event Publishing (`webhooks/events.py`):** Defines the event types and allows us to publish new events to the processing queue.
src/webhooks/README.md:Note that the webhooks require the *deriver* process to be running to facilitate the delivery of the webhook.
docs/v3/guides/discord.mdx:assistant = honcho_client.peer(id="assistant", config={"observe_me": False})
src/webhooks/events.py:from src.utils.queue_payload import create_webhook_payload
src/webhooks/events.py: QUEUE_EMPTY = "queue.empty"
src/webhooks/events.py: """Webhook event for when a queue becomes empty."""
src/webhooks/events.py: queue_type: str
src/webhooks/events.py: observer: str | None = None
src/webhooks/events.py: observed: str | None = None
src/webhooks/events.py: Add a webhook event to our DB queue.
src/webhooks/events.py: # It's stored directly on the queue item
src/webhooks/events.py: queue_item = QueueItem(
src/webhooks/events.py: db.add(queue_item)
tests/routes/test_peers.py:def test_get_peer_representation_with_session(
tests/routes/test_peers.py: """Test peer representation with session_id parameter"""
tests/routes/test_peers.py: # Test representation scoped to session
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py: assert isinstance(data["representation"], str)
tests/routes/test_peers.py:def test_get_peer_representation_global(
tests/routes/test_peers.py: """Test peer representation without session_id (global representation)"""
tests/routes/test_peers.py: # Test global representation (no session_id)
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py: assert isinstance(data["representation"], str)
tests/routes/test_peers.py:def test_get_peer_representation_with_target(
tests/routes/test_peers.py: """Test peer representation with target parameter"""
tests/routes/test_peers.py: # Test representation of target from observer's perspective
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py: assert isinstance(data["representation"], str)
tests/routes/test_peers.py:def test_get_peer_representation_with_search_query(
tests/routes/test_peers.py: """Test peer representation with search_query parameter"""
tests/routes/test_peers.py: # Test representation with semantic search query
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_representation_with_search_top_k(
tests/routes/test_peers.py: """Test peer representation with search_top_k parameter"""
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_representation_with_search_max_distance(
tests/routes/test_peers.py: """Test peer representation with search_max_distance parameter"""
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_representation_with_include_most_frequent(
tests/routes/test_peers.py: """Test peer representation with include_most_frequent parameter"""
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_representation_with_max_observations(
tests/routes/test_peers.py: """Test peer representation with max_observations parameter"""
tests/routes/test_peers.py: # Test with various max_observations values
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: "max_observations": max_obs,
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_representation_with_all_parameters(
tests/routes/test_peers.py: """Test peer representation with all optional parameters"""
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: "max_observations": 30,
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py: assert isinstance(data["representation"], str)
tests/routes/test_peers.py:def test_get_peer_representation_boundary_values(
tests/routes/test_peers.py: """Test peer representation with boundary values for numeric parameters"""
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: "max_observations": 1,
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: "max_observations": 100,
tests/routes/test_peers.py:def test_get_peer_representation_default_max_observations(
tests/routes/test_peers.py: """Test that max_observations defaults to 25 when not provided"""
tests/routes/test_peers.py: # Test without max_observations - should use default of 25
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{test_peer.name}/representation",
tests/routes/test_peers.py: assert "representation" in data
tests/routes/test_peers.py:def test_get_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]):
tests/routes/test_peers.py: test_workspace, observer_peer = sample_data
tests/routes/test_peers.py: # Create a second peer (the target/observed peer)
tests/routes/test_peers.py: # Test getting observer's own card (should return null initially)
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card"
tests/routes/test_peers.py: assert data["peer_card"] is None
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
tests/routes/test_peers.py: assert data["peer_card"] is None
tests/routes/test_peers.py:async def test_get_peer_card_with_data(
tests/routes/test_peers.py: test_workspace, observer_peer = sample_data
tests/routes/test_peers.py: # Create a second peer (the target/observed peer)
tests/routes/test_peers.py: # Set a self-card for the observer peer
tests/routes/test_peers.py: await crud.set_peer_card(
tests/routes/test_peers.py: observer=observer_peer.name,
tests/routes/test_peers.py: observed=observer_peer.name,
tests/routes/test_peers.py: # Set a card for the observer describing the target peer
tests/routes/test_peers.py: await crud.set_peer_card(
tests/routes/test_peers.py: observer=observer_peer.name,
tests/routes/test_peers.py: observed=target_peer_name,
tests/routes/test_peers.py: # Test getting observer's own card
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card"
tests/routes/test_peers.py: assert data["peer_card"] == self_card_content
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
tests/routes/test_peers.py: assert data["peer_card"] == target_card_content
tests/routes/test_peers.py:def test_set_peer_card(client: TestClient, sample_data: tuple[Workspace, Peer]):
tests/routes/test_peers.py: test_workspace, observer_peer = sample_data
tests/routes/test_peers.py: # Set the observer's own card
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
tests/routes/test_peers.py: json={"peer_card": self_card},
tests/routes/test_peers.py: assert data["peer_card"] == self_card
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card"
tests/routes/test_peers.py: assert response.json()["peer_card"] == self_card
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
tests/routes/test_peers.py: json={"peer_card": target_card},
tests/routes/test_peers.py: assert data["peer_card"] == target_card
tests/routes/test_peers.py: f"/v3/workspaces/{test_workspace.name}/peers/{observer_peer.name}/card",
tests/routes/test_peers.py: assert response.json()["peer_card"] == target_card
src/webhooks/webhook_delivery.py:from src.utils.queue_payload import WebhookPayload
docs/v3/guides/granola.mdx:4. Print a summary of what was transferred
docs/v3/guides/granola.mdx:| Meeting summary | Message from note creator | Multi-person calls store the summary |
docs/v3/guides/granola.mdx:Granola's transcript uses `Them:` for all non-creator speakers with no disambiguation — in a 4-person call, everyone else is just `Them:`. Rather than guess incorrectly, the script stores Granola's summary as your record of the meeting, with participants in metadata.
docs/v3/guides/granola.mdx: f"{meeting_summary}",
docs/v3/guides/granola.mdx: "mode": "summary",
docs/v3/guides/granola.mdx:The summary is attributed to you because it's *your* record of what happened. Granola captured your notes from a meeting where those people were present.
docs/v3/guides/granola.mdx:For each meeting, you choose the import mode: two-person (full attribution), summary, or skip. For multi-person calls that are actually 1:1s (extra participants listed but didn't speak), you can override the detection and select the actual speaker.
docs/v3/guides/granola.mdx:Because meetings live in a standard Honcho workspace, you can enrich peer representations with data from other channels:
docs/v3/guides/granola.mdx:| Missing transcripts | Free tier has no transcript access. The script falls back to summary content. |
docs/v3/guides/granola.mdx: has_s = bool(extract_summary(m))
docs/v3/guides/granola.mdx: label = "transcript+summary" if has_t and has_s else "transcript only" if has_t else "summary only" if has_s else "basic only"
docs/v3/guides/granola.mdx:def extract_summary(meeting: dict[str, Any]) -> str:
docs/v3/guides/granola.mdx: """Extract best available summary text from meeting data."""
docs/v3/guides/granola.mdx: for key in ("summary", "notes", "note", "meeting_notes", "description"):
docs/v3/guides/granola.mdx: for tag in ("summary", "notes"):
docs/v3/guides/granola.mdx:def import_summary(
docs/v3/guides/granola.mdx: """Import a meeting as a summary message."""
docs/v3/guides/granola.mdx: summary = extract_summary(meeting)
docs/v3/guides/granola.mdx: if not summary:
docs/v3/guides/granola.mdx: summary = str(parsed.get("transcript", "")) if isinstance(parsed, dict) else raw_t
docs/v3/guides/granola.mdx: summary = raw_t
docs/v3/guides/granola.mdx: summary = summary or "No content available"
docs/v3/guides/granola.mdx: messages = build_messages(me_peer, header + summary, metadata, created_at)
docs/v3/guides/granola.mdx: print(" -> Imported as summary")
docs/v3/guides/granola.mdx: - "summary": import as a single summary message
docs/v3/guides/granola.mdx: print(f" Content: {'summary available' if extract_summary(meeting) else 'metadata only'}")
docs/v3/guides/granola.mdx: return ("summary", None)
docs/v3/guides/granola.mdx: choice = input(" [Enter] summary / [2] 2-person / [k] skip: ").strip().lower()
docs/v3/guides/granola.mdx: choice = input(" [Enter] summary / [2] 2-person / [k] skip: ").strip().lower()
docs/v3/guides/granola.mdx: return ("summary", None)
docs/v3/guides/granola.mdx: return ("summary", None)
docs/v3/guides/granola.mdx: choice = input(" [Enter] summary / [k] skip: ").strip().lower()
docs/v3/guides/granola.mdx: choice = input(" [Enter] summary / [k] skip: ").strip().lower()
docs/v3/guides/granola.mdx: return ("summary", None)
docs/v3/guides/granola.mdx: import_summary(honcho, session, me_peer_id, m, metadata, created_at)
docs/snippets/cli-commands.mdx:<ParamField path="--observer" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observed" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observer" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observed" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observer" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observed" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observer" type="string">
docs/snippets/cli-commands.mdx:<ParamField path="--observed" type="string">
docs/snippets/cli-commands.mdx:Verify config and connectivity. Scope with -w / -p to check workspace, peer, and queue health.
docs/snippets/cli-commands.mdx:List, create, chat with, search, and manage peers and their representations.
docs/snippets/cli-commands.mdx:<ParamField path="--observe-me" type="boolean">
docs/snippets/cli-commands.mdx: Whether Honcho will form a representation of this peer. Negate with `--no-observe-me`.
docs/snippets/cli-commands.mdx:<Accordion title="representation">
docs/snippets/cli-commands.mdx:Get the formatted representation for a peer.
docs/snippets/cli-commands.mdx:honcho peer representation [<peer_id>]
docs/snippets/cli-commands.mdx: Target peer to get representation about.
docs/snippets/cli-commands.mdx:<ParamField path="--summary" type="boolean" default="true">
docs/snippets/cli-commands.mdx: Include summary. Negate with `--no-summary`.
docs/snippets/cli-commands.mdx:<Accordion title="representation">
docs/snippets/cli-commands.mdx:Get the representation of a peer within a session.
docs/snippets/cli-commands.mdx:honcho session representation <peer_id> [<session_id>]
docs/snippets/cli-commands.mdx:<Accordion title="queue-status">
docs/snippets/cli-commands.mdx:Get queue processing status.
docs/snippets/cli-commands.mdx:honcho workspace queue-status
docs/snippets/cli-commands.mdx:<ParamField path="--observer" type="string">
docs/snippets/cli-commands.mdx: Filter by observer peer.
src/startup/__init__.py:"""Startup-time validators that gate API/deriver boot."""
src/startup/__init__.py:from src.startup.embedding_validator import (
src/startup/__init__.py: validate_embedding_schema,
src/startup/__init__.py:__all__ = ("StartupValidationError", "validate_embedding_schema")
tests/routes/test_files.py: configuration = {"skip_deriver": True, "custom_flag": "test"}
tests/routes/test_files.py: configuration = {"skip_deriver": False, "test_mode": True}
src/startup/embedding_validator.py:"""Startup validator for the embedding pipeline.
src/startup/embedding_validator.py:ones. Full enumeration is available via `uv run python scripts/configure_embeddings.py --report`.
src/startup/embedding_validator.py:_EMBEDDING_TABLES: tuple[str, ...] = ("documents", "message_embeddings")
src/startup/embedding_validator.py: """Raised when the embedding configuration cannot be reconciled with the
src/startup/embedding_validator.py: queue task is processed.
src/startup/embedding_validator.py:async def validate_embedding_schema(
src/startup/embedding_validator.py: """Validate that the embedding schema matches the configured dimension.
src/startup/embedding_validator.py: Run after the DB pool is initialized and before the embedding client is
src/startup/embedding_validator.py: Returns a mapping of table name -> raw ``atttypmod`` for the embedding
src/startup/embedding_validator.py: f"could not validate embedding schema: {underlying}"
src/startup/embedding_validator.py: raise StartupValidationError("embedding schema introspection did not run")
src/startup/embedding_validator.py: AND a.attname = 'embedding'
src/startup/embedding_validator.py: listing = ", ".join(sorted(f"{schema}.{t}.embedding" for t in missing))
src/startup/embedding_validator.py: f"{schema}.{table}.embedding has no declared vector dimension"
src/startup/embedding_validator.py: + " `uv run python scripts/configure_embeddings.py`."
src/startup/embedding_validator.py: f"{schema}.{table}.embedding dim ({actual}) does not match"
src/startup/embedding_validator.py: + " `uv run python scripts/configure_embeddings.py`"
src/startup/embedding_validator.py: - Document namespaces — one per existing ``(workspace, observer, observed)``
src/startup/embedding_validator.py: ``configure_embeddings --report`` for full enumeration when a hard
src/startup/embedding_validator.py: for workspace_name, observer, observed in collection_keys:
src/startup/embedding_validator.py: observer=observer,
src/startup/embedding_validator.py: observed=observed,
src/startup/embedding_validator.py: + " `uv run python scripts/configure_embeddings.py --report`."
src/startup/embedding_validator.py: """Pull up to ``limit`` ``(workspace_name, observer, observed)`` triples,
src/startup/embedding_validator.py: select(Collection.workspace_name, Collection.observer, Collection.observed)
docs/v3/guides/migrations/mem0.mdx:**Superior Performance** - Higher accuracy on memory retrieval benchmarks with faster inference times (more details soon!).
docs/v3/guides/migrations/mem0.mdx:**Advanced Multi-Peer Sessions** - Honcho offers configurable observation settings (who builds memories about whom), representation-based queries between participants, and first-class peer objects.
docs/v3/guides/migrations/mem0.mdx:For the best results, we recommend importing your raw messages directly into Honcho. This gives Honcho the full context to build rich, accurate representations and enables features like session summaries.
docs/v3/guides/migrations/mem0.mdx:That's it! The user's Mem0 memories are now searchable in Honcho as conclusions. For richer representations with deductive reasoning and session summaries, consider importing your raw messages as described in the [Step-by-Step Migration](#step-by-step-migration) section.
docs/v3/guides/migrations/mem0.mdx:Importing raw user messages gives Honcho the full conversational context to build the most accurate representations. We recommend using a data structure that preserves the session and peer structure.
docs/v3/guides/migrations/mem0.mdx: Learn more about inference-powered queries
docs/v3/guides/migrations/mem0.mdx:| `session.representation(peer)` | Cached psychological analysis (mental state, intentions) | Real-time adaptation |
docs/v3/guides/integrations/mcp.mdx:**Peers** — `create_peer`, `list_peers`, `chat`, `get_peer_card`, `set_peer_card`, `get_peer_context`, `get_representation`
docs/v3/guides/integrations/mcp.mdx:**System** — `schedule_dream`, `get_queue_status`
docs/v3/guides/integrations/mcp.mdx:On the first conversation there won't be much — but after a few exchanges, Honcho's background reasoning will start building a representation of you. Ask again after a couple of conversations and you'll see the difference.
docs/v3/documentation/reference/cli.mdx: honcho conclusion list --observer <peer_id> --json
docs/v3/documentation/reference/cli.mdx: honcho conclusion search "topic" --…190138 tokens truncated…s/deriver/test_deriver_processing.py: legacy_observer = legacy_payload.get("observer")
tests/deriver/test_deriver_processing.py: observers = [legacy_observer] if legacy_observer else []
tests/deriver/test_deriver_processing.py: assert observers == ["peer_observer"]
tests/deriver/test_deriver_processing.py: def test_new_payload_observers_list_used_directly(self):
tests/deriver/test_deriver_processing.py: """Test that new payloads with 'observers' list are used directly."""
tests/deriver/test_deriver_processing.py: "observers": ["peer1", "peer2"],
tests/deriver/test_deriver_processing.py: "observed": "peer3",
tests/deriver/test_deriver_processing.py: "task_type": "representation",
tests/deriver/test_deriver_processing.py: observers = new_payload.get("observers")
tests/deriver/test_deriver_processing.py: if observers is None:
tests/deriver/test_deriver_processing.py: legacy_observer = new_payload.get("observer")
tests/deriver/test_deriver_processing.py: observers = [legacy_observer] if legacy_observer else []
tests/deriver/test_deriver_processing.py: assert observers == ["peer1", "peer2"]
tests/deriver/test_deriver_processing.py: def test_empty_payload_results_in_empty_observers_list(self):
tests/deriver/test_deriver_processing.py: """Test that payloads with neither observer nor observers return empty list."""
tests/deriver/test_deriver_processing.py: "observed": "peer_observed",
tests/deriver/test_deriver_processing.py: "task_type": "representation",
tests/deriver/test_deriver_processing.py: observers = empty_payload.get("observers")
tests/deriver/test_deriver_processing.py: if observers is None:
tests/deriver/test_deriver_processing.py: legacy_observer = empty_payload.get("observer")
tests/deriver/test_deriver_processing.py: observers = [legacy_observer] if legacy_observer else []
tests/deriver/test_deriver_processing.py: assert observers == []
tests/deriver/test_deriver_processing.py: # async def test_representation_batch_uses_earliest_cutoff(
tests/deriver/test_deriver_processing.py: # "src.deriver.deriver.summarizer.get_session_context_formatted",
tests/deriver/test_deriver_processing.py: # # Provide a stub working representation so embedding lookups are skipped.
tests/deriver/test_deriver_processing.py: # "src.crud.get_working_representation",
tests/deriver/test_deriver_processing.py: # "src.deriver.deriver.CertaintyReasoner.reason",
tests/deriver/test_deriver_processing.py: # await process_representation_tasks_batch(
tests/deriver/test_deriver_processing.py: # observer=alice.name,
tests/deriver/test_deriver_processing.py: # observed=alice.name,
tests/crud/test_representation_manager.py:from src.crud.representation import RepresentationManager
tests/crud/test_representation_manager.py:from src.utils.representation import (
tests/crud/test_representation_manager.py:def _saved_observations(mock_save: AsyncMock):
tests/crud/test_representation_manager.py: if "all_observations" in call.kwargs:
tests/crud/test_representation_manager.py: return call.kwargs["all_observations"]
tests/crud/test_representation_manager.py: raise AssertionError("missing all_observations in await args")
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: async def test_query_documents_most_derived_excludes_soft_deleted(
tests/crud/test_representation_manager.py: """Soft-deleted documents must not appear in the most-derived query."""
tests/crud/test_representation_manager.py: # Create two documents with different times_derived
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: times_derived=5,
tests/crud/test_representation_manager.py: observer=test_peer.name,
tests/crud/test_representation_manager.py: observed=test_peer2.name,
tests/crud/test_representation_manager.py: content="Deleted high-derived observation",
tests/crud/test_representation_manager.py: times_derived=100,
tests/crud/test_representation_manager.py: # Soft-delete the high-derived one
tests/crud/test_representation_manager.py: results = await manager._query_documents_most_derived(db_session, top_k=10) # pyright: ignore[reportPrivateUsage]
tests/crud/test_representation_manager.py: async def test_save_representation_filters_blank_observations_before_embedding(
tests/crud/test_representation_manager.py: observer="observer",
tests/crud/test_representation_manager.py: observed="observed",
tests/crud/test_representation_manager.py: representation = Representation(
tests/crud/test_representation_manager.py: patch("src.crud.representation.tracked_db", _fake_tracked_db),
tests/crud/test_representation_manager.py: "src.crud.representation.embedding_client.simple_batch_embed",
tests/crud/test_representation_manager.py: "_save_representation_internal",
tests/crud/test_representation_manager.py: saved = await manager.save_representation(
tests/crud/test_representation_manager.py: representation,
tests/crud/test_representation_manager.py: dream=SimpleNamespace(enabled=False)
tests/crud/test_representation_manager.py: saved_observations = _saved_observations(mock_save)
tests/crud/test_representation_manager.py: assert len(saved_observations) == 1
tests/crud/test_representation_manager.py: assert saved_observations[0].content == "useful observation"
tests/crud/test_representation_manager.py: async def test_save_representation_filters_blank_deductive_observations(self):
tests/crud/test_representation_manager.py: observer="observer",
tests/crud/test_representation_manager.py: observed="observed",
tests/crud/test_representation_manager.py: representation = Representation(
tests/crud/test_representation_manager.py: patch("src.crud.representation.tracked_db", _fake_tracked_db),
tests/crud/test_representation_manager.py: "src.crud.representation.embedding_client.simple_batch_embed",
tests/crud/test_representation_manager.py: "_save_representation_internal",
tests/crud/test_representation_manager.py: saved = await manager.save_representation(
tests/crud/test_representation_manager.py: representation,
tests/crud/test_representation_manager.py: dream=SimpleNamespace(enabled=False)
tests/crud/test_representation_manager.py: saved_observations = _saved_observations(mock_save)
tests/crud/test_representation_manager.py: assert len(saved_observations) == 1
tests/crud/test_representation_manager.py: assert isinstance(saved_observations[0], DeductiveObservation)
tests/crud/test_representation_manager.py: assert saved_observations[0].conclusion == "inferred conclusion"
tests/crud/test_representation_manager.py: async def test_save_representation_skips_all_blank_observations(self):
tests/crud/test_representation_manager.py: observer="observer",
tests/crud/test_representation_manager.py: observed="observed",
tests/crud/test_representation_manager.py: representation = Representation(
tests/crud/test_representation_manager.py: patch("src.crud.representation.tracked_db", _fake_tracked_db),
tests/crud/test_representation_manager.py: "src.crud.representation.embedding_client.simple_batch_embed",
tests/crud/test_representation_manager.py: "_save_representation_internal",
tests/crud/test_representation_manager.py: saved = await manager.save_representation(
tests/crud/test_representation_manager.py: representation,
tests/crud/test_representation_manager.py: dream=SimpleNamespace(enabled=False)
tests/crud/test_peer_card.py:from src.crud.peer_card import construct_peer_card_label, get_peer_card, set_peer_card
tests/crud/test_peer_card.py:async def test_peer_card_get_set_roundtrip(
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, value_2, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py:async def test_get_peer_card_missing_peer_raises(
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: observer="missing-peer",
tests/crud/test_peer_card.py: observed="missing-peer",
tests/crud/test_peer_card.py:async def test_get_peer_card_missing_workspace_raises(
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py:async def test_peer_card_empty_list(
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py:async def test_peer_card_multiple_lines(
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py: result = await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py:async def test_peer_card_different_observer_observed(
tests/crud/test_peer_card.py: """Test peer card with different observer and observed peers."""
tests/crud/test_peer_card.py: # Peer1 observes peer2
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer2.name,
tests/crud/test_peer_card.py: # Peer2 observes peer1
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer2.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer1.name, observed=peer2.name
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer2.name, observed=peer1.name
tests/crud/test_peer_card.py:async def test_peer_card_self_and_other_observations(
tests/crud/test_peer_card.py: """Test that a peer can have both a self-observation and observations of others."""
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer2.name,
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer1.name, observed=peer1.name
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer1.name, observed=peer2.name
tests/crud/test_peer_card.py:async def test_peer_card_multiple_observers_same_observed(
tests/crud/test_peer_card.py: """Test that multiple peers can observe the same peer with different cards."""
tests/crud/test_peer_card.py: # Both peer2 and peer3 observe peer1
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer2.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer3.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: # Each observer should have their own independent card
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer2.name, observed=peer1.name
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer3.name, observed=peer1.name
tests/crud/test_peer_card.py:async def test_peer_card_update_does_not_affect_others(
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer2.name,
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer1.name,
tests/crud/test_peer_card.py: observed=peer1.name,
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer1.name, observed=peer1.name
tests/crud/test_peer_card.py: await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer1.name, observed=peer2.name
tests/crud/test_peer_card.py:async def test_peer_card_special_characters(
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py: result = await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py:async def test_peer_card_large_content(
tests/crud/test_peer_card.py: # Create a large peer card with many observations
tests/crud/test_peer_card.py: await set_peer_card(
tests/crud/test_peer_card.py: observer=peer.name,
tests/crud/test_peer_card.py: observed=peer.name,
tests/crud/test_peer_card.py: result = await get_peer_card(
tests/crud/test_peer_card.py: db_session, workspace.name, observer=peer.name, observed=peer.name
tests/crud/test_peer_card.py: assert construct_peer_card_label(observer="a", observed="a") == "peer_card"
tests/crud/test_peer_card.py: assert construct_peer_card_label(observer="a", observed="b") == "b_peer_card"
tests/crud/test_peer_card.py:def test_construct_peer_card_label_with_special_chars():
tests/crud/test_peer_card.py: construct_peer_card_label(observer="peer-1", observed="peer-2")
tests/crud/test_peer_card.py: == "peer-2_peer_card"
tests/crud/test_peer_card.py: construct_peer_card_label(observer="peer_1", observed="peer_2")
tests/crud/test_peer_card.py: == "peer_2_peer_card"
tests/crud/test_peer_card.py: # Test with same observer and observed with special chars
tests/crud/test_peer_card.py: construct_peer_card_label(observer="peer-1", observed="peer-1") == "peer_card"
tests/crud/test_peer_card.py:def test_construct_peer_card_label_edge_cases():
tests/crud/test_peer_card.py: assert construct_peer_card_label(observer="", observed="") == "peer_card"
tests/crud/test_peer_card.py: assert construct_peer_card_label(observer="a", observed="") == "_peer_card"
tests/crud/test_peer_card.py: construct_peer_card_label(observer=long_name, observed=long_name) == "peer_card"
tests/crud/test_peer_card.py: construct_peer_card_label(observer="a", observed=long_name)
tests/crud/test_peer_card.py: == f"{long_name}_peer_card"
tests/crud/test_workspace.py: observer=test_peer.name,
tests/crud/test_workspace.py: observed=test_peer.name,
tests/crud/test_workspace.py: observer=test_peer.name,
tests/crud/test_workspace.py: observed=test_peer.name,
tests/crud/test_workspace.py: observer=test_peer.name,
tests/crud/test_workspace.py: observed=test_peer.name,
tests/crud/test_workspace.py: async def test_delete_workspace_cascade_queue_items(
tests/crud/test_workspace.py: """Test that deleting a workspace cascades to delete queue items"""
tests/crud/test_workspace.py: # Create queue item with work_unit_key containing workspace name
tests/crud/test_workspace.py: queue_item = models.QueueItem(
tests/crud/test_workspace.py: work_unit_key=[REDACTED]
tests/crud/test_workspace.py: task_type="representation",
tests/crud/test_workspace.py: db_session.add(queue_item)
tests/crud/test_workspace.py: # Verify queue item exists
tests/crud/test_workspace.py: queue_items = result.scalars().all()
tests/crud/test_workspace.py: assert len(queue_items) == 1
tests/crud/test_workspace.py: # Verify queue item is deleted
tests/crud/test_workspace.py: queue_items = result.scalars().all()
tests/crud/test_workspace.py: assert len(queue_items) == 0
tests/crud/test_workspace.py: async def test_delete_workspace_cascade_active_queue_sessions(
tests/crud/test_workspace.py: """Test that deleting a workspace cascades to delete active queue sessions"""
tests/crud/test_workspace.py: # Create active queue session with work_unit_key containing workspace name
tests/crud/test_workspace.py: active_queue = models.ActiveQueueSession(
tests/crud/test_workspace.py: work_unit_key=[REDACTED]
tests/crud/test_workspace.py: db_session.add(active_queue)
tests/crud/test_workspace.py: # Verify active queue session exists
tests/crud/test_workspace.py: active_queues = result.scalars().all()
tests/crud/test_workspace.py: assert len(active_queues) == 1
tests/crud/test_workspace.py: # Verify active queue session is deleted
tests/crud/test_workspace.py: active_queues = result.scalars().all()
tests/crud/test_workspace.py: assert len(active_queues) == 0
tests/crud/test_workspace.py: observer=test_peer.name,
tests/crud/test_workspace.py: observed=peer2.name,
tests/crud/test_workspace.py: observer=test_peer.name,
tests/crud/test_workspace.py: observed=peer2.name,
>ls src && rg --files src | sed -n '1,160p'Chunk ID: bcbd31
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 786
Output:
__init__.py
__pycache__
cache
config.py
crud
db.py
dependencies.py
deriver
dialectic
dreamer
embedding_client.py
exceptions.py
llm
main.py
models.py
reconciler
routers
schemas
security.py
startup
telemetry
utils
vector_store
webhooks
src/dependencies.py
src/webhooks/README.md
src/webhooks/events.py
src/webhooks/webhook_delivery.py
src/startup/__init__.py
src/startup/embedding_validator.py
src/main.py
src/reconciler/scheduler.py
src/reconciler/sync_vectors.py
src/reconciler/__init__.py
src/reconciler/queue_cleanup.py
src/telemetry/emitter.py
src/telemetry/events/base.py
src/telemetry/events/representation.py
src/telemetry/events/dialectic.py
src/telemetry/events/deletion.py
src/telemetry/events/agent.py
src/telemetry/events/dream.py
src/telemetry/events/__init__.py
src/telemetry/events/reconciliation.py
src/telemetry/sentry.py
src/telemetry/prometheus/__init__.py
src/telemetry/prometheus/metrics.py
src/telemetry/metrics_collector.py
src/telemetry/__init__.py
src/telemetry/reasoning_traces.py
src/telemetry/logging.py
src/exceptions.py
src/deriver/queue_manager.py
src/deriver/__main__.py
src/deriver/deriver.py
src/deriver/consumer.py
src/deriver/prompts.py
src/deriver/__init__.py
src/deriver/enqueue.py
src/crud/representation.py
src/crud/workspace.py
src/crud/collection.py
src/crud/deriver.py
src/crud/document.py
src/crud/peer.py
src/crud/message.py
src/crud/__init__.py
src/crud/session.py
src/crud/webhook.py
src/crud/peer_card.py
src/dreamer/dream_scheduler.py
src/dreamer/orchestrator.py
src/dreamer/__init__.py
src/dreamer/specialists.py
src/dreamer/trees/base.py
src/dreamer/trees/prototype.py
src/dreamer/trees/lsh.py
src/dreamer/trees/rptree.py
src/dreamer/trees/__init__.py
src/dreamer/trees/graph.py
src/dreamer/trees/covertree.py
src/dreamer/trees/sklearn_wrapper.py
src/dreamer/surprisal.py
src/vector_store/lancedb.py
src/vector_store/__init__.py
src/vector_store/turbopuffer.py
src/schemas/api.py
src/schemas/__init__.py
src/schemas/internal.py
src/schemas/configuration.py
src/utils/agent_tools.py
src/utils/json_parser.py
src/utils/representation.py
src/utils/filter.py
src/utils/summarizer.py
src/utils/search.py
src/utils/queue_payload.py
src/utils/formatting.py
src/utils/types.py
src/utils/tokens.py
src/utils/__init__.py
src/utils/work_unit.py
src/utils/config_helpers.py
src/utils/files.py
src/__init__.py
src/security.py
src/models.py
src/cache/__init__.py
src/cache/client.py
src/embedding_client.py
src/config.py
src/llm/executor.py
src/llm/tool_loop.py
src/llm/caching.py
src/llm/api.py
src/llm/runtime.py
src/llm/types.py
src/llm/__init__.py
src/llm/request_builder.py
src/llm/backends/anthropic.py
src/llm/backends/openai.py
src/llm/backends/gemini.py
src/llm/backends/__init__.py
src/llm/conversation.py
src/llm/registry.py
src/llm/credentials.py
src/llm/history_adapters.py
src/llm/backend.py
src/llm/structured_output.py
src/dialectic/chat.py
src/dialectic/prompts.py
src/dialectic/core.py
src/dialectic/__init__.py
src/db.py
src/routers/workspaces.py
src/routers/webhooks.py
src/routers/messages.py
src/routers/conclusions.py
src/routers/keys.py
src/routers/peers.py
src/routers/sessions.py
The schema is centered on peer-to-peer observations, not just users and chats. I’m narrowing in on the persisted models and the code paths that turn raw messages into conclusions, cards, summaries, and dialectic responses.
>sed -n '1,280p' src/models.pyChunk ID: 7cdc6d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2421
Output:
import datetime
from logging import getLogger
from typing import Any, final
from dotenv import load_dotenv
from nanoid import generate as generate_nanoid
from pgvector.sqlalchemy import Vector
from sqlalchemy import (
BigInteger,
Boolean,
CheckConstraint,
Column,
DateTime,
ForeignKey,
ForeignKeyConstraint,
Identity,
Index,
Integer,
Table,
UniqueConstraint,
text,
)
from sqlalchemy.dialects.postgresql import JSONB, TEXT
from sqlalchemy.orm import Mapped, MappedColumn, mapped_column, relationship
from sqlalchemy.sql import func
from typing_extensions import override
from src.config import settings
from src.utils.types import DocumentLevel, TaskType, VectorSyncState
from .db import Base
load_dotenv(override=True)
_VECTOR_DIM: int = settings.EMBEDDING.VECTOR_DIMENSIONS
logger = getLogger(__name__)
# Association table for many-to-many relationship between sessions and peers
session_peers_table = Table(
"session_peers",
Base.metadata,
Column(
"workspace_name",
TEXT,
ForeignKey("workspaces.name"),
primary_key=[REDACTED]
nullable=False,
),
Column(
"session_name",
TEXT,
primary_key=[REDACTED]
nullable=False,
),
Column("peer_name", TEXT, primary_key=[REDACTED] nullable=False),
Column(
"configuration",
JSONB,
default=dict,
nullable=False,
server_default=text("'{}'::jsonb"),
),
Column(
"internal_metadata",
JSONB,
default=dict,
nullable=False,
server_default=text("'{}'::jsonb"),
),
Column(
"joined_at",
DateTime(timezone=True),
nullable=False,
server_default=func.now(),
),
Column(
"left_at",
DateTime(timezone=True),
nullable=True,
),
# Composite foreign key constraint for sessions
ForeignKeyConstraint(
["session_name", "workspace_name"],
["sessions.name", "sessions.workspace_name"],
),
# Composite foreign key constraint for peers
ForeignKeyConstraint(
["peer_name", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
)
@final
class Workspace(Base):
__tablename__: str = "workspaces"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
name: Mapped[str] = mapped_column(TEXT, unique=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
h_metadata: Mapped[dict[str, Any]] = mapped_column(
"metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
configuration: Mapped[dict[str, Any]] = mapped_column(
JSONB, default=dict, server_default=text("'{}'::jsonb")
)
sessions = relationship(
"Session", back_populates="workspace", cascade="all, delete, delete-orphan"
)
peers = relationship(
"Peer", back_populates="workspace", cascade="all, delete, delete-orphan"
)
webhook_endpoints = relationship("WebhookEndpoint", back_populates="workspace")
__table_args__ = (
CheckConstraint("length(id) = 21", name="id_length"),
CheckConstraint("length(name) <= 512", name="name_length"),
CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
)
@final
class Peer(Base):
__tablename__: str = "peers"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
name: Mapped[str] = mapped_column(TEXT, nullable=False)
h_metadata: Mapped[dict[str, Any]] = mapped_column(
"metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
configuration: Mapped[dict[str, Any]] = mapped_column(
JSONB, default=dict, server_default=text("'{}'::jsonb")
)
workspace = relationship("Workspace", back_populates="peers")
sessions = relationship(
"Session", secondary=session_peers_table, back_populates="peers"
)
__table_args__ = (
UniqueConstraint("name", "workspace_name"),
CheckConstraint("length(id) = 21", name="id_length"),
CheckConstraint("length(name) <= 512", name="name_length"),
CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
)
def __repr__(self) -> str:
return f"Peer(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, created_at={self.created_at}, h_metadata={self.h_metadata}, configuration={self.configuration})"
@final
class Session(Base):
__tablename__: str = "sessions"
id: Mapped[str] = mapped_column(TEXT, primary_key=[REDACTED] default=generate_nanoid)
name: Mapped[str] = mapped_column(TEXT)
is_active: Mapped[bool] = mapped_column(default=True, server_default=text("true"))
h_metadata: Mapped[dict[str, Any]] = mapped_column(
"metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
configuration: Mapped[dict[str, Any]] = mapped_column(
JSONB, default=dict, server_default=text("'{}'::jsonb")
)
workspace = relationship("Workspace", back_populates="sessions")
peers = relationship(
"Peer", secondary=session_peers_table, back_populates="sessions"
)
messages = relationship("Message", back_populates="session")
__table_args__ = (
UniqueConstraint("name", "workspace_name"),
CheckConstraint("length(name) <= 512", name="name_length"),
CheckConstraint("length(id) = 21", name="id_length"),
CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
)
def __repr__(self) -> str:
return f"Session(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, is_active={self.is_active}, created_at={self.created_at}, h_metadata={self.h_metadata})"
@final
class Message(Base):
__tablename__: str = "messages"
id: Mapped[int] = mapped_column(
BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
)
public_id: Mapped[str] = mapped_column(
TEXT,
unique=True,
default=generate_nanoid,
)
# NOTE: Messages in Honcho 2.0 could historically be stored outside of a session.
# We have since assigned all of these messages to a default session.
session_name: Mapped[str] = mapped_column(TEXT, nullable=False)
content: Mapped[str] = mapped_column(TEXT)
h_metadata: Mapped[dict[str, Any]] = mapped_column(
"metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
token_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
seq_in_session: Mapped[int] = mapped_column(BigInteger, nullable=False)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
# Note: Foreign key relationships established via composite ForeignKeyConstraint below
peer_name: Mapped[str] = mapped_column(TEXT, index=True)
workspace_name: Mapped[str] = mapped_column(TEXT, index=True)
session = relationship("Session", back_populates="messages")
__table_args__ = (
CheckConstraint("length(public_id) = 21", name="public_id_length"),
CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
CheckConstraint("length(content) <= 65535", name="content_length"),
# Composite foreign key constraint for sessions
ForeignKeyConstraint(
["session_name", "workspace_name"],
["sessions.name", "sessions.workspace_name"],
),
# Composite foreign key constraint for peers
ForeignKeyConstraint(
["peer_name", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
Index(
"ix_messages_session_lookup",
"session_name",
"id",
postgresql_include=["id", "created_at"],
),
UniqueConstraint(
"workspace_name",
"session_name",
"seq_in_session",
),
# Full text search index on content column
Index(
"ix_messages_content_gin",
text("to_tsvector('english', content)"),
postgresql_using="gin",
),
)
@override
def __repr__(self) -> str:
return f"Message(id={self.id}, session_name={self.session_name}, peer_name={self.peer_name}, content={self.content})"
@final
class MessageEmbedding(Base):
__tablename__: str = "message_embeddings"
id: Mapped[int] = mapped_column(
>sed -n '1,260p' src/utils/representation.pyChunk ID: 7366d7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2511
Output:
from collections.abc import Sequence
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field, field_validator
from src import models
from src.utils.formatting import parse_datetime_iso
def _strip_microseconds_and_timezone(timestamp: datetime) -> datetime:
"""
Remove microseconds and timezone info from a datetime for stable string formatting.
"""
return timestamp.replace(microsecond=0, tzinfo=None)
def flatten_message_ids(
message_ids: list[int] | list[list[int]] | list[tuple[int, int]],
) -> list[int]:
"""
Flatten message_ids that may be in old tuple format or nested list format.
This handles backwards compatibility with the old schema where message_ids
was list[tuple[int, int]] representing ranges, and the new schema where
it's list[int] representing individual message IDs.
Args:
message_ids: Either a flat list of ints, nested list, or list of tuples
Returns:
A flat list of unique message IDs, sorted
Examples:
[1, 2, 3] -> [1, 2, 3]
[[1, 2], [3, 4]] -> [1, 2, 3, 4]
[(105, 105)] -> [105]
[[105, 105]] -> [105]
"""
result: list[int] = []
for item in message_ids:
if isinstance(item, (list | tuple)):
# Nested list or tuple - flatten it
result.extend(item)
else:
# Already flat
result.append(item)
# Remove duplicates and sort
return sorted(set(result))
class ObservationMetadata(BaseModel):
id: str = Field(default="", description="Document ID for this observation")
created_at: datetime
message_ids: list[int]
session_name: str | None = None
class ExplicitObservationBase(BaseModel):
content: str = Field(description="The explicit observation")
class DeductiveObservationBase(BaseModel):
source_ids: list[str] = Field(
description="Document IDs of premise observations for tree traversal",
default_factory=list,
)
premises: list[str] = Field(
description="Human-readable premise text for display",
default_factory=list,
)
conclusion: str = Field(description="The deductive conclusion")
class InductiveObservationBase(BaseModel):
"""Base model for inductive observations - patterns, generalizations, and personality insights."""
source_ids: list[str] = Field(
description="Document IDs of source observations for tree traversal",
default_factory=list,
)
sources: list[str] = Field(
description="Human-readable source text for display",
default_factory=list,
)
pattern_type: str = Field(
description="Type of pattern: 'preference', 'behavior', 'personality', 'tendency', 'correlation'",
default="pattern",
)
conclusion: str = Field(description="The inductive generalization or pattern")
confidence: str = Field(
description="Confidence level: 'high', 'medium', 'low'",
default="medium",
)
class ContradictionObservationBase(BaseModel):
"""Base model for contradiction observations - when user has made conflicting statements."""
source_ids: list[str] = Field(
description="Document IDs of the contradicting observations",
default_factory=list,
)
sources: list[str] = Field(
description="Human-readable text of the contradicting statements",
default_factory=list,
)
content: str = Field(description="Description of the contradiction")
class PromptRepresentation(BaseModel):
"""
The representation format that is used when getting structured output from an LLM.
"""
explicit: list[ExplicitObservationBase] = Field(
description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: ['The user is 25 years old', 'The user has a dog named Rover']",
default_factory=list,
)
@field_validator("explicit", mode="before")
@classmethod
def convert_none_to_empty_list(cls, v: Any) -> Any:
"""Convert None to empty list - handles LLMs returning null instead of []."""
if v is None:
return []
return v
class ExplicitObservation(ExplicitObservationBase, ObservationMetadata):
"""Explicit observation with content and metadata."""
def __str__(self) -> str:
return f"[{_strip_microseconds_and_timezone(self.created_at)}] {self.content}"
def str_with_id(self) -> str:
"""Format with ID prefix for use by agents that need to reference observations."""
id_prefix = f"[id:{self.id}] " if self.id else ""
return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] {self.content}"
def __hash__(self) -> int:
"""
Make ExplicitObservation hashable for use in sets.
"""
return hash((self.content, self.created_at, self.session_name))
def __eq__(self, other: object) -> bool:
"""
Define equality for ExplicitObservation objects.
Two observations are equal if all their fields match.
"""
if not isinstance(other, ExplicitObservation):
return False
return (
self.content == other.content
and self.created_at == other.created_at
and self.session_name == other.session_name
)
class DeductiveObservation(DeductiveObservationBase, ObservationMetadata):
"""Deductive observation with multiple premises and one conclusion, plus metadata."""
def __str__(self) -> str:
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
return f"[{_strip_microseconds_and_timezone(self.created_at)}] {self.conclusion}\n{premises_text}"
def str_with_id(self) -> str:
"""Format with ID prefix for use by agents that need to reference observations."""
id_prefix = f"[id:{self.id}] " if self.id else ""
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] {self.conclusion}\n{premises_text}"
def str_no_timestamps(self) -> str:
premises_text = "\n".join(f" - {premise}" for premise in self.premises)
return f"{self.conclusion}\n{premises_text}"
def __hash__(self) -> int:
"""
Make DeductiveObservation hashable for use in sets. NOTE: premises are not included in the hash.
"""
return hash((self.conclusion, self.created_at, self.session_name))
def __eq__(self, other: object) -> bool:
"""
Define equality for DeductiveObservation objects.
Two observations are equal if all their fields match -- NOTE: premises are not included in the equality check.
"""
if not isinstance(other, DeductiveObservation):
return False
return (
self.conclusion == other.conclusion
and self.created_at == other.created_at
and self.session_name == other.session_name
)
class InductiveObservation(InductiveObservationBase, ObservationMetadata):
"""Inductive observation with sources, pattern type, and confidence, plus metadata."""
def __str__(self) -> str:
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"[{_strip_microseconds_and_timezone(self.created_at)}] [{self.confidence}] {self.conclusion}{sources_text}"
def str_with_id(self) -> str:
"""Format with ID prefix for use by agents that need to reference observations."""
id_prefix = f"[id:{self.id}] " if self.id else ""
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] [{self.confidence}] {self.conclusion}{sources_text}"
def str_no_timestamps(self) -> str:
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"[{self.confidence}] {self.conclusion}{sources_text}"
def __hash__(self) -> int:
"""Make InductiveObservation hashable for use in sets."""
return hash((self.conclusion, self.created_at, self.session_name))
def __eq__(self, other: object) -> bool:
"""Define equality for InductiveObservation objects."""
if not isinstance(other, InductiveObservation):
return False
return (
self.conclusion == other.conclusion
and self.created_at == other.created_at
and self.session_name == other.session_name
)
class ContradictionObservation(ContradictionObservationBase, ObservationMetadata):
"""Contradiction observation - notes when user has made conflicting statements, plus metadata."""
def __str__(self) -> str:
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
def str_with_id(self) -> str:
"""Format with ID prefix for use by agents that need to reference observations."""
id_prefix = f"[id:{self.id}] " if self.id else ""
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
def str_no_timestamps(self) -> str:
sources_text = ""
if self.sources:
>sed -n '1,280p' src/schemas/configuration.pyChunk ID: 6262a5
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1826
Output:
"""Configuration schemas for hierarchical settings resolution.
Covers workspace, session, and message-level configuration as well as
the fully-resolved variants used at runtime.
"""
from enum import Enum
from typing import Any, Self, cast
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from src.config import settings
from src.utils.tokens import estimate_tokens
class DreamType(str, Enum):
"""Types of dreams that can be triggered."""
OMNI = "omni"
class ReasoningConfiguration(BaseModel):
enabled: bool | None = Field(
default=None,
description="Whether to enable reasoning functionality.",
)
custom_instructions: str | None = Field(
default=None,
description="Optional custom instructions for the reasoning system on this workspace/session/message. Rejected if they exceed the deriver custom-instruction token cap.",
)
@field_validator("custom_instructions")
@classmethod
def validate_custom_instructions(cls, value: str | None) -> str | None:
return _validate_custom_instructions_budget(value)
class PeerCardConfiguration(BaseModel):
use: bool | None = Field(
default=None,
description="Whether to use peer card related to this peer during reasoning process.",
)
create: bool | None = Field(
default=None,
description="Whether to generate peer card based on content.",
)
class SummaryConfiguration(BaseModel):
enabled: bool | None = Field(
default=None,
description="Whether to enable summary functionality.",
)
messages_per_short_summary: int | None = Field(
default=None,
ge=10,
description="Number of messages per short summary. Must be positive, greater than or equal to 10, and less than messages_per_long_summary.",
)
messages_per_long_summary: int | None = Field(
default=None,
ge=20,
description="Number of messages per long summary. Must be positive, greater than or equal to 20, and greater than messages_per_short_summary.",
)
@model_validator(mode="after")
def validate_summary_thresholds(self) -> Self:
"""Validate that short summary threshold <= long summary threshold."""
short = self.messages_per_short_summary
long = self.messages_per_long_summary
if short is not None and long is not None and short >= long:
raise ValueError(
"messages_per_short_summary must be less than messages_per_long_summary"
)
return self
class DreamConfiguration(BaseModel):
enabled: bool | None = Field(
default=None,
description="Whether to enable dream functionality. If reasoning is disabled, dreams will also be disabled and this setting will be ignored.",
)
def _validate_custom_instructions_budget(
custom_instructions: str | None,
) -> str | None:
if custom_instructions is None:
return None
if not custom_instructions.strip():
return custom_instructions
max_tokens = settings.DERIVER.MAX_CUSTOM_INSTRUCTIONS_TOKENS
if max_tokens <= 0:
raise ValueError("custom_instructions are not enabled for this deployment")
if estimate_tokens(custom_instructions) > max_tokens:
raise ValueError(
f"custom_instructions exceeds DERIVER.MAX_CUSTOM_INSTRUCTIONS_TOKENS ({max_tokens} tokens)"
)
return custom_instructions
class WorkspaceConfiguration(BaseModel):
"""
The set of options that can be in a workspace DB-level configuration dictionary.
All fields are optional. Session-level configuration overrides workspace-level configuration, which overrides global configuration.
"""
model_config = ConfigDict(extra="allow") # pyright: ignore
reasoning: ReasoningConfiguration | None = Field(
default=None,
description="Configuration for reasoning functionality.",
)
peer_card: PeerCardConfiguration | None = Field(
default=None,
description="Configuration for peer card functionality. If reasoning is disabled, peer cards will also be disabled and these settings will be ignored.",
)
summary: SummaryConfiguration | None = Field(
default=None,
description="Configuration for summary functionality.",
)
dream: DreamConfiguration | None = Field(
default=None,
description="Configuration for dream functionality. If reasoning is disabled, dreams will also be disabled and these settings will be ignored.",
)
class SessionConfiguration(WorkspaceConfiguration):
"""
The set of options that can be in a session DB-level configuration dictionary.
All fields are optional. Session-level configuration overrides workspace-level configuration, which overrides global configuration.
"""
pass
class MessageConfiguration(BaseModel):
"""
The set of options that can be in a message DB-level configuration dictionary.
All fields are optional. Message-level configuration overrides all other configurations.
"""
reasoning: ReasoningConfiguration | None = Field(
default=None,
description="Configuration for reasoning functionality.",
)
class ResolvedReasoningConfiguration(BaseModel):
enabled: bool
custom_instructions: str | None = None
@field_validator("custom_instructions")
@classmethod
def validate_custom_instructions(cls, value: str | None) -> str | None:
return _validate_custom_instructions_budget(value)
class ResolvedPeerCardConfiguration(BaseModel):
use: bool
create: bool
class ResolvedSummaryConfiguration(BaseModel):
enabled: bool
messages_per_short_summary: int
messages_per_long_summary: int
class ResolvedDreamConfiguration(BaseModel):
enabled: bool
class ResolvedConfiguration(BaseModel):
"""
The final resolved configuration for a given message.
Hierarchy: message > session > workspace > global configuration
"""
reasoning: ResolvedReasoningConfiguration
peer_card: ResolvedPeerCardConfiguration
summary: ResolvedSummaryConfiguration
dream: ResolvedDreamConfiguration
@model_validator(mode="before")
@classmethod
def migrate_deriver_to_reasoning(cls, data: Any) -> Any:
"""Handle v3.0.0 migration: 'deriver' was renamed to 'reasoning'."""
if not isinstance(data, dict):
return data
config = cast(dict[str, Any], data)
if "deriver" in config and "reasoning" not in config:
config["reasoning"] = config.pop("deriver")
return config
class PeerConfig(BaseModel):
# TODO: Update description - should say "Whether honcho forms a representation of the peer itself"
observe_me: bool | None = Field(
default=None,
description="Whether Honcho will use reasoning to form a representation of this peer",
)
class SessionPeerConfig(PeerConfig):
# TODO: Update description - should say "Whether this peer forms representations of other peers in the session"
observe_others: bool | None = Field(
default=None,
description="Whether this peer should form a session-level theory-of-mind representation of other peers in the session",
)
>sed -n '1,240p' src/utils/queue_payload.pyChunk ID: 1e9d49
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1678
Output:
from datetime import datetime
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict
from src.schemas import DreamType, ReconcilerType, ResolvedConfiguration
class BasePayload(BaseModel):
"""Base payload with common fields."""
model_config = ConfigDict(extra="forbid") # pyright: ignore[reportUnannotatedClassAttribute]
class RepresentationPayload(BasePayload):
"""Payload for representation tasks."""
task_type: Literal["representation"] = "representation"
session_name: str
content: str
observers: list[str]
observed: str
created_at: datetime
configuration: ResolvedConfiguration
class RepresentationPayloads(BasePayload):
"""Payload for a batch of representation tasks."""
payloads: list[RepresentationPayload]
class SummaryPayload(BasePayload):
"""Payload for summary tasks."""
task_type: Literal["summary"] = "summary"
session_name: str
message_seq_in_session: int
configuration: ResolvedConfiguration
# Optional for backward compatibility with older queue items
message_public_id: str | None = None
class WebhookPayload(BasePayload):
"""Payload for webhook delivery tasks."""
task_type: Literal["webhook"] = "webhook"
event_type: str
data: dict[str, Any]
class DreamPayload(BasePayload):
"""Payload for dream tasks."""
task_type: Literal["dream"] = "dream"
dream_type: DreamType
observer: str
observed: str
session_name: str | None = None
class DeletionPayload(BasePayload):
"""Payload for deletion tasks."""
task_type: Literal["deletion"] = "deletion"
deletion_type: Literal["session", "observation", "workspace"]
resource_id: str
class ReconcilerPayload(BasePayload):
"""Payload for reconciler tasks (vector sync, queue cleanup, self-healing)."""
task_type: Literal["reconciler"] = "reconciler"
reconciler_type: ReconcilerType
def create_webhook_payload(
event_type: str,
data: dict[str, Any],
) -> dict[str, Any]:
"""Create a webhook payload."""
return WebhookPayload(event_type=event_type, data=data).model_dump(
mode="json", exclude_none=True
)
def create_dream_payload(
dream_type: DreamType,
*,
observer: str,
observed: str,
session_name: str | None = None,
) -> dict[str, Any]:
"""Create a dream payload."""
return DreamPayload(
dream_type=dream_type,
observer=observer,
observed=observed,
session_name=session_name,
).model_dump(mode="json", exclude_none=True)
def create_deletion_payload(
deletion_type: Literal["session", "observation", "workspace"],
resource_id: str,
) -> dict[str, Any]:
"""Create a deletion payload."""
return DeletionPayload(
deletion_type=deletion_type,
resource_id=resource_id,
).model_dump(mode="json", exclude_none=True)
def create_payload(
message: dict[str, Any],
configuration: ResolvedConfiguration,
task_type: Literal["representation", "summary"],
message_seq_in_session: int | None = None,
*,
observers: list[str] | None = None,
observed: str | None = None,
) -> dict[str, Any]:
"""
Create a processed payload from a message for queue processing.
Note: workspace_name and message_id are no longer included in the returned payload
as they are now stored in dedicated columns on the queue table. The caller is
responsible for extracting and passing these values separately.
Args:
message: The original message dictionary
task_type: Type of task ('representation' or 'summary')
message_seq_in_session: Required for summary tasks, must be None for representation
observers: List of observer peer names (required for representation tasks)
observed: Name of the observed peer (*always* the peer who sent the message) (required for representation tasks)
Returns:
Processed payload dictionary ready for queue processing (without workspace_name and message_id)
Raises:
ValueError: If the payload doesn't match the expected schema
"""
workspace_name = message.get("workspace_name")
session_name = message.get("session_name")
message_id = message.get("message_id")
if not isinstance(workspace_name, str):
raise TypeError("Workspace name must be a string")
if not isinstance(session_name, str):
raise TypeError("Session name must be a string")
if not isinstance(message_id, int):
raise TypeError("Message ID must be an integer")
# Create the appropriate payload type based on task_type
try:
if task_type == "representation":
content = message.get("content")
created_at = message.get("created_at")
if not isinstance(content, str):
raise TypeError("Message content must be a string")
if not isinstance(created_at, datetime):
raise TypeError("created_at must be a datetime object")
if observers is None or len(observers) == 0:
raise ValueError("observers is required for representation tasks")
if observed is None:
raise ValueError("observed is required for representation tasks")
validated_payload = RepresentationPayload(
content=content,
session_name=session_name,
created_at=created_at,
observers=observers,
observed=observed,
configuration=configuration,
)
elif task_type == "summary":
if message_seq_in_session is None:
raise ValueError("message_seq_in_session is required for summary tasks")
message_public_id = message.get("message_public_id")
if message_public_id is not None and (
not isinstance(message_public_id, str) or not message_public_id.strip()
):
raise ValueError(
"message_public_id must be a non-empty string if provided"
)
validated_payload = SummaryPayload(
session_name=session_name,
message_seq_in_session=message_seq_in_session,
configuration=configuration,
message_public_id=message_public_id,
)
# Convert back to dict for compatibility with JSON serialization
# mode='json' ensures datetime is converted to ISO string
payload = validated_payload.model_dump(mode="json", exclude_none=True)
except Exception as e:
raise ValueError(f"Failed to create valid payload: {str(e)}") from e
return payload
>sed -n '280,620p' src/models.pyChunk ID: 674ca7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2840
Output:
id: Mapped[int] = mapped_column(
BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
)
content: Mapped[str] = mapped_column(TEXT)
embedding: MappedColumn[Any] = mapped_column(Vector(_VECTOR_DIM), nullable=True)
message_id: Mapped[str] = mapped_column(
ForeignKey("messages.public_id", ondelete="CASCADE"), nullable=False, index=True
)
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
session_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
peer_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
# Vector sync state tracking
sync_state: Mapped[VectorSyncState] = mapped_column(
TEXT, nullable=False, server_default="pending", index=True
)
last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
sync_attempts: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default=text("0")
)
__table_args__ = (
# Compound foreign key constraints
ForeignKeyConstraint(
["session_name", "workspace_name"],
["sessions.name", "sessions.workspace_name"],
),
ForeignKeyConstraint(
["peer_name", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
# HNSW index on embedding column for efficient similarity search
Index(
"ix_message_embeddings_embedding_hnsw",
"embedding",
postgresql_using="hnsw",
postgresql_with={"m": 16, "ef_construction": 64},
postgresql_ops={"embedding": "vector_cosine_ops"},
),
# Composite index for efficient reconciliation queries
Index(
"ix_message_embeddings_sync_state_last_sync_at",
"sync_state",
"last_sync_at",
),
)
@final
class Collection(Base):
__tablename__: str = "collections"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
observer: Mapped[str] = mapped_column(TEXT, index=True)
observed: Mapped[str] = mapped_column(TEXT, index=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
h_metadata: Mapped[dict[str, Any]] = mapped_column(
"metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
documents = relationship(
"Document", back_populates="collection", cascade="all, delete, delete-orphan"
)
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
__table_args__ = (
UniqueConstraint(
"observer",
"observed",
"workspace_name",
),
CheckConstraint("length(id) = 21", name="id_length"),
CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
# Composite foreign key constraint for observer peer
ForeignKeyConstraint(
["observer", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
# Composite foreign key constraint for observed peer
ForeignKeyConstraint(
["observed", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
)
@final
class Document(Base):
__tablename__: str = "documents"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
internal_metadata: Mapped[dict[str, Any]] = mapped_column(
"internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
)
content: Mapped[str] = mapped_column(TEXT)
level: Mapped[DocumentLevel] = mapped_column(
TEXT, nullable=False, server_default="explicit"
)
times_derived: Mapped[int] = mapped_column(
Integer, nullable=False, server_default=text("1")
)
embedding: MappedColumn[Any] = mapped_column(Vector(_VECTOR_DIM), nullable=True)
source_ids: Mapped[list[str] | None] = mapped_column(
JSONB, nullable=True, server_default=text("NULL")
)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
observer: Mapped[str] = mapped_column(TEXT, index=True)
observed: Mapped[str] = mapped_column(TEXT, index=True)
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
session_name: Mapped[str | None] = mapped_column(TEXT, nullable=True, index=True)
deleted_at: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True, index=True, default=None
)
# Vector sync state tracking
sync_state: Mapped[VectorSyncState] = mapped_column(
TEXT, nullable=False, server_default="pending", index=True
)
last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
DateTime(timezone=True), nullable=True
)
sync_attempts: Mapped[int] = mapped_column(
Integer, nullable=False, default=0, server_default=text("0")
)
collection = relationship("Collection", back_populates="documents")
__table_args__ = (
CheckConstraint("length(id) = 21", name="id_length"),
CheckConstraint("length(content) <= 65535", name="content_length"),
CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
# Composite foreign key constraint for collections
ForeignKeyConstraint(
["observer", "observed", "workspace_name"],
[
"collections.observer",
"collections.observed",
"collections.workspace_name",
],
),
# Composite foreign key constraint for observer peer
ForeignKeyConstraint(
["observer", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
# Composite foreign key constraint for observed peer
ForeignKeyConstraint(
["observed", "workspace_name"],
["peers.name", "peers.workspace_name"],
),
# Composite foreign key constraint for sessions
ForeignKeyConstraint(
["session_name", "workspace_name"],
["sessions.name", "sessions.workspace_name"],
),
# HNSW index on embedding column
Index(
"ix_documents_embedding_hnsw",
"embedding",
postgresql_using="hnsw", # HNSW index type
postgresql_with={"m": 16, "ef_construction": 64}, # HNSW parameters
postgresql_ops={
"embedding": "vector_cosine_ops"
}, # Cosine distance operator
),
# GIN index for efficient tree traversal (finding children by source IDs)
Index(
"ix_documents_source_ids_gin",
"source_ids",
postgresql_using="gin",
),
# Composite index for efficient reconciliation queries
Index(
"ix_documents_sync_state_last_sync_at",
"sync_state",
"last_sync_at",
),
)
@final
class QueueItem(Base):
__tablename__: str = "queue"
id: Mapped[int] = mapped_column(
BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
)
session_id: Mapped[str | None] = mapped_column(
ForeignKey("sessions.id"), nullable=True, index=True
)
work_unit_key: Mapped[str] = mapped_column(TEXT, nullable=False)
task_type: Mapped[TaskType] = mapped_column(TEXT, nullable=False)
payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
processed: Mapped[bool] = mapped_column(
Boolean, default=False, server_default=text("false"), index=True
)
error: Mapped[str | None] = mapped_column(TEXT, nullable=True)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), index=True
)
workspace_name: Mapped[str | None] = mapped_column(
ForeignKey("workspaces.name"), nullable=True, index=True
)
message_id: Mapped[int | None] = mapped_column(
BigInteger, ForeignKey("messages.id"), nullable=True
)
__table_args__ = (
Index(
"ix_queue_message_id_not_null",
"message_id",
postgresql_where=text("message_id IS NOT NULL"),
),
Index(
"ix_queue_work_unit_key_processed_id",
"work_unit_key",
"processed",
"id",
),
# Partial unique index for reconciler task deduplication
Index(
"uq_queue_reconciler_pending_work_unit_key",
"work_unit_key",
unique=True,
postgresql_where=text("task_type = 'reconciler' AND processed = false"),
),
# Partial unique index for dream task deduplication
Index(
"uq_queue_dream_pending_work_unit_key",
"work_unit_key",
unique=True,
postgresql_where=text("task_type = 'dream' AND processed = false"),
),
)
def __repr__(self) -> str:
return f"QueueItem(id={self.id}, session_id={self.session_id}, work_unit_key=[REDACTED] task_type={self.task_type}, payload={self.payload}, processed={self.processed}, workspace_name={self.workspace_name}, message_id={self.message_id})"
@final
class ActiveQueueSession(Base):
__tablename__: str = "active_queue_sessions"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
work_unit_key: Mapped[str] = mapped_column(TEXT, unique=True)
last_updated: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
@final
class WebhookEndpoint(Base):
__tablename__: str = "webhook_endpoints"
id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
workspace_name: Mapped[str] = mapped_column(
ForeignKey("workspaces.name"), nullable=False, index=True
)
url: Mapped[str] = mapped_column(TEXT, nullable=False)
created_at: Mapped[datetime.datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
workspace = relationship("Workspace", back_populates="webhook_endpoints")
__table_args__ = (CheckConstraint("length(url) <= 2048", name="url_length"),)
def __repr__(self) -> str:
return f"WebhookEndpoint(id={self.id}, workspace_name={self.workspace_name}, url={self.url})"
@final
class SessionPeer(Base):
__table__: Table = session_peers_table
# Type annotations for the columns
workspace_name: Mapped[str]
session_name: Mapped[str]
peer_name: Mapped[str]
configuration: Mapped[dict[str, Any]]
internal_metadata: Mapped[dict[str, Any]]
joined_at: Mapped[datetime.datetime]
left_at: Mapped[datetime.datetime | None]
>sed -n '240,520p' src/utils/representation.pyChunk ID: 390b27
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2868
Output:
"""Contradiction observation - notes when user has made conflicting statements, plus metadata."""
def __str__(self) -> str:
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
def str_with_id(self) -> str:
"""Format with ID prefix for use by agents that need to reference observations."""
id_prefix = f"[id:{self.id}] " if self.id else ""
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
def str_no_timestamps(self) -> str:
sources_text = ""
if self.sources:
source_lines = [f" - {source}" for source in self.sources]
sources_text = "\n" + "\n".join(source_lines)
return f"CONTRADICTION: {self.content}{sources_text}"
def __hash__(self) -> int:
"""Make ContradictionObservation hashable for use in sets."""
return hash((self.content, self.created_at, self.session_name))
def __eq__(self, other: object) -> bool:
"""Define equality for ContradictionObservation objects."""
if not isinstance(other, ContradictionObservation):
return False
return (
self.content == other.content
and self.created_at == other.created_at
and self.session_name == other.session_name
)
class Representation(BaseModel):
"""
A Representation is a traversable and diffable map of observations.
At the base, we have a list of explicit observations, derived from a peer's messages.
From there, deductive observations can be made by establishing logical relationships between explicit observations.
In the future, we can add more levels of reasoning on top of these.
All of a peer's observations are stored as documents in a collection. These documents can be queried in various ways
to produce this Representation object.
Additionally, a "working representation" is a version of this data structure representing the most recent observations
within a single session.
A representation can have a maximum number of observations, which is applied individually to each level of reasoning.
If a maximum is set, observations are added and removed in FIFO order.
"""
explicit: list[ExplicitObservation] = Field(
description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: ['The user is 25 years old', 'The user has a dog']",
default_factory=list,
)
deductive: list[DeductiveObservation] = Field(
description="Conclusions that MUST be true given explicit facts and premises - strict logical necessities. Each deduction should have premises and a single conclusion.",
default_factory=list,
)
inductive: list[InductiveObservation] = Field(
description="Patterns, generalizations, and personality insights inferred from multiple observations. Higher-level reasoning created by the Dreamer agent.",
default_factory=list,
)
contradiction: list[ContradictionObservation] = Field(
description="Conflicting statements made by the user that need clarification. The dialectic agent should surface these when relevant.",
default_factory=list,
)
def is_empty(self) -> bool:
"""
Check if the representation is empty.
"""
return (
len(self.explicit) == 0
and len(self.deductive) == 0
and len(self.inductive) == 0
and len(self.contradiction) == 0
)
def len(self) -> int:
"""
Return the total number of observations in the representation.
"""
return (
len(self.explicit)
+ len(self.deductive)
+ len(self.inductive)
+ len(self.contradiction)
)
def diff_representation(self, other: "Representation") -> "Representation":
"""
Given this and another representation, return a new representation with only observations that are unique to the other.
Note that this only removes literal duplicates, not semantically equivalent ones.
"""
diff = Representation()
diff.explicit = [o for o in other.explicit if o not in self.explicit]
diff.deductive = [o for o in other.deductive if o not in self.deductive]
diff.inductive = [o for o in other.inductive if o not in self.inductive]
diff.contradiction = [
o for o in other.contradiction if o not in self.contradiction
]
return diff
def merge_representation(
self, other: "Representation", max_observations: int | None = None
):
"""
Merge another representation object into this one.
This will automatically deduplicate explicit, deductive, inductive, and contradiction observations.
This *preserves order* of observations so that they retain FIFO order.
NOTE: observations with the *same* timestamp will not have order preserved.
That's fine though, because they are from the same timestamp...
"""
# removing duplicates by going list->set->list
self.explicit = list(set(self.explicit + other.explicit))
self.deductive = list(set(self.deductive + other.deductive))
self.inductive = list(set(self.inductive + other.inductive))
self.contradiction = list(set(self.contradiction + other.contradiction))
# sort by created_at
self.explicit.sort(key=lambda x: x.created_at)
self.deductive.sort(key=lambda x: x.created_at)
self.inductive.sort(key=lambda x: x.created_at)
self.contradiction.sort(key=lambda x: x.created_at)
if max_observations:
self.explicit = self.explicit[-max_observations:]
self.deductive = self.deductive[-max_observations:]
self.inductive = self.inductive[-max_observations:]
self.contradiction = self.contradiction[-max_observations:]
def __str__(self) -> str:
"""
Format representation into a clean, readable string for LLM prompts.
NOTE: we always strip subsecond precision from the timestamps.
Returns:
Formatted string with clear sections and bullet points including temporal metadata
Example:
EXPLICIT:
1. [2025-01-01 12:00:00] The user has a dog named Rover
2. [2025-01-01 12:01:00] The user's dog is 5 years old
3. [2025-01-01 12:05:00] The user is 25 years old
DEDUCTIVE:
1. [2025-01-01 12:01:00] Rover is 5 years old
- The user has a dog named Rover
- The user's dog is 5 years old
"""
parts: list[str] = []
parts.append("EXPLICIT:\n")
for i, observation in enumerate(self.explicit, 1):
parts.append(f"{i}. {observation}")
parts.append("")
parts.append("DEDUCTIVE:\n")
for i, observation in enumerate(self.deductive, 1):
parts.append(f"{i}. {observation}")
parts.append("")
parts.append("INDUCTIVE:\n")
for i, observation in enumerate(self.inductive, 1):
parts.append(f"{i}. {observation}")
parts.append("")
parts.append("CONTRADICTION:\n")
for i, observation in enumerate(self.contradiction, 1):
parts.append(f"{i}. {observation}")
parts.append("")
return "\n".join(parts)
def str_with_ids(self) -> str:
"""
Format representation with observation IDs for agents that need to reference/delete observations.
Returns:
Formatted string with IDs included
Example:
EXPLICIT:
1. [id:abc123] [2025-01-01 12:00:00] The user has a dog named Rover
2. [id:def456] [2025-01-01 12:01:00] The user's dog is 5 years old
DEDUCTIVE:
1. [id:ghi789] [2025-01-01 12:01:00] Rover is 5 years old
- The user has a dog named Rover
- The user's dog is 5 years old
INDUCTIVE:
1. [id:jkl012] [2025-01-01 12:05:00] [high] User tends to be methodical
- id:abc123
- id:def456
"""
parts: list[str] = []
parts.append("EXPLICIT:\n")
for i, observation in enumerate(self.explicit, 1):
parts.append(f"{i}. {observation.str_with_id()}")
parts.append("")
parts.append("DEDUCTIVE:\n")
for i, observation in enumerate(self.deductive, 1):
parts.append(f"{i}. {observation.str_with_id()}")
parts.append("")
parts.append("INDUCTIVE:\n")
for i, observation in enumerate(self.inductive, 1):
parts.append(f"{i}. {observation.str_with_id()}")
parts.append("")
parts.append("CONTRADICTION:\n")
for i, observation in enumerate(self.contradiction, 1):
parts.append(f"{i}. {observation.str_with_id()}")
parts.append("")
return "\n".join(parts)
def str_no_timestamps(self) -> str:
"""
Format representation into a clean, readable string for LLM prompts... but without timestamps.
Returns:
Formatted string with clear sections and bullet points including temporal metadata
Example:
EXPLICIT:
1. The user has a dog named Rover
2. The user's dog is 5 years old
3. The user is 25 years old
DEDUCTIVE:
1. Rover is 5 years old
- The user has a dog named Rover
- The user's dog is 5 years old
INDUCTIVE:
1. [high] User tends to be methodical
- id:abc123
- id:def456
"""
parts: list[str] = []
parts.append("EXPLICIT:\n")
for i, observation in enumerate(self.explicit, 1):
parts.append(f"{i}. {observation.content}")
parts.append("")
parts.append("DEDUCTIVE:\n")
for i, observation in enumerate(self.deductive, 1):
parts.append(f"{i}. {observation.str_no_timestamps()}")
parts.append("")
parts.append("INDUCTIVE:\n")
for i, observation in enumerate(self.inductive, 1):
parts.append(f"{i}. {observation.str_no_timestamps()}")
parts.append("")
parts.append("CONTRADICTION:\n")
for i, observation in enumerate(self.contradiction, 1):
parts.append(f"{i}. {observation.str_no_timestamps()}")
parts.append("")
return "\n".join(parts)
def format_as_markdown(self, include_ids: bool = False) -> str:
"""
Format a Representation object as markdown.
NOTE: we always strip subsecond precision from the timestamps.
Args:
include_ids: If True, include observation IDs for use with get_reasoning_chain
Returns:
Formatted markdown string
>sed -n '1,320p' src/deriver/enqueue.pyChunk ID: 5a8069
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2688
Output:
import logging
from typing import Any, Literal
from sqlalchemy import exists, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from src import crud, models, schemas
from src.config import settings
from src.dependencies import tracked_db
from src.dreamer.dream_scheduler import get_dream_scheduler
from src.exceptions import ValidationException
from src.models import QueueItem
from src.schemas import MessageConfiguration, ResolvedConfiguration
from src.utils.config_helpers import get_configuration
from src.utils.queue_payload import (
create_deletion_payload,
create_dream_payload,
create_payload,
)
from src.utils.work_unit import construct_work_unit_key
logger = logging.getLogger(__name__)
async def enqueue(payload: list[dict[str, Any]]) -> None:
"""
Add message(s) to the deriver queue for processing.
Args:
payload: List of message payload dictionaries
"""
# Cancel any pending dreams for affected collections since user is active again.
# This cancels dreams for all collections where observed=peer_name, which covers
# both self-observation and peer-to-peer observation cases.
dream_scheduler = get_dream_scheduler()
if dream_scheduler and payload:
cancelled_dreams: set[str] = set()
for message in payload:
workspace_name = message.get("workspace_name")
peer_name = message.get("peer_name")
if workspace_name and peer_name:
cancelled = await dream_scheduler.cancel_dreams_for_observed(
workspace_name, peer_name
)
cancelled_dreams.update(cancelled)
if cancelled_dreams:
logger.info(
f"Cancelled {len(cancelled_dreams)} pending dreams due to new activity"
)
async with tracked_db("message_enqueue") as db_session:
try:
# Determine if batch or single processing
if not payload: # Empty list check
return
workspace_name = payload[0]["workspace_name"]
session_name = payload[0]["session_name"]
if session_name is None or workspace_name is None:
raise ValidationException("Session and workspace are required")
queue_records = await handle_session(
db_session, payload, workspace_name, session_name
)
if queue_records:
stmt = insert(QueueItem).returning(QueueItem)
await db_session.execute(stmt, queue_records)
await db_session.commit()
except Exception as e:
logger.exception("Failed to enqueue message(s)!")
if settings.SENTRY.ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
async def handle_session(
db_session: AsyncSession,
payload: list[dict[str, Any]],
workspace_name: str,
session_name: str,
) -> list[dict[str, Any]]:
"""
Handle enqueueing for normal session cases, creating appropriate queue items based on configurations.
Args:
db_session: The database session
payload: List of message payloads
workspace_name: Name of the workspace
session_name: Name of the session
Returns:
List of queue records to insert
"""
session = (
await crud.get_or_create_session(
db_session,
session=schemas.SessionCreate(name=session_name),
workspace_name=workspace_name,
)
).resource
# Fetch workspace for configuration resolution
workspace = await crud.get_workspace(db_session, workspace_name=workspace_name)
# Resolve summary configuration with hierarchical fallback
session_level_configuration = get_configuration(None, session, workspace)
peers_with_configuration = await get_peers_with_configuration(
db_session, workspace_name, session_name
)
queue_records: list[dict[str, Any]] = []
for message in payload:
message_config: MessageConfiguration | None = message.get("configuration")
if message_config is not None:
message_level_configuration = get_configuration(
message_config, session, workspace
)
else:
message_level_configuration = session_level_configuration
queue_records.extend(
await generate_queue_records(
db_session,
message,
peers_with_configuration,
session.id,
message_level_configuration,
)
)
return queue_records
async def get_peers_with_configuration(
db_session: AsyncSession, workspace_name: str, session_name: str
) -> dict[str, list[dict[str, Any]]]:
"""
Retrieve peers with their configurations for a given session.
Args:
db_session: The database session
workspace_name: Name of the workspace
session_name: Name of the session
Returns:
Dictionary mapping peer names to their configurations
"""
configuration_query = await crud.get_session_peer_configuration(
workspace_name=workspace_name, session_name=session_name
)
peers_with_configuration_result = await db_session.execute(configuration_query)
peers_with_configuration_list = peers_with_configuration_result.all()
return {
row.peer_name: [
row.peer_configuration,
row.session_peer_configuration,
row.is_active,
]
for row in peers_with_configuration_list
}
def create_representation_record(
message: dict[str, Any],
conf: ResolvedConfiguration,
session_id: str | None = None,
*,
observers: list[str],
observed: str,
) -> dict[str, Any]:
"""
Create a queue record for representation task.
Args:
message: The message payload
conf: Resolved configuration for this particular message
session_id: Optional session ID
observers: List of observer peer names
observed: Name of the sender
Returns:
Queue record dictionary with workspace_name and message_id as separate fields
"""
workspace_name = message.get("workspace_name")
message_id = message.get("message_id")
if not isinstance(workspace_name, str):
raise TypeError("workspace_name is required and must be a string")
if not isinstance(message_id, int):
raise TypeError("message_id is required and must be an integer")
processed_payload: dict[str, Any] = create_payload(
message=message,
configuration=conf,
task_type="representation",
observers=observers,
observed=observed,
)
return {
"work_unit_key": construct_work_unit_key(workspace_name, processed_payload),
"payload": processed_payload,
"session_id": session_id,
"task_type": "representation",
"workspace_name": workspace_name,
"message_id": message_id,
}
def create_summary_record(
message: dict[str, Any],
configuration: ResolvedConfiguration,
session_id: str,
message_seq_in_session: int,
) -> dict[str, Any]:
"""
Create a queue record for summary task.
Args:
message: The message payload
session_id: Session ID
message_seq_in_session: The sequence number of the message in the session
Returns:
Queue record dictionary with workspace_name and message_id as separate fields
"""
workspace_name = message.get("workspace_name")
message_id = message.get("message_id")
if not isinstance(workspace_name, str):
raise ValueError("workspace_name is required and must be a string")
if not isinstance(message_id, int):
raise ValueError("message_id is required and must be an integer")
processed_payload = create_payload(
message=message,
configuration=configuration,
task_type="summary",
message_seq_in_session=message_seq_in_session,
)
return {
"work_unit_key": construct_work_unit_key(workspace_name, processed_payload),
"payload": processed_payload,
"session_id": session_id,
"task_type": "summary",
"workspace_name": workspace_name,
"message_id": message_id,
}
def get_effective_observe_me(
observed: str, peers_with_configuration: dict[str, list[dict[str, Any]]]
) -> bool:
"""
Determine the effective observe_me setting for a sender, considering session and peer configurations.
Args:
observed: Name of the sender
peers_with_configuration: Dictionary of peer configurations
Returns:
True if observe_me is enabled, False otherwise
"""
# If the sender is not in peers_with_configuration, they left after sending a message.
# We'll use the default behavior of observing the sender by instantiating the default
# peer-level and session-level configs.
configuration: list[Any] = peers_with_configuration.get(observed, [{}, {}])
sender_session_peer_config = (
schemas.SessionPeerConfig(**configuration[1]) if configuration[1] else None
)
sender_peer_config = (
schemas.PeerConfig(**configuration[0])
if configuration[0]
else schemas.PeerConfig()
)
# Session peer config takes precedence if it exists and has observe_me set
if sender_session_peer_config and sender_session_peer_config.observe_me is not None:
return sender_session_peer_config.observe_me
# Otherwise use peer config
return (
sender_peer_config.observe_me
if sender_peer_config.observe_me is not None
else True
)
async def generate_queue_records(
db_session: AsyncSession,
message: dict[str, Any],
peers_with_configuration: dict[str, list[dict[str, Any]]],
session_id: str,
conf: ResolvedConfiguration,
) -> list[dict[str, Any]]:
"""
Process a single message and generate queue records based on configurations.
Args:
db_session: The database session
message: The message payload
peers_with_configuration: Dictionary of peer configurations
session_id: Session ID
configuration: Resolved configuration for this particular message
Returns:
List of queue records for this message
"""
observed = message["peer_name"]
message_id: int = message["message_id"]
# Prefer the sequence captured during message creation; fallback only if missing
message_seq_in_session = int(message.get("seq_in_session") or 0)
if message_seq_in_session <= 0:
message_seq_in_session = await crud.get_message_seq_in_session(
db_session,
>sed -n '1,320p' src/deriver/deriver.pyChunk ID: d5d11b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2246
Output:
import logging
import time
from src import crud
from src.config import ConfiguredModelSettings, settings
from src.crud.representation import RepresentationManager
from src.dependencies import tracked_db
from src.llm import honcho_llm_call
from src.models import Message
from src.schemas import ResolvedConfiguration
from src.telemetry import prometheus_metrics
from src.telemetry.events import RepresentationCompletedEvent, emit
from src.telemetry.logging import accumulate_metric, log_performance_metrics
from src.telemetry.prometheus.metrics import (
DeriverComponents,
DeriverTaskTypes,
TokenTypes,
)
from src.telemetry.sentry import with_sentry_transaction
from src.utils.config_helpers import get_configuration
from src.utils.formatting import format_new_turn_with_timestamp
from src.utils.representation import PromptRepresentation, Representation
from src.utils.tokens import track_deriver_input_tokens
from .prompts import estimate_deriver_prompt_tokens, minimal_deriver_prompt
logger = logging.getLogger(__name__)
def _get_deriver_model_config() -> ConfiguredModelSettings:
return settings.DERIVER.MODEL_CONFIG
@with_sentry_transaction("minimal_deriver_batch", op="deriver")
async def process_representation_tasks_batch(
messages: list[Message],
message_level_configuration: ResolvedConfiguration | None,
*,
observers: list[str],
observed: str,
queue_item_message_ids: list[int],
) -> None:
"""
Process messages with minimal overhead - single LLM call, save to multiple collections.
Args:
messages: List of messages to process (includes interleaving context).
message_level_configuration: Optional configuration override.
observers: List of observer peer IDs (collections to save to).
observed: The observed peer ID.
queue_item_message_ids: Message IDs from queue items being processed
"""
if not messages:
return
overall_start = time.perf_counter()
messages.sort(key=lambda x: x.id)
latest_message = messages[-1]
earliest_message = messages[0]
# Get configuration if not provided
# TODO: this appears to be a very rare edge case coming out of `get_queue_item_batch` in queue_manager.py,
# possible that we can remove this and require configuration to come through with the payload.
if message_level_configuration is None:
async with tracked_db("minimal_deriver.get_config") as db:
message_level_configuration = get_configuration(
None,
await crud.get_session(
db, latest_message.session_name, latest_message.workspace_name
),
await crud.get_workspace(
db, workspace_name=latest_message.workspace_name
),
)
# Skip if disabled
if message_level_configuration.reasoning.enabled is False:
return
custom_instructions = message_level_configuration.reasoning.custom_instructions
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"starting_message_id",
earliest_message.id,
"id",
)
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"ending_message_id",
latest_message.id,
"id",
)
# Format messages with timestamps
formatted_messages = "\n".join(
format_new_turn_with_timestamp(msg.content, msg.created_at, msg.peer_name)
for msg in messages
)
# Track token usage - count only tokens from messages being processed
prompt_tokens = estimate_deriver_prompt_tokens(custom_instructions)
queue_item_message_ids_set = set(queue_item_message_ids)
messages_tokens = sum(
msg.token_count for msg in messages if msg.id in queue_item_message_ids_set
)
track_deriver_input_tokens(
task_type=DeriverTaskTypes.INGESTION,
components={
DeriverComponents.PROMPT: prompt_tokens,
DeriverComponents.MESSAGES: messages_tokens,
},
)
# Build prompt
prompt = minimal_deriver_prompt(
peer_id=observed,
messages=formatted_messages,
custom_instructions=custom_instructions,
)
context_prep_duration = (time.perf_counter() - overall_start) * 1000
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"context_preparation",
context_prep_duration,
"ms",
)
# validation on settings means max_tokens will always be > 0
base_model_config = _get_deriver_model_config()
max_tokens = base_model_config.max_output_tokens or settings.LLM.DEFAULT_MAX_TOKENS
model_config = base_model_config
# Single LLM call
llm_start = time.perf_counter()
response = await honcho_llm_call(
model_config=model_config,
prompt=prompt,
max_tokens=[REDACTED]
track_name="Minimal Deriver",
response_model=PromptRepresentation,
json_mode=True,
max_input_tokens=[REDACTED]
enable_retry=True,
retry_attempts=3,
trace_name="minimal_deriver",
)
llm_duration = (time.perf_counter() - llm_start) * 1000
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"llm_call_duration",
llm_duration,
"ms",
)
# Prometheus metrics
if settings.METRICS.ENABLED:
prometheus_metrics.record_deriver_tokens(
count=response.output_tokens,
task_type=DeriverTaskTypes.INGESTION.value,
token_type=[REDACTED]
component=DeriverComponents.OUTPUT_TOTAL.value,
)
message_ids = [m.id for m in messages if m.peer_name == observed]
# Convert to Representation and save
observations = Representation.from_prompt_representation(
response.content,
message_ids,
latest_message.session_name,
latest_message.created_at,
)
if observations.is_empty() or not message_ids:
logger.warning(
"Deriver generated zero observations for messages %s:%s in %s/%s!",
earliest_message.id,
latest_message.id,
latest_message.workspace_name,
latest_message.session_name,
)
else:
# Save to all observer collections
for observer in observers:
representation_manager = RepresentationManager(
workspace_name=latest_message.workspace_name,
observer=observer,
observed=observed,
)
try:
await representation_manager.save_representation(
observations,
message_ids,
latest_message.session_name,
latest_message.created_at,
message_level_configuration,
)
except Exception as e:
logger.error(
"Failed to save representation for observer %s: %s", observer, e
)
# Log metrics
overall_duration = (time.perf_counter() - overall_start) * 1000
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"total_processing_time",
overall_duration,
"ms",
)
total_observations = len(observations.explicit) + len(observations.deductive)
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"observation_count",
total_observations,
"count",
)
if settings.DERIVER.LOG_OBSERVATIONS:
# Log messages fed into deriver
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"messages",
formatted_messages,
"blob",
)
# Log actual observations created as blob metrics
accumulate_metric(
f"minimal_deriver_{latest_message.id}_{observed}",
"explicit_observations",
"\n".join(f" • {obs}" for obs in observations.explicit),
"blob",
)
log_performance_metrics("minimal_deriver", f"{latest_message.id}_{observed}")
# Emit telemetry event
emit(
RepresentationCompletedEvent(
workspace_name=latest_message.workspace_name,
session_name=latest_message.session_name,
observed=observed,
queue_items_processed=len(queue_item_message_ids),
earliest_message_id=earliest_message.public_id,
latest_message_id=latest_message.public_id,
message_count=len(messages),
explicit_conclusion_count=len(observations.explicit),
context_preparation_ms=context_prep_duration,
llm_call_ms=llm_duration,
total_duration_ms=overall_duration,
input_tokens=[REDACTED]
output_tokens=[REDACTED]
)
)
>sed -n '320,760p' src/deriver/enqueue.pyChunk ID: 2e24cc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2300
Output:
db_session,
workspace_name=message["workspace_name"],
session_name=message["session_name"],
message_id=message_id,
)
records: list[dict[str, Any]] = []
if conf.summary.enabled and (
message_seq_in_session % conf.summary.messages_per_short_summary == 0
or message_seq_in_session % conf.summary.messages_per_long_summary == 0
):
records.append(
create_summary_record(
message,
configuration=conf,
session_id=session_id,
message_seq_in_session=message_seq_in_session,
)
)
# Check if the sender should be observed based on peer configuration
should_observe = get_effective_observe_me(observed, peers_with_configuration)
if not conf.reasoning.enabled:
return records
# Collect all observers into a single list
observers: list[str] = []
if should_observe:
# Self-observation: the sender observes themselves
observers.append(observed)
# Other peers who want to observe
for peer_name, peer_conf in peers_with_configuration.items():
if peer_name == observed:
continue
# If the observer peer has left the session, skip them
if not peer_conf[2]:
continue
session_peer_config = (
schemas.SessionPeerConfig(**peer_conf[1]) if peer_conf[1] else None
)
if session_peer_config is None or not session_peer_config.observe_others:
continue
observers.append(peer_name)
# Create a single record with all observers (if any)
if observers:
records.append(
create_representation_record(
message,
conf,
observed=observed,
observers=observers,
session_id=session_id,
)
)
logger.debug(
"message %s from %s created %s queue items with %s observers",
message_id,
observed,
len(records),
len(observers),
)
return records
def create_dream_record(
workspace_name: str,
*,
observer: str,
observed: str,
dream_type: schemas.DreamType,
session_name: str | None = None,
) -> dict[str, Any]:
"""
Create a queue record for a dream task.
Args:
workspace_name: Name of the workspace
observer: Name of the observer peer
observed: Name of the observed peer
dream_type: Type of dream to execute
session_name: Name of the session to scope the dream to if specified
Returns:
Queue record dictionary with workspace_name and other fields
"""
dream_payload = create_dream_payload(
dream_type,
observer=observer,
observed=observed,
session_name=session_name,
)
return {
"work_unit_key": construct_work_unit_key(workspace_name, dream_payload),
"payload": dream_payload,
"session_id": None,
"task_type": "dream",
"workspace_name": workspace_name,
"message_id": None,
}
async def enqueue_dream(
workspace_name: str,
observer: str,
observed: str,
dream_type: schemas.DreamType,
session_name: str | None = None,
) -> None:
"""
Enqueue a dream task for immediate processing by the deriver.
Does not touch collection.internal_metadata["dream"] — both guard fields
are written atomically in process_dream on successful completion.
Deduplication: If a dream with the same work_unit_key is already in-progress
(has an ActiveQueueSession) or pending in the queue, the enqueue is skipped.
Args:
workspace_name: Name of the workspace
observer: Name of the observer peer
observed: Name of the observed peer
dream_type: Type of dream to execute
session_name: Name of the session to scope the dream to if specified
"""
async with tracked_db("dream_enqueue") as db_session:
try:
dream_record = create_dream_record(
workspace_name,
observer=observer,
observed=observed,
dream_type=dream_type,
session_name=session_name,
)
work_unit_key = dream_record["work_unit_key"]
in_progress_check = select(
exists(
select(models.ActiveQueueSession.id).where(
models.ActiveQueueSession.work_unit_key == work_unit_key
)
)
)
is_in_progress = await db_session.scalar(in_progress_check)
if is_in_progress:
logger.info(
"Skipping dream enqueue - already in progress: %s/%s/%s (type: %s)",
workspace_name,
observer,
observed,
dream_type.value,
)
return
pending_check = select(
exists(
select(QueueItem.id).where(
QueueItem.work_unit_key == work_unit_key,
QueueItem.processed == False, # noqa: E712
)
)
)
is_pending = await db_session.scalar(pending_check)
if is_pending:
logger.info(
"Dream already pending in queue: %s/%s/%s (type: %s)",
workspace_name,
observer,
observed,
dream_type.value,
)
return
stmt = insert(QueueItem).returning(QueueItem)
await db_session.execute(stmt, [dream_record])
await db_session.commit()
logger.info(
"Enqueued dream task for %s/%s/%s (type: %s)",
workspace_name,
observer,
observed,
dream_type.value,
)
except Exception as e:
logger.exception("Failed to enqueue dream task!")
if settings.SENTRY.ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
raise
def create_deletion_record(
workspace_name: str,
deletion_type: Literal["session", "observation", "workspace"],
resource_id: str,
) -> dict[str, Any]:
"""
Create a queue record for a deletion task.
Args:
workspace_name: Name of the workspace
deletion_type: Type of resource to delete ("session" or "observation")
resource_id: ID of the resource to delete
Returns:
Queue record dictionary for insertion into the queue
"""
deletion_payload = create_deletion_payload(
deletion_type=deletion_type,
resource_id=resource_id,
)
return {
"work_unit_key": construct_work_unit_key(workspace_name, deletion_payload),
"payload": deletion_payload,
"session_id": None,
"task_type": "deletion",
"workspace_name": workspace_name,
"message_id": None,
}
async def enqueue_deletion(
workspace_name: str,
deletion_type: Literal["session", "observation", "workspace"],
resource_id: str,
db_session: AsyncSession | None = None,
) -> None:
"""
Enqueue a deletion task for processing by the deriver.
This function adds a deletion task to the queue for asynchronous processing.
The deletion will be handled by the queue consumer with retry support.
Args:
workspace_name: Name of the workspace
deletion_type: Type of resource to delete ("session" or "observation")
resource_id: ID of the resource to delete
db_session: Optional database session. If provided, uses this session
instead of creating a new one. The caller is responsible for committing.
"""
async def _do_enqueue(session: AsyncSession, should_commit: bool) -> None:
deletion_record = create_deletion_record(
workspace_name,
deletion_type,
resource_id,
)
stmt = insert(QueueItem).returning(QueueItem)
await session.execute(stmt, [deletion_record])
if should_commit:
await session.commit()
logger.info(
"Enqueued deletion task: type=%s, resource_id=%s, workspace=%s",
deletion_type,
resource_id,
workspace_name,
)
try:
if db_session is not None:
# Use the provided session - caller is responsible for committing
await _do_enqueue(db_session, should_commit=False)
else:
# Create a new session and commit
async with tracked_db("deletion_enqueue") as new_session:
await _do_enqueue(new_session, should_commit=True)
except Exception as e:
logger.exception("Failed to enqueue deletion task!")
if settings.SENTRY.ENABLED:
import sentry_sdk
sentry_sdk.capture_exception(e)
raise
>sed -n '1,360p' src/crud/representation.pyChunk ID: 1114b6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3279
Output:
from __future__ import annotations
import datetime
import logging
import time
from contextlib import suppress
from typing import Any
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src import crud, exceptions, models, schemas
from src.config import settings
from src.dependencies import tracked_db
from src.dreamer.dream_scheduler import check_and_schedule_dream
from src.embedding_client import embedding_client
from src.schemas import ResolvedConfiguration
from src.telemetry.logging import accumulate_metric
from src.utils.formatting import format_datetime_utc
from src.utils.representation import (
DeductiveObservation,
ExplicitObservation,
Representation,
)
logger = logging.getLogger(__name__)
def _observation_text(obs: ExplicitObservation | DeductiveObservation) -> str:
"""Return the canonical text payload for an explicit or deductive observation."""
return obs.conclusion if isinstance(obs, DeductiveObservation) else obs.content
def _normalized_observation(
obs: ExplicitObservation | DeductiveObservation,
) -> ExplicitObservation | DeductiveObservation:
"""Return an observation with its persisted/embed text normalized."""
text = _observation_text(obs).strip()
if isinstance(obs, DeductiveObservation):
return obs.model_copy(update={"conclusion": text})
return obs.model_copy(update={"content": text})
class RepresentationManager:
"""Unified manager for representation and document queries."""
def __init__(
self,
workspace_name: str,
*,
observer: str,
observed: str,
) -> None:
self.workspace_name: str = workspace_name
self.observer: str = observer
self.observed: str = observed
async def save_representation(
self,
representation: Representation,
message_ids: list[int],
session_name: str,
message_created_at: datetime.datetime,
message_level_configuration: ResolvedConfiguration,
) -> int:
"""
Save Representation objects to the collection as a set of documents.
Args:
representation: Representation object
message_ids: Message ID range to link with observations
session_name: Session name to link with existing summary context
message_created_at: Timestamp when the message was created
Returns:
The number of *new documents saved*
"""
new_documents = 0
if not representation.deductive and not representation.explicit:
logger.debug("No observations to save")
return new_documents
all_observations = [
_normalized_observation(obs)
for obs in representation.deductive + representation.explicit
if _observation_text(obs).strip()
]
if not all_observations:
logger.debug("No non-empty observations to save")
return new_documents
# Batch embed all observations
batch_embed_start = time.perf_counter()
observation_texts = [_observation_text(obs) for obs in all_observations]
try:
embeddings = await embedding_client.simple_batch_embed(observation_texts)
except ValueError as e:
raise exceptions.ValidationException(
"Observation content exceeds maximum token limit of "
+ f"{settings.EMBEDDING.MAX_INPUT_TOKENS}."
) from e
batch_embed_duration = (time.perf_counter() - batch_embed_start) * 1000
accumulate_metric(
f"deriver_{message_ids[-1]}_{self.observer}",
"embed_new_observations",
batch_embed_duration,
"ms",
)
# Batch create document objects
create_document_start = time.perf_counter()
async with tracked_db("representation_manager.save_representation") as db:
new_documents = await self._save_representation_internal(
db,
all_observations,
embeddings,
message_ids,
session_name,
message_created_at,
message_level_configuration,
)
create_document_duration = (time.perf_counter() - create_document_start) * 1000
accumulate_metric(
f"deriver_{message_ids[-1]}_{self.observer}",
"save_new_observations",
create_document_duration,
"ms",
)
return new_documents
async def _save_representation_internal(
self,
db: AsyncSession,
all_observations: list[ExplicitObservation | DeductiveObservation],
embeddings: list[list[float]],
message_ids: list[int],
session_name: str,
message_created_at: datetime.datetime,
message_level_configuration: ResolvedConfiguration,
) -> int:
# get_or_create_collection already handles IntegrityError with rollback and a retry
collection = await crud.get_or_create_collection(
db,
self.workspace_name,
observer=self.observer,
observed=self.observed,
)
# Prepare all documents for bulk creation
documents_to_create: list[schemas.DocumentCreate] = []
for obs, embedding in zip(all_observations, embeddings, strict=True):
# NOTE: will add additional levels of reasoning in the future
if isinstance(obs, DeductiveObservation):
obs_level = "deductive"
obs_content = obs.conclusion
obs_premises = obs.premises
else:
obs_level = "explicit"
obs_content = obs.content
obs_premises = None
metadata: schemas.DocumentMetadata = schemas.DocumentMetadata(
message_ids=message_ids,
premises=obs_premises,
message_created_at=format_datetime_utc(message_created_at),
)
documents_to_create.append(
schemas.DocumentCreate(
content=obs_content,
session_name=session_name,
level=obs_level,
metadata=metadata,
embedding=embedding,
)
)
# Use bulk creation with optional duplicate detection
accepted_documents = await crud.create_documents(
db,
documents_to_create,
self.workspace_name,
observer=self.observer,
observed=self.observed,
deduplicate=settings.DERIVER.DEDUPLICATE,
)
if message_level_configuration.dream.enabled:
try:
await check_and_schedule_dream(db, collection)
except Exception as e:
logger.warning(f"Failed to check dream scheduling: {e}")
return len(accepted_documents)
async def get_working_representation(
self,
*,
db: AsyncSession | None = None,
session_name: str | None = None,
include_semantic_query: str | None = None,
embedding: list[float] | None = None,
semantic_search_top_k: int | None = None,
semantic_search_max_distance: float | None = None,
include_most_derived: bool = False,
max_observations: int = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
) -> Representation:
"""
Get working representation with flexible query options.
Args:
db: Optional database session. If provided, uses it directly;
otherwise creates a new session via tracked_db.
session_name: Optional session to filter by
include_semantic_query: Query for semantic search
embedding: Pre-computed embedding for the semantic query.
semantic_search_top_k: Number of semantic results
semantic_search_max_distance: Maximum distance for semantic search
include_most_derived: Include most derived observations
max_observations: Maximum total observations to return
Returns:
Representation combining various query strategies
"""
if include_semantic_query and embedding is None:
with suppress(Exception):
# Best-effort precompute
embedding = await embedding_client.embed(include_semantic_query)
if db is not None:
return await self._get_working_representation_internal(
db,
session_name=session_name,
include_semantic_query=include_semantic_query,
embedding=embedding,
semantic_search_top_k=semantic_search_top_k,
semantic_search_max_distance=semantic_search_max_distance,
include_most_derived=include_most_derived,
max_observations=max_observations,
)
async with tracked_db(
"representation_manager.get_working_representation"
) as new_db:
return await self._get_working_representation_internal(
new_db,
session_name=session_name,
include_semantic_query=include_semantic_query,
embedding=embedding,
semantic_search_top_k=semantic_search_top_k,
semantic_search_max_distance=semantic_search_max_distance,
include_most_derived=include_most_derived,
max_observations=max_observations,
)
# Private helper methods
async def _get_working_representation_internal(
self,
db: AsyncSession,
*,
session_name: str | None = None,
include_semantic_query: str | None = None,
embedding: list[float] | None = None,
semantic_search_top_k: int | None = None,
semantic_search_max_distance: float | None = None,
include_most_derived: bool = False,
max_observations: int = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
) -> Representation:
"""Internal implementation of get_working_representation."""
total = max_observations
# Calculate how many observations to get from each source
semantic_observations = (
min(
max(
0,
semantic_search_top_k
if semantic_search_top_k is not None
else total // 3,
),
total,
)
if include_semantic_query
else 0
)
if include_semantic_query and include_most_derived:
# three-way blend: both semantic and derived requested
top_observations = min(max(0, total // 3), total - semantic_observations)
elif include_most_derived:
# two-way blend: only derived requested
top_observations = min(max(0, total // 2), total - semantic_observations)
else:
# no derived observations requested
top_observations = 0
# remaining observations are recent
recent_observations = total - semantic_observations - top_observations
representation = Representation()
# Get semantic observations if requested
if include_semantic_query:
semantic_docs = await self._query_documents_semantic(
db,
query=include_semantic_query,
top_k=semantic_observations,
max_distance=semantic_search_max_distance,
embedding=embedding,
)
representation.merge_representation(
Representation.from_documents(semantic_docs)
)
# Get most derived observations if requested
if include_most_derived:
derived_docs = await self._query_documents_most_derived(
db, top_k=top_observations
)
representation.merge_representation(
Representation.from_documents(derived_docs)
)
# Get recent observations
recent_docs = await self._query_documents_recent(
db, top_k=recent_observations, session_name=session_name
)
representation.merge_representation(Representation.from_documents(recent_docs))
return representation
async def _query_documents_semantic(
self,
db: AsyncSession,
query: str,
top_k: int,
max_distance: float | None = None,
level: str | None = None,
embedding: list[float] | None = None,
) -> list[models.Document]:
"""Query documents by semantic similarity."""
try:
if level:
return await self._query_documents_for_level(
db,
query,
level,
top_k,
max_distance,
embedding=embedding,
)
else:
>sed -n '1,300p' src/utils/summarizer.pyChunk ID: cab345
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2571
Output:
import asyncio
import logging
import time
from enum import Enum
from functools import cache
from inspect import cleandoc as c
from typing import TypedDict
from sqlalchemy import update
from sqlalchemy.ext.asyncio import AsyncSession
from src import schemas
from src.cache.client import cache as cache_client
from src.config import ConfiguredModelSettings, settings
from src.crud.session import session_cache_key
from src.dependencies import tracked_db
from src.exceptions import ResourceNotFoundException
from src.llm import HonchoLLMCallResponse, honcho_llm_call
from src.models import Message
from src.telemetry import prometheus_metrics
from src.telemetry.events import AgentToolSummaryCreatedEvent, emit
from src.telemetry.logging import accumulate_metric, conditional_observe
from src.telemetry.prometheus.metrics import (
DeriverComponents,
DeriverTaskTypes,
TokenTypes,
)
from src.utils.formatting import utc_now_iso
from src.utils.tokens import estimate_tokens, track_deriver_input_tokens
from .. import crud, models
logger = logging.getLogger(__name__)
# TypedDict definitions for summary data
class Summary(TypedDict):
"""
A summary object. Stored in session metadata and used in a session's get_context.
Attributes:
content: The summary text.
message_id: The primary key ID of the message that this summary covers up to.
summary_type: The type of summary (short or long).
created_at: The timestamp of when the summary was created (ISO format string).
token_count: The number of tokens in the summary text.
"""
content: str
message_id: int
summary_type: str
created_at: str
token_count: int
message_public_id: str
def to_schema_summary(s: Summary) -> schemas.Summary:
return schemas.Summary(
content=s["content"],
message_id=s["message_id"],
summary_type=s["summary_type"],
created_at=s["created_at"],
token_count=[REDACTED]
message_public_id=s.get("message_public_id", ""),
)
# Export the public functions
__all__ = [
"get_summary",
"get_both_summaries",
"get_summarized_history",
"get_session_context",
"get_session_context_formatted",
"SummaryType",
"Summary",
"to_schema_summary",
]
def _get_summary_model_config() -> ConfiguredModelSettings:
return settings.SUMMARY.MODEL_CONFIG
# Configuration constants for summaries
MESSAGES_PER_SHORT_SUMMARY = settings.SUMMARY.MESSAGES_PER_SHORT_SUMMARY
MESSAGES_PER_LONG_SUMMARY = settings.SUMMARY.MESSAGES_PER_LONG_SUMMARY
SUMMARIES_KEY = "summaries"
# The types of summary to store in the session metadata
class SummaryType(Enum):
SHORT = "honcho_chat_summary_short"
LONG = "honcho_chat_summary_long"
def short_summary_prompt(
formatted_messages: str,
output_words: int,
previous_summary_text: str,
) -> str:
"""Generate the short summary prompt."""
return c(f"""
You are a system that summarizes parts of a conversation to create a concise and accurate summary. Focus on capturing:
1. Key facts and information shared (**Capture as many explicit facts as possible**)
2. User preferences, opinions, and questions
3. Important context and requests
4. Core topics discussed
If there is a previous summary, ALWAYS make your new summary inclusive of both it and the new messages, therefore capturing the ENTIRE conversation. Prioritize key facts across the entire conversation.
Provide a concise, factual summary that captures the essence of the conversation. Your summary should be detailed enough to serve as context for future messages, but brief enough to be helpful. Prefer a thorough chronological narrative over a list of bullet points.
Return only the summary without any explanation or meta-commentary.
<previous_summary>
{previous_summary_text}
</previous_summary>
<conversation>
{formatted_messages}
</conversation>
Hard limit: {output_words} words maximum. If needed, drop lower-priority detail to stay within the limit.
""")
def long_summary_prompt(
formatted_messages: str,
output_words: int,
previous_summary_text: str,
) -> str:
"""Generate the long summary prompt."""
return c(f"""
You are a system that creates thorough, comprehensive summaries of conversations. Focus on capturing:
1. Key facts and information shared (**Capture as many explicit facts as possible**)
2. User preferences, opinions, and questions
3. Important context and requests
4. Core topics discussed in detail
5. User's apparent emotional state and personality traits
6. Important themes and patterns across the conversation
If there is a previous summary, ALWAYS make your new summary inclusive of both it and the new messages, therefore capturing the ENTIRE conversation. Prioritize key facts across the entire conversation.
Provide a thorough and detailed summary that captures the essence of the conversation. Your summary should serve as a comprehensive record of the important information in this conversation. Prefer an exhaustive chronological narrative over a list of bullet points.
Return only the summary without any explanation or meta-commentary.
<previous_summary>
{previous_summary_text}
</previous_summary>
<conversation>
{formatted_messages}
</conversation>
Hard limit: {output_words} words maximum. If needed, drop lower-priority detail to stay within the limit.
""")
@cache
def estimate_short_summary_prompt_tokens() -> int:
"""Estimate tokens for the short summary prompt (without messages/previous_summary)."""
try:
return estimate_tokens(
short_summary_prompt(
formatted_messages="",
output_words=0,
previous_summary_text="",
)
)
except Exception:
# Return a rough estimate if estimation fails
return 200
@cache
def estimate_long_summary_prompt_tokens() -> int:
"""Estimate tokens for the long summary prompt (without messages/previous_summary)."""
try:
return estimate_tokens(
long_summary_prompt(
formatted_messages="",
output_words=0,
previous_summary_text="",
)
)
except Exception:
# Return a rough estimate if estimation fails
return 200
@conditional_observe(name="Create Short Summary")
async def create_short_summary(
formatted_messages: str,
input_tokens: int,
previous_summary: str | None = None,
) -> HonchoLLMCallResponse[str]:
# input_tokens indicates how many tokens the message list + previous summary take up
# we want to optimize short summaries to be smaller than the actual content being summarized
# so we ask the agent to produce a word count roughly equal to either the input, or the max
# size if the input is larger. the word/token ratio is roughly 4:3 so we multiply by 0.75.
# LLMs *seem* to respond better to getting asked for a word count but should workshop this.
output_words = int(min(input_tokens, settings.SUMMARY.MAX_TOKENS_SHORT) * 0.75)
if previous_summary:
previous_summary_text = previous_summary
else:
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
prompt = short_summary_prompt(
formatted_messages, output_words, previous_summary_text
)
return await honcho_llm_call(
model_config=_get_summary_model_config(),
prompt=prompt,
max_tokens=[REDACTED]
)
@conditional_observe(name="Create Long Summary")
async def create_long_summary(
formatted_messages: str,
previous_summary: str | None = None,
) -> HonchoLLMCallResponse[str]:
# the word/token ratio is roughly 4:3 so we multiply by 0.75.
# LLMs *seem* to respond better to getting asked for a word count but should workshop this.
output_words = int(settings.SUMMARY.MAX_TOKENS_LONG * 0.75)
if previous_summary:
previous_summary_text = previous_summary
else:
previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
prompt = long_summary_prompt(
formatted_messages, output_words, previous_summary_text
)
return await honcho_llm_call(
model_config=_get_summary_model_config(),
prompt=prompt,
max_tokens=[REDACTED]
)
async def summarize_if_needed(
workspace_name: str,
session_name: str,
message_id: int,
message_seq_in_session: int,
message_public_id: str,
configuration: schemas.ResolvedConfiguration,
) -> None:
"""
Create short/long summaries if thresholds met.
This function checks for both short and long summary needs independently,
without assuming any relationship between their thresholds.
Args:
workspace_name: The workspace name
session_name: The session name
message_id: The message ID
message_seq_in_session: The sequence number of the message in the session
message_public_id: The public ID of the message
configuration: The resolved configuration for the message
"""
if configuration.summary.enabled is False:
return
should_create_long: bool = (
message_seq_in_session % configuration.summary.messages_per_long_summary == 0
)
should_create_short: bool = (
message_seq_in_session % configuration.summary.messages_per_short_summary == 0
)
if should_create_long is False and should_create_short is False:
return
# If both summaries need to be created, run them in parallel
if should_create_long and should_create_short:
async def create_long_summary_task():
await _create_and_save_summary(
workspace_name,
session_name,
message_id=message_id,
message_seq_in_session=message_seq_in_session,
message_public_id=message_public_id,
summary_type=SummaryType.LONG,
configuration=configuration,
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
"long_summary_up_to_message",
>sed -n '1,280p' src/dialectic/chat.pyChunk ID: 37e844
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1209
Output:
"""
Chat functionality for the Dialectic API.
Provides the agentic_chat function for answering queries about peers
using the DialecticAgent.
"""
import logging
from collections.abc import AsyncIterator
from src import crud, schemas
from src.config import ReasoningLevel
from src.dependencies import tracked_db
from src.dialectic.core import DialecticAgent
from src.utils.config_helpers import get_configuration
logger = logging.getLogger(__name__)
async def agentic_chat(
workspace_name: str,
session_name: str | None,
query: str,
observer: str,
observed: str,
reasoning_level: ReasoningLevel = "low",
) -> str:
"""
Answer a query about a peer using the agentic dialectic.
Args:
workspace_name: Workspace identifier
session_name: Session identifier (may be None for global queries)
query: The question to answer about the peer
observer: The peer making the query
observed: The peer being queried about
reasoning_level: Level of reasoning to apply
Returns:
The synthesized answer string
"""
# Short-lived DB session for validation + config
async with tracked_db("dialectic.preflight") as db:
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
if observer != observed:
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
session = None
if session_name:
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
observer_peer_card = None
observed_peer_card = None
if configuration.peer_card.use:
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
)
# DB session closed — agent runs without holding a connection
agent = DialecticAgent(
workspace_name=workspace_name,
session_name=session_name,
observer=observer,
observed=observed,
observer_peer_card=observer_peer_card,
observed_peer_card=observed_peer_card,
reasoning_level=reasoning_level,
)
return await agent.answer(query)
async def agentic_chat_stream(
workspace_name: str,
session_name: str | None,
query: str,
observer: str,
observed: str,
reasoning_level: ReasoningLevel = "low",
) -> AsyncIterator[str]:
"""
Stream an answer to a query about a peer using the agentic dialectic.
Args:
workspace_name: Workspace identifier
session_name: Session identifier (may be None for global queries)
query: The question to answer about the peer
observer: The peer making the query
observed: The peer being queried about
reasoning_level: Level of reasoning to apply
Yields:
Chunks of the response text as they are generated
"""
# Short-lived DB session for validation + config
async with tracked_db("dialectic.preflight") as db:
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
if observer != observed:
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observed))
session = None
if session_name:
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
observer_peer_card = None
observed_peer_card = None
if configuration.peer_card.use:
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
)
# DB session closed — agent streams without holding a connection
agent = DialecticAgent(
workspace_name=workspace_name,
session_name=session_name,
observer=observer,
observed=observed,
observer_peer_card=observer_peer_card,
observed_peer_card=observed_peer_card,
reasoning_level=reasoning_level,
)
async for chunk in agent.answer_stream(query):
yield chunk
>sed -n '1,360p' src/dreamer/orchestrator.pyChunk ID: 838293
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3434
Output:
"""
Dream orchestrator for the specialist-based architecture.
This module coordinates the full dream cycle:
0. [Optional] Surprisal sampling: Pre-filter observations by geometric surprisal
1. Run deduction specialist (self-directed exploration, creates deductive observations)
2. Run induction specialist (self-directed exploration, creates inductive observations)
Specialists are self-directed agents that explore the observation space and create
higher-level observations. When surprisal sampling finds interesting observations,
they're passed as hints, but specialists are free to follow the evidence wherever it leads.
"""
from __future__ import annotations
import logging
import time
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
import sentry_sdk
from sqlalchemy import func, select
from src import crud, models
from src.config import settings
from src.dependencies import tracked_db
from src.dreamer.specialists import SPECIALISTS, SpecialistResult
from src.dreamer.surprisal import SurprisalScore # type: ignore
from src.exceptions import SpecialistExecutionError, SurprisalError
from src.schemas import DreamType
from src.telemetry.events import DreamRunEvent, emit
from src.telemetry.logging import (
accumulate_metric,
log_performance_metrics,
)
from src.utils.config_helpers import get_configuration
from src.utils.queue_payload import DreamPayload
logger = logging.getLogger(__name__)
@dataclass
class DreamResult:
"""Result of a dream cycle for telemetry reporting."""
# Run identification
run_id: str
specialists_run: list[str]
# Specialist outcomes
deduction_success: bool
induction_success: bool
# Surprisal sampling
surprisal_enabled: bool
surprisal_conclusion_count: int
# Aggregate metrics
total_iterations: int
total_duration_ms: float
input_tokens: int
output_tokens: int
async def run_dream(
workspace_name: str,
observer: str,
observed: str,
session_name: str | None = None,
) -> DreamResult | None:
"""
Run a full dream cycle with optional surprisal-based sampling.
The dream cycle runs specialists sequentially:
0. [Optional] Surprisal sampling: Pre-filter observations by geometric surprisal
1. Deduction specialist: Creates deductive observations from explicit facts
2. Induction specialist: Creates inductive observations from patterns
Uses short-lived DB sessions to avoid holding connections during LLM calls.
Args:
workspace_name: Workspace identifier
observer: Observer peer name
observed: Observed peer name
session_name: Session identifier if specified
"""
if not settings.DREAM.ENABLED:
return None
run_id = str(uuid.uuid4())[:8]
task_name = f"dream_orchestrator_{run_id}"
start_time = time.perf_counter()
logger.info(
f"[{run_id}] Starting dream cycle for {workspace_name}/{observer}/{observed}"
)
# Short-lived DB session for config resolution
async with tracked_db("dream.config") as db:
if session_name is not None:
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
else:
session = None
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
if not configuration.dream.enabled:
logger.info(
f"[{run_id}] Dreams disabled for {workspace_name}/{session_name}, skipping dream"
)
return None
# Track specialist outcomes
deduction_success = False
induction_success = False
surprisal_observation_count = 0
deduction_result: SpecialistResult | None = None
induction_result: SpecialistResult | None = None
# Phase 0: Surprisal-based sampling (if enabled)
# Specialists are self-directed by default - hints are optional suggestions
exploration_hints: list[str] | None = None
if settings.DREAM.SURPRISAL.ENABLED:
logger.info(f"[{run_id}] Phase 0: Computing surprisal scores")
try:
from src.dreamer.surprisal import sample_observations_with_surprisal
high_surprisal_obs = await sample_observations_with_surprisal(
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
logger.info(
f"[{run_id}] Surprisal: Found {len(high_surprisal_obs)} high-surprisal observations"
)
surprisal_observation_count = len(high_surprisal_obs)
accumulate_metric(
task_name, "surprisal_observations", len(high_surprisal_obs), "count"
)
if len(high_surprisal_obs) > 0:
# Use high-surprisal observations as hints for exploration
exploration_hints = _create_queries_from_surprisal(high_surprisal_obs)
logger.info(
f"[{run_id}] ✨ SURPRISAL HINTS: Suggesting {len(exploration_hints)} "
+ "high-surprisal topics for specialists to investigate"
)
logger.info(
f"[{run_id}] Targeting observations with surprisal range: "
+ f"{high_surprisal_obs[-1].surprisal:.3f} to {high_surprisal_obs[0].surprisal:.3f}"
)
else:
logger.info(
f"[{run_id}] No high-surprisal observations - specialists will explore freely"
)
except SurprisalError as e:
logger.error(f"[{run_id}] Surprisal sampling failed: {e}", exc_info=True)
accumulate_metric(task_name, "surprisal_error", str(e), "blob")
# Specialists will explore freely without hints
# Phase 1: Run deduction specialist (manages its own DB sessions)
logger.info(f"[{run_id}] Phase 1: Running deduction specialist")
deduction_specialist = SPECIALISTS["deduction"]
try:
deduction_result = await deduction_specialist.run(
workspace_name=workspace_name,
observer=observer,
observed=observed,
session_name=session_name,
hints=exploration_hints,
configuration=configuration,
parent_run_id=run_id,
)
logger.info(
f"[{run_id}] Deduction completed: {deduction_result.content[:200]}..."
)
accumulate_metric(
task_name, "deduction_result", deduction_result.content, "blob"
)
deduction_success = deduction_result.success
except SpecialistExecutionError as e:
logger.error(f"[{run_id}] Deduction specialist failed: {e}", exc_info=True)
accumulate_metric(task_name, "deduction_error", str(e), "blob")
# Phase 2: Run induction specialist (after deduction so it can see new deductive obs)
logger.info(f"[{run_id}] Phase 2: Running induction specialist")
induction_specialist = SPECIALISTS["induction"]
try:
induction_result = await induction_specialist.run(
workspace_name=workspace_name,
observer=observer,
observed=observed,
session_name=session_name,
hints=exploration_hints,
configuration=configuration,
parent_run_id=run_id,
)
logger.info(
f"[{run_id}] Induction completed: {induction_result.content[:200]}..."
)
accumulate_metric(
task_name, "induction_result", induction_result.content, "blob"
)
induction_success = induction_result.success
except SpecialistExecutionError as e:
logger.error(f"[{run_id}] Induction specialist failed: {e}", exc_info=True)
accumulate_metric(task_name, "induction_error", str(e), "blob")
# Log final metrics
duration_ms = (time.perf_counter() - start_time) * 1000
accumulate_metric(task_name, "total_duration", duration_ms, "ms")
logger.info(f"[{run_id}] Dream cycle completed in {duration_ms:.0f}ms")
log_performance_metrics("dream_orchestrator", run_id)
# Aggregate metrics from specialist results
total_iterations = (deduction_result.iterations if deduction_result else 0) + (
induction_result.iterations if induction_result else 0
)
total_input_tokens = (deduction_result.input_tokens if deduction_result else 0) + (
induction_result.input_tokens if induction_result else 0
)
total_output_tokens = (
deduction_result.output_tokens if deduction_result else 0
) + (induction_result.output_tokens if induction_result else 0)
# Emit DreamRunEvent with aggregated metrics
emit(
DreamRunEvent(
run_id=run_id,
workspace_name=workspace_name,
session_name=session_name,
observer=observer,
observed=observed,
specialists_run=["deduction", "induction"],
deduction_success=deduction_success,
induction_success=induction_success,
surprisal_enabled=settings.DREAM.SURPRISAL.ENABLED,
surprisal_conclusion_count=surprisal_observation_count,
total_iterations=total_iterations,
total_input_tokens=[REDACTED]
total_output_tokens=[REDACTED]
total_duration_ms=duration_ms,
)
)
return DreamResult(
run_id=run_id,
specialists_run=["deduction", "induction"],
deduction_success=deduction_success,
induction_success=induction_success,
surprisal_enabled=settings.DREAM.SURPRISAL.ENABLED,
surprisal_conclusion_count=surprisal_observation_count,
total_iterations=total_iterations,
total_duration_ms=duration_ms,
input_tokens=[REDACTED]
output_tokens=[REDACTED]
)
def _create_queries_from_surprisal(
high_surprisal_obs: list[SurprisalScore],
) -> list[str]:
"""
Create search queries from high-surprisal observations.
Strategy: Use observation content as semantic search queries.
Truncate if too long (>200 chars).
Args:
high_surprisal_obs: List of SurprisalScore objects
Returns:
List of query strings (max 10)
"""
queries: list[Any] = []
for score in high_surprisal_obs:
content = score.observation.content
if len(content) > 200:
content = content[:200] + "..."
queries.append(content)
return queries[:10] # Limit to 10 queries
@sentry_sdk.trace
async def process_dream(
payload: DreamPayload,
workspace_name: str,
) -> None:
"""
Process a dream task by performing collection maintenance operations.
Args:
payload: The dream task payload containing workspace, peer, and dream type information
"""
logger.info(
f"""
(っ- ‸ - ς)ᶻ z 𐰁 ᶻ z 𐰁 ᶻ z 𐰁\n
DREAM: {payload.dream_type} documents for {workspace_name}/{payload.observer}/{payload.observed}\n
𐰁 z ᶻ 𐰁 z ᶻ 𐰁 z ᶻ(っ- ‸ - ς)"""
)
try:
match payload.dream_type:
case DreamType.OMNI:
result = await run_dream(
workspace_name=workspace_name,
observer=payload.observer,
observed=payload.observed,
session_name=payload.session_name,
)
# Log completion (telemetry event already emitted in run_dream)
if result is not None:
logger.info(
f"Dream completed: run_id={result.run_id}, "
+ f"iterations={result.total_iterations}, "
+ f"duration={result.total_duration_ms:.0f}ms"
)
# Both guard fields advance together only on successful consolidation.
now_iso = datetime.now(timezone.utc).isoformat()
async with tracked_db("dream.guard_pair_write") as db:
collection = await crud.get_collection(
db,
workspace_name,
observer=payload.observer,
observed=payload.observed,
with_for_update=True,
)
count_stmt = select(func.count(models.Document.id)).where(
models.Document.workspace_name == workspace_name,
models.Document.observer == payload.observer,
models.Document.observed == payload.observed,
models.Document.level == "explicit",
)
current_explicit_count = int(await db.scalar(count_stmt) or 0)
dream_meta = dict(collection.internal_metadata.get("dream", {}))
dream_meta["last_dream_at"] = now_iso
dream_meta["last_dream_document_count"] = current_explicit_count
await crud.update_collection_internal_metadata(
db,
workspace_name,
payload.observer,
payload.observed,
update_data={"dream": dream_meta},
)
except Exception as e:
logger.error(
f"Error processing dream task {payload.dream_type} for {payload.observer}/{payload.observed}: {str(e)}",
exc_info=True,
)
>sed -n '360,820p' src/crud/representation.pyChunk ID: e19314
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1373
Output:
else:
documents = await crud.query_documents(
db,
workspace_name=self.workspace_name,
observer=self.observer,
observed=self.observed,
query=query,
max_distance=max_distance,
top_k=top_k,
embedding=embedding,
)
db.expunge_all()
return list(documents)
except Exception as e:
logger.error(f"Error getting relevant observations: {e}")
return []
async def _query_documents_recent(
self, db: AsyncSession, top_k: int, session_name: str | None = None
) -> list[models.Document]:
"""Query most recent documents."""
stmt = (
select(models.Document)
.limit(top_k)
.where(
models.Document.workspace_name == self.workspace_name,
models.Document.observer == self.observer,
models.Document.observed == self.observed,
models.Document.deleted_at.is_(None),
*(
[models.Document.session_name == session_name]
if session_name is not None
else []
),
)
.order_by(models.Document.created_at.desc())
)
result = await db.execute(stmt)
documents = result.scalars().all()
db.expunge_all()
return list(documents)
async def _query_documents_most_derived(
self, db: AsyncSession, top_k: int
) -> list[models.Document]:
"""Query most derived documents."""
stmt = (
select(models.Document)
.limit(top_k)
.where(
models.Document.workspace_name == self.workspace_name,
models.Document.observer == self.observer,
models.Document.observed == self.observed,
models.Document.deleted_at.is_(None),
)
.order_by(models.Document.times_derived.desc())
)
result = await db.execute(stmt)
documents = result.scalars().all()
db.expunge_all()
return list(documents)
async def _get_observations_internal(
self,
db: AsyncSession,
query: str,
top_k: int,
max_distance: float,
level: str | None,
) -> list[models.Document]:
"""Internal method that does the actual observation retrieval."""
return await self._query_documents_semantic(
db, query, top_k, max_distance, level
)
async def _query_documents_for_level(
self,
db: AsyncSession,
query: str,
level: str,
count: int,
max_distance: float | None = None,
embedding: list[float] | None = None,
) -> list[models.Document]:
"""Query documents for a specific level."""
documents = await crud.query_documents(
db,
workspace_name=self.workspace_name,
observer=self.observer,
observed=self.observed,
query=query,
max_distance=max_distance,
top_k=count,
filters=self._build_filter_conditions(level),
embedding=embedding,
)
# Sort by creation time
docs_sorted: list[models.Document] = sorted(
list(documents), key=lambda x: x.created_at, reverse=True
)
return docs_sorted
def _build_filter_conditions(
self,
level: str | None = None,
) -> dict[str, Any]:
"""
Build filter conditions for document queries.
Returns a flat dict of key-value pairs for vector store filtering.
"""
filters: dict[str, Any] = {}
if level:
filters["level"] = level
return filters
# Module-level functions for backward compatibility and convenience
async def get_working_representation(
workspace_name: str,
*,
db: AsyncSession | None = None,
observer: str,
observed: str,
session_name: str | None = None,
include_semantic_query: str | None = None,
embedding: list[float] | None = None,
semantic_search_top_k: int | None = None,
semantic_search_max_distance: float | None = None,
include_most_derived: bool = False,
max_observations: int = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
) -> Representation:
"""
Get raw working representation data from the relevant document collection.
This is a convenience function that creates a RepresentationManager and calls
get_working_representation on it.
Args:
db: Optional database session. If provided, uses it directly;
otherwise creates a new session via tracked_db.
embedding: Pre-computed embedding for the semantic query.
"""
manager = RepresentationManager(
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
return await manager.get_working_representation(
db=db,
session_name=session_name,
include_semantic_query=include_semantic_query,
embedding=embedding,
semantic_search_top_k=semantic_search_top_k,
semantic_search_max_distance=semantic_search_max_distance,
include_most_derived=include_most_derived,
max_observations=max_observations,
)
>sed -n '300,700p' src/utils/summarizer.pyChunk ID: 7cbf40
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3516
Output:
"long_summary_up_to_message",
message_seq_in_session,
"count",
)
async def create_short_summary_task():
await _create_and_save_summary(
workspace_name,
session_name,
message_id=message_id,
message_seq_in_session=message_seq_in_session,
message_public_id=message_public_id,
summary_type=SummaryType.SHORT,
configuration=configuration,
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
"short_summary_up_to_message",
message_seq_in_session,
"count",
)
await asyncio.gather(
create_long_summary_task(),
create_short_summary_task(),
return_exceptions=True,
)
else:
# If only one summary needs to be created, run individually
if should_create_long:
await _create_and_save_summary(
workspace_name,
session_name,
message_id=message_id,
message_seq_in_session=message_seq_in_session,
message_public_id=message_public_id,
summary_type=SummaryType.LONG,
configuration=configuration,
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
"long_summary_up_to_message",
message_seq_in_session,
"count",
)
elif should_create_short:
await _create_and_save_summary(
workspace_name,
session_name,
message_id=message_id,
message_seq_in_session=message_seq_in_session,
message_public_id=message_public_id,
summary_type=SummaryType.SHORT,
configuration=configuration,
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
"short_summary_up_to_message",
message_seq_in_session,
"count",
)
async def _create_and_save_summary(
workspace_name: str,
session_name: str,
*,
message_id: int,
message_seq_in_session: int,
message_public_id: str,
summary_type: SummaryType,
configuration: schemas.ResolvedConfiguration,
) -> None:
"""
Create a new summary and save it to the database.
1. Get the latest summary
2. Get the messages since the latest summary
3. Generate a new summary using the messages and the previous summary
4. Save the new summary to the database
"""
logger.debug("Creating new %s summary", summary_type.name)
summary_start = time.perf_counter()
async with tracked_db("summary.fetch_data") as db:
latest_summary = await get_summary(
db, workspace_name, session_name, summary_type
)
if latest_summary:
latest_summary_message_id = latest_summary["message_id"]
# Skip if latest summary already covers message.
if latest_summary_message_id >= message_id:
return
previous_summary_text = latest_summary["content"] if latest_summary else None
# Calculate the sequence range for messages to summarize
# We want to get the last N messages where N is the configured summary interval
messages_per_summary = (
configuration.summary.messages_per_long_summary
if summary_type == SummaryType.LONG
else configuration.summary.messages_per_short_summary
)
start_seq = max(message_seq_in_session - messages_per_summary + 1, 1)
messages: list[Message] = await crud.get_messages_by_seq_range(
db,
workspace_name,
session_name,
start_seq=start_seq,
end_seq=message_seq_in_session,
)
if not messages:
logger.warning("No messages to summarize for message %s", message_id)
return
# Extract values before closing session
formatted_messages = _format_messages(messages)
last_message_id = messages[-1].id
last_message_content_preview = messages[-1].content[:30]
message_count = len(messages)
messages_tokens = sum([message.token_count for message in messages])
previous_summary_tokens = latest_summary["token_count"] if latest_summary else 0
input_tokens = messages_tokens + previous_summary_tokens
(
new_summary,
is_fallback,
llm_input_tokens,
llm_output_tokens,
) = await _create_summary(
formatted_messages=formatted_messages,
previous_summary_text=previous_summary_text,
summary_type=summary_type,
input_tokens=[REDACTED]
message_public_id=message_public_id,
last_message_id=last_message_id,
last_message_content_preview=last_message_content_preview,
message_count=message_count,
)
# Step 3: Save to database with new transaction
if not is_fallback:
# Get base prompt tokens based on summary type
if summary_type == SummaryType.SHORT:
prompt_tokens = estimate_short_summary_prompt_tokens()
else:
prompt_tokens = estimate_long_summary_prompt_tokens()
track_deriver_input_tokens(
task_type=DeriverTaskTypes.SUMMARY,
components={
DeriverComponents.PROMPT: prompt_tokens,
DeriverComponents.MESSAGES: messages_tokens,
DeriverComponents.PREVIOUS_SUMMARY: previous_summary_tokens,
},
)
# Track output tokens
if settings.METRICS.ENABLED:
prometheus_metrics.record_deriver_tokens(
count=new_summary["token_count"],
task_type=DeriverTaskTypes.SUMMARY.value,
token_type=[REDACTED]
component=DeriverComponents.OUTPUT_TOTAL.value,
)
# Save summary to database with new transaction
async with tracked_db("summary.save") as db:
await _save_summary(
db,
new_summary,
workspace_name,
session_name,
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
f"{summary_type.name}_summary_text",
new_summary["content"],
"blob",
)
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
f"{summary_type.name}_summary_size",
new_summary["token_count"],
"tokens",
)
summary_duration = (time.perf_counter() - summary_start) * 1000
accumulate_metric(
f"summary_{workspace_name}_{message_id}",
f"{summary_type.name}_summary_creation",
summary_duration,
"ms",
)
# Emit telemetry event (only for non-fallback summaries)
# Note: Using AgentToolSummaryCreatedEvent with dummy run_id/iteration since
# this is called from the deriver, not from an agentic loop
if not is_fallback:
emit(
AgentToolSummaryCreatedEvent(
run_id="deriver", # Placeholder - not from an agentic run
iteration=0, # Placeholder - not from an agentic loop
parent_category="deriver",
agent_type="summarizer",
workspace_name=workspace_name,
session_name=session_name,
message_id=message_public_id,
message_count=len(messages),
message_seq_in_session=message_seq_in_session,
summary_type="short" if summary_type == SummaryType.SHORT else "long",
input_tokens=[REDACTED]
output_tokens=[REDACTED]
)
)
async def _create_summary(
formatted_messages: str,
previous_summary_text: str | None,
summary_type: SummaryType,
input_tokens: int,
message_public_id: str,
last_message_id: int,
last_message_content_preview: str,
message_count: int,
) -> tuple[Summary, bool, int, int]:
"""
Generate a summary of the provided messages using an LLM.
Args:
formatted_messages: Pre-formatted message string
previous_summary_text: Optional previous summary to provide context
summary_type: Type of summary to create ("short" or "long")
input_tokens: Token count for input
message_public_id: Public ID of the last message
last_message_id: ID of the last message
last_message_content_preview: Preview of last message content for fallback
message_count: Number of messages for fallback
Returns:
A tuple of (Summary, is_fallback, llm_input_tokens, llm_output_tokens)
where is_fallback indicates if the summary was generated using a
fallback instead of an LLM call, and the token counts are from the LLM call
(0 if fallback was used)
"""
response: HonchoLLMCallResponse[str] | None = None
is_fallback = False
llm_input_tokens = 0
llm_output_tokens = 0
try:
if summary_type == SummaryType.SHORT:
response = await create_short_summary(
formatted_messages, input_tokens, previous_summary_text
)
else:
response = await create_long_summary(
formatted_messages, previous_summary_text
)
summary_text = response.content
summary_tokens = response.output_tokens
llm_input_tokens = response.input_tokens
llm_output_tokens = response.output_tokens
# Detect potential issues with the summary
if not summary_text.strip():
logger.error(
"Generated summary is empty (finish_reasons=%s). Falling back to basic summary.",
response.finish_reasons,
)
is_fallback = True
summary_text = (
f"Conversation with {message_count} messages about {last_message_content_preview}..."
if message_count > 0
else ""
)
summary_tokens = estimate_tokens(summary_text) if summary_text else 0
llm_input_tokens = 0
llm_output_tokens = 0
except Exception:
logger.exception("Error generating summary!")
# Fallback to a basic summary in case of error
summary_text = (
f"Conversation with {message_count} messages about {last_message_content_preview}..."
if message_count > 0
else ""
)
summary_tokens = 0
is_fallback = True
return (
Summary(
content=summary_text,
message_id=last_message_id,
summary_type=summary_type.value,
created_at=utc_now_iso(),
token_count=[REDACTED]
message_public_id=message_public_id,
),
is_fallback,
llm_input_tokens,
llm_output_tokens,
)
async def _save_summary(
db: AsyncSession,
summary: Summary,
workspace_name: str,
session_name: str,
) -> None:
"""
Save a summary as metadata on a session.
Args:
db: Database session
summary: The summary to save
workspace_name: Workspace name
session_name: Session name
"""
from src.exceptions import ResourceNotFoundException
# Get the label value from the enum
label_value = summary["summary_type"]
try:
session = await crud.get_session(db, session_name, workspace_name)
except ResourceNotFoundException:
# If session doesn't exist, we can't save the summary
logger.warning(
f"Cannot save summary: session {session_name} not found in workspace {workspace_name}"
)
return
# Use SQLAlchemy update() with PostgreSQL's || operator to properly merge JSONB
# We need to merge the new summary into the existing summaries structure
update_data = {}
existing_summaries = session.internal_metadata.get(SUMMARIES_KEY, {})
existing_summaries[label_value] = summary
update_data[SUMMARIES_KEY] = existing_summaries
stmt = (
update(models.Session)
.where(models.Session.workspace_name == workspace_name)
.where(models.Session.name == session_name)
.values(
internal_metadata=models.Session.internal_metadata.op("||")(update_data)
)
)
await db.execute(stmt)
await db.commit()
cache_key = session_cache_key(workspace_name, session_name)
await cache_client.delete(cache_key)
async def get_summarized_history(
db: AsyncSession,
workspace_name: str,
session_name: str,
cutoff: int | None = None,
summary_type: SummaryType = SummaryType.SHORT,
) -> str:
"""
Get a summarized version of the chat history by combining the latest summary
with all messages since that summary.
Note: history is exclusive of the cutoff message.
Args:
db: Database session
workspace_name: The workspace name
session_name: The session name
cutoff: (Optional) message ID to cutoff at
summary_type: Type of summary to get ("short" or "long")
Returns:
A string formatted history text with summary and recent messages
"""
# Get messages since the latest summary and the summary itself
summary = await get_summary(db, workspace_name, session_name, summary_type)
# Check if we have a valid summary with a message_id
if summary:
messages = await crud.get_messages_id_range(
db,
workspace_name,
session_name,
start_id=summary["message_id"],
end_id=cutoff,
)
else:
messages = await crud.get_messages_id_range(
db, workspace_name, session_name, end_id=cutoff
)
>sed -n '1,360p' src/dialectic/core.pyChunk ID: 374989
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3361
Output:
"""
Core Dialectic Agent implementation.
This agent uses tools to gather context from the memory system
and synthesize responses to queries about a peer.
"""
import logging
import time
import uuid
from collections.abc import AsyncIterator, Callable
from typing import Any, cast
from src import crud
from src.config import ConfiguredModelSettings, ReasoningLevel, settings
from src.dependencies import tracked_db
from src.dialectic import prompts
from src.embedding_client import embedding_client
from src.llm import (
HonchoLLMCallResponse,
StreamingResponseWithMetadata,
honcho_llm_call,
)
from src.telemetry import prometheus_metrics
from src.telemetry.events import DialecticCompletedEvent, emit
from src.telemetry.logging import (
accumulate_metric,
log_performance_metrics,
log_token_usage_metrics,
)
from src.telemetry.prometheus.metrics import DialecticComponents, TokenTypes
from src.utils.agent_tools import (
DIALECTIC_TOOLS,
DIALECTIC_TOOLS_MINIMAL,
create_tool_executor,
search_memory,
)
from src.utils.formatting import format_new_turn_with_timestamp
logger = logging.getLogger(__name__)
def _get_dialectic_level_model_config(
reasoning_level: ReasoningLevel,
) -> ConfiguredModelSettings:
return settings.DIALECTIC.LEVELS[reasoning_level].MODEL_CONFIG
class DialecticAgent:
"""
An agentic dialectic that iteratively gathers context to answer queries.
Unlike the standard dialectic which pre-gathers all context before a single
LLM call, this agent uses tools to strategically gather only the context
needed to answer the specific query.
"""
def __init__(
self,
workspace_name: str,
session_name: str | None,
observer: str,
observed: str,
observer_peer_card: list[str] | None = None,
observed_peer_card: list[str] | None = None,
metric_key: str | None = None,
reasoning_level: ReasoningLevel = "low",
):
"""
Initialize the dialectic agent.
Args:
workspace_name: Workspace identifier
session_name: Session identifier (may be None for global queries)
observer: The peer making the query
observed: The peer being queried about
observer_peer_card: Biographical information about the observer
observed_peer_card: Biographical information about the observed peer
metric_key: Optional key for logging metrics (if provided, agent won't log separately)
reasoning_level: Level of reasoning to apply
"""
self.workspace_name: str = workspace_name
self.session_name: str | None = session_name
self.observer: str = observer
self.observed: str = observed
self.observer_peer_card: list[str] | None = observer_peer_card
self.observed_peer_card: list[str] | None = observed_peer_card
self.metric_key: str | None = metric_key
self.reasoning_level: ReasoningLevel = reasoning_level
# Initialize conversation history with system prompt
self.messages: list[dict[str, str]] = [
{
"role": "system",
"content": prompts.agent_system_prompt(
observer, observed, observer_peer_card, observed_peer_card
),
}
]
self._session_history_initialized: bool = False
self._prefetched_conclusion_count: int = 0
self._run_id: str = str(uuid.uuid4())[
:8
] # Always generate for event correlation
async def _initialize_session_history(self) -> None:
"""Fetch and inject session history into the system prompt if configured."""
if self._session_history_initialized:
return
self._session_history_initialized = True
max_tokens = settings.DIALECTIC.SESSION_HISTORY_MAX_TOKENS
if max_tokens == 0 or not self.session_name:
return
# Fetch recent messages up to the token limit
stmt = await crud.get_messages(
workspace_name=self.workspace_name,
session_name=self.session_name,
token_limit=[REDACTED]
reverse=False, # chronological order
)
async with tracked_db("dialectic.session_history") as db:
result = await db.execute(stmt)
messages = result.scalars().all()
if not messages:
return
# Format messages for injection (must access ORM attrs before session closes)
formatted_messages: list[str] = []
for msg in messages:
formatted = format_new_turn_with_timestamp(
msg.content, msg.created_at, msg.peer_name
)
formatted_messages.append(formatted)
session_history_section = (
"\n\n## SESSION HISTORY\n\n"
"The following is the recent conversation history from this session. "
"Use this as immediate context when answering the query.\n\n"
"<session_history>\n"
f"{chr(10).join(formatted_messages)}\n"
"</session_history>"
)
# Append session history to the system prompt
self.messages[0]["content"] += session_history_section
async def _prefetch_relevant_observations(self, query: str) -> str | None:
"""
Prefetch semantically relevant observations for the query.
This provides immediate context to the agent without requiring
tool calls, improving response quality and speed.
Performs two separate searches to prevent retrieval dilution:
- Explicit observations (produced by deriver)
- Higher-level observations (produced in dreaming/background/chat)
The number of observations fetched depends on reasoning level:
- minimal: 10 of each type (reduced context for cost savings)
- all others: 25 of each type
Args:
query: The user's query
Returns:
Formatted observations string or None if no observations found
"""
# Use reduced prefetch for minimal reasoning to save tokens
prefetch_limit = 10 if self.reasoning_level == "minimal" else 25
try:
# Pre-compute embedding once for both searches (no DB needed)
query_embedding = await embedding_client.embed(query)
# search_memory manages its own short-lived DB sessions so no
# connection is held during external vector-store calls.
explicit_repr = await search_memory(
workspace_name=self.workspace_name,
observer=self.observer,
observed=self.observed,
query=query,
limit=prefetch_limit,
levels=["explicit"],
embedding=query_embedding,
)
derived_repr = await search_memory(
workspace_name=self.workspace_name,
observer=self.observer,
observed=self.observed,
query=query,
limit=prefetch_limit,
levels=["deductive", "inductive", "contradiction"],
embedding=query_embedding,
)
if explicit_repr.is_empty() and derived_repr.is_empty():
return None
# Count prefetched conclusions for telemetry
explicit_count = len(explicit_repr.explicit) + len(explicit_repr.deductive)
derived_count = len(derived_repr.explicit) + len(derived_repr.deductive)
self._prefetched_conclusion_count = explicit_count + derived_count
# Format as two separate sections
parts: list[str] = []
if not explicit_repr.is_empty():
parts.append(explicit_repr.format_as_markdown(include_ids=False))
if not derived_repr.is_empty():
# Include IDs for derived so agent can use get_reasoning_chain
parts.append(derived_repr.format_as_markdown(include_ids=True))
return "\n".join(parts)
except Exception as e:
logger.warning(f"Failed to prefetch observations: {e}")
return None
async def _prepare_query(
self, query: str
) -> tuple[Callable[[str, dict[str, Any]], Any], str, str | None, float]:
"""
Prepare common state for answering a query.
Handles session history initialization, metrics setup, observation prefetching,
user message construction, and tool executor creation.
Args:
query: The question to answer about the peer
Returns:
A tuple of (tool_executor, task_name, run_id, start_time)
"""
await self._initialize_session_history()
run_id: str | None = None
if self.metric_key:
task_name = self.metric_key
else:
run_id = str(uuid.uuid4())[:8]
task_name = f"dialectic_chat_{run_id}"
start_time = time.perf_counter()
accumulate_metric(
task_name,
"context",
(
f"workspace: {self.workspace_name}\n"
f"session: {self.session_name or '(global)'}\n"
f"observer: {self.observer}\n"
f"observed: {self.observed}\n"
f"reasoning_level: {self.reasoning_level}"
),
"blob",
)
accumulate_metric(task_name, "query", query, "blob")
prefetched_observations = await self._prefetch_relevant_observations(query)
if prefetched_observations:
user_content = (
f"Query: {query}\n\n"
f"## Relevant Observations (prefetched)\n"
f"The following observations were found to be semantically relevant to your query. "
f"Use these as primary context. You may still use tools to find additional information if needed.\n\n"
f"{prefetched_observations}"
)
accumulate_metric(
task_name, "prefetched_observations", prefetched_observations, "blob"
)
else:
user_content = f"Query: {query}"
self.messages.append({"role": "user", "content": user_content})
tool_executor: Callable[
[str, dict[str, Any]], Any
] = await create_tool_executor(
workspace_name=self.workspace_name,
session_name=self.session_name,
observer=self.observer,
observed=self.observed,
history_token_limit=[REDACTED]
run_id=self._run_id,
agent_type="dialectic",
parent_category="dialectic",
)
return tool_executor, task_name, run_id, start_time
def _log_response_metrics(
self,
task_name: str,
run_id: str | None,
start_time: float,
response_content: str,
input_tokens: int,
output_tokens: int,
cache_read_input_tokens: int | None,
cache_creation_input_tokens: int | None,
tool_calls_count: int,
thinking_content: str | None,
iterations: int,
) -> None:
"""
Log metrics common to both streaming and non-streaming responses.
Args:
task_name: Metrics task identifier
run_id: Run identifier (None if using caller-provided metric_key)
start_time: Start time from time.perf_counter()
response_content: The full response text
input_tokens: Input token count (actual from API)
output_tokens: Output token count (actual from API)
cache_read_input_tokens: Cache read tokens (if any)
cache_creation_input_tokens: Cache creation tokens (if any)
tool_calls_count: Number of tool calls made
thinking_content: Thinking trace content (if any)
iterations: Number of iterations in the tool execution loop
"""
accumulate_metric(task_name, "tool_calls", tool_calls_count, "count")
if thinking_content:
accumulate_metric(task_name, "thinking", thinking_content, "blob")
log_token_usage_metrics(
task_name,
input_tokens,
output_tokens,
cache_read_input_tokens or 0,
cache_creation_input_tokens or 0,
)
accumulate_metric(task_name, "response", response_content, "blob")
elapsed_ms = (time.perf_counter() - start_time) * 1000
accumulate_metric(task_name, "total_duration", elapsed_ms, "ms")
if not self.metric_key and run_id is not None:
log_performance_metrics("dialectic_chat", run_id)
# Prometheus metrics
if settings.METRICS.ENABLED:
prometheus_metrics.record_dialectic_tokens(
count=input_tokens,
token_type=[REDACTED]
component=DialecticComponents.TOTAL.value,
reasoning_level=self.reasoning_level,
)
prometheus_metrics.record_dialectic_tokens(
count=output_tokens,
token_type=[REDACTED]
component=DialecticComponents.TOTAL.value,
reasoning_level=self.reasoning_level,
)
>sed -n '1,420p' src/dreamer/specialists.pyChunk ID: fb2835
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3541
Output:
"""
Agentic specialists for the dream cycle.
Each specialist is a fully autonomous agent that:
1. Receives probing questions as entry points
2. Uses tools to search for relevant observations
3. Creates new observations (deductive or inductive)
4. Can delete duplicates (deduction only)
"""
from __future__ import annotations
import logging
import time
import uuid
from abc import ABC, abstractmethod
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from src import crud, schemas
from src.config import ConfiguredModelSettings, settings
from src.dependencies import tracked_db
from src.exceptions import ValidationException
from src.llm import HonchoLLMCallResponse, honcho_llm_call
from src.schemas import ResolvedConfiguration
from src.telemetry import prometheus_metrics
from src.telemetry.events import DreamSpecialistEvent, emit
from src.telemetry.logging import accumulate_metric, log_performance_metrics
from src.telemetry.prometheus.metrics import TokenTypes
from src.utils.agent_tools import (
DEDUCTION_SPECIALIST_TOOLS,
INDUCTION_SPECIALIST_TOOLS,
create_tool_executor,
)
logger = logging.getLogger(__name__)
def _require_specialist_model_config(
model_config: ConfiguredModelSettings | None,
*,
specialist_name: str,
) -> ConfiguredModelSettings:
if model_config is None:
raise ValidationException(
f"{specialist_name} MODEL_CONFIG must be resolved before use"
)
return model_config
@dataclass
class SpecialistResult:
"""Result of a specialist run for telemetry and aggregation."""
run_id: str
specialist_type: str
iterations: int
tool_calls_count: int
input_tokens: int
output_tokens: int
duration_ms: float
success: bool
content: str
# Tool names to exclude when peer card creation is disabled
PEER_CARD_TOOL_NAMES = {"update_peer_card"}
class BaseSpecialist(ABC):
"""Base class for agentic specialists."""
name: str = "base"
# Subclasses can override to customize the peer card update instruction
peer_card_update_instruction: str = (
"Only update this with durable profile facts via `update_peer_card`."
)
@abstractmethod
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
"""Get the tools available to this specialist."""
...
@abstractmethod
def get_model_config(self) -> ConfiguredModelSettings:
"""Get the configured model to use for this specialist."""
...
def get_max_tokens(self) -> int:
"""Get max output tokens for this specialist."""
return 16384
def get_max_iterations(self) -> int:
"""Get max tool iterations."""
return 15
@abstractmethod
def build_system_prompt(
self, observed: str, *, peer_card_enabled: bool = True
) -> str:
"""Build the system prompt for this specialist."""
...
@abstractmethod
def build_user_prompt(
self,
hints: list[str] | None,
peer_card: list[str] | None = None,
) -> str:
"""Build the user prompt with optional exploration hints and current peer card."""
...
def _build_peer_card_context(self, peer_card: list[str] | None) -> str:
"""Build the peer card context section for user prompts."""
if not peer_card:
return ""
facts = "\n".join(f"- {fact}" for fact in peer_card)
return f"""
## CURRENT PEER CARD
{facts}
{self.peer_card_update_instruction}
If you update it, send the full deduplicated list and remove stale entries.
"""
async def run(
self,
workspace_name: str,
observer: str,
observed: str,
session_name: str | None,
hints: list[str] | None = None,
configuration: ResolvedConfiguration | None = None,
parent_run_id: str | None = None,
) -> SpecialistResult:
"""
Run the specialist agent.
Uses short-lived DB sessions to avoid holding connections during LLM calls.
Args:
workspace_name: Workspace identifier
observer: The observing peer
observed: The peer being observed
session_name: Session identifier
hints: Optional hints to guide exploration (specialists explore freely if None)
configuration: Resolved configuration for checking feature flags (optional)
parent_run_id: Optional run_id from orchestrator for correlation
Returns:
SpecialistResult with metrics and content
"""
run_id = parent_run_id or str(uuid.uuid4())[:8]
task_name = f"dreamer_{self.name}_{run_id}"
start_time = time.perf_counter()
# Short-lived DB session for preflight operations
async with tracked_db("dream.specialist.preflight") as db:
await crud.get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
if observer != observed:
await crud.get_peer(
db, workspace_name, schemas.PeerCreate(name=observed)
)
# Determine if peer card tools should be included
peer_card_enabled = configuration is None or configuration.peer_card.create
# Fetch current peer card to inject into prompt (saves a tool call)
current_peer_card: list[str] | None = None
if peer_card_enabled:
current_peer_card = await crud.get_peer_card(
db,
workspace_name=workspace_name,
observer=observer,
observed=observed,
)
# DB session closed — LLM calls happen without holding a connection
# Build messages
messages: list[dict[str, str]] = [
{
"role": "system",
"content": self.build_system_prompt(
observed, peer_card_enabled=peer_card_enabled
),
},
{
"role": "user",
"content": self.build_user_prompt(hints, current_peer_card),
},
]
# Create tool executor with telemetry context
tool_executor: Callable[
[str, dict[str, Any]], Any
] = await create_tool_executor(
workspace_name=workspace_name,
observer=observer,
observed=observed,
session_name=session_name,
include_observation_ids=True,
history_token_limit=[REDACTED]
configuration=configuration,
run_id=run_id,
agent_type=self.name,
parent_category="dream",
)
model_config = self.get_model_config()
# Respect operator-configured max_output_tokens on the specialist's
# ModelConfig (e.g. DREAM_DEDUCTION_MODEL_CONFIG__MAX_OUTPUT_TOKENS).
# Only fall back to the specialist's hardcoded default when the
# config leaves max_output_tokens unset or non-positive.
configured_max = model_config.max_output_tokens
effective_max_tokens = (
configured_max
if configured_max and configured_max > 0
else self.get_max_tokens()
)
# Track iterations via callback
iteration_count = 0
def iteration_callback(data: Any) -> None:
nonlocal iteration_count
iteration_count = data.iteration
# Run the agent loop
response: HonchoLLMCallResponse[str] = await honcho_llm_call(
model_config=model_config,
prompt="", # Ignored since we pass messages
max_tokens=[REDACTED]
tools=self.get_tools(peer_card_enabled=peer_card_enabled),
tool_choice=None,
tool_executor=tool_executor,
max_tool_iterations=self.get_max_iterations(),
messages=messages,
track_name=f"Dreamer/{self.name}",
iteration_callback=iteration_callback,
)
# Log metrics
duration_ms = (time.perf_counter() - start_time) * 1000
accumulate_metric(task_name, "total_duration", duration_ms, "ms")
accumulate_metric(
task_name, "tool_calls", len(response.tool_calls_made), "count"
)
accumulate_metric(task_name, "input_tokens", response.input_tokens, "count")
accumulate_metric(task_name, "output_tokens", response.output_tokens, "count")
# Prometheus metrics
if settings.METRICS.ENABLED:
prometheus_metrics.record_dreamer_tokens(
count=response.input_tokens,
specialist_name=self.name,
token_type=[REDACTED]
)
prometheus_metrics.record_dreamer_tokens(
count=response.output_tokens,
specialist_name=self.name,
token_type=[REDACTED]
)
logger.info(
f"{self.name}: Completed in {duration_ms:.0f}ms, "
+ f"{len(response.tool_calls_made)} tool calls, "
+ f"{response.input_tokens} in / {response.output_tokens} out"
)
log_performance_metrics(f"dreamer_{self.name}", run_id)
# Emit telemetry event
emit(
DreamSpecialistEvent(
run_id=run_id,
specialist_type=self.name,
workspace_name=workspace_name,
observer=observer,
observed=observed,
iterations=iteration_count,
tool_calls_count=len(response.tool_calls_made),
input_tokens=[REDACTED]
output_tokens=[REDACTED]
duration_ms=duration_ms,
success=True,
)
)
return SpecialistResult(
run_id=run_id,
specialist_type=self.name,
iterations=iteration_count,
tool_calls_count=len(response.tool_calls_made),
input_tokens=[REDACTED]
output_tokens=[REDACTED]
duration_ms=duration_ms,
success=True,
content=response.content,
)
class DeductionSpecialist(BaseSpecialist):
"""
Creates deductive observations from explicit observations.
This specialist:
1. Explores recent observations and messages to understand what's there
2. Identifies logical implications, knowledge updates, and contradictions
3. Creates new deductive observations with premise linkage
4. Deletes outdated observations
5. Updates peer card with biographical facts
"""
name: str = "deduction"
peer_card_update_instruction: str = "Update this with `update_peer_card` only for stable biographical/profile facts."
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
if peer_card_enabled:
return DEDUCTION_SPECIALIST_TOOLS
return [
t
for t in DEDUCTION_SPECIALIST_TOOLS
if t["name"] not in PEER_CARD_TOOL_NAMES
]
def get_model_config(self) -> ConfiguredModelSettings:
return _require_specialist_model_config(
settings.DREAM.DEDUCTION_MODEL_CONFIG,
specialist_name="DREAM DEDUCTION",
)
def get_max_tokens(self) -> int:
return 8192
def get_max_iterations(self) -> int:
return 12
def build_system_prompt(
self, observed: str, *, peer_card_enabled: bool = True
) -> str:
peer_card_section = ""
if peer_card_enabled:
peer_card_section = """
## PEER CARD (REQUIRED)
The peer card is a summary of stable biographical facts. You MUST update it when you learn:
- Name, age, location, occupation
- Family members and relationships
- Standing instructions ("call me X", "don't mention Y")
- Core preferences and traits
Never add temporary event summaries, one-off conclusions, reasoning traces, or contradiction notes.
Format entries as:
- Plain facts: "Name: Alice", "Works at Google", "Lives in NYC"
- `INSTRUCTION: ...` for standing instructions
- `PREFERENCE: ...` for preferences
- `TRAIT: ...` for personality traits
Call `update_peer_card` with the complete updated list when you have new biographical info.
Keep it concise (max 40 entries), deduplicated, and current."""
return f"""You are a deductive reasoning agent analyzing observations about {observed}.
## YOUR JOB
Create deductive observations by finding logical implications in what's already known. Think like a detective connecting evidence.
## PHASE 1: DISCOVERY
Explore what's actually in memory. Use these tools freely:
- `get_recent_observations` - See what's been learned recently
- `search_memory` - Search for specific topics
- `search_messages` - See actual conversation content
Spend a few tool calls understanding the landscape before creating anything.
## PHASE 2: ACTION
Once you understand what's there, create observations and clean up:
### Knowledge Updates (HIGH PRIORITY)
When the same fact has different values at different times:
- "meeting Tuesday" [old] → "meeting moved to Thursday" [new]
- Create a deductive update observation
- DELETE the outdated observation immediately
### Logical Implications
Extract implicit information:
- "works as SWE at Google" → "has software engineering skills", "employed in tech"
- "has kids ages 5 and 8" → "is a parent", "has school-age children"
### Contradictions
When statements can't both be true (not just updates), flag them:
- "I love coffee" vs "I hate coffee" → contradiction observation
{peer_card_section}
## CREATING OBSERVATIONS
Use `create_observations_deductive`.
```json
{{
"observations": [{{
"content": "The logical conclusion",
"source_ids": ["id1", "id2"],
"premises": ["premise 1 text", "premise 2 text"]
}}]
}}
```
## RULES
1. Don't explain your reasoning - just call tools
2. Create observations based on what you ACTUALLY FIND, not what you expect
>sed -n '420,900p' src/dreamer/specialists.pyChunk ID: e4df85
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1498
Output:
2. Create observations based on what you ACTUALLY FIND, not what you expect
3. Always include source_ids linking to the observations you're synthesizing
4. Empty or missing source_ids will be rejected
5. Delete outdated observations - don't leave duplicates
6. Quality over quantity - fewer good deductions beat many weak ones"""
def build_user_prompt(
self,
hints: list[str] | None,
peer_card: list[str] | None = None,
) -> str:
peer_card_context = self._build_peer_card_context(peer_card)
if hints:
hints_str = "\n".join(f"- {q}" for q in hints[:5])
return f"""{peer_card_context}Start by exploring recent observations and messages. These topics may be worth investigating:
{hints_str}
But follow the evidence - if you find something more interesting, pursue that instead.
Begin with `get_recent_observations` to see what's there."""
return f"""{peer_card_context}Explore the observation space and create deductive observations.
Start with `get_recent_observations` to see what's been learned recently, then investigate whatever seems most promising.
Look for:
1. Knowledge updates (same fact, different values over time)
2. Logical implications that haven't been made explicit
3. Contradictions that need flagging
Go."""
class InductionSpecialist(BaseSpecialist):
"""
Creates inductive observations from explicit and deductive observations.
This specialist:
1. Explores observations to understand what's there
2. Identifies patterns and generalizations across multiple observations
3. Creates new inductive observations with source linkage
4. Updates peer card with high-confidence traits and tendencies
"""
name: str = "induction"
peer_card_update_instruction: str = "Only add highly stable profile traits/preferences; do not copy transient conclusions."
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
if peer_card_enabled:
return INDUCTION_SPECIALIST_TOOLS
return [
t
for t in INDUCTION_SPECIALIST_TOOLS
if t["name"] not in PEER_CARD_TOOL_NAMES
]
def get_model_config(self) -> ConfiguredModelSettings:
return _require_specialist_model_config(
settings.DREAM.INDUCTION_MODEL_CONFIG,
specialist_name="DREAM INDUCTION",
)
def get_max_tokens(self) -> int:
return 8192
def get_max_iterations(self) -> int:
return 10
def build_system_prompt(
self, observed: str, *, peer_card_enabled: bool = True
) -> str:
peer_card_section = ""
if peer_card_enabled:
peer_card_section = """
## PEER CARD (REQUIRED)
After identifying patterns, only update the peer card for durable profile-level traits/preferences:
- `TRAIT: Analytical thinker`
- `TRAIT: Tends to reschedule when stressed`
- `PREFERENCE: Prefers detailed explanations`
Do NOT add temporary patterns, episode-specific conclusions, or reasoning summaries.
Call `update_peer_card` with the complete deduplicated list only when a durable profile update is warranted.
Keep it concise (max 40 entries)."""
return f"""You are an inductive reasoning agent identifying patterns about {observed}.
## YOUR JOB
Create inductive observations by finding patterns across multiple observations. Think like a psychologist identifying behavioral tendencies.
## PHASE 1: DISCOVERY
Explore broadly to find patterns. Use these tools:
- `get_recent_observations` - Recent learnings
- `search_memory` - Topic-specific search
- `search_messages` - Actual conversation content
Look at BOTH explicit observations AND deductive ones. Patterns often emerge from synthesizing across both levels.
## PHASE 2: ACTION
Create inductive observations when you see patterns:
### Behavioral Patterns
- "Tends to reschedule meetings when stressed"
- "Makes decisions after consulting with partner"
- "Projects follow: enthusiasm → doubt → completion"
### Preferences
- "Prefers morning meetings"
- "Likes detailed technical explanations"
### Personality Traits
- "Generally optimistic about outcomes"
- "Detail-oriented in planning"
### Temporal Patterns
- "Career goals have remained consistent"
- "Living situation changes frequently"
{peer_card_section}
## CREATING OBSERVATIONS
Use `create_observations_inductive`.
```json
{{
"observations": [{{
"content": "The pattern or generalization",
"source_ids": ["id1", "id2", "id3"],
"sources": ["evidence 1", "evidence 2"],
"pattern_type": "tendency", // preference|behavior|personality|tendency|correlation
"confidence": "medium" // low (2 sources), medium (3-4), high (5+)
}}]
}}
```
## RULES
1. Minimum 2 source observations required - patterns need evidence
2. Don't just restate a single fact as a pattern
3. Confidence based on evidence count: 2=low, 3-4=medium, 5+=high
4. Look for HOW things change over time, not just static facts
5. Include source_ids - always link back to evidence
6. Empty or missing source_ids will be rejected"""
def build_user_prompt(
self,
hints: list[str] | None,
peer_card: list[str] | None = None,
) -> str:
peer_card_context = self._build_peer_card_context(peer_card)
if hints:
hints_str = "\n".join(f"- {q}" for q in hints[:5])
return f"""{peer_card_context}Explore and find patterns. These areas may be worth investigating:
{hints_str}
But follow the evidence - if you find patterns elsewhere, pursue those.
Start with `get_recent_observations`."""
return f"""{peer_card_context}Explore the observation space and identify patterns.
Remember: patterns need 2+ sources. Look for tendencies, preferences, and behavioral regularities.
Go."""
# Singleton instances
SPECIALISTS: dict[str, BaseSpecialist] = {
"deduction": DeductionSpecialist(),
"induction": InductionSpecialist(),
}
>sed -n '1,260p' src/utils/agent_tools.pyChunk ID: 32efae
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2257
Output:
import asyncio
import logging
import weakref
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Any, cast
from pydantic import ValidationError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from src import crud, models, schemas
from src.config import settings
from src.dependencies import tracked_db
from src.embedding_client import embedding_client
from src.models import Document
from src.schemas import ResolvedConfiguration
from src.telemetry.events import (
AgentToolConclusionsCreatedEvent,
AgentToolConclusionsDeletedEvent,
AgentToolPeerCardUpdatedEvent,
emit,
)
from src.utils import summarizer
from src.utils.formatting import format_new_turn_with_timestamp, utc_now_iso
from src.utils.representation import Representation
from src.utils.types import get_current_iteration
logger = logging.getLogger(__name__)
# Hard cap to prevent unbounded peer card growth from repeated agent updates.
MAX_PEER_CARD_FACTS = 40
def _normalized_observation_input(
obs: schemas.ObservationInput,
) -> schemas.ObservationInput:
"""Return an observation input with content normalized for persistence/embedding."""
return obs.model_copy(update={"content": obs.content.strip()})
def _base_observation_properties() -> dict[str, Any]:
return {
"content": {
"type": "string",
"description": "The observation content",
},
"level": {
"type": "string",
"enum": [
"explicit",
"deductive",
"inductive",
"contradiction",
],
"description": (
"Level: 'explicit' for direct facts, 'deductive' for logical "
+ "necessities, 'inductive' for patterns, 'contradiction' for "
+ "conflicting statements"
),
},
"source_ids": {
"type": "array",
"items": {"type": "string"},
"description": (
"Document IDs of source or premise observations. Required and "
+ "must be non-empty for deductive, inductive, and contradiction "
+ "observations."
),
},
"premises": {
"type": "array",
"items": {"type": "string"},
"description": "(For deductive) Human-readable premise text for display",
},
"sources": {
"type": "array",
"items": {"type": "string"},
"description": "(For inductive/contradiction) Human-readable source text for display",
},
"pattern_type": {
"type": "string",
"enum": [
"preference",
"behavior",
"personality",
"tendency",
"correlation",
],
"description": "(For inductive only) Type of pattern being identified",
},
"confidence": {
"type": "string",
"enum": ["high", "medium", "low"],
"description": (
"(For inductive only) Confidence level: 'high' for 5+ sources, "
+ "'medium' for 3-4, 'low' for 2"
),
},
}
def _generic_observation_item_schema() -> dict[str, Any]:
return {
"type": "object",
"properties": _base_observation_properties(),
"required": ["content", "level"],
"additionalProperties": False,
"allOf": [
{
"if": {"properties": {"level": {"const": "deductive"}}},
"then": {
"required": ["source_ids", "premises"],
"properties": {
"source_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
},
"premises": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
},
},
},
},
{
"if": {"properties": {"level": {"const": "inductive"}}},
"then": {
"required": [
"source_ids",
"sources",
"pattern_type",
"confidence",
],
"properties": {
"source_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
},
"sources": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
},
},
},
},
{
"if": {"properties": {"level": {"const": "contradiction"}}},
"then": {
"required": ["source_ids", "sources"],
"properties": {
"source_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
},
"sources": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
},
},
},
},
],
}
def _deductive_observation_item_schema() -> dict[str, Any]:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The deductive conclusion as a self-contained statement",
},
"source_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"description": "Required non-empty list of source observation IDs supporting the deduction",
},
"premises": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"description": "Required human-readable premise text matching the source observations",
},
},
"required": ["content", "source_ids", "premises"],
"additionalProperties": False,
}
def _inductive_observation_item_schema() -> dict[str, Any]:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The inductive pattern or generalization as a self-contained statement",
},
"source_ids": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"description": "Required list of at least two source observation IDs supporting the pattern",
},
"sources": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"description": "Required human-readable evidence text matching the source observations",
},
"pattern_type": {
"type": "string",
"enum": [
"preference",
"behavior",
"personality",
"tendency",
"correlation",
],
"description": "Required pattern category",
},
"confidence": {
"type": "string",
"enum": ["high", "medium", "low"],
"description": "Required confidence level based on evidence count",
},
},
"required": ["content", "source_ids", "sources", "pattern_type", "confidence"],
"additionalProperties": False,
}
def _safe_int(value: Any, default: int) -> int:
"""Coerce a tool input value to int, returning default on failure.
LLMs sometimes pass non-numeric strings (e.g. 'Infinity') for integer
parameters which would crash ``min()`` comparisons.
"""
try:
return int(value)
except (TypeError, ValueError, OverflowError):
return default
# Module-level lock registry for thread-safe observation creation.
# Keyed by (workspace_name, observer, observed) to ensure all tool executors
# operating on the same data share the same lock.
#
# Uses WeakValueDictionary so entries are automatically removed when no
# ToolContext holds a reference to the lock (i.e., all executors for that
# key have finished and been garbage collected). This prevents unbounded
>sed -n '260,620p' src/utils/agent_tools.pyChunk ID: 4b2f04
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3666
Output:
# key have finished and been garbage collected). This prevents unbounded
# growth over the lifetime of a long-running deriver process.
_observation_locks: weakref.WeakValueDictionary[tuple[str, str, str], asyncio.Lock] = (
weakref.WeakValueDictionary()
)
_registry_lock = asyncio.Lock()
async def get_observation_lock(
workspace_name: str, observer: str, observed: str
) -> asyncio.Lock:
"""
Get or create a lock for a specific workspace/observer/observed combination.
This ensures that concurrent tool executors operating on the same observation
space share a lock, preventing race conditions during document creation.
The lock is stored as a weak reference — it stays alive as long as at least
one ToolContext (via create_tool_executor) holds a strong reference. Once all
executors for a key finish and are garbage collected, the entry is
automatically removed from the registry.
Args:
workspace_name: Workspace identifier
observer: The observing peer
observed: The peer being observed
Returns:
An asyncio.Lock shared by all executors for this combination
"""
key = (workspace_name, observer, observed)
async with _registry_lock:
lock = _observation_locks.get(key)
if lock is None:
lock = asyncio.Lock()
_observation_locks[key] = lock
return lock
@dataclass
class ObservationFailure:
"""Records a single observation that failed during creation."""
content_preview: str
error: str
@dataclass
class ObservationsCreatedResult:
"""Result of a batch create_observations call."""
created_count: int
created_levels: list[str]
failed: list[ObservationFailure]
def _truncate_tool_output(output: str, max_chars: int | None = None) -> str:
"""Truncate tool output to prevent token explosion."""
if max_chars is None:
max_chars = settings.LLM.MAX_TOOL_OUTPUT_CHARS
if len(output) <= max_chars:
return output
truncated = output[:max_chars]
return (
truncated
+ f"\n\n[OUTPUT TRUNCATED - showing {max_chars:,} of {len(output):,} characters]"
)
def _truncate_message_content(content: str, max_chars: int | None = None) -> str:
"""Truncate individual message content (simple beginning truncation)."""
if max_chars is None:
max_chars = settings.LLM.MAX_MESSAGE_CONTENT_CHARS
if len(content) <= max_chars:
return content
return content[:max_chars] + "..."
def _extract_pattern_snippet(
content: str, pattern: str, max_chars: int | None = None
) -> str:
"""Extract snippet around a regex pattern match.
For grep/exact text search, finds the pattern and extracts context around it.
"""
import re
if max_chars is None:
max_chars = settings.LLM.MAX_MESSAGE_CONTENT_CHARS
if len(content) <= max_chars:
return content
match = re.search(re.escape(pattern), content, re.IGNORECASE)
if not match:
# No match, return beginning
return content[:max_chars] + "..."
match_start = match.start()
match_end = match.end()
# Calculate window around match
match_len = match_end - match_start
remaining = max_chars - match_len
before = remaining // 2
after = remaining - before
start = max(0, match_start - before)
end = min(len(content), match_end + after)
# Adjust if we hit boundaries
if start == 0:
end = min(len(content), max_chars)
elif end == len(content):
start = max(0, len(content) - max_chars)
snippet = content[start:end]
prefix = "..." if start > 0 else ""
suffix = "..." if end < len(content) else ""
return f"{prefix}{snippet}{suffix}"
TOOLS: dict[str, dict[str, Any]] = {
"create_observations": {
"name": "create_observations",
"description": "Create observations at any level: explicit (facts), deductive (logical necessities), inductive (patterns), or contradiction (conflicting statements). For deductive, inductive, and contradiction observations, missing or empty source_ids are invalid and will be rejected.",
"input_schema": {
"type": "object",
"properties": {
"observations": {
"type": "array",
"description": "List of observations to create",
"items": _generic_observation_item_schema(),
},
},
"required": ["observations"],
},
},
"create_observations_deductive": {
"name": "create_observations_deductive",
"description": "Create new deductive observations discovered while answering the query. Every observation must include non-empty source_ids and premise text. Use this only for novel deductions grounded in existing observations.",
"input_schema": {
"type": "object",
"properties": {
"observations": {
"type": "array",
"description": "List of new deductive observations to create",
"items": _deductive_observation_item_schema(),
},
},
"required": ["observations"],
},
},
"create_observations_inductive": {
"name": "create_observations_inductive",
"description": "Create new inductive observations discovered while answering the query. Every observation must include source_ids, source text, pattern_type, and confidence. Use this only for patterns supported by multiple observations.",
"input_schema": {
"type": "object",
"properties": {
"observations": {
"type": "array",
"description": "List of new inductive observations to create",
"items": _inductive_observation_item_schema(),
},
},
"required": ["observations"],
},
},
"update_peer_card": {
"name": "update_peer_card",
"description": (
"Update the peer card with durable profile facts about the observed peer. "
+ "Only include stable biographical facts, standing instructions, and long-lived preferences/traits. "
+ "Do not include one-off conclusions, temporary events, or duplicate entries."
),
"input_schema": {
"type": "object",
"properties": {
"content": {
"type": "array",
"description": (
"Complete deduplicated peer card list (max 40 entries). "
+ "Each entry should be a concise standalone profile fact."
),
"items": {"type": "string"},
},
},
"required": ["content"],
},
},
"get_recent_history": {
"name": "get_recent_history",
"description": "Retrieve recent conversation history to get more context about the conversation.",
"input_schema": {
"type": "object",
"properties": {},
},
},
"search_memory": {
"name": "search_memory",
"description": "Search for observations in memory using semantic similarity. Use this to find relevant information about the peer when you need to recall specific details.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query text",
},
"top_k": {
"type": "integer",
"description": "(Optional) number of results to return (default: 20, max: 40)",
"default": 20,
},
},
"required": ["query"],
},
},
"get_observation_context": {
"name": "get_observation_context",
"description": "Retrieve messages for given message IDs along with surrounding context. Takes message IDs (from an observation's message_ids field) and retrieves those messages plus the messages immediately before and after each one to provide conversation context.",
"input_schema": {
"type": "object",
"properties": {
"message_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of message IDs to retrieve (get these from observation.message_ids in search results)",
},
},
"required": ["message_ids"],
},
},
"search_messages": {
"name": "search_messages",
"description": "Search for messages using semantic similarity and retrieve conversation snippets. Returns matching messages with surrounding context (2 messages before and after). Nearby matches within the same session are merged into a single snippet to avoid repetition.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query text to find relevant messages",
},
"limit": {
"type": "integer",
"description": "Maximum number of matching messages to return (default: 10, max: 20)",
"default": 10,
},
},
"required": ["query"],
},
},
"grep_messages": {
"name": "grep_messages",
"description": "Search for messages containing specific text (case-insensitive). Unlike semantic search, this finds EXACT text matches. Use for finding specific names, dates, phrases, or keywords mentioned in conversations. Returns messages with surrounding context.",
"input_schema": {
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Text to search for (case-insensitive substring match)",
},
"limit": {
"type": "integer",
"description": "Maximum messages to return (default: 10, max: 30)",
"default": 10,
},
"context_window": {
"type": "integer",
"description": "Number of messages before/after each match to include (default: 2)",
"default": 2,
},
},
"required": ["text"],
},
},
"get_messages_by_date_range": {
"name": "get_messages_by_date_range",
"description": "Get messages from a specific date range. Use this to find what was discussed during a particular time period, or to compare information before vs after a date. Essential for knowledge update questions.",
"input_schema": {
"type": "object",
"properties": {
"after_date": {
"type": "string",
"description": "Start date (ISO format, e.g., '2024-01-15'). Returns messages after this date.",
},
"before_date": {
"type": "string",
"description": "End date (ISO format). Returns messages before this date.",
},
"limit": {
"type": "integer",
"description": "Maximum messages to return (default: 20, max: 50)",
"default": 20,
},
"order": {
"type": "string",
"enum": ["asc", "desc"],
"description": "Sort order: 'asc' for oldest first, 'desc' for newest first (default: desc)",
"default": "desc",
},
},
},
},
"search_messages_temporal": {
"name": "search_messages_temporal",
"description": "Semantic search for messages with optional date filtering. Combines the power of semantic search with time constraints. Use after_date to find recent mentions of a topic, or before_date to find what was said about something before a certain point. Best for knowledge update questions where you need to find the MOST RECENT discussion of a topic.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Semantic search query",
},
"after_date": {
"type": "string",
"description": "Only return messages after this date (ISO format, e.g., '2024-01-15')",
},
"before_date": {
"type": "string",
"description": "Only return messages before this date (ISO format)",
},
"limit": {
"type": "integer",
"description": "Maximum messages to return (default: 10, max: 20)",
"default": 10,
},
"context_window": {
"type": "integer",
"description": "Messages before/after each match (default: 2)",
"default": 2,
},
},
"required": ["query"],
},
},
"get_recent_observations": {
"name": "get_recent_observations",
"description": "Get the most recent observations about the peer. Useful for understanding what's been learned recently.",
"input_schema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of observations to return (default: 10)",
"default": 10,
},
"session_only": {
"type": "boolean",
"description": "If true, only return observations from the current session (default: false)",
"default": False,
},
},
},
},
"get_most_derived_observations": {
"name": "get_most_derived_observations",
"description": "Get observations that have been reinforced most frequently across conversations. These represent the most established facts about the peer.",
"input_schema": {
"type": "object",
"properties": {
>sed -n '1,260p' src/crud/peer_card.pyChunk ID: 7dd77d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 755
Output:
from __future__ import annotations
import logging
from typing import Any, cast
from sqlalchemy import update
from sqlalchemy.engine import CursorResult
from sqlalchemy.ext.asyncio import AsyncSession
from src import exceptions, models, schemas
from src.cache.client import safe_cache_delete
from src.crud.peer import get_or_create_peers, get_peer, peer_cache_key
logger = logging.getLogger(__name__)
async def get_peer_card(
db: AsyncSession,
workspace_name: str,
*,
observer: str,
observed: str,
) -> list[str] | None:
"""
Get peer card from internal_metadata.
The peer card is returned for the observer/observed relationship.
Args:
db: Database session
workspace_name: Name of the workspace
observed: Peer name of the peer described in the peer card
observer: Peer name of the observer
Returns:
The peer's card text if present, otherwise None.
Raises:
ResourceNotFoundException: If the peer does not exist.
"""
peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
return cast(
list[str] | None,
peer.internal_metadata.get(
construct_peer_card_label(observer=observer, observed=observed)
),
)
async def set_peer_card(
db: AsyncSession,
workspace_name: str,
peer_card: list[str],
*,
observer: str,
observed: str,
) -> None:
"""
Set peer card for a peer.
If observer_name is provided, the peer card is set for the observer/observed relationship.
Args:
db: Database session
workspace_name: Name of the workspace
peer_card: List of strings to set as the peer card
observed: Peer name of the peer described in the peer card
observer: Peer name of the observer
"""
# Ensure the peer exists (get-or-create)
peers_result = await get_or_create_peers(
db, workspace_name, [schemas.PeerCreate(name=observer)]
)
stmt = (
update(models.Peer)
.where(models.Peer.workspace_name == workspace_name)
.where(models.Peer.name == observer)
.values(
internal_metadata=models.Peer.internal_metadata.op("||")(
{
construct_peer_card_label(
observer=observer, observed=observed
): peer_card
}
)
)
)
result = cast(CursorResult[Any], await db.execute(stmt))
if result.rowcount == 0:
raise exceptions.ResourceNotFoundException(
f"Peer {observer} not found in workspace {workspace_name}"
)
await db.commit()
await peers_result.post_commit()
# Invalidate cache - read-through pattern
cache_key = peer_cache_key(workspace_name, observer)
await safe_cache_delete(cache_key)
def construct_peer_card_label(*, observer: str, observed: str) -> str:
if observer == observed:
return "peer_card"
return f"{observed}_peer_card"
>sed -n '1,260p' src/routers/messages.pyChunk ID: 3cbdc4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2131
Output:
import logging
from fastapi import (
APIRouter,
BackgroundTasks,
Body,
Depends,
File,
Form,
Path,
Query,
UploadFile,
)
from fastapi_pagination import Page
from fastapi_pagination.ext.sqlalchemy import apaginate
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm.attributes import flag_modified
from src import crud, schemas
from src.config import settings
from src.dependencies import db
from src.deriver import enqueue
from src.exceptions import FileTooLargeError, ResourceNotFoundException
from src.security import require_auth
from src.telemetry import prometheus_metrics
from src.utils.files import process_file_uploads_for_messages
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/workspaces/{workspace_id}/sessions/{session_id}/messages",
tags=["messages"],
dependencies=[
Depends(require_auth(workspace_name="workspace_id", session_name="session_id"))
],
)
async def parse_upload_form(
peer_id: str = Form(...),
metadata: str | None = Form(None),
configuration: str | None = Form(None),
created_at: str | None = Form(None),
) -> schemas.MessageUploadCreate:
"""Parse form data for file upload requests"""
import json
from datetime import datetime
parsed_metadata = None
if metadata:
try:
parsed_metadata = json.loads(metadata)
except json.JSONDecodeError:
logger.warning(f"Failed to parse metadata JSON: {metadata}")
parsed_metadata = None
parsed_configuration = None
if configuration:
try:
parsed_configuration = json.loads(configuration)
except json.JSONDecodeError:
logger.warning(f"Failed to parse configuration JSON: {configuration}")
parsed_configuration = None
parsed_created_at = None
if created_at:
try:
parsed_created_at = datetime.fromisoformat(
created_at.replace("Z", "+00:00")
)
except (ValueError, AttributeError):
logger.warning(f"Failed to parse created_at: {created_at}")
parsed_created_at = None
return schemas.MessageUploadCreate(
peer_id=peer_id,
metadata=parsed_metadata,
configuration=parsed_configuration,
created_at=parsed_created_at,
)
@router.post("", response_model=list[schemas.Message], status_code=201)
@router.post(
"/", response_model=list[schemas.Message], status_code=201, include_in_schema=False
) # backwards compatibility with pre-2.6.0 faulty route endpoint
async def create_messages_for_session(
background_tasks: BackgroundTasks,
messages: schemas.MessageBatchCreate,
workspace_id: str = Path(...),
session_id: str = Path(...),
db: AsyncSession = db,
):
"""Add new message(s) to a session."""
try:
created_messages = await crud.create_messages(
db,
messages=messages.messages,
workspace_name=workspace_id,
session_name=session_id,
)
# Prometheus metrics
if settings.METRICS.ENABLED:
prometheus_metrics.record_messages_created(
count=len(created_messages),
workspace_name=workspace_id,
)
# Enqueue for processing (existing logic)
payloads = [
{
"workspace_name": workspace_id,
"session_name": session_id,
"message_id": message.id,
"content": message.content,
"peer_name": message.peer_name,
"created_at": message.created_at,
"message_public_id": message.public_id,
"seq_in_session": message.seq_in_session,
"configuration": original.configuration,
}
for message, original in zip(
created_messages, messages.messages, strict=True
)
]
# Enqueue all messages in one call
background_tasks.add_task(enqueue, payloads)
return created_messages
except ValueError as e:
logger.warning(f"Failed to create messages for session {session_id}: {str(e)}")
raise
@router.post("/upload", response_model=list[schemas.Message], status_code=201)
async def create_messages_with_file(
background_tasks: BackgroundTasks,
workspace_id: str = Path(...),
session_id: str = Path(...),
form_data: schemas.MessageUploadCreate = Depends(parse_upload_form),
file: UploadFile = File(...),
db: AsyncSession = db,
):
"""Create messages from uploaded files. Files are converted to text and split into multiple messages."""
# Validate file size
if file.size and file.size > settings.MAX_FILE_SIZE:
raise FileTooLargeError(
f"File size ({file.size} bytes) exceeds maximum allowed size ({settings.MAX_FILE_SIZE} bytes)",
)
# Process files using shared utility function
all_message_data = await process_file_uploads_for_messages(
file=file,
peer_id=form_data.peer_id,
metadata=form_data.metadata,
configuration=form_data.configuration,
created_at=form_data.created_at,
)
# Create messages
message_creates = [item["message_create"] for item in all_message_data]
created_messages = await crud.create_messages(
db,
messages=message_creates,
workspace_name=workspace_id,
session_name=session_id,
)
# Update internal_metadata for file-related messages
for i, message in enumerate(created_messages):
file_metadata = all_message_data[i]["file_metadata"]
message.internal_metadata.update(file_metadata)
flag_modified(message, "internal_metadata")
await db.commit()
# Enqueue for processing (same as regular messages)
payloads = [
{
"workspace_name": workspace_id,
"session_name": session_id,
"message_id": message.id,
"content": message.content,
"peer_name": message.peer_name,
"created_at": message.created_at,
"message_public_id": message.public_id,
"seq_in_session": message.seq_in_session,
"configuration": form_data.configuration,
}
for message in created_messages
]
background_tasks.add_task(enqueue, payloads)
logger.debug(
"Batch of %s messages created from file uploads and queued for processing",
len(created_messages),
)
# Prometheus metrics
if settings.METRICS.ENABLED:
prometheus_metrics.record_messages_created(
count=len(created_messages),
workspace_name=workspace_id,
)
return created_messages
@router.post("/list", response_model=Page[schemas.Message])
async def get_messages(
workspace_id: str = Path(...),
session_id: str = Path(...),
options: schemas.MessageGet | None = Body(
None, description="Filtering options for the message list"
),
reverse: bool | None = Query(
False, description="Whether to reverse the order of results"
),
db: AsyncSession = db,
):
"""Get all messages for a Session with optional filters. Results are paginated."""
try:
filters = None
if options and hasattr(options, "filters"):
filters = options.filters
if filters == {}:
filters = None
messages_query = await crud.get_messages(
workspace_name=workspace_id,
session_name=session_id,
filters=filters,
reverse=reverse,
)
return await apaginate(db, messages_query)
except ValueError as e:
logger.warning(f"Failed to get messages for session {session_id}: {str(e)}")
raise ResourceNotFoundException("Session not found") from e
@router.get("/{message_id}", response_model=schemas.Message)
async def get_message(
workspace_id: str = Path(...),
session_id: str = Path(...),
message_id: str = Path(...),
db: AsyncSession = db,
):
"""Get a single message by ID from a Session."""
honcho_message = await crud.get_message(
db, workspace_name=workspace_id, session_name=session_id, message_id=message_id
)
if honcho_message is None:
logger.warning(f"Message {message_id} not found in session {session_id}")
raise ResourceNotFoundException(f"Message with ID {message_id} not found")
return honcho_message
>sed -n '620,1120p' src/utils/agent_tools.pyChunk ID: fadb4e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4640
Output:
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of observations to return (default: 10)",
"default": 10,
},
},
},
},
"get_session_summary": {
"name": "get_session_summary",
"description": "Get the session summary (short or long form). Useful for understanding the overall conversation context.",
"input_schema": {
"type": "object",
"properties": {
"summary_type": {
"type": "string",
"enum": ["short", "long"],
"description": "Type of summary to retrieve (default: short)",
"default": "short",
},
},
},
},
"get_peer_card": {
"name": "get_peer_card",
"description": "Get the peer card containing known biographical information about the peer (name, age, location, etc.).",
"input_schema": {
"type": "object",
"properties": {},
},
},
"delete_observations": {
"name": "delete_observations",
"description": "Delete observations by their IDs. Use the exact ID shown in [id:xxx] format from search results. Example: if observation shows '[id:abc123XYZ]', pass 'abc123XYZ' to delete it.",
"input_schema": {
"type": "object",
"properties": {
"observation_ids": {
"type": "array",
"items": {"type": "string"},
"description": "List of observation IDs to delete (use the exact ID from [id:xxx] in search results)",
},
},
"required": ["observation_ids"],
},
},
"finish_consolidation": {
"name": "finish_consolidation",
"description": "Signal that consolidation is complete. Call this when you have finished your consolidation work and are ready to stop. You MUST call this tool when done - do not keep exploring indefinitely.",
"input_schema": {
"type": "object",
"properties": {
"summary": {
"type": "string",
"description": "Brief summary of what was accomplished (peer card updates, observations consolidated, observations deleted)",
},
},
"required": ["summary"],
},
},
"extract_preferences": {
"name": "extract_preferences",
"description": "Extract user preferences and standing instructions from conversation history. This tool performs both semantic and text searches for preferences, instructions, and communication style preferences, then returns them for adding to the peer card. Call this FIRST during consolidation.",
"input_schema": {
"type": "object",
"properties": {},
},
},
"get_reasoning_chain": {
"name": "get_reasoning_chain",
"description": "Get the reasoning chain for an observation - traverse the tree to find premises (for deductive) or sources (for inductive), and/or find conclusions derived from this observation. Use this to understand how an observation was derived or what conclusions depend on it.",
"input_schema": {
"type": "object",
"properties": {
"observation_id": {
"type": "string",
"description": "The document ID of the observation to get the reasoning chain for",
},
"direction": {
"type": "string",
"enum": ["premises", "conclusions", "both"],
"description": "'premises' to get what this observation is based on, 'conclusions' to get what depends on it, 'both' for full context",
"default": "both",
},
},
"required": ["observation_id"],
},
},
}
# Tools for the dialectic agent (analysis)
DIALECTIC_TOOLS: list[dict[str, Any]] = [
TOOLS["search_memory"],
TOOLS["search_messages"],
TOOLS["get_observation_context"],
# TOOLS["create_observations_deductive"],
TOOLS["grep_messages"], # For exact text search (names, dates, keywords)
TOOLS["get_messages_by_date_range"], # For temporal/date-based queries
TOOLS["search_messages_temporal"], # Semantic search + date filtering
TOOLS["get_reasoning_chain"], # Traverse reasoning trees
]
# Minimal tools for dialectic agent at "minimal" reasoning level
# Reduces cost by limiting tool definitions in context
DIALECTIC_TOOLS_MINIMAL: list[dict[str, Any]] = [
TOOLS["search_memory"],
TOOLS["search_messages"],
]
# Tools for the dreamer agent (consolidation + peer card + deduplication)
DREAMER_TOOLS: list[dict[str, Any]] = [
# Preference extraction (should be called first)
TOOLS["extract_preferences"],
TOOLS["get_recent_observations"],
TOOLS["get_most_derived_observations"],
TOOLS["search_memory"],
TOOLS["get_peer_card"],
TOOLS["create_observations"],
TOOLS["delete_observations"],
TOOLS["update_peer_card"],
# Message access tools for context verification
TOOLS["search_messages"],
TOOLS["get_observation_context"],
# Tree traversal
TOOLS["get_reasoning_chain"],
# Completion signal
TOOLS["finish_consolidation"],
]
# Tools for the deduction specialist (dreamer phase 1)
# Creates deductive observations from explicit observations, can delete duplicates
# Includes message access for context and self-directed exploration
# Note: get_peer_card is not included - peer card is injected into the prompt directly
DEDUCTION_SPECIALIST_TOOLS: list[dict[str, Any]] = [
# Discovery tools
TOOLS["get_recent_observations"],
TOOLS["search_memory"],
TOOLS["search_messages"],
# Action tools
TOOLS["create_observations_deductive"],
TOOLS["delete_observations"],
TOOLS["update_peer_card"],
]
# Tools for the induction specialist (dreamer phase 2)
# Creates inductive observations from explicit and deductive observations
# Includes message access for context and self-directed exploration
# Note: get_peer_card is not included - peer card is injected into the prompt directly
INDUCTION_SPECIALIST_TOOLS: list[dict[str, Any]] = [
# Discovery tools
TOOLS["get_recent_observations"],
TOOLS["search_memory"],
TOOLS["search_messages"],
# Action tools
TOOLS["create_observations_inductive"],
TOOLS["update_peer_card"],
]
async def create_observations(
observations: list[schemas.ObservationInput],
observer: str,
observed: str,
session_name: str | None,
workspace_name: str,
message_ids: list[int],
message_created_at: str,
) -> ObservationsCreatedResult:
"""
Create multiple observations (documents) in the memory system in a single call.
Uses short-lived DB sessions to avoid holding connections during embedding API calls.
Args:
observations: List of validated observation inputs
observer: The peer making the observation
observed: The peer being observed
session_name: Session identifier
workspace_name: Workspace identifier
message_ids: List of message IDs these observations are based on
message_created_at: Timestamp of the message that triggered these observations
Returns:
ObservationsCreatedResult with created count and any per-observation failures
"""
if not observations:
logger.warning("create_observations called with empty list")
return ObservationsCreatedResult(created_count=0, created_levels=[], failed=[])
normalized_observations = [
_normalized_observation_input(obs) for obs in observations if obs.content.strip()
]
if not normalized_observations:
logger.info("No non-empty observations to create")
return ObservationsCreatedResult(created_count=0, created_levels=[], failed=[])
# Phase 1: Ensure collection exists (short DB scope)
async with tracked_db("create_observations.collection") as db:
await crud.get_or_create_collection(
db,
workspace_name,
observer=observer,
observed=observed,
)
# Phase 2: Compute embeddings (no DB needed)
contents = [obs.content for obs in normalized_observations]
embeddings_by_index: dict[int, list[float]] | None = None
try:
embeddings = await embedding_client.simple_batch_embed(contents)
embeddings_by_index = dict(
zip(range(len(normalized_observations)), embeddings, strict=True)
)
except Exception as e:
logger.warning(
"Batch embedding failed for create_observations; falling back to per-observation embedding: %s",
e,
)
# Build document objects with pre-computed embeddings
documents: list[schemas.DocumentCreate] = []
failed: list[ObservationFailure] = []
for i, obs in enumerate(normalized_observations):
embedding: list[float]
if embeddings_by_index is not None:
embedding = embeddings_by_index[i]
else:
try:
embedding = await embedding_client.embed(obs.content)
except Exception as e:
logger.warning(
"Error embedding observation content for level '%s': %s",
obs.level,
e,
)
failed.append(
ObservationFailure(
content_preview=obs.content[:50],
error=f"Embedding failed: {e}",
)
)
continue
# Build metadata with level-specific fields
metadata = schemas.DocumentMetadata(
message_ids=message_ids,
message_created_at=message_created_at,
source_ids=obs.source_ids
if obs.level in ("deductive", "inductive", "contradiction")
else None,
premises=obs.premises if obs.level == "deductive" else None,
sources=obs.sources
if obs.level in ("inductive", "contradiction")
else None,
pattern_type=obs.pattern_type if obs.level == "inductive" else None,
confidence=(obs.confidence or "medium")
if obs.level == "inductive"
else None,
)
doc = schemas.DocumentCreate(
content=obs.content,
session_name=session_name,
level=obs.level,
metadata=metadata,
embedding=embedding,
source_ids=obs.source_ids
if obs.level in ("deductive", "inductive", "contradiction")
else None,
)
documents.append(doc)
# Phase 3: Bulk create all documents (short DB scope)
accepted: list[schemas.DocumentCreate] = []
if documents:
async with tracked_db("create_observations.save") as db:
accepted = await crud.create_documents(
db,
documents=documents,
workspace_name=workspace_name,
observer=observer,
observed=observed,
deduplicate=True,
)
logger.info(
"Created %d observations in %s/%s/%s",
len(accepted),
workspace_name,
observer,
observed,
)
return ObservationsCreatedResult(
created_count=len(accepted),
created_levels=[doc.level for doc in accepted],
failed=failed,
)
async def get_recent_history(
db: AsyncSession,
workspace_name: str,
session_name: str | None,
observed: str | None = None,
token_limit: int = 8192,
) -> list[models.Message]:
"""
Retrieve recent conversation history.
If session_name is provided, retrieves messages from that session.
If session_name is None but observed is provided, retrieves recent messages
sent by the observed peer across all their sessions.
Args:
db: Database session
workspace_name: Workspace identifier
session_name: Session identifier (optional)
observed: Peer name to filter by when no session specified (optional)
token_limit: Maximum tokens to retrieve (default: 8192)
Returns:
List of messages in chronological order
"""
if session_name:
# Get messages from a specific session
messages_stmt = await crud.get_messages(
workspace_name=workspace_name,
session_name=session_name,
token_limit=[REDACTED]
reverse=True, # Get most recent first
)
result = await db.execute(messages_stmt)
messages = result.scalars().all()
# Return in chronological order
return list(reversed(messages))
elif observed:
# Get recent messages from the observed peer across all sessions
stmt = (
select(models.Message)
.where(models.Message.workspace_name == workspace_name)
.where(models.Message.peer_name == observed)
.order_by(models.Message.created_at.desc())
.limit(50) # Limit to recent messages
)
result = await db.execute(stmt)
messages = list(result.scalars().all())
# Return in chronological order
return list(reversed(messages))
else:
# No session and no observed peer - can't retrieve history
return []
async def search_memory(
workspace_name: str,
observer: str,
observed: str,
query: str,
limit: int,
levels: list[str] | None = None,
embedding: list[float] | None = None,
) -> Representation:
"""
Search for observations in memory using semantic similarity.
Does not require a DB session — ``query_documents`` manages its own
short-lived sessions so no connection is held during external calls.
Args:
workspace_name: Workspace identifier
observer: The peer who made the observations
observed: The peer who was observed
query: Search query text
limit: Maximum number of results
levels: Optional list of observation levels to filter by
(e.g., ["explicit"], ["deductive", "inductive", "contradiction"])
embedding: Optional pre-computed embedding to avoid redundant API calls
Returns:
Representation object containing relevant observations
"""
# Build filter for levels if specified
filters: dict[str, Any] | None = None
if levels:
filters = {"level": {"in": levels}}
documents = await crud.query_documents(
db=None,
workspace_name=workspace_name,
observer=observer,
observed=observed,
query=query,
top_k=limit,
filters=filters,
embedding=embedding,
)
return Representation.from_documents(documents)
async def get_observation_context(
db: AsyncSession,
workspace_name: str,
session_name: str | None,
message_ids: list[str],
observer: str | None = None,
) -> list[models.Message]:
"""
Retrieve messages for given message IDs along with surrounding context.
Takes message IDs (from an observation's message_ids field) and retrieves those
messages plus the messages immediately before and after each one to provide
conversation context.
Args:
db: Database session
workspace_name: Workspace identifier
session_name: Session identifier (optional)
message_ids: List of message IDs to retrieve
observer: When provided and session_name is None, scope results
to sessions this peer belongs to
Returns:
List of messages in chronological order, including the requested messages and surrounding context
"""
if not message_ids:
return []
# Pre-fetch peer session scope if needed
allowed_session_names: list[str] | None = None
if observer and not session_name:
from src.crud.message import get_peer_session_names
allowed_session_names = await get_peer_session_names(
db, workspace_name, observer
)
if not allowed_session_names:
return []
# Use a CTE to get seq_in_session values for target messages
stmt = (
select(models.Message.seq_in_session)
.where(models.Message.workspace_name == workspace_name)
.where(models.Message.public_id.in_(message_ids))
)
if session_name:
stmt = stmt.where(models.Message.session_name == session_name)
elif allowed_session_names is not None:
stmt = stmt.where(models.Message.session_name.in_(allowed_session_names))
target_seqs_cte = stmt.cte("target_seqs")
# Query messages where seq_in_session is within ±1 of any target sequence
# We use EXISTS with arithmetic to check if the message is adjacent to any target
stmt = (
select(models.Message)
.where(models.Message.workspace_name == workspace_name)
.where(
select(target_seqs_cte.c.seq_in_session)
.where(
(
target_seqs_cte.c.seq_in_session - models.Message.seq_in_session
).between(-1, 1)
)
.exists()
)
.order_by(models.Message.seq_in_session.asc())
)
if session_name:
stmt = stmt.where(models.Message.session_name == session_name)
elif allowed_session_names is not None:
stmt = stmt.where(models.Message.session_name.in_(allowed_session_names))
result = await db.execute(stmt)
messages = list(result.scalars().all())
return messages
async def extract_preferences(
workspace_name: str,
session_name: str | None,
observed: str,
observer: str | None = None,
) -> dict[str, list[str]]:
"""
Extract user preferences and standing instructions from conversation history.
Uses semantic search to find messages that might contain preferences or instructions.
This is language-agnostic and doesn't rely on keyword matching.
Args:
workspace_name: Workspace identifier
session_name: Session identifier (optional)
observed: The peer whose preferences to extract
observer: When provided and session_name is None, scope results
to sessions this peer belongs to
>nl -ba src/models.py | sed -n '1,430p'Chunk ID: 108705
Wall time: 0.1629 seconds
Process exited with code 0
Original token count: 4662
Output:
1 import datetime
2 from logging import getLogger
3 from typing import Any, final
4
5 from dotenv import load_dotenv
6 from nanoid import generate as generate_nanoid
7 from pgvector.sqlalchemy import Vector
8 from sqlalchemy import (
9 BigInteger,
10 Boolean,
11 CheckConstraint,
12 Column,
13 DateTime,
14 ForeignKey,
15 ForeignKeyConstraint,
16 Identity,
17 Index,
18 Integer,
19 Table,
20 UniqueConstraint,
21 text,
22 )
23 from sqlalchemy.dialects.postgresql import JSONB, TEXT
24 from sqlalchemy.orm import Mapped, MappedColumn, mapped_column, relationship
25 from sqlalchemy.sql import func
26 from typing_extensions import override
27
28 from src.config import settings
29 from src.utils.types import DocumentLevel, TaskType, VectorSyncState
30
31 from .db import Base
32
33 load_dotenv(override=True)
34
35 _VECTOR_DIM: int = settings.EMBEDDING.VECTOR_DIMENSIONS
36
37 logger = getLogger(__name__)
38
39
40 # Association table for many-to-many relationship between sessions and peers
41 session_peers_table = Table(
42 "session_peers",
43 Base.metadata,
44 Column(
45 "workspace_name",
46 TEXT,
47 ForeignKey("workspaces.name"),
48 primary_key=[REDACTED]
49 nullable=False,
50 ),
51 Column(
52 "session_name",
53 TEXT,
54 primary_key=[REDACTED]
55 nullable=False,
56 ),
57 Column("peer_name", TEXT, primary_key=[REDACTED] nullable=False),
58 Column(
59 "configuration",
60 JSONB,
61 default=dict,
62 nullable=False,
63 server_default=text("'{}'::jsonb"),
64 ),
65 Column(
66 "internal_metadata",
67 JSONB,
68 default=dict,
69 nullable=False,
70 server_default=text("'{}'::jsonb"),
71 ),
72 Column(
73 "joined_at",
74 DateTime(timezone=True),
75 nullable=False,
76 server_default=func.now(),
77 ),
78 Column(
79 "left_at",
80 DateTime(timezone=True),
81 nullable=True,
82 ),
83 # Composite foreign key constraint for sessions
84 ForeignKeyConstraint(
85 ["session_name", "workspace_name"],
86 ["sessions.name", "sessions.workspace_name"],
87 ),
88 # Composite foreign key constraint for peers
89 ForeignKeyConstraint(
90 ["peer_name", "workspace_name"],
91 ["peers.name", "peers.workspace_name"],
92 ),
93 )
94
95
96 @final
97 class Workspace(Base):
98 __tablename__: str = "workspaces"
99 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
100 name: Mapped[str] = mapped_column(TEXT, unique=True)
101 created_at: Mapped[datetime.datetime] = mapped_column(
102 DateTime(timezone=True), server_default=func.now(), index=True
103 )
104 h_metadata: Mapped[dict[str, Any]] = mapped_column(
105 "metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
106 )
107 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
108 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
109 )
110 configuration: Mapped[dict[str, Any]] = mapped_column(
111 JSONB, default=dict, server_default=text("'{}'::jsonb")
112 )
113
114 sessions = relationship(
115 "Session", back_populates="workspace", cascade="all, delete, delete-orphan"
116 )
117 peers = relationship(
118 "Peer", back_populates="workspace", cascade="all, delete, delete-orphan"
119 )
120 webhook_endpoints = relationship("WebhookEndpoint", back_populates="workspace")
121
122 __table_args__ = (
123 CheckConstraint("length(id) = 21", name="id_length"),
124 CheckConstraint("length(name) <= 512", name="name_length"),
125 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
126 )
127
128
129 @final
130 class Peer(Base):
131 __tablename__: str = "peers"
132 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
133 name: Mapped[str] = mapped_column(TEXT, nullable=False)
134 h_metadata: Mapped[dict[str, Any]] = mapped_column(
135 "metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
136 )
137 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
138 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
139 )
140 created_at: Mapped[datetime.datetime] = mapped_column(
141 DateTime(timezone=True), server_default=func.now(), index=True
142 )
143 workspace_name: Mapped[str] = mapped_column(
144 ForeignKey("workspaces.name"), nullable=False, index=True
145 )
146 configuration: Mapped[dict[str, Any]] = mapped_column(
147 JSONB, default=dict, server_default=text("'{}'::jsonb")
148 )
149
150 workspace = relationship("Workspace", back_populates="peers")
151 sessions = relationship(
152 "Session", secondary=session_peers_table, back_populates="peers"
153 )
154
155 __table_args__ = (
156 UniqueConstraint("name", "workspace_name"),
157 CheckConstraint("length(id) = 21", name="id_length"),
158 CheckConstraint("length(name) <= 512", name="name_length"),
159 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
160 )
161
162 def __repr__(self) -> str:
163 return f"Peer(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, created_at={self.created_at}, h_metadata={self.h_metadata}, configuration={self.configuration})"
164
165
166 @final
167 class Session(Base):
168 __tablename__: str = "sessions"
169 id: Mapped[str] = mapped_column(TEXT, primary_key=[REDACTED] default=generate_nanoid)
170 name: Mapped[str] = mapped_column(TEXT)
171 is_active: Mapped[bool] = mapped_column(default=True, server_default=text("true"))
172 h_metadata: Mapped[dict[str, Any]] = mapped_column(
173 "metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
174 )
175 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
176 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
177 )
178 created_at: Mapped[datetime.datetime] = mapped_column(
179 DateTime(timezone=True), server_default=func.now(), index=True
180 )
181 workspace_name: Mapped[str] = mapped_column(
182 ForeignKey("workspaces.name"), nullable=False, index=True
183 )
184 configuration: Mapped[dict[str, Any]] = mapped_column(
185 JSONB, default=dict, server_default=text("'{}'::jsonb")
186 )
187
188 workspace = relationship("Workspace", back_populates="sessions")
189 peers = relationship(
190 "Peer", secondary=session_peers_table, back_populates="sessions"
191 )
192 messages = relationship("Message", back_populates="session")
193
194 __table_args__ = (
195 UniqueConstraint("name", "workspace_name"),
196 CheckConstraint("length(name) <= 512", name="name_length"),
197 CheckConstraint("length(id) = 21", name="id_length"),
198 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
199 )
200
201 def __repr__(self) -> str:
202 return f"Session(id={self.id}, name={self.name}, workspace_name={self.workspace_name}, is_active={self.is_active}, created_at={self.created_at}, h_metadata={self.h_metadata})"
203
204
205 @final
206 class Message(Base):
207 __tablename__: str = "messages"
208 id: Mapped[int] = mapped_column(
209 BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
210 )
211 public_id: Mapped[str] = mapped_column(
212 TEXT,
213 unique=True,
214 default=generate_nanoid,
215 )
216 # NOTE: Messages in Honcho 2.0 could historically be stored outside of a session.
217 # We have since assigned all of these messages to a default session.
218 session_name: Mapped[str] = mapped_column(TEXT, nullable=False)
219 content: Mapped[str] = mapped_column(TEXT)
220 h_metadata: Mapped[dict[str, Any]] = mapped_column(
221 "metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
222 )
223 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
224 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
225 )
226 token_count: Mapped[int] = mapped_column(Integer, default=0, nullable=False)
227 seq_in_session: Mapped[int] = mapped_column(BigInteger, nullable=False)
228
229 created_at: Mapped[datetime.datetime] = mapped_column(
230 DateTime(timezone=True), server_default=func.now(), index=True
231 )
232 # Note: Foreign key relationships established via composite ForeignKeyConstraint below
233 peer_name: Mapped[str] = mapped_column(TEXT, index=True)
234 workspace_name: Mapped[str] = mapped_column(TEXT, index=True)
235
236 session = relationship("Session", back_populates="messages")
237
238 __table_args__ = (
239 CheckConstraint("length(public_id) = 21", name="public_id_length"),
240 CheckConstraint("public_id ~ '^[A-Za-z0-9_-]+$'", name="public_id_format"),
241 CheckConstraint("length(content) <= 65535", name="content_length"),
242 # Composite foreign key constraint for sessions
243 ForeignKeyConstraint(
244 ["session_name", "workspace_name"],
245 ["sessions.name", "sessions.workspace_name"],
246 ),
247 # Composite foreign key constraint for peers
248 ForeignKeyConstraint(
249 ["peer_name", "workspace_name"],
250 ["peers.name", "peers.workspace_name"],
251 ),
252 Index(
253 "ix_messages_session_lookup",
254 "session_name",
255 "id",
256 postgresql_include=["id", "created_at"],
257 ),
258 UniqueConstraint(
259 "workspace_name",
260 "session_name",
261 "seq_in_session",
262 ),
263 # Full text search index on content column
264 Index(
265 "ix_messages_content_gin",
266 text("to_tsvector('english', content)"),
267 postgresql_using="gin",
268 ),
269 )
270
271 @override
272 def __repr__(self) -> str:
273 return f"Message(id={self.id}, session_name={self.session_name}, peer_name={self.peer_name}, content={self.content})"
274
275
276 @final
277 class MessageEmbedding(Base):
278 __tablename__: str = "message_embeddings"
279
280 id: Mapped[int] = mapped_column(
281 BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
282 )
283 content: Mapped[str] = mapped_column(TEXT)
284 embedding: MappedColumn[Any] = mapped_column(Vector(_VECTOR_DIM), nullable=True)
285 message_id: Mapped[str] = mapped_column(
286 ForeignKey("messages.public_id", ondelete="CASCADE"), nullable=False, index=True
287 )
288 workspace_name: Mapped[str] = mapped_column(
289 ForeignKey("workspaces.name"), nullable=False, index=True
290 )
291 session_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
292 peer_name: Mapped[str] = mapped_column(TEXT, nullable=False, index=True)
293 created_at: Mapped[datetime.datetime] = mapped_column(
294 DateTime(timezone=True), server_default=func.now(), index=True
295 )
296 # Vector sync state tracking
297 sync_state: Mapped[VectorSyncState] = mapped_column(
298 TEXT, nullable=False, server_default="pending", index=True
299 )
300 last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
301 DateTime(timezone=True), nullable=True
302 )
303 sync_attempts: Mapped[int] = mapped_column(
304 Integer, nullable=False, default=0, server_default=text("0")
305 )
306
307 __table_args__ = (
308 # Compound foreign key constraints
309 ForeignKeyConstraint(
310 ["session_name", "workspace_name"],
311 ["sessions.name", "sessions.workspace_name"],
312 ),
313 ForeignKeyConstraint(
314 ["peer_name", "workspace_name"],
315 ["peers.name", "peers.workspace_name"],
316 ),
317 # HNSW index on embedding column for efficient similarity search
318 Index(
319 "ix_message_embeddings_embedding_hnsw",
320 "embedding",
321 postgresql_using="hnsw",
322 postgresql_with={"m": 16, "ef_construction": 64},
323 postgresql_ops={"embedding": "vector_cosine_ops"},
324 ),
325 # Composite index for efficient reconciliation queries
326 Index(
327 "ix_message_embeddings_sync_state_last_sync_at",
328 "sync_state",
329 "last_sync_at",
330 ),
331 )
332
333
334 @final
335 class Collection(Base):
336 __tablename__: str = "collections"
337
338 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
339 observer: Mapped[str] = mapped_column(TEXT, index=True)
340 observed: Mapped[str] = mapped_column(TEXT, index=True)
341 created_at: Mapped[datetime.datetime] = mapped_column(
342 DateTime(timezone=True), server_default=func.now(), index=True
343 )
344 h_metadata: Mapped[dict[str, Any]] = mapped_column(
345 "metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
346 )
347 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
348 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
349 )
350 documents = relationship(
351 "Document", back_populates="collection", cascade="all, delete, delete-orphan"
352 )
353 workspace_name: Mapped[str] = mapped_column(
354 ForeignKey("workspaces.name"), nullable=False, index=True
355 )
356
357 __table_args__ = (
358 UniqueConstraint(
359 "observer",
360 "observed",
361 "workspace_name",
362 ),
363 CheckConstraint("length(id) = 21", name="id_length"),
364 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
365 # Composite foreign key constraint for observer peer
366 ForeignKeyConstraint(
367 ["observer", "workspace_name"],
368 ["peers.name", "peers.workspace_name"],
369 ),
370 # Composite foreign key constraint for observed peer
371 ForeignKeyConstraint(
372 ["observed", "workspace_name"],
373 ["peers.name", "peers.workspace_name"],
374 ),
375 )
376
377
378 @final
379 class Document(Base):
380 __tablename__: str = "documents"
381 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
382 internal_metadata: Mapped[dict[str, Any]] = mapped_column(
383 "internal_metadata", JSONB, default=dict, server_default=text("'{}'::jsonb")
384 )
385 content: Mapped[str] = mapped_column(TEXT)
386 level: Mapped[DocumentLevel] = mapped_column(
387 TEXT, nullable=False, server_default="explicit"
388 )
389 times_derived: Mapped[int] = mapped_column(
390 Integer, nullable=False, server_default=text("1")
391 )
392 embedding: MappedColumn[Any] = mapped_column(Vector(_VECTOR_DIM), nullable=True)
393 source_ids: Mapped[list[str] | None] = mapped_column(
394 JSONB, nullable=True, server_default=text("NULL")
395 )
396 created_at: Mapped[datetime.datetime] = mapped_column(
397 DateTime(timezone=True), server_default=func.now(), index=True
398 )
399
400 observer: Mapped[str] = mapped_column(TEXT, index=True)
401 observed: Mapped[str] = mapped_column(TEXT, index=True)
402 workspace_name: Mapped[str] = mapped_column(
403 ForeignKey("workspaces.name"), nullable=False, index=True
404 )
405 session_name: Mapped[str | None] = mapped_column(TEXT, nullable=True, index=True)
406 deleted_at: Mapped[datetime.datetime | None] = mapped_column(
407 DateTime(timezone=True), nullable=True, index=True, default=None
408 )
409
410 # Vector sync state tracking
411 sync_state: Mapped[VectorSyncState] = mapped_column(
412 TEXT, nullable=False, server_default="pending", index=True
413 )
414 last_sync_at: Mapped[datetime.datetime | None] = mapped_column(
415 DateTime(timezone=True), nullable=True
416 )
417 sync_attempts: Mapped[int] = mapped_column(
418 Integer, nullable=False, default=0, server_default=text("0")
419 )
420
421 collection = relationship("Collection", back_populates="documents")
422
423 __table_args__ = (
424 CheckConstraint("length(id) = 21", name="id_length"),
425 CheckConstraint("length(content) <= 65535", name="content_length"),
426 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
427 # Composite foreign key constraint for collections
428 ForeignKeyConstraint(
429 ["observer", "observed", "workspace_name"],
430 [
>nl -ba src/deriver/enqueue.py | sed -n '200,390p'Chunk ID: f6f36b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1894
Output:
200 task_type="representation",
201 observers=observers,
202 observed=observed,
203 )
204 return {
205 "work_unit_key": construct_work_unit_key(workspace_name, processed_payload),
206 "payload": processed_payload,
207 "session_id": session_id,
208 "task_type": "representation",
209 "workspace_name": workspace_name,
210 "message_id": message_id,
211 }
212
213
214 def create_summary_record(
215 message: dict[str, Any],
216 configuration: ResolvedConfiguration,
217 session_id: str,
218 message_seq_in_session: int,
219 ) -> dict[str, Any]:
220 """
221 Create a queue record for summary task.
222
223 Args:
224 message: The message payload
225 session_id: Session ID
226 message_seq_in_session: The sequence number of the message in the session
227
228 Returns:
229 Queue record dictionary with workspace_name and message_id as separate fields
230 """
231 workspace_name = message.get("workspace_name")
232 message_id = message.get("message_id")
233
234 if not isinstance(workspace_name, str):
235 raise ValueError("workspace_name is required and must be a string")
236 if not isinstance(message_id, int):
237 raise ValueError("message_id is required and must be an integer")
238
239 processed_payload = create_payload(
240 message=message,
241 configuration=configuration,
242 task_type="summary",
243 message_seq_in_session=message_seq_in_session,
244 )
245 return {
246 "work_unit_key": construct_work_unit_key(workspace_name, processed_payload),
247 "payload": processed_payload,
248 "session_id": session_id,
249 "task_type": "summary",
250 "workspace_name": workspace_name,
251 "message_id": message_id,
252 }
253
254
255 def get_effective_observe_me(
256 observed: str, peers_with_configuration: dict[str, list[dict[str, Any]]]
257 ) -> bool:
258 """
259 Determine the effective observe_me setting for a sender, considering session and peer configurations.
260
261 Args:
262 observed: Name of the sender
263 peers_with_configuration: Dictionary of peer configurations
264
265 Returns:
266 True if observe_me is enabled, False otherwise
267 """
268 # If the sender is not in peers_with_configuration, they left after sending a message.
269 # We'll use the default behavior of observing the sender by instantiating the default
270 # peer-level and session-level configs.
271 configuration: list[Any] = peers_with_configuration.get(observed, [{}, {}])
272 sender_session_peer_config = (
273 schemas.SessionPeerConfig(**configuration[1]) if configuration[1] else None
274 )
275 sender_peer_config = (
276 schemas.PeerConfig(**configuration[0])
277 if configuration[0]
278 else schemas.PeerConfig()
279 )
280
281 # Session peer config takes precedence if it exists and has observe_me set
282 if sender_session_peer_config and sender_session_peer_config.observe_me is not None:
283 return sender_session_peer_config.observe_me
284
285 # Otherwise use peer config
286 return (
287 sender_peer_config.observe_me
288 if sender_peer_config.observe_me is not None
289 else True
290 )
291
292
293 async def generate_queue_records(
294 db_session: AsyncSession,
295 message: dict[str, Any],
296 peers_with_configuration: dict[str, list[dict[str, Any]]],
297 session_id: str,
298 conf: ResolvedConfiguration,
299 ) -> list[dict[str, Any]]:
300 """
301 Process a single message and generate queue records based on configurations.
302
303 Args:
304 db_session: The database session
305 message: The message payload
306 peers_with_configuration: Dictionary of peer configurations
307 session_id: Session ID
308 configuration: Resolved configuration for this particular message
309
310 Returns:
311 List of queue records for this message
312 """
313 observed = message["peer_name"]
314 message_id: int = message["message_id"]
315
316 # Prefer the sequence captured during message creation; fallback only if missing
317 message_seq_in_session = int(message.get("seq_in_session") or 0)
318 if message_seq_in_session <= 0:
319 message_seq_in_session = await crud.get_message_seq_in_session(
320 db_session,
321 workspace_name=message["workspace_name"],
322 session_name=message["session_name"],
323 message_id=message_id,
324 )
325
326 records: list[dict[str, Any]] = []
327
328 if conf.summary.enabled and (
329 message_seq_in_session % conf.summary.messages_per_short_summary == 0
330 or message_seq_in_session % conf.summary.messages_per_long_summary == 0
331 ):
332 records.append(
333 create_summary_record(
334 message,
335 configuration=conf,
336 session_id=session_id,
337 message_seq_in_session=message_seq_in_session,
338 )
339 )
340
341 # Check if the sender should be observed based on peer configuration
342 should_observe = get_effective_observe_me(observed, peers_with_configuration)
343
344 if not conf.reasoning.enabled:
345 return records
346
347 # Collect all observers into a single list
348 observers: list[str] = []
349
350 if should_observe:
351 # Self-observation: the sender observes themselves
352 observers.append(observed)
353
354 # Other peers who want to observe
355 for peer_name, peer_conf in peers_with_configuration.items():
356 if peer_name == observed:
357 continue
358
359 # If the observer peer has left the session, skip them
360 if not peer_conf[2]:
361 continue
362
363 session_peer_config = (
364 schemas.SessionPeerConfig(**peer_conf[1]) if peer_conf[1] else None
365 )
366
367 if session_peer_config is None or not session_peer_config.observe_others:
368 continue
369
370 observers.append(peer_name)
371
372 # Create a single record with all observers (if any)
373 if observers:
374 records.append(
375 create_representation_record(
376 message,
377 conf,
378 observed=observed,
379 observers=observers,
380 session_id=session_id,
381 )
382 )
383
384 logger.debug(
385 "message %s from %s created %s queue items with %s observers",
386 message_id,
387 observed,
388 len(records),
389 len(observers),
390 )
>nl -ba src/deriver/deriver.py | sed -n '1,210p'Chunk ID: 6751f6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2172
Output:
1 import logging
2 import time
3
4 from src import crud
5 from src.config import ConfiguredModelSettings, settings
6 from src.crud.representation import RepresentationManager
7 from src.dependencies import tracked_db
8 from src.llm import honcho_llm_call
9 from src.models import Message
10 from src.schemas import ResolvedConfiguration
11 from src.telemetry import prometheus_metrics
12 from src.telemetry.events import RepresentationCompletedEvent, emit
13 from src.telemetry.logging import accumulate_metric, log_performance_metrics
14 from src.telemetry.prometheus.metrics import (
15 DeriverComponents,
16 DeriverTaskTypes,
17 TokenTypes,
18 )
19 from src.telemetry.sentry import with_sentry_transaction
20 from src.utils.config_helpers import get_configuration
21 from src.utils.formatting import format_new_turn_with_timestamp
22 from src.utils.representation import PromptRepresentation, Representation
23 from src.utils.tokens import track_deriver_input_tokens
24
25 from .prompts import estimate_deriver_prompt_tokens, minimal_deriver_prompt
26
27 logger = logging.getLogger(__name__)
28
29
30 def _get_deriver_model_config() -> ConfiguredModelSettings:
31 return settings.DERIVER.MODEL_CONFIG
32
33
34 @with_sentry_transaction("minimal_deriver_batch", op="deriver")
35 async def process_representation_tasks_batch(
36 messages: list[Message],
37 message_level_configuration: ResolvedConfiguration | None,
38 *,
39 observers: list[str],
40 observed: str,
41 queue_item_message_ids: list[int],
42 ) -> None:
43 """
44 Process messages with minimal overhead - single LLM call, save to multiple collections.
45
46 Args:
47 messages: List of messages to process (includes interleaving context).
48 message_level_configuration: Optional configuration override.
49 observers: List of observer peer IDs (collections to save to).
50 observed: The observed peer ID.
51 queue_item_message_ids: Message IDs from queue items being processed
52 """
53 if not messages:
54 return
55
56 overall_start = time.perf_counter()
57
58 messages.sort(key=lambda x: x.id)
59 latest_message = messages[-1]
60 earliest_message = messages[0]
61
62 # Get configuration if not provided
63 # TODO: this appears to be a very rare edge case coming out of `get_queue_item_batch` in queue_manager.py,
64 # possible that we can remove this and require configuration to come through with the payload.
65 if message_level_configuration is None:
66 async with tracked_db("minimal_deriver.get_config") as db:
67 message_level_configuration = get_configuration(
68 None,
69 await crud.get_session(
70 db, latest_message.session_name, latest_message.workspace_name
71 ),
72 await crud.get_workspace(
73 db, workspace_name=latest_message.workspace_name
74 ),
75 )
76
77 # Skip if disabled
78 if message_level_configuration.reasoning.enabled is False:
79 return
80
81 custom_instructions = message_level_configuration.reasoning.custom_instructions
82
83 accumulate_metric(
84 f"minimal_deriver_{latest_message.id}_{observed}",
85 "starting_message_id",
86 earliest_message.id,
87 "id",
88 )
89 accumulate_metric(
90 f"minimal_deriver_{latest_message.id}_{observed}",
91 "ending_message_id",
92 latest_message.id,
93 "id",
94 )
95
96 # Format messages with timestamps
97 formatted_messages = "\n".join(
98 format_new_turn_with_timestamp(msg.content, msg.created_at, msg.peer_name)
99 for msg in messages
100 )
101
102 # Track token usage - count only tokens from messages being processed
103 prompt_tokens = estimate_deriver_prompt_tokens(custom_instructions)
104 queue_item_message_ids_set = set(queue_item_message_ids)
105 messages_tokens = sum(
106 msg.token_count for msg in messages if msg.id in queue_item_message_ids_set
107 )
108 track_deriver_input_tokens(
109 task_type=DeriverTaskTypes.INGESTION,
110 components={
111 DeriverComponents.PROMPT: prompt_tokens,
112 DeriverComponents.MESSAGES: messages_tokens,
113 },
114 )
115
116 # Build prompt
117 prompt = minimal_deriver_prompt(
118 peer_id=observed,
119 messages=formatted_messages,
120 custom_instructions=custom_instructions,
121 )
122
123 context_prep_duration = (time.perf_counter() - overall_start) * 1000
124 accumulate_metric(
125 f"minimal_deriver_{latest_message.id}_{observed}",
126 "context_preparation",
127 context_prep_duration,
128 "ms",
129 )
130
131 # validation on settings means max_tokens will always be > 0
132 base_model_config = _get_deriver_model_config()
133 max_tokens = base_model_config.max_output_tokens or settings.LLM.DEFAULT_MAX_TOKENS
134 model_config = base_model_config
135
136 # Single LLM call
137 llm_start = time.perf_counter()
138 response = await honcho_llm_call(
139 model_config=model_config,
140 prompt=prompt,
141 max_tokens=[REDACTED]
142 track_name="Minimal Deriver",
143 response_model=PromptRepresentation,
144 json_mode=True,
145 max_input_tokens=[REDACTED]
146 enable_retry=True,
147 retry_attempts=3,
148 trace_name="minimal_deriver",
149 )
150 llm_duration = (time.perf_counter() - llm_start) * 1000
151
152 accumulate_metric(
153 f"minimal_deriver_{latest_message.id}_{observed}",
154 "llm_call_duration",
155 llm_duration,
156 "ms",
157 )
158
159 # Prometheus metrics
160 if settings.METRICS.ENABLED:
161 prometheus_metrics.record_deriver_tokens(
162 count=response.output_tokens,
163 task_type=DeriverTaskTypes.INGESTION.value,
164 token_type=[REDACTED]
165 component=DeriverComponents.OUTPUT_TOTAL.value,
166 )
167
168 message_ids = [m.id for m in messages if m.peer_name == observed]
169
170 # Convert to Representation and save
171 observations = Representation.from_prompt_representation(
172 response.content,
173 message_ids,
174 latest_message.session_name,
175 latest_message.created_at,
176 )
177
178 if observations.is_empty() or not message_ids:
179 logger.warning(
180 "Deriver generated zero observations for messages %s:%s in %s/%s!",
181 earliest_message.id,
182 latest_message.id,
183 latest_message.workspace_name,
184 latest_message.session_name,
185 )
186 else:
187 # Save to all observer collections
188 for observer in observers:
189 representation_manager = RepresentationManager(
190 workspace_name=latest_message.workspace_name,
191 observer=observer,
192 observed=observed,
193 )
194
195 try:
196 await representation_manager.save_representation(
197 observations,
198 message_ids,
199 latest_message.session_name,
200 latest_message.created_at,
201 message_level_configuration,
202 )
203 except Exception as e:
204 logger.error(
205 "Failed to save representation for observer %s: %s", observer, e
206 )
207
208 # Log metrics
209 overall_duration = (time.perf_counter() - overall_start) * 1000
210 accumulate_metric(
>nl -ba src/utils/representation.py | sed -n '1,360p'Chunk ID: b2643d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4258
Output:
1 from collections.abc import Sequence
2 from datetime import datetime
3 from typing import Any
4
5 from pydantic import BaseModel, Field, field_validator
6
7 from src import models
8 from src.utils.formatting import parse_datetime_iso
9
10
11 def _strip_microseconds_and_timezone(timestamp: datetime) -> datetime:
12 """
13 Remove microseconds and timezone info from a datetime for stable string formatting.
14 """
15 return timestamp.replace(microsecond=0, tzinfo=None)
16
17
18 def flatten_message_ids(
19 message_ids: list[int] | list[list[int]] | list[tuple[int, int]],
20 ) -> list[int]:
21 """
22 Flatten message_ids that may be in old tuple format or nested list format.
23
24 This handles backwards compatibility with the old schema where message_ids
25 was list[tuple[int, int]] representing ranges, and the new schema where
26 it's list[int] representing individual message IDs.
27
28 Args:
29 message_ids: Either a flat list of ints, nested list, or list of tuples
30
31 Returns:
32 A flat list of unique message IDs, sorted
33
34 Examples:
35 [1, 2, 3] -> [1, 2, 3]
36 [[1, 2], [3, 4]] -> [1, 2, 3, 4]
37 [(105, 105)] -> [105]
38 [[105, 105]] -> [105]
39 """
40 result: list[int] = []
41 for item in message_ids:
42 if isinstance(item, (list | tuple)):
43 # Nested list or tuple - flatten it
44 result.extend(item)
45 else:
46 # Already flat
47 result.append(item)
48 # Remove duplicates and sort
49 return sorted(set(result))
50
51
52 class ObservationMetadata(BaseModel):
53 id: str = Field(default="", description="Document ID for this observation")
54 created_at: datetime
55 message_ids: list[int]
56 session_name: str | None = None
57
58
59 class ExplicitObservationBase(BaseModel):
60 content: str = Field(description="The explicit observation")
61
62
63 class DeductiveObservationBase(BaseModel):
64 source_ids: list[str] = Field(
65 description="Document IDs of premise observations for tree traversal",
66 default_factory=list,
67 )
68 premises: list[str] = Field(
69 description="Human-readable premise text for display",
70 default_factory=list,
71 )
72 conclusion: str = Field(description="The deductive conclusion")
73
74
75 class InductiveObservationBase(BaseModel):
76 """Base model for inductive observations - patterns, generalizations, and personality insights."""
77
78 source_ids: list[str] = Field(
79 description="Document IDs of source observations for tree traversal",
80 default_factory=list,
81 )
82 sources: list[str] = Field(
83 description="Human-readable source text for display",
84 default_factory=list,
85 )
86 pattern_type: str = Field(
87 description="Type of pattern: 'preference', 'behavior', 'personality', 'tendency', 'correlation'",
88 default="pattern",
89 )
90 conclusion: str = Field(description="The inductive generalization or pattern")
91 confidence: str = Field(
92 description="Confidence level: 'high', 'medium', 'low'",
93 default="medium",
94 )
95
96
97 class ContradictionObservationBase(BaseModel):
98 """Base model for contradiction observations - when user has made conflicting statements."""
99
100 source_ids: list[str] = Field(
101 description="Document IDs of the contradicting observations",
102 default_factory=list,
103 )
104 sources: list[str] = Field(
105 description="Human-readable text of the contradicting statements",
106 default_factory=list,
107 )
108 content: str = Field(description="Description of the contradiction")
109
110
111 class PromptRepresentation(BaseModel):
112 """
113 The representation format that is used when getting structured output from an LLM.
114 """
115
116 explicit: list[ExplicitObservationBase] = Field(
117 description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: ['The user is 25 years old', 'The user has a dog named Rover']",
118 default_factory=list,
119 )
120
121 @field_validator("explicit", mode="before")
122 @classmethod
123 def convert_none_to_empty_list(cls, v: Any) -> Any:
124 """Convert None to empty list - handles LLMs returning null instead of []."""
125 if v is None:
126 return []
127 return v
128
129
130 class ExplicitObservation(ExplicitObservationBase, ObservationMetadata):
131 """Explicit observation with content and metadata."""
132
133 def __str__(self) -> str:
134 return f"[{_strip_microseconds_and_timezone(self.created_at)}] {self.content}"
135
136 def str_with_id(self) -> str:
137 """Format with ID prefix for use by agents that need to reference observations."""
138 id_prefix = f"[id:{self.id}] " if self.id else ""
139 return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] {self.content}"
140
141 def __hash__(self) -> int:
142 """
143 Make ExplicitObservation hashable for use in sets.
144 """
145 return hash((self.content, self.created_at, self.session_name))
146
147 def __eq__(self, other: object) -> bool:
148 """
149 Define equality for ExplicitObservation objects.
150 Two observations are equal if all their fields match.
151 """
152 if not isinstance(other, ExplicitObservation):
153 return False
154 return (
155 self.content == other.content
156 and self.created_at == other.created_at
157 and self.session_name == other.session_name
158 )
159
160
161 class DeductiveObservation(DeductiveObservationBase, ObservationMetadata):
162 """Deductive observation with multiple premises and one conclusion, plus metadata."""
163
164 def __str__(self) -> str:
165 premises_text = "\n".join(f" - {premise}" for premise in self.premises)
166 return f"[{_strip_microseconds_and_timezone(self.created_at)}] {self.conclusion}\n{premises_text}"
167
168 def str_with_id(self) -> str:
169 """Format with ID prefix for use by agents that need to reference observations."""
170 id_prefix = f"[id:{self.id}] " if self.id else ""
171 premises_text = "\n".join(f" - {premise}" for premise in self.premises)
172 return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] {self.conclusion}\n{premises_text}"
173
174 def str_no_timestamps(self) -> str:
175 premises_text = "\n".join(f" - {premise}" for premise in self.premises)
176 return f"{self.conclusion}\n{premises_text}"
177
178 def __hash__(self) -> int:
179 """
180 Make DeductiveObservation hashable for use in sets. NOTE: premises are not included in the hash.
181 """
182 return hash((self.conclusion, self.created_at, self.session_name))
183
184 def __eq__(self, other: object) -> bool:
185 """
186 Define equality for DeductiveObservation objects.
187 Two observations are equal if all their fields match -- NOTE: premises are not included in the equality check.
188 """
189 if not isinstance(other, DeductiveObservation):
190 return False
191 return (
192 self.conclusion == other.conclusion
193 and self.created_at == other.created_at
194 and self.session_name == other.session_name
195 )
196
197
198 class InductiveObservation(InductiveObservationBase, ObservationMetadata):
199 """Inductive observation with sources, pattern type, and confidence, plus metadata."""
200
201 def __str__(self) -> str:
202 sources_text = ""
203 if self.sources:
204 source_lines = [f" - {source}" for source in self.sources]
205 sources_text = "\n" + "\n".join(source_lines)
206 return f"[{_strip_microseconds_and_timezone(self.created_at)}] [{self.confidence}] {self.conclusion}{sources_text}"
207
208 def str_with_id(self) -> str:
209 """Format with ID prefix for use by agents that need to reference observations."""
210 id_prefix = f"[id:{self.id}] " if self.id else ""
211 sources_text = ""
212 if self.sources:
213 source_lines = [f" - {source}" for source in self.sources]
214 sources_text = "\n" + "\n".join(source_lines)
215 return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] [{self.confidence}] {self.conclusion}{sources_text}"
216
217 def str_no_timestamps(self) -> str:
218 sources_text = ""
219 if self.sources:
220 source_lines = [f" - {source}" for source in self.sources]
221 sources_text = "\n" + "\n".join(source_lines)
222 return f"[{self.confidence}] {self.conclusion}{sources_text}"
223
224 def __hash__(self) -> int:
225 """Make InductiveObservation hashable for use in sets."""
226 return hash((self.conclusion, self.created_at, self.session_name))
227
228 def __eq__(self, other: object) -> bool:
229 """Define equality for InductiveObservation objects."""
230 if not isinstance(other, InductiveObservation):
231 return False
232 return (
233 self.conclusion == other.conclusion
234 and self.created_at == other.created_at
235 and self.session_name == other.session_name
236 )
237
238
239 class ContradictionObservation(ContradictionObservationBase, ObservationMetadata):
240 """Contradiction observation - notes when user has made conflicting statements, plus metadata."""
241
242 def __str__(self) -> str:
243 sources_text = ""
244 if self.sources:
245 source_lines = [f" - {source}" for source in self.sources]
246 sources_text = "\n" + "\n".join(source_lines)
247 return f"[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
248
249 def str_with_id(self) -> str:
250 """Format with ID prefix for use by agents that need to reference observations."""
251 id_prefix = f"[id:{self.id}] " if self.id else ""
252 sources_text = ""
253 if self.sources:
254 source_lines = [f" - {source}" for source in self.sources]
255 sources_text = "\n" + "\n".join(source_lines)
256 return f"{id_prefix}[{_strip_microseconds_and_timezone(self.created_at)}] CONTRADICTION: {self.content}{sources_text}"
257
258 def str_no_timestamps(self) -> str:
259 sources_text = ""
260 if self.sources:
261 source_lines = [f" - {source}" for source in self.sources]
262 sources_text = "\n" + "\n".join(source_lines)
263 return f"CONTRADICTION: {self.content}{sources_text}"
264
265 def __hash__(self) -> int:
266 """Make ContradictionObservation hashable for use in sets."""
267 return hash((self.content, self.created_at, self.session_name))
268
269 def __eq__(self, other: object) -> bool:
270 """Define equality for ContradictionObservation objects."""
271 if not isinstance(other, ContradictionObservation):
272 return False
273 return (
274 self.content == other.content
275 and self.created_at == other.created_at
276 and self.session_name == other.session_name
277 )
278
279
280 class Representation(BaseModel):
281 """
282 A Representation is a traversable and diffable map of observations.
283 At the base, we have a list of explicit observations, derived from a peer's messages.
284
285 From there, deductive observations can be made by establishing logical relationships between explicit observations.
286
287 In the future, we can add more levels of reasoning on top of these.
288
289 All of a peer's observations are stored as documents in a collection. These documents can be queried in various ways
290 to produce this Representation object.
291
292 Additionally, a "working representation" is a version of this data structure representing the most recent observations
293 within a single session.
294
295 A representation can have a maximum number of observations, which is applied individually to each level of reasoning.
296 If a maximum is set, observations are added and removed in FIFO order.
297 """
298
299 explicit: list[ExplicitObservation] = Field(
300 description="Facts LITERALLY stated by the user - direct quotes or clear paraphrases only, no interpretation or inference. Example: ['The user is 25 years old', 'The user has a dog']",
301 default_factory=list,
302 )
303 deductive: list[DeductiveObservation] = Field(
304 description="Conclusions that MUST be true given explicit facts and premises - strict logical necessities. Each deduction should have premises and a single conclusion.",
305 default_factory=list,
306 )
307 inductive: list[InductiveObservation] = Field(
308 description="Patterns, generalizations, and personality insights inferred from multiple observations. Higher-level reasoning created by the Dreamer agent.",
309 default_factory=list,
310 )
311 contradiction: list[ContradictionObservation] = Field(
312 description="Conflicting statements made by the user that need clarification. The dialectic agent should surface these when relevant.",
313 default_factory=list,
314 )
315
316 def is_empty(self) -> bool:
317 """
318 Check if the representation is empty.
319 """
320 return (
321 len(self.explicit) == 0
322 and len(self.deductive) == 0
323 and len(self.inductive) == 0
324 and len(self.contradiction) == 0
325 )
326
327 def len(self) -> int:
328 """
329 Return the total number of observations in the representation.
330 """
331 return (
332 len(self.explicit)
333 + len(self.deductive)
334 + len(self.inductive)
335 + len(self.contradiction)
336 )
337
338 def diff_representation(self, other: "Representation") -> "Representation":
339 """
340 Given this and another representation, return a new representation with only observations that are unique to the other.
341 Note that this only removes literal duplicates, not semantically equivalent ones.
342 """
343 diff = Representation()
344 diff.explicit = [o for o in other.explicit if o not in self.explicit]
345 diff.deductive = [o for o in other.deductive if o not in self.deductive]
346 diff.inductive = [o for o in other.inductive if o not in self.inductive]
347 diff.contradiction = [
348 o for o in other.contradiction if o not in self.contradiction
349 ]
350 return diff
351
352 def merge_representation(
353 self, other: "Representation", max_observations: int | None = None
354 ):
355 """
356 Merge another representation object into this one.
357 This will automatically deduplicate explicit, deductive, inductive, and contradiction observations.
358 This *preserves order* of observations so that they retain FIFO order.
359
360 NOTE: observations with the *same* timestamp will not have order preserved.
>nl -ba src/models.py | sed -n '423,560p'Chunk ID: 031fe9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1531
Output:
423 __table_args__ = (
424 CheckConstraint("length(id) = 21", name="id_length"),
425 CheckConstraint("length(content) <= 65535", name="content_length"),
426 CheckConstraint("id ~ '^[A-Za-z0-9_-]+$'", name="id_format"),
427 # Composite foreign key constraint for collections
428 ForeignKeyConstraint(
429 ["observer", "observed", "workspace_name"],
430 [
431 "collections.observer",
432 "collections.observed",
433 "collections.workspace_name",
434 ],
435 ),
436 # Composite foreign key constraint for observer peer
437 ForeignKeyConstraint(
438 ["observer", "workspace_name"],
439 ["peers.name", "peers.workspace_name"],
440 ),
441 # Composite foreign key constraint for observed peer
442 ForeignKeyConstraint(
443 ["observed", "workspace_name"],
444 ["peers.name", "peers.workspace_name"],
445 ),
446 # Composite foreign key constraint for sessions
447 ForeignKeyConstraint(
448 ["session_name", "workspace_name"],
449 ["sessions.name", "sessions.workspace_name"],
450 ),
451 # HNSW index on embedding column
452 Index(
453 "ix_documents_embedding_hnsw",
454 "embedding",
455 postgresql_using="hnsw", # HNSW index type
456 postgresql_with={"m": 16, "ef_construction": 64}, # HNSW parameters
457 postgresql_ops={
458 "embedding": "vector_cosine_ops"
459 }, # Cosine distance operator
460 ),
461 # GIN index for efficient tree traversal (finding children by source IDs)
462 Index(
463 "ix_documents_source_ids_gin",
464 "source_ids",
465 postgresql_using="gin",
466 ),
467 # Composite index for efficient reconciliation queries
468 Index(
469 "ix_documents_sync_state_last_sync_at",
470 "sync_state",
471 "last_sync_at",
472 ),
473 )
474
475
476 @final
477 class QueueItem(Base):
478 __tablename__: str = "queue"
479 id: Mapped[int] = mapped_column(
480 BigInteger, Identity(), primary_key=[REDACTED] autoincrement=True
481 )
482 session_id: Mapped[str | None] = mapped_column(
483 ForeignKey("sessions.id"), nullable=True, index=True
484 )
485 work_unit_key: Mapped[str] = mapped_column(TEXT, nullable=False)
486
487 task_type: Mapped[TaskType] = mapped_column(TEXT, nullable=False)
488 payload: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False)
489 processed: Mapped[bool] = mapped_column(
490 Boolean, default=False, server_default=text("false"), index=True
491 )
492 error: Mapped[str | None] = mapped_column(TEXT, nullable=True)
493 created_at: Mapped[datetime.datetime] = mapped_column(
494 DateTime(timezone=True), server_default=func.now(), index=True
495 )
496 workspace_name: Mapped[str | None] = mapped_column(
497 ForeignKey("workspaces.name"), nullable=True, index=True
498 )
499 message_id: Mapped[int | None] = mapped_column(
500 BigInteger, ForeignKey("messages.id"), nullable=True
501 )
502
503 __table_args__ = (
504 Index(
505 "ix_queue_message_id_not_null",
506 "message_id",
507 postgresql_where=text("message_id IS NOT NULL"),
508 ),
509 Index(
510 "ix_queue_work_unit_key_processed_id",
511 "work_unit_key",
512 "processed",
513 "id",
514 ),
515 # Partial unique index for reconciler task deduplication
516 Index(
517 "uq_queue_reconciler_pending_work_unit_key",
518 "work_unit_key",
519 unique=True,
520 postgresql_where=text("task_type = 'reconciler' AND processed = false"),
521 ),
522 # Partial unique index for dream task deduplication
523 Index(
524 "uq_queue_dream_pending_work_unit_key",
525 "work_unit_key",
526 unique=True,
527 postgresql_where=text("task_type = 'dream' AND processed = false"),
528 ),
529 )
530
531 def __repr__(self) -> str:
532 return f"QueueItem(id={self.id}, session_id={self.session_id}, work_unit_key=[REDACTED] task_type={self.task_type}, payload={self.payload}, processed={self.processed}, workspace_name={self.workspace_name}, message_id={self.message_id})"
533
534
535 @final
536 class ActiveQueueSession(Base):
537 __tablename__: str = "active_queue_sessions"
538
539 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
540
541 work_unit_key: Mapped[str] = mapped_column(TEXT, unique=True)
542
543 last_updated: Mapped[datetime.datetime] = mapped_column(
544 DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
545 )
546
547
548 @final
549 class WebhookEndpoint(Base):
550 __tablename__: str = "webhook_endpoints"
551 id: Mapped[str] = mapped_column(TEXT, default=generate_nanoid, primary_key=[REDACTED]
552 workspace_name: Mapped[str] = mapped_column(
553 ForeignKey("workspaces.name"), nullable=False, index=True
554 )
555 url: Mapped[str] = mapped_column(TEXT, nullable=False)
556 created_at: Mapped[datetime.datetime] = mapped_column(
557 DateTime(timezone=True), server_default=func.now()
558 )
559
560 workspace = relationship("Workspace", back_populates="webhook_endpoints")
>nl -ba src/crud/representation.py | sed -n '1,260p'Chunk ID: 435b07
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2864
Output:
1 from __future__ import annotations
2
3 import datetime
4 import logging
5 import time
6 from contextlib import suppress
7 from typing import Any
8
9 from sqlalchemy import select
10 from sqlalchemy.ext.asyncio import AsyncSession
11
12 from src import crud, exceptions, models, schemas
13 from src.config import settings
14 from src.dependencies import tracked_db
15 from src.dreamer.dream_scheduler import check_and_schedule_dream
16 from src.embedding_client import embedding_client
17 from src.schemas import ResolvedConfiguration
18 from src.telemetry.logging import accumulate_metric
19 from src.utils.formatting import format_datetime_utc
20 from src.utils.representation import (
21 DeductiveObservation,
22 ExplicitObservation,
23 Representation,
24 )
25
26 logger = logging.getLogger(__name__)
27
28
29 def _observation_text(obs: ExplicitObservation | DeductiveObservation) -> str:
30 """Return the canonical text payload for an explicit or deductive observation."""
31 return obs.conclusion if isinstance(obs, DeductiveObservation) else obs.content
32
33
34 def _normalized_observation(
35 obs: ExplicitObservation | DeductiveObservation,
36 ) -> ExplicitObservation | DeductiveObservation:
37 """Return an observation with its persisted/embed text normalized."""
38 text = _observation_text(obs).strip()
39 if isinstance(obs, DeductiveObservation):
40 return obs.model_copy(update={"conclusion": text})
41 return obs.model_copy(update={"content": text})
42
43
44 class RepresentationManager:
45 """Unified manager for representation and document queries."""
46
47 def __init__(
48 self,
49 workspace_name: str,
50 *,
51 observer: str,
52 observed: str,
53 ) -> None:
54 self.workspace_name: str = workspace_name
55 self.observer: str = observer
56 self.observed: str = observed
57
58 async def save_representation(
59 self,
60 representation: Representation,
61 message_ids: list[int],
62 session_name: str,
63 message_created_at: datetime.datetime,
64 message_level_configuration: ResolvedConfiguration,
65 ) -> int:
66 """
67 Save Representation objects to the collection as a set of documents.
68
69 Args:
70 representation: Representation object
71 message_ids: Message ID range to link with observations
72 session_name: Session name to link with existing summary context
73 message_created_at: Timestamp when the message was created
74
75 Returns:
76 The number of *new documents saved*
77 """
78
79 new_documents = 0
80
81 if not representation.deductive and not representation.explicit:
82 logger.debug("No observations to save")
83 return new_documents
84
85 all_observations = [
86 _normalized_observation(obs)
87 for obs in representation.deductive + representation.explicit
88 if _observation_text(obs).strip()
89 ]
90 if not all_observations:
91 logger.debug("No non-empty observations to save")
92 return new_documents
93
94 # Batch embed all observations
95 batch_embed_start = time.perf_counter()
96
97 observation_texts = [_observation_text(obs) for obs in all_observations]
98 try:
99 embeddings = await embedding_client.simple_batch_embed(observation_texts)
100 except ValueError as e:
101 raise exceptions.ValidationException(
102 "Observation content exceeds maximum token limit of "
103 + f"{settings.EMBEDDING.MAX_INPUT_TOKENS}."
104 ) from e
105
106 batch_embed_duration = (time.perf_counter() - batch_embed_start) * 1000
107 accumulate_metric(
108 f"deriver_{message_ids[-1]}_{self.observer}",
109 "embed_new_observations",
110 batch_embed_duration,
111 "ms",
112 )
113
114 # Batch create document objects
115 create_document_start = time.perf_counter()
116 async with tracked_db("representation_manager.save_representation") as db:
117 new_documents = await self._save_representation_internal(
118 db,
119 all_observations,
120 embeddings,
121 message_ids,
122 session_name,
123 message_created_at,
124 message_level_configuration,
125 )
126
127 create_document_duration = (time.perf_counter() - create_document_start) * 1000
128 accumulate_metric(
129 f"deriver_{message_ids[-1]}_{self.observer}",
130 "save_new_observations",
131 create_document_duration,
132 "ms",
133 )
134
135 return new_documents
136
137 async def _save_representation_internal(
138 self,
139 db: AsyncSession,
140 all_observations: list[ExplicitObservation | DeductiveObservation],
141 embeddings: list[list[float]],
142 message_ids: list[int],
143 session_name: str,
144 message_created_at: datetime.datetime,
145 message_level_configuration: ResolvedConfiguration,
146 ) -> int:
147 # get_or_create_collection already handles IntegrityError with rollback and a retry
148 collection = await crud.get_or_create_collection(
149 db,
150 self.workspace_name,
151 observer=self.observer,
152 observed=self.observed,
153 )
154
155 # Prepare all documents for bulk creation
156 documents_to_create: list[schemas.DocumentCreate] = []
157 for obs, embedding in zip(all_observations, embeddings, strict=True):
158 # NOTE: will add additional levels of reasoning in the future
159 if isinstance(obs, DeductiveObservation):
160 obs_level = "deductive"
161 obs_content = obs.conclusion
162 obs_premises = obs.premises
163 else:
164 obs_level = "explicit"
165 obs_content = obs.content
166 obs_premises = None
167
168 metadata: schemas.DocumentMetadata = schemas.DocumentMetadata(
169 message_ids=message_ids,
170 premises=obs_premises,
171 message_created_at=format_datetime_utc(message_created_at),
172 )
173
174 documents_to_create.append(
175 schemas.DocumentCreate(
176 content=obs_content,
177 session_name=session_name,
178 level=obs_level,
179 metadata=metadata,
180 embedding=embedding,
181 )
182 )
183
184 # Use bulk creation with optional duplicate detection
185 accepted_documents = await crud.create_documents(
186 db,
187 documents_to_create,
188 self.workspace_name,
189 observer=self.observer,
190 observed=self.observed,
191 deduplicate=settings.DERIVER.DEDUPLICATE,
192 )
193
194 if message_level_configuration.dream.enabled:
195 try:
196 await check_and_schedule_dream(db, collection)
197 except Exception as e:
198 logger.warning(f"Failed to check dream scheduling: {e}")
199
200 return len(accepted_documents)
201
202 async def get_working_representation(
203 self,
204 *,
205 db: AsyncSession | None = None,
206 session_name: str | None = None,
207 include_semantic_query: str | None = None,
208 embedding: list[float] | None = None,
209 semantic_search_top_k: int | None = None,
210 semantic_search_max_distance: float | None = None,
211 include_most_derived: bool = False,
212 max_observations: int = settings.DERIVER.WORKING_REPRESENTATION_MAX_OBSERVATIONS,
213 ) -> Representation:
214 """
215 Get working representation with flexible query options.
216
217 Args:
218 db: Optional database session. If provided, uses it directly;
219 otherwise creates a new session via tracked_db.
220 session_name: Optional session to filter by
221 include_semantic_query: Query for semantic search
222 embedding: Pre-computed embedding for the semantic query.
223 semantic_search_top_k: Number of semantic results
224 semantic_search_max_distance: Maximum distance for semantic search
225 include_most_derived: Include most derived observations
226 max_observations: Maximum total observations to return
227
228 Returns:
229 Representation combining various query strategies
230 """
231 if include_semantic_query and embedding is None:
232 with suppress(Exception):
233 # Best-effort precompute
234 embedding = await embedding_client.embed(include_semantic_query)
235
236 if db is not None:
237 return await self._get_working_representation_internal(
238 db,
239 session_name=session_name,
240 include_semantic_query=include_semantic_query,
241 embedding=embedding,
242 semantic_search_top_k=semantic_search_top_k,
243 semantic_search_max_distance=semantic_search_max_distance,
244 include_most_derived=include_most_derived,
245 max_observations=max_observations,
246 )
247
248 async with tracked_db(
249 "representation_manager.get_working_representation"
250 ) as new_db:
251 return await self._get_working_representation_internal(
252 new_db,
253 session_name=session_name,
254 include_semantic_query=include_semantic_query,
255 embedding=embedding,
256 semantic_search_top_k=semantic_search_top_k,
257 semantic_search_max_distance=semantic_search_max_distance,
258 include_most_derived=include_most_derived,
259 max_observations=max_observations,
260 )
>nl -ba src/utils/summarizer.py | sed -n '90,240p'Chunk ID: c8972e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1680
Output:
90
91
92 # The types of summary to store in the session metadata
93 class SummaryType(Enum):
94 SHORT = "honcho_chat_summary_short"
95 LONG = "honcho_chat_summary_long"
96
97
98 def short_summary_prompt(
99 formatted_messages: str,
100 output_words: int,
101 previous_summary_text: str,
102 ) -> str:
103 """Generate the short summary prompt."""
104 return c(f"""
105 You are a system that summarizes parts of a conversation to create a concise and accurate summary. Focus on capturing:
106
107 1. Key facts and information shared (**Capture as many explicit facts as possible**)
108 2. User preferences, opinions, and questions
109 3. Important context and requests
110 4. Core topics discussed
111
112 If there is a previous summary, ALWAYS make your new summary inclusive of both it and the new messages, therefore capturing the ENTIRE conversation. Prioritize key facts across the entire conversation.
113
114 Provide a concise, factual summary that captures the essence of the conversation. Your summary should be detailed enough to serve as context for future messages, but brief enough to be helpful. Prefer a thorough chronological narrative over a list of bullet points.
115
116 Return only the summary without any explanation or meta-commentary.
117
118 <previous_summary>
119 {previous_summary_text}
120 </previous_summary>
121
122 <conversation>
123 {formatted_messages}
124 </conversation>
125
126 Hard limit: {output_words} words maximum. If needed, drop lower-priority detail to stay within the limit.
127 """)
128
129
130 def long_summary_prompt(
131 formatted_messages: str,
132 output_words: int,
133 previous_summary_text: str,
134 ) -> str:
135 """Generate the long summary prompt."""
136 return c(f"""
137 You are a system that creates thorough, comprehensive summaries of conversations. Focus on capturing:
138
139 1. Key facts and information shared (**Capture as many explicit facts as possible**)
140 2. User preferences, opinions, and questions
141 3. Important context and requests
142 4. Core topics discussed in detail
143 5. User's apparent emotional state and personality traits
144 6. Important themes and patterns across the conversation
145
146 If there is a previous summary, ALWAYS make your new summary inclusive of both it and the new messages, therefore capturing the ENTIRE conversation. Prioritize key facts across the entire conversation.
147
148 Provide a thorough and detailed summary that captures the essence of the conversation. Your summary should serve as a comprehensive record of the important information in this conversation. Prefer an exhaustive chronological narrative over a list of bullet points.
149
150 Return only the summary without any explanation or meta-commentary.
151
152 <previous_summary>
153 {previous_summary_text}
154 </previous_summary>
155
156 <conversation>
157 {formatted_messages}
158 </conversation>
159
160 Hard limit: {output_words} words maximum. If needed, drop lower-priority detail to stay within the limit.
161 """)
162
163
164 @cache
165 def estimate_short_summary_prompt_tokens() -> int:
166 """Estimate tokens for the short summary prompt (without messages/previous_summary)."""
167 try:
168 return estimate_tokens(
169 short_summary_prompt(
170 formatted_messages="",
171 output_words=0,
172 previous_summary_text="",
173 )
174 )
175 except Exception:
176 # Return a rough estimate if estimation fails
177 return 200
178
179
180 @cache
181 def estimate_long_summary_prompt_tokens() -> int:
182 """Estimate tokens for the long summary prompt (without messages/previous_summary)."""
183 try:
184 return estimate_tokens(
185 long_summary_prompt(
186 formatted_messages="",
187 output_words=0,
188 previous_summary_text="",
189 )
190 )
191 except Exception:
192 # Return a rough estimate if estimation fails
193 return 200
194
195
196 @conditional_observe(name="Create Short Summary")
197 async def create_short_summary(
198 formatted_messages: str,
199 input_tokens: int,
200 previous_summary: str | None = None,
201 ) -> HonchoLLMCallResponse[str]:
202 # input_tokens indicates how many tokens the message list + previous summary take up
203 # we want to optimize short summaries to be smaller than the actual content being summarized
204 # so we ask the agent to produce a word count roughly equal to either the input, or the max
205 # size if the input is larger. the word/token ratio is roughly 4:3 so we multiply by 0.75.
206 # LLMs *seem* to respond better to getting asked for a word count but should workshop this.
207 output_words = int(min(input_tokens, settings.SUMMARY.MAX_TOKENS_SHORT) * 0.75)
208
209 if previous_summary:
210 previous_summary_text = previous_summary
211 else:
212 previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
213
214 prompt = short_summary_prompt(
215 formatted_messages, output_words, previous_summary_text
216 )
217
218 return await honcho_llm_call(
219 model_config=_get_summary_model_config(),
220 prompt=prompt,
221 max_tokens=[REDACTED]
222 )
223
224
225 @conditional_observe(name="Create Long Summary")
226 async def create_long_summary(
227 formatted_messages: str,
228 previous_summary: str | None = None,
229 ) -> HonchoLLMCallResponse[str]:
230 # the word/token ratio is roughly 4:3 so we multiply by 0.75.
231 # LLMs *seem* to respond better to getting asked for a word count but should workshop this.
232 output_words = int(settings.SUMMARY.MAX_TOKENS_LONG * 0.75)
233
234 if previous_summary:
235 previous_summary_text = previous_summary
236 else:
237 previous_summary_text = "There is no previous summary -- the messages are the beginning of the conversation."
238
239 prompt = long_summary_prompt(
240 formatted_messages, output_words, previous_summary_text
>nl -ba src/dialectic/core.py | sed -n '70,240p'Chunk ID: 789bf3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2016
Output:
70 Initialize the dialectic agent.
71
72 Args:
73 workspace_name: Workspace identifier
74 session_name: Session identifier (may be None for global queries)
75 observer: The peer making the query
76 observed: The peer being queried about
77 observer_peer_card: Biographical information about the observer
78 observed_peer_card: Biographical information about the observed peer
79 metric_key: Optional key for logging metrics (if provided, agent won't log separately)
80 reasoning_level: Level of reasoning to apply
81 """
82 self.workspace_name: str = workspace_name
83 self.session_name: str | None = session_name
84 self.observer: str = observer
85 self.observed: str = observed
86 self.observer_peer_card: list[str] | None = observer_peer_card
87 self.observed_peer_card: list[str] | None = observed_peer_card
88 self.metric_key: str | None = metric_key
89 self.reasoning_level: ReasoningLevel = reasoning_level
90
91 # Initialize conversation history with system prompt
92 self.messages: list[dict[str, str]] = [
93 {
94 "role": "system",
95 "content": prompts.agent_system_prompt(
96 observer, observed, observer_peer_card, observed_peer_card
97 ),
98 }
99 ]
100 self._session_history_initialized: bool = False
101 self._prefetched_conclusion_count: int = 0
102 self._run_id: str = str(uuid.uuid4())[
103 :8
104 ] # Always generate for event correlation
105
106 async def _initialize_session_history(self) -> None:
107 """Fetch and inject session history into the system prompt if configured."""
108 if self._session_history_initialized:
109 return
110 self._session_history_initialized = True
111
112 max_tokens = settings.DIALECTIC.SESSION_HISTORY_MAX_TOKENS
113 if max_tokens == 0 or not self.session_name:
114 return
115
116 # Fetch recent messages up to the token limit
117 stmt = await crud.get_messages(
118 workspace_name=self.workspace_name,
119 session_name=self.session_name,
120 token_limit=[REDACTED]
121 reverse=False, # chronological order
122 )
123 async with tracked_db("dialectic.session_history") as db:
124 result = await db.execute(stmt)
125 messages = result.scalars().all()
126
127 if not messages:
128 return
129
130 # Format messages for injection (must access ORM attrs before session closes)
131 formatted_messages: list[str] = []
132 for msg in messages:
133 formatted = format_new_turn_with_timestamp(
134 msg.content, msg.created_at, msg.peer_name
135 )
136 formatted_messages.append(formatted)
137
138 session_history_section = (
139 "\n\n## SESSION HISTORY\n\n"
140 "The following is the recent conversation history from this session. "
141 "Use this as immediate context when answering the query.\n\n"
142 "<session_history>\n"
143 f"{chr(10).join(formatted_messages)}\n"
144 "</session_history>"
145 )
146
147 # Append session history to the system prompt
148 self.messages[0]["content"] += session_history_section
149
150 async def _prefetch_relevant_observations(self, query: str) -> str | None:
151 """
152 Prefetch semantically relevant observations for the query.
153
154 This provides immediate context to the agent without requiring
155 tool calls, improving response quality and speed.
156
157 Performs two separate searches to prevent retrieval dilution:
158 - Explicit observations (produced by deriver)
159 - Higher-level observations (produced in dreaming/background/chat)
160
161 The number of observations fetched depends on reasoning level:
162 - minimal: 10 of each type (reduced context for cost savings)
163 - all others: 25 of each type
164
165 Args:
166 query: The user's query
167
168 Returns:
169 Formatted observations string or None if no observations found
170 """
171 # Use reduced prefetch for minimal reasoning to save tokens
172 prefetch_limit = 10 if self.reasoning_level == "minimal" else 25
173
174 try:
175 # Pre-compute embedding once for both searches (no DB needed)
176 query_embedding = await embedding_client.embed(query)
177
178 # search_memory manages its own short-lived DB sessions so no
179 # connection is held during external vector-store calls.
180 explicit_repr = await search_memory(
181 workspace_name=self.workspace_name,
182 observer=self.observer,
183 observed=self.observed,
184 query=query,
185 limit=prefetch_limit,
186 levels=["explicit"],
187 embedding=query_embedding,
188 )
189
190 derived_repr = await search_memory(
191 workspace_name=self.workspace_name,
192 observer=self.observer,
193 observed=self.observed,
194 query=query,
195 limit=prefetch_limit,
196 levels=["deductive", "inductive", "contradiction"],
197 embedding=query_embedding,
198 )
199
200 if explicit_repr.is_empty() and derived_repr.is_empty():
201 return None
202
203 # Count prefetched conclusions for telemetry
204 explicit_count = len(explicit_repr.explicit) + len(explicit_repr.deductive)
205 derived_count = len(derived_repr.explicit) + len(derived_repr.deductive)
206 self._prefetched_conclusion_count = explicit_count + derived_count
207
208 # Format as two separate sections
209 parts: list[str] = []
210
211 if not explicit_repr.is_empty():
212 parts.append(explicit_repr.format_as_markdown(include_ids=False))
213
214 if not derived_repr.is_empty():
215 # Include IDs for derived so agent can use get_reasoning_chain
216 parts.append(derived_repr.format_as_markdown(include_ids=True))
217
218 return "\n".join(parts)
219
220 except Exception as e:
221 logger.warning(f"Failed to prefetch observations: {e}")
222 return None
223
224 async def _prepare_query(
225 self, query: str
226 ) -> tuple[Callable[[str, dict[str, Any]], Any], str, str | None, float]:
227 """
228 Prepare common state for answering a query.
229
230 Handles session history initialization, metrics setup, observation prefetching,
231 user message construction, and tool executor creation.
232
233 Args:
234 query: The question to answer about the peer
235
236 Returns:
237 A tuple of (tool_executor, task_name, run_id, start_time)
238 """
239 await self._initialize_session_history()
240
>nl -ba src/dialectic/core.py | sed -n '240,430p'Chunk ID: 1b3b1e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2148
Output:
240
241 run_id: str | None = None
242 if self.metric_key:
243 task_name = self.metric_key
244 else:
245 run_id = str(uuid.uuid4())[:8]
246 task_name = f"dialectic_chat_{run_id}"
247 start_time = time.perf_counter()
248
249 accumulate_metric(
250 task_name,
251 "context",
252 (
253 f"workspace: {self.workspace_name}\n"
254 f"session: {self.session_name or '(global)'}\n"
255 f"observer: {self.observer}\n"
256 f"observed: {self.observed}\n"
257 f"reasoning_level: {self.reasoning_level}"
258 ),
259 "blob",
260 )
261 accumulate_metric(task_name, "query", query, "blob")
262
263 prefetched_observations = await self._prefetch_relevant_observations(query)
264
265 if prefetched_observations:
266 user_content = (
267 f"Query: {query}\n\n"
268 f"## Relevant Observations (prefetched)\n"
269 f"The following observations were found to be semantically relevant to your query. "
270 f"Use these as primary context. You may still use tools to find additional information if needed.\n\n"
271 f"{prefetched_observations}"
272 )
273 accumulate_metric(
274 task_name, "prefetched_observations", prefetched_observations, "blob"
275 )
276 else:
277 user_content = f"Query: {query}"
278
279 self.messages.append({"role": "user", "content": user_content})
280
281 tool_executor: Callable[
282 [str, dict[str, Any]], Any
283 ] = await create_tool_executor(
284 workspace_name=self.workspace_name,
285 session_name=self.session_name,
286 observer=self.observer,
287 observed=self.observed,
288 history_token_limit=[REDACTED]
289 run_id=self._run_id,
290 agent_type="dialectic",
291 parent_category="dialectic",
292 )
293
294 return tool_executor, task_name, run_id, start_time
295
296 def _log_response_metrics(
297 self,
298 task_name: str,
299 run_id: str | None,
300 start_time: float,
301 response_content: str,
302 input_tokens: int,
303 output_tokens: int,
304 cache_read_input_tokens: int | None,
305 cache_creation_input_tokens: int | None,
306 tool_calls_count: int,
307 thinking_content: str | None,
308 iterations: int,
309 ) -> None:
310 """
311 Log metrics common to both streaming and non-streaming responses.
312
313 Args:
314 task_name: Metrics task identifier
315 run_id: Run identifier (None if using caller-provided metric_key)
316 start_time: Start time from time.perf_counter()
317 response_content: The full response text
318 input_tokens: Input token count (actual from API)
319 output_tokens: Output token count (actual from API)
320 cache_read_input_tokens: Cache read tokens (if any)
321 cache_creation_input_tokens: Cache creation tokens (if any)
322 tool_calls_count: Number of tool calls made
323 thinking_content: Thinking trace content (if any)
324 iterations: Number of iterations in the tool execution loop
325 """
326 accumulate_metric(task_name, "tool_calls", tool_calls_count, "count")
327
328 if thinking_content:
329 accumulate_metric(task_name, "thinking", thinking_content, "blob")
330
331 log_token_usage_metrics(
332 task_name,
333 input_tokens,
334 output_tokens,
335 cache_read_input_tokens or 0,
336 cache_creation_input_tokens or 0,
337 )
338 accumulate_metric(task_name, "response", response_content, "blob")
339
340 elapsed_ms = (time.perf_counter() - start_time) * 1000
341 accumulate_metric(task_name, "total_duration", elapsed_ms, "ms")
342
343 if not self.metric_key and run_id is not None:
344 log_performance_metrics("dialectic_chat", run_id)
345
346 # Prometheus metrics
347 if settings.METRICS.ENABLED:
348 prometheus_metrics.record_dialectic_tokens(
349 count=input_tokens,
350 token_type=[REDACTED]
351 component=DialecticComponents.TOTAL.value,
352 reasoning_level=self.reasoning_level,
353 )
354 prometheus_metrics.record_dialectic_tokens(
355 count=output_tokens,
356 token_type=[REDACTED]
357 component=DialecticComponents.TOTAL.value,
358 reasoning_level=self.reasoning_level,
359 )
360
361 # Emit telemetry event
362 emit(
363 DialecticCompletedEvent(
364 run_id=self._run_id,
365 workspace_name=self.workspace_name,
366 peer_name=self.observed,
367 session_name=self.session_name,
368 reasoning_level=self.reasoning_level,
369 total_iterations=iterations,
370 prefetched_conclusion_count=self._prefetched_conclusion_count,
371 tool_calls_count=tool_calls_count,
372 total_duration_ms=elapsed_ms,
373 input_tokens=[REDACTED]
374 output_tokens=[REDACTED]
375 cache_read_tokens=[REDACTED] or 0,
376 cache_creation_tokens=[REDACTED] or 0,
377 )
378 )
379
380 async def answer(self, query: str) -> str:
381 """
382 Answer a query about the peer using agentic tool calling.
383
384 The agent will:
385 1. Receive the query
386 2. Use tools to gather relevant context
387 3. Synthesize a response grounded in the gathered context
388
389 Args:
390 query: The question to answer about the peer
391
392 Returns:
393 The synthesized answer string
394 """
395 tool_executor, task_name, run_id, start_time = await self._prepare_query(query)
396
397 # Get level-specific settings
398 level_settings = settings.DIALECTIC.LEVELS[self.reasoning_level]
399
400 # Use minimal tools for minimal reasoning to reduce cost
401 tools = (
402 DIALECTIC_TOOLS_MINIMAL
403 if self.reasoning_level == "minimal"
404 else DIALECTIC_TOOLS
405 )
406 # Use level-specific max_output_tokens if set, otherwise global default
407 max_tokens = (
408 level_settings.MAX_OUTPUT_TOKENS
409 if level_settings.MAX_OUTPUT_TOKENS is not None
410 else settings.DIALECTIC.MAX_OUTPUT_TOKENS
411 )
412
413 response: HonchoLLMCallResponse[str] = await honcho_llm_call(
414 model_config=_get_dialectic_level_model_config(self.reasoning_level),
415 prompt="", # Ignored since we pass messages
416 max_tokens=[REDACTED]
417 tools=tools,
418 tool_choice=level_settings.TOOL_CHOICE,
419 tool_executor=tool_executor,
420 max_tool_iterations=level_settings.MAX_TOOL_ITERATIONS,
421 messages=self.messages,
422 track_name="Dialectic Agent",
423 max_input_tokens=[REDACTED]
424 trace_name="dialectic_chat",
425 )
426
427 self._log_response_metrics(
428 task_name=task_name,
429 run_id=run_id,
430 start_time=start_time,
>nl -ba src/dreamer/orchestrator.py | sed -n '1,210p'Chunk ID: c4d050
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2289
Output:
1 """
2 Dream orchestrator for the specialist-based architecture.
3
4 This module coordinates the full dream cycle:
5 0. [Optional] Surprisal sampling: Pre-filter observations by geometric surprisal
6 1. Run deduction specialist (self-directed exploration, creates deductive observations)
7 2. Run induction specialist (self-directed exploration, creates inductive observations)
8
9 Specialists are self-directed agents that explore the observation space and create
10 higher-level observations. When surprisal sampling finds interesting observations,
11 they're passed as hints, but specialists are free to follow the evidence wherever it leads.
12 """
13
14 from __future__ import annotations
15
16 import logging
17 import time
18 import uuid
19 from dataclasses import dataclass
20 from datetime import datetime, timezone
21 from typing import Any
22
23 import sentry_sdk
24 from sqlalchemy import func, select
25
26 from src import crud, models
27 from src.config import settings
28 from src.dependencies import tracked_db
29 from src.dreamer.specialists import SPECIALISTS, SpecialistResult
30 from src.dreamer.surprisal import SurprisalScore # type: ignore
31 from src.exceptions import SpecialistExecutionError, SurprisalError
32 from src.schemas import DreamType
33 from src.telemetry.events import DreamRunEvent, emit
34 from src.telemetry.logging import (
35 accumulate_metric,
36 log_performance_metrics,
37 )
38 from src.utils.config_helpers import get_configuration
39 from src.utils.queue_payload import DreamPayload
40
41 logger = logging.getLogger(__name__)
42
43
44 @dataclass
45 class DreamResult:
46 """Result of a dream cycle for telemetry reporting."""
47
48 # Run identification
49 run_id: str
50 specialists_run: list[str]
51
52 # Specialist outcomes
53 deduction_success: bool
54 induction_success: bool
55
56 # Surprisal sampling
57 surprisal_enabled: bool
58 surprisal_conclusion_count: int
59
60 # Aggregate metrics
61 total_iterations: int
62 total_duration_ms: float
63 input_tokens: int
64 output_tokens: int
65
66
67 async def run_dream(
68 workspace_name: str,
69 observer: str,
70 observed: str,
71 session_name: str | None = None,
72 ) -> DreamResult | None:
73 """
74 Run a full dream cycle with optional surprisal-based sampling.
75
76 The dream cycle runs specialists sequentially:
77 0. [Optional] Surprisal sampling: Pre-filter observations by geometric surprisal
78 1. Deduction specialist: Creates deductive observations from explicit facts
79 2. Induction specialist: Creates inductive observations from patterns
80
81 Uses short-lived DB sessions to avoid holding connections during LLM calls.
82
83 Args:
84 workspace_name: Workspace identifier
85 observer: Observer peer name
86 observed: Observed peer name
87 session_name: Session identifier if specified
88 """
89 if not settings.DREAM.ENABLED:
90 return None
91
92 run_id = str(uuid.uuid4())[:8]
93 task_name = f"dream_orchestrator_{run_id}"
94 start_time = time.perf_counter()
95
96 logger.info(
97 f"[{run_id}] Starting dream cycle for {workspace_name}/{observer}/{observed}"
98 )
99
100 # Short-lived DB session for config resolution
101 async with tracked_db("dream.config") as db:
102 if session_name is not None:
103 session = await crud.get_session(
104 db, workspace_name=workspace_name, session_name=session_name
105 )
106 else:
107 session = None
108
109 workspace = await crud.get_workspace(db, workspace_name=workspace_name)
110 configuration = get_configuration(None, session, workspace)
111 if not configuration.dream.enabled:
112 logger.info(
113 f"[{run_id}] Dreams disabled for {workspace_name}/{session_name}, skipping dream"
114 )
115 return None
116
117 # Track specialist outcomes
118 deduction_success = False
119 induction_success = False
120 surprisal_observation_count = 0
121 deduction_result: SpecialistResult | None = None
122 induction_result: SpecialistResult | None = None
123
124 # Phase 0: Surprisal-based sampling (if enabled)
125 # Specialists are self-directed by default - hints are optional suggestions
126 exploration_hints: list[str] | None = None
127
128 if settings.DREAM.SURPRISAL.ENABLED:
129 logger.info(f"[{run_id}] Phase 0: Computing surprisal scores")
130 try:
131 from src.dreamer.surprisal import sample_observations_with_surprisal
132
133 high_surprisal_obs = await sample_observations_with_surprisal(
134 workspace_name=workspace_name,
135 observer=observer,
136 observed=observed,
137 )
138
139 logger.info(
140 f"[{run_id}] Surprisal: Found {len(high_surprisal_obs)} high-surprisal observations"
141 )
142 surprisal_observation_count = len(high_surprisal_obs)
143 accumulate_metric(
144 task_name, "surprisal_observations", len(high_surprisal_obs), "count"
145 )
146
147 if len(high_surprisal_obs) > 0:
148 # Use high-surprisal observations as hints for exploration
149 exploration_hints = _create_queries_from_surprisal(high_surprisal_obs)
150 logger.info(
151 f"[{run_id}] ✨ SURPRISAL HINTS: Suggesting {len(exploration_hints)} "
152 + "high-surprisal topics for specialists to investigate"
153 )
154 logger.info(
155 f"[{run_id}] Targeting observations with surprisal range: "
156 + f"{high_surprisal_obs[-1].surprisal:.3f} to {high_surprisal_obs[0].surprisal:.3f}"
157 )
158 else:
159 logger.info(
160 f"[{run_id}] No high-surprisal observations - specialists will explore freely"
161 )
162
163 except SurprisalError as e:
164 logger.error(f"[{run_id}] Surprisal sampling failed: {e}", exc_info=True)
165 accumulate_metric(task_name, "surprisal_error", str(e), "blob")
166 # Specialists will explore freely without hints
167
168 # Phase 1: Run deduction specialist (manages its own DB sessions)
169 logger.info(f"[{run_id}] Phase 1: Running deduction specialist")
170 deduction_specialist = SPECIALISTS["deduction"]
171 try:
172 deduction_result = await deduction_specialist.run(
173 workspace_name=workspace_name,
174 observer=observer,
175 observed=observed,
176 session_name=session_name,
177 hints=exploration_hints,
178 configuration=configuration,
179 parent_run_id=run_id,
180 )
181 logger.info(
182 f"[{run_id}] Deduction completed: {deduction_result.content[:200]}..."
183 )
184 accumulate_metric(
185 task_name, "deduction_result", deduction_result.content, "blob"
186 )
187 deduction_success = deduction_result.success
188 except SpecialistExecutionError as e:
189 logger.error(f"[{run_id}] Deduction specialist failed: {e}", exc_info=True)
190 accumulate_metric(task_name, "deduction_error", str(e), "blob")
191
192 # Phase 2: Run induction specialist (after deduction so it can see new deductive obs)
193 logger.info(f"[{run_id}] Phase 2: Running induction specialist")
194 induction_specialist = SPECIALISTS["induction"]
195 try:
196 induction_result = await induction_specialist.run(
197 workspace_name=workspace_name,
198 observer=observer,
199 observed=observed,
200 session_name=session_name,
201 hints=exploration_hints,
202 configuration=configuration,
203 parent_run_id=run_id,
204 )
205 logger.info(
206 f"[{run_id}] Induction completed: {induction_result.content[:200]}..."
207 )
208 accumulate_metric(
209 task_name, "induction_result", induction_result.content, "blob"
210 )
>nl -ba src/dreamer/specialists.py | sed -n '300,560p'Chunk ID: c81285
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2618
Output:
300 duration_ms=duration_ms,
301 success=True,
302 content=response.content,
303 )
304
305
306 class DeductionSpecialist(BaseSpecialist):
307 """
308 Creates deductive observations from explicit observations.
309
310 This specialist:
311 1. Explores recent observations and messages to understand what's there
312 2. Identifies logical implications, knowledge updates, and contradictions
313 3. Creates new deductive observations with premise linkage
314 4. Deletes outdated observations
315 5. Updates peer card with biographical facts
316 """
317
318 name: str = "deduction"
319 peer_card_update_instruction: str = "Update this with `update_peer_card` only for stable biographical/profile facts."
320
321 def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
322 if peer_card_enabled:
323 return DEDUCTION_SPECIALIST_TOOLS
324 return [
325 t
326 for t in DEDUCTION_SPECIALIST_TOOLS
327 if t["name"] not in PEER_CARD_TOOL_NAMES
328 ]
329
330 def get_model_config(self) -> ConfiguredModelSettings:
331 return _require_specialist_model_config(
332 settings.DREAM.DEDUCTION_MODEL_CONFIG,
333 specialist_name="DREAM DEDUCTION",
334 )
335
336 def get_max_tokens(self) -> int:
337 return 8192
338
339 def get_max_iterations(self) -> int:
340 return 12
341
342 def build_system_prompt(
343 self, observed: str, *, peer_card_enabled: bool = True
344 ) -> str:
345 peer_card_section = ""
346 if peer_card_enabled:
347 peer_card_section = """
348
349 ## PEER CARD (REQUIRED)
350
351 The peer card is a summary of stable biographical facts. You MUST update it when you learn:
352 - Name, age, location, occupation
353 - Family members and relationships
354 - Standing instructions ("call me X", "don't mention Y")
355 - Core preferences and traits
356
357 Never add temporary event summaries, one-off conclusions, reasoning traces, or contradiction notes.
358
359 Format entries as:
360 - Plain facts: "Name: Alice", "Works at Google", "Lives in NYC"
361 - `INSTRUCTION: ...` for standing instructions
362 - `PREFERENCE: ...` for preferences
363 - `TRAIT: ...` for personality traits
364
365 Call `update_peer_card` with the complete updated list when you have new biographical info.
366 Keep it concise (max 40 entries), deduplicated, and current."""
367
368 return f"""You are a deductive reasoning agent analyzing observations about {observed}.
369
370 ## YOUR JOB
371
372 Create deductive observations by finding logical implications in what's already known. Think like a detective connecting evidence.
373
374 ## PHASE 1: DISCOVERY
375
376 Explore what's actually in memory. Use these tools freely:
377 - `get_recent_observations` - See what's been learned recently
378 - `search_memory` - Search for specific topics
379 - `search_messages` - See actual conversation content
380
381 Spend a few tool calls understanding the landscape before creating anything.
382
383 ## PHASE 2: ACTION
384
385 Once you understand what's there, create observations and clean up:
386
387 ### Knowledge Updates (HIGH PRIORITY)
388 When the same fact has different values at different times:
389 - "meeting Tuesday" [old] → "meeting moved to Thursday" [new]
390 - Create a deductive update observation
391 - DELETE the outdated observation immediately
392
393 ### Logical Implications
394 Extract implicit information:
395 - "works as SWE at Google" → "has software engineering skills", "employed in tech"
396 - "has kids ages 5 and 8" → "is a parent", "has school-age children"
397
398 ### Contradictions
399 When statements can't both be true (not just updates), flag them:
400 - "I love coffee" vs "I hate coffee" → contradiction observation
401 {peer_card_section}
402
403 ## CREATING OBSERVATIONS
404
405 Use `create_observations_deductive`.
406
407 ```json
408 {{
409 "observations": [{{
410 "content": "The logical conclusion",
411 "source_ids": ["id1", "id2"],
412 "premises": ["premise 1 text", "premise 2 text"]
413 }}]
414 }}
415 ```
416
417 ## RULES
418
419 1. Don't explain your reasoning - just call tools
420 2. Create observations based on what you ACTUALLY FIND, not what you expect
421 3. Always include source_ids linking to the observations you're synthesizing
422 4. Empty or missing source_ids will be rejected
423 5. Delete outdated observations - don't leave duplicates
424 6. Quality over quantity - fewer good deductions beat many weak ones"""
425
426 def build_user_prompt(
427 self,
428 hints: list[str] | None,
429 peer_card: list[str] | None = None,
430 ) -> str:
431 peer_card_context = self._build_peer_card_context(peer_card)
432
433 if hints:
434 hints_str = "\n".join(f"- {q}" for q in hints[:5])
435 return f"""{peer_card_context}Start by exploring recent observations and messages. These topics may be worth investigating:
436
437 {hints_str}
438
439 But follow the evidence - if you find something more interesting, pursue that instead.
440
441 Begin with `get_recent_observations` to see what's there."""
442
443 return f"""{peer_card_context}Explore the observation space and create deductive observations.
444
445 Start with `get_recent_observations` to see what's been learned recently, then investigate whatever seems most promising.
446
447 Look for:
448 1. Knowledge updates (same fact, different values over time)
449 2. Logical implications that haven't been made explicit
450 3. Contradictions that need flagging
451
452 Go."""
453
454
455 class InductionSpecialist(BaseSpecialist):
456 """
457 Creates inductive observations from explicit and deductive observations.
458
459 This specialist:
460 1. Explores observations to understand what's there
461 2. Identifies patterns and generalizations across multiple observations
462 3. Creates new inductive observations with source linkage
463 4. Updates peer card with high-confidence traits and tendencies
464 """
465
466 name: str = "induction"
467 peer_card_update_instruction: str = "Only add highly stable profile traits/preferences; do not copy transient conclusions."
468
469 def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
470 if peer_card_enabled:
471 return INDUCTION_SPECIALIST_TOOLS
472 return [
473 t
474 for t in INDUCTION_SPECIALIST_TOOLS
475 if t["name"] not in PEER_CARD_TOOL_NAMES
476 ]
477
478 def get_model_config(self) -> ConfiguredModelSettings:
479 return _require_specialist_model_config(
480 settings.DREAM.INDUCTION_MODEL_CONFIG,
481 specialist_name="DREAM INDUCTION",
482 )
483
484 def get_max_tokens(self) -> int:
485 return 8192
486
487 def get_max_iterations(self) -> int:
488 return 10
489
490 def build_system_prompt(
491 self, observed: str, *, peer_card_enabled: bool = True
492 ) -> str:
493 peer_card_section = ""
494 if peer_card_enabled:
495 peer_card_section = """
496
497 ## PEER CARD (REQUIRED)
498
499 After identifying patterns, only update the peer card for durable profile-level traits/preferences:
500 - `TRAIT: Analytical thinker`
501 - `TRAIT: Tends to reschedule when stressed`
502 - `PREFERENCE: Prefers detailed explanations`
503
504 Do NOT add temporary patterns, episode-specific conclusions, or reasoning summaries.
505 Call `update_peer_card` with the complete deduplicated list only when a durable profile update is warranted.
506 Keep it concise (max 40 entries)."""
507
508 return f"""You are an inductive reasoning agent identifying patterns about {observed}.
509
510 ## YOUR JOB
511
512 Create inductive observations by finding patterns across multiple observations. Think like a psychologist identifying behavioral tendencies.
513
514 ## PHASE 1: DISCOVERY
515
516 Explore broadly to find patterns. Use these tools:
517 - `get_recent_observations` - Recent learnings
518 - `search_memory` - Topic-specific search
519 - `search_messages` - Actual conversation content
520
521 Look at BOTH explicit observations AND deductive ones. Patterns often emerge from synthesizing across both levels.
522
523 ## PHASE 2: ACTION
524
525 Create inductive observations when you see patterns:
526
527 ### Behavioral Patterns
528 - "Tends to reschedule meetings when stressed"
529 - "Makes decisions after consulting with partner"
530 - "Projects follow: enthusiasm → doubt → completion"
531
532 ### Preferences
533 - "Prefers morning meetings"
534 - "Likes detailed technical explanations"
535
536 ### Personality Traits
537 - "Generally optimistic about outcomes"
538 - "Detail-oriented in planning"
539
540 ### Temporal Patterns
541 - "Career goals have remained consistent"
542 - "Living situation changes frequently"
543 {peer_card_section}
544
545 ## CREATING OBSERVATIONS
546
547 Use `create_observations_inductive`.
548
549 ```json
550 {{
551 "observations": [{{
552 "content": "The pattern or generalization",
553 "source_ids": ["id1", "id2", "id3"],
554 "sources": ["evidence 1", "evidence 2"],
555 "pattern_type": "tendency", // preference|behavior|personality|tendency|correlation
556 "confidence": "medium" // low (2 sources), medium (3-4), high (5+)
557 }}]
558 }}
559 ```
560
>nl -ba src/crud/peer_card.py | sed -n '1,120p'Chunk ID: 21b4da
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 941
Output:
1 from __future__ import annotations
2
3 import logging
4 from typing import Any, cast
5
6 from sqlalchemy import update
7 from sqlalchemy.engine import CursorResult
8 from sqlalchemy.ext.asyncio import AsyncSession
9
10 from src import exceptions, models, schemas
11 from src.cache.client import safe_cache_delete
12 from src.crud.peer import get_or_create_peers, get_peer, peer_cache_key
13
14 logger = logging.getLogger(__name__)
15
16
17 async def get_peer_card(
18 db: AsyncSession,
19 workspace_name: str,
20 *,
21 observer: str,
22 observed: str,
23 ) -> list[str] | None:
24 """
25 Get peer card from internal_metadata.
26
27 The peer card is returned for the observer/observed relationship.
28
29 Args:
30 db: Database session
31 workspace_name: Name of the workspace
32 observed: Peer name of the peer described in the peer card
33 observer: Peer name of the observer
34
35 Returns:
36 The peer's card text if present, otherwise None.
37
38 Raises:
39 ResourceNotFoundException: If the peer does not exist.
40 """
41 peer = await get_peer(db, workspace_name, schemas.PeerCreate(name=observer))
42 return cast(
43 list[str] | None,
44 peer.internal_metadata.get(
45 construct_peer_card_label(observer=observer, observed=observed)
46 ),
47 )
48
49
50 async def set_peer_card(
51 db: AsyncSession,
52 workspace_name: str,
53 peer_card: list[str],
54 *,
55 observer: str,
56 observed: str,
57 ) -> None:
58 """
59 Set peer card for a peer.
60
61 If observer_name is provided, the peer card is set for the observer/observed relationship.
62
63 Args:
64 db: Database session
65 workspace_name: Name of the workspace
66 peer_card: List of strings to set as the peer card
67 observed: Peer name of the peer described in the peer card
68 observer: Peer name of the observer
69
70 """
71 # Ensure the peer exists (get-or-create)
72 peers_result = await get_or_create_peers(
73 db, workspace_name, [schemas.PeerCreate(name=observer)]
74 )
75
76 stmt = (
77 update(models.Peer)
78 .where(models.Peer.workspace_name == workspace_name)
79 .where(models.Peer.name == observer)
80 .values(
81 internal_metadata=models.Peer.internal_metadata.op("||")(
82 {
83 construct_peer_card_label(
84 observer=observer, observed=observed
85 ): peer_card
86 }
87 )
88 )
89 )
90 result = cast(CursorResult[Any], await db.execute(stmt))
91 if result.rowcount == 0:
92 raise exceptions.ResourceNotFoundException(
93 f"Peer {observer} not found in workspace {workspace_name}"
94 )
95 await db.commit()
96 await peers_result.post_commit()
97
98 # Invalidate cache - read-through pattern
99 cache_key = peer_cache_key(workspace_name, observer)
100 await safe_cache_delete(cache_key)
101
102
103 def construct_peer_card_label(*, observer: str, observed: str) -> str:
104 if observer == observed:
105 return "peer_card"
106 return f"{observed}_peer_card"
>rg "MessageEmbedding|message_embeddings|embed_messages|create.*embedding|simple_batch_embed" src/crud src/reconciler src/routers src/deriver src/utils -g'*.py'Chunk ID: f277ae
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1869
Output:
src/utils/agent_tools.py: embeddings = await embedding_client.simple_batch_embed(contents)
src/utils/agent_tools.py: "Batch embedding failed for create_observations; falling back to per-observation embedding: %s",
src/utils/agent_tools.py: query_embeddings = await embedding_client.simple_batch_embed(semantic_queries)
src/crud/representation.py: embeddings = await embedding_client.simple_batch_embed(observation_texts)
src/reconciler/sync_vectors.py: message_embeddings_synced: int = 0
src/reconciler/sync_vectors.py: message_embeddings_failed: int = 0
src/reconciler/sync_vectors.py: return self.documents_synced + self.message_embeddings_synced
src/reconciler/sync_vectors.py: return self.documents_failed + self.message_embeddings_failed
src/reconciler/sync_vectors.py:async def _get_message_embeddings_needing_sync(
src/reconciler/sync_vectors.py:) -> list[models.MessageEmbedding]:
src/reconciler/sync_vectors.py: select(models.MessageEmbedding)
src/reconciler/sync_vectors.py: models.MessageEmbedding.sync_state == "pending",
src/reconciler/sync_vectors.py: _backoff_eligible(models.MessageEmbedding.last_sync_at),
src/reconciler/sync_vectors.py: .order_by(models.MessageEmbedding.last_sync_at.asc().nullsfirst())
src/reconciler/sync_vectors.py: embeddings: list[models.MessageEmbedding],
src/reconciler/sync_vectors.py: update(models.MessageEmbedding)
src/reconciler/sync_vectors.py: .where(models.MessageEmbedding.id == emb.id)
src/reconciler/sync_vectors.py: new_embeddings = await embedding_client.simple_batch_embed(contents)
src/reconciler/sync_vectors.py:async def _sync_message_embeddings(
src/reconciler/sync_vectors.py: embeddings: list[models.MessageEmbedding],
src/reconciler/sync_vectors.py: embs_needing_embed: list[models.MessageEmbedding] = [
src/reconciler/sync_vectors.py: new_embeddings = await embedding_client.simple_batch_embed(contents)
src/reconciler/sync_vectors.py: failed_to_embed: list[models.MessageEmbedding] = [
src/reconciler/sync_vectors.py: # TODO: chunk_position is computed from MessageEmbedding row ordering by ID, which is
src/reconciler/sync_vectors.py: # fragile. If rows are deleted and re-created (e.g., during re-embedding), IDs change
src/reconciler/sync_vectors.py: # 1. Persisting chunk_position in the MessageEmbedding table
src/reconciler/sync_vectors.py: # 2. Removing MessageEmbedding table entirely if it becomes unnecessary
src/reconciler/sync_vectors.py: select(models.MessageEmbedding.id, models.MessageEmbedding.message_id)
src/reconciler/sync_vectors.py: .where(models.MessageEmbedding.message_id.in_(message_ids))
src/reconciler/sync_vectors.py: .order_by(models.MessageEmbedding.message_id, models.MessageEmbedding.id)
src/reconciler/sync_vectors.py: by_namespace: dict[str, list[models.MessageEmbedding]] = {}
src/reconciler/sync_vectors.py: embs_to_sync: list[models.MessageEmbedding] = []
src/reconciler/sync_vectors.py: update(models.MessageEmbedding)
src/reconciler/sync_vectors.py: .where(models.MessageEmbedding.id.in_([e.id for e in embs_to_sync]))
src/reconciler/sync_vectors.py:async def _reconcile_message_embeddings_batch(
src/reconciler/sync_vectors.py: embs = await _get_message_embeddings_needing_sync(db)
src/reconciler/sync_vectors.py: synced, failed = await _sync_message_embeddings(db, embs, external_vector_store)
src/reconciler/sync_vectors.py: metrics.message_embeddings_synced += synced
src/reconciler/sync_vectors.py: metrics.message_embeddings_failed += failed
src/reconciler/sync_vectors.py: embs_work = await _reconcile_message_embeddings_batch(
src/crud/workspace.py: delete(models.MessageEmbedding).where(
src/crud/workspace.py: models.MessageEmbedding.workspace_name == workspace_name
src/deriver/consumer.py: metrics.message_embeddings_synced,
src/deriver/consumer.py: metrics.message_embeddings_failed,
src/deriver/consumer.py: message_embeddings_synced=metrics.message_embeddings_synced,
src/deriver/consumer.py: message_embeddings_failed=metrics.message_embeddings_failed,
src/utils/search.py: distance_expr = models.MessageEmbedding.embedding.cosine_distance(embedding_query)
src/utils/search.py: models.MessageEmbedding,
src/utils/search.py: models.Message.public_id == models.MessageEmbedding.message_id,
src/utils/search.py: .where(models.MessageEmbedding.embedding.isnot(None))
src/utils/search.py: .where(models.MessageEmbedding.workspace_name == workspace_name)
src/crud/session.py: # Fetch all MessageEmbedding records to build vector IDs with {message_id}_{chunk_index}
src/crud/session.py: select(models.MessageEmbedding).where(
src/crud/session.py: models.MessageEmbedding.session_name == session_name,
src/crud/session.py: models.MessageEmbedding.workspace_name == workspace_name,
src/crud/session.py: message_chunks: dict[str, list[models.MessageEmbedding]] = {}
src/crud/session.py: # Delete MessageEmbedding entries in batches
src/crud/session.py: models.MessageEmbedding,
src/crud/session.py: models.MessageEmbedding.session_name == session_name,
src/crud/session.py: models.MessageEmbedding.workspace_name == workspace_name,
src/crud/document.py: embeddings = await embedding_client.simple_batch_embed(contents)
src/crud/message.py: # Create MessageEmbedding entries
src/crud/message.py: embedding_objects: list[models.MessageEmbedding] = []
src/crud/message.py: embedding_obj = models.MessageEmbedding(
src/crud/message.py: # Always create MessageEmbedding rows so reconciliation can track sync state
src/crud/message.py: update(models.MessageEmbedding)
src/crud/message.py: .where(models.MessageEmbedding.id.in_(embedding_ids))
src/crud/message.py: update(models.MessageEmbedding)
src/crud/message.py: .where(models.MessageEmbedding.id.in_(embedding_ids))
src/crud/message.py: update(models.MessageEmbedding)
src/crud/message.py: .where(models.MessageEmbedding.id.in_(embedding_ids))
src/crud/message.py: sync_attempts=models.MessageEmbedding.sync_attempts
src/crud/message.py: update(models.MessageEmbedding)
src/crud/message.py: .where(models.MessageEmbedding.id.in_(embedding_ids))
src/crud/message.py: sync_attempts=models.MessageEmbedding.sync_attempts
src/crud/message.py: models.MessageEmbedding,
src/crud/message.py: models.Message.public_id == models.MessageEmbedding.message_id,
src/crud/message.py: .where(models.MessageEmbedding.workspace_name == workspace_name)
src/crud/message.py: .order_by(models.MessageEmbedding.embedding.cosine_distance(query_embedding))
src/crud/message.py: models.MessageEmbedding.session_name == session_name
src/crud/message.py: models.MessageEmbedding.session_name.in_(allowed_session_names)
>nl -ba src/utils/agent_tools.py | sed -n '1120,1500p'Chunk ID: 2962b7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4172
Output:
1120
1121 Returns:
1122 Dict with 'messages' list containing potentially relevant messages
1123 """
1124 messages: list[str] = []
1125 seen_content: set[str] = set() # Dedupe by content hash
1126
1127 # Semantic queries to find preference-like content
1128 semantic_queries = [
1129 "user preferences and communication style",
1130 "standing instructions and rules to follow",
1131 "how user wants responses formatted",
1132 "user requirements and constraints",
1133 "things user wants or does not want",
1134 ]
1135
1136 # Batch embed all queries in a single API call (no DB needed).
1137 # If batching fails, each search call will generate its own embedding.
1138 query_embeddings_by_query: dict[str, list[float]] | None = None
1139 try:
1140 query_embeddings = await embedding_client.simple_batch_embed(semantic_queries)
1141 query_embeddings_by_query = dict(
1142 zip(semantic_queries, query_embeddings, strict=True)
1143 )
1144 except Exception as e:
1145 logger.warning(
1146 "Batch embedding failed for extract_preferences; falling back to per-query embedding in search_messages: %s",
1147 e,
1148 )
1149
1150 for query in semantic_queries:
1151 try:
1152 snippets = await crud.search_messages(
1153 workspace_name=workspace_name,
1154 session_name=session_name,
1155 query=query,
1156 limit=10,
1157 context_window=0,
1158 embedding=(
1159 query_embeddings_by_query.get(query)
1160 if query_embeddings_by_query is not None
1161 else None
1162 ),
1163 observer=observer,
1164 )
1165 for matches, _ in snippets:
1166 for msg in matches:
1167 if msg.peer_name == observed:
1168 content_key = msg.content[:100].lower()
1169 if content_key not in seen_content:
1170 seen_content.add(content_key)
1171 messages.append(f"'{msg.content.strip()}'")
1172 except Exception as e:
1173 logger.warning("Error in semantic search for '%s': %s", query, e)
1174
1175 return {
1176 "instructions": [], # Deprecated - LLM will categorize
1177 "preferences": [], # Deprecated - LLM will categorize
1178 "messages": messages[:30], # Raw messages for LLM to process
1179 }
1180
1181
1182 @dataclass
1183 class ToolContext:
1184 """Context object passed to tool handlers."""
1185
1186 workspace_name: str
1187 observer: str
1188 observed: str
1189 session_name: str | None
1190 current_messages: list[models.Message] | None
1191 include_observation_ids: bool
1192 history_token_limit: int
1193 # Shared lock for serializing writes to the same workspace/observer/observed.
1194 # This lock is obtained from the module-level registry to ensure all concurrent
1195 # tool executors for the same data share the same lock.
1196 db_lock: asyncio.Lock
1197 # Optional resolved configuration for checking feature flags
1198 configuration: ResolvedConfiguration | None = None
1199 # Telemetry context fields
1200 run_id: str | None = None
1201 agent_type: str | None = None # "dialectic", "deriver", "dreamer"
1202 parent_category: str | None = None # Parent category for CloudEvents
1203
1204
1205 async def _handle_create_observations_impl(
1206 ctx: ToolContext,
1207 tool_input: dict[str, Any],
1208 *,
1209 forced_level: str | None = None,
1210 ) -> str:
1211 """Handle create_observations tool."""
1212 raw_observations = tool_input.get("observations", [])
1213
1214 if not raw_observations:
1215 return "ERROR: observations list is empty"
1216
1217 # Set context-specific default level before Pydantic validation
1218 default_level = "explicit" if ctx.current_messages else "deductive"
1219 for obs in raw_observations:
1220 if forced_level is not None:
1221 obs["level"] = forced_level
1222 else:
1223 obs.setdefault("level", default_level)
1224
1225 # Validate observations individually so valid ones are still processed
1226 observations: list[schemas.ObservationInput] = []
1227 validation_failures: list[ObservationFailure] = []
1228 for obs in raw_observations:
1229 try:
1230 validated = schemas.ObservationInput.model_validate(obs)
1231 except ValidationError as e:
1232 validation_failures.append(
1233 ObservationFailure(
1234 content_preview=str(obs.get("content", ""))[:50],
1235 error=f"Validation failed: {e}",
1236 )
1237 )
1238 continue
1239 # Deriver can only create explicit observations
1240 if ctx.current_messages and validated.level != "explicit":
1241 validation_failures.append(
1242 ObservationFailure(
1243 content_preview=validated.content[:50],
1244 error=f"Deriver can only create 'explicit' observations, got '{validated.level}'",
1245 )
1246 )
1247 continue
1248 observations.append(validated)
1249
1250 if not observations:
1251 failure_details = "; ".join(
1252 f"'{f.content_preview}': {f.error}" for f in validation_failures
1253 )
1254 return f"ERROR: All observations failed validation: {failure_details}"
1255
1256 # Determine message context
1257 if ctx.current_messages:
1258 message_ids = [msg.id for msg in ctx.current_messages]
1259 message_created_at = str(ctx.current_messages[-1].created_at)
1260 else:
1261 message_ids = []
1262 message_created_at = utc_now_iso()
1263
1264 # Use lock to serialize database writes (prevents concurrent commit issues)
1265 async with ctx.db_lock:
1266 result = await create_observations(
1267 observations=observations,
1268 observer=ctx.observer,
1269 observed=ctx.observed,
1270 session_name=ctx.session_name,
1271 workspace_name=ctx.workspace_name,
1272 message_ids=message_ids,
1273 message_created_at=message_created_at,
1274 )
1275
1276 # Merge validation and embedding failures
1277 all_failures = validation_failures + result.failed
1278
1279 # Count levels from actually-created observations
1280 levels = result.created_levels
1281 explicit_count = levels.count("explicit")
1282 deductive_count = levels.count("deductive")
1283 inductive_count = levels.count("inductive")
1284 contradiction_count = levels.count("contradiction")
1285
1286 # Emit telemetry event if context is available
1287 if ctx.run_id and ctx.agent_type and ctx.parent_category:
1288 emit(
1289 AgentToolConclusionsCreatedEvent(
1290 run_id=ctx.run_id,
1291 iteration=get_current_iteration(),
1292 parent_category=ctx.parent_category,
1293 agent_type=ctx.agent_type,
1294 workspace_name=ctx.workspace_name,
1295 observer=ctx.observer,
1296 observed=ctx.observed,
1297 conclusion_count=result.created_count,
1298 levels=levels,
1299 )
1300 )
1301
1302 response = (
1303 f"Created {result.created_count} observations for {ctx.observed} by {ctx.observer} "
1304 f"({explicit_count} explicit, {deductive_count} deductive, "
1305 f"{inductive_count} inductive, {contradiction_count} contradiction)"
1306 )
1307
1308 if all_failures:
1309 failure_details = "; ".join(
1310 f"'{f.content_preview}': {f.error}" for f in all_failures
1311 )
1312 response += f"\nFailed {len(all_failures)}: {failure_details}"
1313
1314 return response
1315
1316
1317 async def _handle_create_observations(
1318 ctx: ToolContext, tool_input: dict[str, Any]
1319 ) -> str:
1320 return await _handle_create_observations_impl(ctx, tool_input)
1321
1322
1323 async def _handle_create_observations_deductive(
1324 ctx: ToolContext, tool_input: dict[str, Any]
1325 ) -> str:
1326 return await _handle_create_observations_impl(
1327 ctx,
1328 tool_input,
1329 forced_level="deductive",
1330 )
1331
1332
1333 async def _handle_create_observations_inductive(
1334 ctx: ToolContext, tool_input: dict[str, Any]
1335 ) -> str:
1336 return await _handle_create_observations_impl(
1337 ctx,
1338 tool_input,
1339 forced_level="inductive",
1340 )
1341
1342
1343 async def _handle_update_peer_card(ctx: ToolContext, tool_input: dict[str, Any]) -> str:
1344 """Handle update_peer_card tool."""
1345 # Check if peer card creation is disabled via configuration
1346 if ctx.configuration is not None and not ctx.configuration.peer_card.create:
1347 logger.info(
1348 "Peer card creation disabled for %s, skipping update",
1349 ctx.workspace_name,
1350 )
1351 return (
1352 "Peer card creation is disabled for this workspace/session configuration."
1353 )
1354
1355 raw_peer_card_content = tool_input.get("content")
1356
1357 # Guard against None or empty content — keep the existing peer card.
1358 if raw_peer_card_content is None:
1359 logger.warning(
1360 "Peer card update called with None content for %s, keeping existing card",
1361 ctx.workspace_name,
1362 )
1363 return "Peer card content was empty, no update performed."
1364
1365 # Normalize and deduplicate to keep peer cards bounded and stable.
1366 normalized_peer_card: list[str] = []
1367 seen: set[str] = set()
1368 items = (
1369 cast(list[str], raw_peer_card_content)
1370 if isinstance(raw_peer_card_content, list)
1371 else [str(raw_peer_card_content)]
1372 )
1373 for item in items:
1374 line = str(item).strip()
1375 if not line:
1376 continue
1377
1378 # Case-insensitive dedupe with whitespace normalization.
1379 normalized_key = " ".join(line.lower().split())
1380 if normalized_key in seen:
1381 continue
1382 seen.add(normalized_key)
1383 normalized_peer_card.append(line)
1384
1385 # Don't clear the peer card if all content normalized to empty.
1386 if not normalized_peer_card:
1387 logger.warning(
1388 "Peer card update normalized to empty for %s, keeping existing card",
1389 ctx.workspace_name,
1390 )
1391 return "Peer card content was empty after normalization, no update performed."
1392
1393 if len(normalized_peer_card) > MAX_PEER_CARD_FACTS:
1394 logger.warning(
1395 "Peer card update exceeded max facts (%s), truncating from %s to %s",
1396 MAX_PEER_CARD_FACTS,
1397 len(normalized_peer_card),
1398 MAX_PEER_CARD_FACTS,
1399 )
1400 normalized_peer_card = normalized_peer_card[:MAX_PEER_CARD_FACTS]
1401
1402 async with ctx.db_lock, tracked_db("tool.update_peer_card") as db:
1403 await crud.set_peer_card(
1404 db,
1405 workspace_name=ctx.workspace_name,
1406 peer_card=normalized_peer_card,
1407 observer=ctx.observer,
1408 observed=ctx.observed,
1409 )
1410 logger.info(
1411 f"Updated peer card for {ctx.workspace_name}/{ctx.observer}/{ctx.observed}"
1412 )
1413
1414 # Emit telemetry event if context is available
1415 if ctx.run_id and ctx.agent_type and ctx.parent_category:
1416 # Count facts in peer card (content is a list of strings per tool schema)
1417 emit(
1418 AgentToolPeerCardUpdatedEvent(
1419 run_id=ctx.run_id,
1420 iteration=get_current_iteration(),
1421 parent_category=ctx.parent_category,
1422 agent_type=ctx.agent_type,
1423 workspace_name=ctx.workspace_name,
1424 observer=ctx.observer,
1425 observed=ctx.observed,
1426 facts_count=len(normalized_peer_card),
1427 )
1428 )
1429
1430 return f"Updated peer card for {ctx.observed} by {ctx.observer}"
1431
1432
1433 async def _handle_get_recent_history(
1434 ctx: ToolContext, tool_input: dict[str, Any]
1435 ) -> str:
1436 """Handle get_recent_history tool."""
1437 _ = tool_input
1438 async with tracked_db("tool.get_recent_history") as db:
1439 history: list[models.Message] = await get_recent_history(
1440 db,
1441 workspace_name=ctx.workspace_name,
1442 session_name=ctx.session_name,
1443 observed=ctx.observed,
1444 token_limit=[REDACTED]
1445 )
1446 if not history:
1447 return "No conversation history available"
1448 history_text = "\n".join(
1449 [f"{m.peer_name}: {_truncate_message_content(m.content)}" for m in history]
1450 )
1451 scope = (
1452 f"from session {ctx.session_name}"
1453 if ctx.session_name
1454 else f"from {ctx.observed} across sessions"
1455 )
1456 output = f"Conversation history ({len(history)} messages {scope}):\n{history_text}"
1457 return _truncate_tool_output(output)
1458
1459
1460 async def _handle_search_memory(ctx: ToolContext, tool_input: dict[str, Any]) -> str:
1461 """Handle search_memory tool."""
1462 top_k = min(_safe_int(tool_input.get("top_k"), 20), 40)
1463 query = tool_input["query"]
1464 try:
1465 query_embedding = await embedding_client.embed(query)
1466 except ValueError:
1467 return (
1468 "ERROR: Query exceeds maximum token limit of "
1469 + f"{settings.EMBEDDING.MAX_INPUT_TOKENS}. Please use a shorter query."
1470 )
1471
1472 documents = await crud.query_documents(
1473 db=None,
1474 workspace_name=ctx.workspace_name,
1475 observer=ctx.observer,
1476 observed=ctx.observed,
1477 query=query,
1478 top_k=top_k,
1479 embedding=query_embedding,
1480 )
1481 mem = Representation.from_documents(documents)
1482 total_count = mem.len()
1483 if total_count == 0:
1484 # fallback behavior: if the memory is *empty*, that means we're quite
1485 # early in a workspace/peer/session -- in order to give good answers in
1486 # this stage, and be efficient with tool calls, and make sure the model
1487 # doesn't short-circuit and think there's nothing here, we
1488 # automatically search the message history for relevant information.
1489 if ctx.agent_type == "dialectic":
1490 limit = min(_safe_int(tool_input.get("top_k"), 20), 20)
1491 message_output = None
1492 snippets = await crud.search_messages(
1493 workspace_name=ctx.workspace_name,
1494 session_name=ctx.session_name,
1495 query=query,
1496 limit=limit,
1497 context_window=0,
1498 embedding=query_embedding,
1499 observer=ctx.observer,
1500 )
>nl -ba src/crud/message.py | sed -n '1,260p'Chunk ID: 95ee10
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2734
Output:
1 from collections.abc import Sequence
2 from datetime import datetime
3 from logging import getLogger
4 from typing import Any
5
6 from nanoid import generate as generate_nanoid
7 from sqlalchemy import ColumnElement, Select, and_, func, or_, select, text, update
8 from sqlalchemy.ext.asyncio import AsyncSession
9
10 from src import models, schemas
11 from src.config import settings
12 from src.dependencies import tracked_db
13 from src.embedding_client import embedding_client
14 from src.exceptions import VectorStoreError
15 from src.utils.filter import apply_filter
16 from src.utils.formatting import ILIKE_ESCAPE_CHAR, escape_ilike_pattern
17 from src.vector_store import VectorRecord, get_external_vector_store
18
19 from .session import get_or_create_session
20
21 logger = getLogger(__name__)
22
23
24 def _deduplicate_messages(
25 messages: Sequence[models.Message], limit: int
26 ) -> list[models.Message]:
27 """Deduplicate messages by public_id, preserving input order."""
28 seen: set[str] = set()
29 result: list[models.Message] = []
30 for msg in messages:
31 if msg.public_id not in seen:
32 seen.add(msg.public_id)
33 result.append(msg)
34 if len(result) >= limit:
35 break
36 return result
37
38
39 def _expunge_snippets(
40 db: AsyncSession, snippets: list[tuple[list[models.Message], list[models.Message]]]
41 ) -> None:
42 """Detach snippet messages from the session, guarding against duplicates."""
43 seen: set[int] = set()
44 for matches, context in snippets:
45 for msg in [*matches, *context]:
46 obj_id = id(msg)
47 if obj_id in seen:
48 continue
49 db.expunge(msg)
50 seen.add(obj_id)
51
52
53 async def get_peer_session_names(
54 db: AsyncSession,
55 workspace_name: str,
56 peer_name: str,
57 ) -> list[str]:
58 """Get all session names where a peer has any membership record.
59
60 Any membership record (regardless of joined_at/left_at) grants visibility
61 to all messages in that session.
62 """
63 stmt = (
64 select(models.session_peers_table.c.session_name)
65 .where(models.session_peers_table.c.workspace_name == workspace_name)
66 .where(models.session_peers_table.c.peer_name == peer_name)
67 .distinct()
68 )
69 result = await db.execute(stmt)
70 return [row[0] for row in result.all()]
71
72
73 def _apply_token_limit(
74 base_conditions: list[ColumnElement[Any]], token_limit: int
75 ) -> Select[tuple[models.Message]]:
76 """
77 Helper function to apply token limit logic to a message query.
78
79 Creates a subquery that calculates running sum of tokens for most recent messages
80 and returns a select statement that joins with this subquery to limit results
81 based on token count.
82
83 Args:
84 base_conditions: List of conditions to apply to the base query
85 token_limit: Maximum number of tokens to include in the messages
86
87 Returns:
88 Select statement with token limit applied
89 """
90 # Create a subquery that calculates running sum of tokens for most recent messages
91 token_subquery = (
92 select(
93 models.Message.id,
94 func.sum(models.Message.token_count)
95 .over(order_by=models.Message.id.desc())
96 .label("running_token_sum"),
97 )
98 .where(*base_conditions)
99 .subquery()
100 )
101
102 # Select Message objects where running sum doesn't exceed token_limit
103 return (
104 select(models.Message)
105 .join(token_subquery, models.Message.id == token_subquery.c.id)
106 .where(token_subquery.c.running_token_sum <= token_limit)
107 )
108
109
110 async def _build_merged_snippets(
111 db: AsyncSession,
112 workspace_name: str,
113 matched_messages: list[models.Message],
114 context_window: int,
115 ) -> list[tuple[list[models.Message], list[models.Message]]]:
116 """
117 Group matched messages by session, merge overlapping context ranges, and fetch context.
118
119 Takes a list of matched messages and builds conversation snippets by:
120 1. Grouping matches by session name
121 2. Sorting matches within each session by sequence number
122 3. Merging overlapping context windows to avoid duplicate context
123 4. Fetching the full context for each merged range from the database
124
125 Args:
126 db: Database session
127 workspace_name: Name of the workspace
128 matched_messages: List of messages that matched a search query
129 context_window: Number of messages before/after each match to include
130
131 Returns:
132 List of tuples: (matched_messages_in_range, context_messages)
133 Each tuple represents a snippet where context_messages includes all messages
134 in the merged range (including the matched messages), ordered chronologically.
135 """
136 if not matched_messages:
137 return []
138
139 session_matches: dict[str, list[models.Message]] = {}
140 for msg in matched_messages:
141 session_matches.setdefault(msg.session_name, []).append(msg)
142
143 # Build merged ranges per session, then issue a single batched query
144 session_ranges: dict[str, list[tuple[int, int, list[models.Message]]]] = {}
145 for sess_name, matches in session_matches.items():
146 matches.sort(key=lambda m: m.seq_in_session)
147
148 merged_ranges: list[tuple[int, int, list[models.Message]]] = []
149 for match in matches:
150 start = match.seq_in_session - context_window
151 end = match.seq_in_session + context_window
152
153 if merged_ranges and start <= merged_ranges[-1][1] + 1:
154 prev_start, prev_end, prev_matches = merged_ranges[-1]
155 merged_ranges[-1] = (
156 prev_start,
157 max(prev_end, end),
158 [*prev_matches, match],
159 )
160 else:
161 merged_ranges.append((start, end, [match]))
162
163 session_ranges[sess_name] = merged_ranges
164
165 # One OR-of-ANDs predicate covers every (session, range) pair
166 session_predicates = [
167 and_(
168 models.Message.session_name == sess_name,
169 or_(
170 *(
171 models.Message.seq_in_session.between(start_seq, end_seq)
172 for start_seq, end_seq, _ in merged_ranges
173 )
174 ),
175 )
176 for sess_name, merged_ranges in session_ranges.items()
177 ]
178
179 context_stmt = (
180 select(models.Message)
181 .where(models.Message.workspace_name == workspace_name)
182 .where(or_(*session_predicates))
183 .order_by(
184 models.Message.session_name.asc(),
185 models.Message.seq_in_session.asc(),
186 )
187 )
188
189 context_result = await db.execute(context_stmt)
190 by_session: dict[str, list[models.Message]] = {}
191 for msg in context_result.scalars().all():
192 by_session.setdefault(msg.session_name, []).append(msg)
193
194 snippets: list[
195 tuple[list[models.Message], list[models.Message]]
196 ] = [] # list of tuples, each containing query matches and context messages
197 for sess_name, merged_ranges in session_ranges.items():
198 all_context_messages = by_session.get(sess_name, [])
199 for start_seq, end_seq, range_matches in merged_ranges:
200 context_messages = [
201 msg
202 for msg in all_context_messages
203 if start_seq <= msg.seq_in_session <= end_seq
204 ]
205 snippets.append((range_matches, context_messages))
206
207 return snippets
208
209
210 async def create_messages(
211 db: AsyncSession,
212 messages: list[schemas.MessageCreate],
213 workspace_name: str,
214 session_name: str,
215 ) -> list[models.Message]:
216 """
217 Bulk create messages for a session while maintaining order.
218
219 Args:
220 db: Database session
221 messages: List of messages to create
222 workspace_name: Name of the workspace
223 session_name: Name of the session to create messages in
224
225 Returns:
226 List of created message objects
227 """
228 # Get or create session with peers in messages list
229 peers = {message.peer_name: schemas.SessionPeerConfig() for message in messages}
230 await get_or_create_session(
231 db,
232 session=schemas.SessionCreate(name=session_name, peers=peers),
233 workspace_name=workspace_name,
234 )
235
236 await db.execute(text("SET LOCAL lock_timeout = '5s'"))
237 await db.execute(
238 text(
239 "SELECT pg_advisory_xact_lock(hashtext(:workspace_name), hashtext(:session_name))"
240 ),
241 {"workspace_name": workspace_name, "session_name": session_name},
242 )
243
244 # Get the last sequence number on a session - uses (workspace_name, session_name, seq_in_session) index
245 last_seq = (
246 await db.scalar(
247 select(models.Message.seq_in_session)
248 .where(
249 models.Message.workspace_name == workspace_name,
250 models.Message.session_name == session_name,
251 )
252 .order_by(models.Message.seq_in_session.desc())
253 .limit(1)
254 )
255 or 0
256 )
257
258 # Create list of message objects (this will trigger the before_insert event)
259 message_objects: list[models.Message] = []
260 for offset, message in enumerate(messages, start=1):
>nl -ba src/crud/message.py | sed -n '260,430p'Chunk ID: e86182
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2196
Output:
260 for offset, message in enumerate(messages, start=1):
261 message_seq_in_session = last_seq + offset
262 message_obj = models.Message(
263 session_name=session_name,
264 peer_name=message.peer_name,
265 content=message.content,
266 h_metadata=message.metadata or {},
267 workspace_name=workspace_name,
268 public_id=generate_nanoid(),
269 token_count=[REDACTED]
270 created_at=message.created_at, # Use provided created_at if available
271 seq_in_session=message_seq_in_session,
272 )
273 message_objects.append(message_obj)
274
275 db.add_all(message_objects)
276
277 # Commit here to release the advisory lock before generating embeddings
278 await db.commit()
279 try:
280 if settings.EMBED_MESSAGES:
281 id_resource_dict = {
282 message.public_id: message.content
283 for message in message_objects
284 if message.content and message.content.strip()
285 }
286 embedding_dict = (
287 await embedding_client.batch_embed(id_resource_dict)
288 if id_resource_dict
289 else {}
290 )
291
292 external_vector_store = get_external_vector_store()
293
294 # Determine if we need to persist embeddings to postgres
295 # True when: TYPE=pgvector OR still migrating (dual-write to both stores)
296 store_embeddings_in_postgres = (
297 settings.VECTOR_STORE.TYPE == "pgvector"
298 or not settings.VECTOR_STORE.MIGRATED
299 )
300
301 # Create MessageEmbedding entries
302 embedding_objects: list[models.MessageEmbedding] = []
303 # Maps emb index -> (chunk_position, embedding vector)
304 pending_embedding_data: dict[int, tuple[int, list[float]]] = {}
305 for message_obj in message_objects:
306 embeddings = embedding_dict.get(message_obj.public_id, [])
307 for chunk_position, embedding in enumerate(embeddings):
308 embedding_obj = models.MessageEmbedding(
309 content=message_obj.content,
310 message_id=message_obj.public_id,
311 workspace_name=workspace_name,
312 session_name=session_name,
313 peer_name=message_obj.peer_name,
314 sync_state="pending",
315 embedding=embedding if store_embeddings_in_postgres else None,
316 )
317 emb_idx = len(embedding_objects)
318 pending_embedding_data[emb_idx] = (chunk_position, embedding)
319 embedding_objects.append(embedding_obj)
320
321 # Always create MessageEmbedding rows so reconciliation can track sync state
322 # even when embeddings aren't stored in postgres
323 embedding_ids: list[int] = []
324 if embedding_objects:
325 db.add_all(embedding_objects)
326 await db.flush()
327 embedding_ids = [emb.id for emb in embedding_objects]
328
329 await db.commit()
330
331 # If no external vector store (pgvector-only mode), mark as synced immediately
332 if external_vector_store is None:
333 if embedding_ids:
334 await db.execute(
335 update(models.MessageEmbedding)
336 .where(models.MessageEmbedding.id.in_(embedding_ids))
337 .values(
338 sync_state="synced",
339 last_sync_at=func.now(),
340 sync_attempts=0,
341 )
342 )
343 await db.commit()
344 else:
345 # External vector store - build and upsert vector records
346 namespace = external_vector_store.get_vector_namespace(
347 "message", workspace_name
348 )
349
350 # Build vector records with {message_id}_{chunk_position} as vector ID
351 vector_records: list[VectorRecord] = []
352 for emb_idx, emb in enumerate(embedding_objects):
353 chunk_position, embedding = pending_embedding_data[emb_idx]
354 vector_id = f"{emb.message_id}_{chunk_position}"
355 vector_records.append(
356 VectorRecord(
357 id=vector_id,
358 embedding=list(embedding),
359 metadata={
360 "message_id": emb.message_id,
361 "session_name": emb.session_name,
362 "peer_name": emb.peer_name,
363 },
364 )
365 )
366
367 # Upsert to external vector store and update sync state
368 if vector_records:
369 try:
370 await external_vector_store.upsert_many(
371 namespace, vector_records
372 )
373 # Success: mark as synced if we have DB rows
374 if embedding_ids:
375 await db.execute(
376 update(models.MessageEmbedding)
377 .where(models.MessageEmbedding.id.in_(embedding_ids))
378 .values(
379 sync_state="synced",
380 last_sync_at=func.now(),
381 sync_attempts=0,
382 )
383 )
384 await db.commit()
385
386 except VectorStoreError:
387 logger.warning(
388 "Vector store unavailable; leaving message vectors unsynced"
389 )
390 if embedding_ids:
391 await db.execute(
392 update(models.MessageEmbedding)
393 .where(models.MessageEmbedding.id.in_(embedding_ids))
394 .values(
395 sync_attempts=models.MessageEmbedding.sync_attempts
396 + 1,
397 last_sync_at=func.now(),
398 )
399 )
400 await db.commit()
401
402 except Exception:
403 logger.exception("Unexpected error upserting message vectors")
404 if embedding_ids:
405 await db.execute(
406 update(models.MessageEmbedding)
407 .where(models.MessageEmbedding.id.in_(embedding_ids))
408 .values(
409 sync_attempts=models.MessageEmbedding.sync_attempts
410 + 1,
411 last_sync_at=func.now(),
412 )
413 )
414 await db.commit()
415
416 except Exception:
417 logger.exception(
418 "Failed to generate message embeddings for %s messages in workspace %s and session %s.",
419 len(message_objects),
420 workspace_name,
421 session_name,
422 )
423
424 return message_objects
425
426
427 async def get_messages(
428 workspace_name: str,
429 session_name: str,
430 reverse: bool | None = False,
Honcho’s core model is a pairwise memory system layered on top of ordinary chat data.
At the bottom:
Workspace: tenant/container for everything.Peer: any participant, human or agent. Peers are unique by (workspace_name, name).Session: a conversation context inside a workspace.session_peers: many-to-many membership table, with per-peer session config like observe_me / observe_others.Message: raw utterance from a peer in a session, ordered by seq_in_session.MessageEmbedding: vector-search rows for raw messages, used when agents need to search original conversation text.See src/models.py, src/models.py, src/models.py, src/models.py, src/models.py, and src/models.py.
The memory layer is where Honcho becomes unusual:
Collection: one memory collection per (workspace, observer, observed).Document: one persisted observation/conclusion inside that pairwise collection.Document.level: explicit, deductive, inductive, or contradiction.Document.source_ids: links higher-level observations back to source observations, giving Honcho a traversable reasoning graph.Document.embedding: vector index for semantic memory search.That means Alice’s model of Bob is separate from Bob’s model of Bob, and separate from Carol’s model of Bob. The schema enforces that with the observer, observed, and workspace_name keys on collections and documents; see src/models.py and src/models.py.
A Representation is the in-memory view over those documents. It is not a table. It groups observations into four levels:
explicit: facts directly stated.deductive: logical implications.inductive: patterns/generalizations.contradiction: conflicting claims.That shape is defined in src/utils/representation.py.
Message Flow
When a message is created, Honcho stores the raw message, optionally embeds it, then enqueues background work. Message creation creates MessageEmbedding rows if EMBED_MESSAGES is enabled; see src/crud/message.py.
Then enqueueing decides what inference tasks are needed:
observe_me and observe_others.That logic is in src/deriver/enqueue.py. A message from peer observed may produce a self-observation collection (observed -> observed) and also other peer-perspective collections like (assistant -> observed) if session config says those peers observe others.
What Inference Is For Honcho uses inference for several distinct jobs because each job produces a different kind of artifact.
Raw messages are unstructured. The deriver asks an LLM to turn recent messages into structured explicit facts. It uses a structured PromptRepresentation whose only field is explicit, then saves those observations into every relevant observer/observed collection. See src/deriver/deriver.py and src/deriver/deriver.py.
This is inference, but constrained: “facts literally stated,” not broader reasoning.
Honcho embeds messages and observations so agents can do semantic search over raw conversation and memory. This is not “reasoning” in the same sense as the LLM deriver, but it is still model inference. Observation embeddings are created when representations are saved; see src/crud/representation.py.
Summaries are stored in Session.internal_metadata, not as first-class rows. Honcho creates short and long rolling summaries to keep session context bounded while preserving key facts. The prompts explicitly ask for factual summaries, preferences, context, and, for long summaries, emotional state/personality themes. See src/utils/summarizer.py and src/utils/summarizer.py.
The Dreamer runs later over accumulated observations. It has two specialist agents:
The orchestrator runs deduction then induction; see src/dreamer/orchestrator.py. The specialist prompts make the facets explicit in src/dreamer/specialists.py and src/dreamer/specialists.py.
A peer card is a compact durable profile stored on the observing peer’s internal_metadata, keyed by the observed peer. For self-observation it uses peer_card; for another peer it uses {observed}_peer_card. See src/crud/peer_card.py and src/crud/peer_card.py.
This is another “facet” because it is not the full memory graph. It is a small, durable profile cache: name, preferences, stable traits, standing instructions.
The Dialectic API is inference at query time. It prefetches semantically relevant explicit and derived observations, injects recent session history, then lets an LLM use tools to search memory/messages and synthesize an answer. See src/dialectic/core.py and src/dialectic/core.py.
So the short answer is: Honcho needs inference because its product is not “chat storage.” It is trying to maintain a layered, perspective-specific model of people. The different inference requests correspond to different compression/abstraction layers: raw messages -> explicit observations -> deductions/contradictions -> inductive patterns -> peer cards -> query-time dialectic answers.
I would like to visualize this
Here are two useful views.
Data Model
Inference Flow
Mental Model
The key visual idea: Message is the event log, Document is the memory graph, and (observer, observed) is the lens that keeps one peer’s model of another peer separate from everyone else’s.