Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,15 @@ HINDSIGHT_API_LOG_LEVEL=info
# Cross-encoder rerank of the fused candidates (false = use the RRF order):
# HINDSIGHT_API_ENABLE_RERANKING=true

# Mental model refresh: let a delta refresh skip the agentic reflect loop. It
# reads the memories created since the last refresh first — an empty window
# preserves the document with no LLM call, a non-empty one becomes edit
# operations in a single call — and hands back to the loop whenever a surgical
# edit is not obviously safe, so the same outcomes stay reachable either way.
# Hierarchical, and overridable per mental model via trigger.delta_fast_path.
# Set false to always run the loop.
# HINDSIGHT_API_MENTAL_MODEL_DELTA_FAST_PATH=true

# Reranker Configuration (Optional - uses local by default)
# Provider: "local" (default) or "tei" (HuggingFace Text Embeddings Inference)
# HINDSIGHT_API_RERANKER_PROVIDER=local
Expand Down
43 changes: 41 additions & 2 deletions hindsight-api-slim/hindsight_api/api/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
RecallScores,
TokenUsage,
)
from hindsight_api.engine.search.tags import TagGroup, TagsMatch
from hindsight_api.engine.search.tags import TagGroup, TagsMatch, validate_entity_leaf_placement
from hindsight_api.engine.structured_output import validate_response_schema
from hindsight_api.extensions import HttpExtension, OperationValidationError, load_extension
from hindsight_api.liveness import LivenessResponse, liveness_response
Expand Down Expand Up @@ -366,6 +366,13 @@ def validate_tags_exclusive(self) -> "RecallRequest":
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
return self

@field_validator("tag_groups")
@classmethod
def validate_recall_entity_leaf_placement(cls, v: list[TagGroup] | None) -> list[TagGroup] | None:
# Entity leaves may not sit under 'not' -- see validate_entity_leaf_placement.
validate_entity_leaf_placement(v)
return v


class RecallResult(BaseModel):
"""Single recall result item."""
Expand Down Expand Up @@ -1030,6 +1037,13 @@ def validate_tags_exclusive(self) -> "ReflectRequest":
raise ValueError("'tags' and 'tag_groups' are mutually exclusive. Use 'tag_groups' for compound filtering.")
return self

@field_validator("tag_groups")
@classmethod
def validate_reflect_entity_leaf_placement(cls, v: list[TagGroup] | None) -> list[TagGroup] | None:
# Entity leaves may not sit under 'not' -- see validate_entity_leaf_placement.
validate_entity_leaf_placement(v)
return v


class ReflectFact(BaseModel):
"""A fact used in think response."""
Expand Down Expand Up @@ -2196,9 +2210,22 @@ class MentalModelTrigger(BaseModel):
description=(
"Compound boolean tag expressions to use during refresh instead of the model's own tags. "
"When set, these tag groups are passed to reflect and the model's flat tags are NOT used for filtering. "
"Supports nested and/or/not expressions for complex tag-based scoping."
"Supports nested and/or/not expressions for complex tag-based scoping, plus entity leaves "
"({entities: [names], match: any|all}) that scope by what a memory is ABOUT rather than which "
"tag compartment it lives in -- matched case-insensitively against canonical entity names, "
"including entities reached through an observation's source memories. The same expressions "
"drive the staleness gate, so an entity-scoped model refreshes exactly when facts about its "
"entities arrive. Entity leaves may not appear under 'not'."
),
)

@field_validator("tag_groups")
@classmethod
def validate_trigger_entity_leaf_placement(cls, v: list[TagGroup] | None) -> list[TagGroup] | None:
# Entity leaves may not sit under 'not' -- see validate_entity_leaf_placement.
validate_entity_leaf_placement(v)
return v

include_chunks: bool | None = Field(
default=None,
description=(
Expand All @@ -2220,6 +2247,18 @@ class MentalModelTrigger(BaseModel):
"None means use the bank/global config default (recall_chunks_max_tokens)."
),
)
delta_fast_path: bool | None = Field(
default=None,
description=(
"Override whether a delta refresh may take the deterministic fast path: fetch the "
"memories created since the last refresh and, if there are any, turn them into edit "
"operations with a single LLM call instead of running the agentic reflect loop. An "
"empty window costs no LLM call at all. The fast path hands back to the loop whenever "
"a surgical edit is not obviously safe, so every outcome is preserved either way. "
"None means use the bank/global config default (mental_model_delta_fast_path). "
"Ignored in full mode, which never takes the fast path."
),
)
response_schema: dict | None = Field(
default=None,
description=(
Expand Down
18 changes: 18 additions & 0 deletions hindsight-api-slim/hindsight_api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,9 @@ def _parse_worker_slot_reservations() -> dict[str, int]:
ENV_RECALL_MAX_TOKENS = "HINDSIGHT_API_RECALL_MAX_TOKENS"
ENV_RECALL_CHUNKS_MAX_TOKENS = "HINDSIGHT_API_RECALL_CHUNKS_MAX_TOKENS"

# Mental model refresh
ENV_MENTAL_MODEL_DELTA_FAST_PATH = "HINDSIGHT_API_MENTAL_MODEL_DELTA_FAST_PATH"

# Recall pipeline stages. Each arm of recall costs latency, and a bank whose
# content has no temporal or relational structure pays for stages it cannot use
# (e.g. a chunk-extraction bank used as plain retrieval). These switch the
Expand Down Expand Up @@ -1336,6 +1339,12 @@ def _parse_strategy_boosts(raw: str | None) -> dict[str, str]:
DEFAULT_RECALL_MAX_TOKENS = 2048 # Token budget for facts returned by internal recall
DEFAULT_RECALL_CHUNKS_MAX_TOKENS = 1000 # Token budget for raw chunks returned by internal recall

# Mental model refresh: run the deterministic delta fast path before the agentic
# reflect loop. On by default — the fast path preserves every outcome and hands
# back to the loop on any doubt, so the loop still produces the result whenever
# a surgical edit is not obviously safe. Set false to always run the loop.
DEFAULT_MENTAL_MODEL_DELTA_FAST_PATH = True

# Recall pipeline stages — all on by default, so recall behaviour is unchanged
# unless a bank opts out.
DEFAULT_ENABLE_TEMPORAL_RETRIEVAL = True # Temporal retrieval arm + the date-aware query analysis feeding it
Expand Down Expand Up @@ -2472,6 +2481,9 @@ class HindsightConfig:
recall_max_tokens: int
recall_chunks_max_tokens: int

# Mental model refresh: deterministic delta fast path before the agentic loop
mental_model_delta_fast_path: bool

# Recall budget mapping: how the Budget enum (LOW/MID/HIGH) maps to thinking_budget integer.
# function="fixed": use the recall_budget_fixed_* values directly (legacy behavior).
# function="adaptive": compute round(max_tokens * recall_budget_adaptive_*),
Expand Down Expand Up @@ -2723,6 +2735,8 @@ class HindsightConfig:
"recall_include_chunks",
"recall_max_tokens",
"recall_chunks_max_tokens",
# Mental model refresh (per-model override lives on trigger.delta_fast_path)
"mental_model_delta_fast_path",
# Recall budget mapping (Budget enum -> thinking_budget integer)
"recall_budget_function",
"recall_budget_fixed_low",
Expand Down Expand Up @@ -3814,6 +3828,10 @@ def from_env(cls) -> "HindsightConfig":
recall_chunks_max_tokens=int(
os.getenv(ENV_RECALL_CHUNKS_MAX_TOKENS, str(DEFAULT_RECALL_CHUNKS_MAX_TOKENS))
),
mental_model_delta_fast_path=os.getenv(
ENV_MENTAL_MODEL_DELTA_FAST_PATH, str(DEFAULT_MENTAL_MODEL_DELTA_FAST_PATH)
).lower()
in ("true", "1", "yes"),
recall_budget_function=_validate_recall_budget_function(
os.getenv(ENV_RECALL_BUDGET_FUNCTION, DEFAULT_RECALL_BUDGET_FUNCTION)
),
Expand Down
Loading