Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 8d5972fe36 test(typescript-client): increase Jest timeout to 120s for LLM-dependent tests 2026-02-23 17:46:39 +01:00
Nicolò Boschi 5d7d7095a3 fix(ci): fix test_hierarchical_fields count and enable bank config API for doc examples
- Update test_hierarchical_fields_categorization to expect 10 configurable fields
  (reflect_mission + 3 disposition fields were added as hierarchical)
- Add HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true to test-doc-examples .env
  so update_bank_config calls in doc examples work without 404
2026-02-23 17:44:46 +01:00
Nicolò Boschi 7eb0728411 fix: add reflect_mission as proper field in CreateBankRequest, fix CI failures
- Add reflect_mission as non-deprecated field to CreateBankRequest (mission/background remain deprecated aliases)
- Fix priority order in get_config_updates(): reflect_mission > mission > background
- Fix Rust CLI CreateBankRequest struct inits with ..Default::default() (fixes test-rust-cli)
- Fix _acreate_bank to return BankProfileResponse instead of raw dict (fixes test-hindsight-all)
- Fix body mapping: reflect_mission sent as its own key, not mapped to mission
- Update TypeScript createBank: reflectMission is now non-deprecated primary field
- Regenerate OpenAPI spec and all clients
2026-02-23 17:19:34 +01:00
Nicolò Boschi da7c744c31 feat(config): migrate disposition and mission to hierarchical config, deprecate profile endpoint
- Add disposition_skepticism/literalism/empathy as hierarchical config fields (default None → falls back to DB)
- Deprecate mission/reflect_mission/disposition in CreateBankRequest — use update_bank_config instead
- Deprecate GET/PUT /banks/{bank_id}/profile endpoints in OpenAPI spec
- Update DispositionEditDialog and MissionEditDialog to use updateBankConfig
- Remove bank-mission/bank-with-disposition create_bank examples from docs; show update_bank_config instead
- Flatten Bank Configuration doc section: remove ### Retain/Observations/Reflect groupings, each field is its own ### heading
- Regenerate OpenAPI spec and all clients (Python, TypeScript, Go, Rust)
2026-02-23 17:08:29 +01:00
Nicolò Boschi 5cf15b44a6 feat(ui): split config view into per-operation cards with Reflect section
Each operation (Retain, Observations, Reflect) now gets its own card in
the configuration tab for clear visual separation. The Reflect card shows
mission and disposition traits (1-5 dot picker in edit mode), reading from
the profile API alongside the config API. BankProfileView hides its
Mission/Disposition cards when hideReflectFields=true to avoid duplication.
BankConfigView is now always visible (not gated by bankConfigEnabled).
2026-02-23 14:30:10 +01:00
Nicolò Boschi 9d9df3a903 feat(ui): hide retain_custom_instructions unless extraction mode is custom
Add showWhen conditional visibility so the Custom Extraction Prompt field
only appears when retain_extraction_mode=custom, making the distinction
between retain_spec (Focus) and retain_custom_instructions crystal clear.
2026-02-23 13:09:28 +01:00
Nicolò Boschi 8d4af4e1ed fix(tests): fix CI failures and strengthen test coverage for retain_spec/observations_spec
- Fix test_consolidation_with_observations_spec: use _global_config not _base_config
- Fix test_hierarchical_fields_categorization: update count 4→6 for new fields
- Add test_retain_spec_injected_into_prompt: verify FOCUS section injection
- Add test_retain_spec_absent_when_not_set: verify no pollution when unset
- Add test_retain_spec_config_loaded_from_env: verify env loading + configurable
- Improve docs: add "when to use what" decision table for retain customization
- Improve docs: expand observations_spec with use-case guidance and 3 examples
2026-02-23 12:24:53 +01:00
Nicolò Boschi 023e317d6a refactor(consolidation): replace prompt mode system with retain_spec + observations_spec
- Remove consolidation_prompt_mode and consolidation_custom_instructions config fields
- Add retain_spec: declarative focus injected into any retain extraction mode
- Add observations_spec: drives consolidation prompt, replaces built-in rules when set
- Remove mission from consolidation (now reflect-only)
- Trim default consolidation rules from ~40 lines to 6, remove markdown formatting
- Update UI to group fields by operation with clear descriptions
- Update docs with examples for both new fields
2026-02-23 12:07:59 +01:00
Nicolò Boschi 4d4019da1b feat(consolidation): expose prompt mode as bank config in UI and docs
- Add consolidation_prompt_mode (select) and consolidation_custom_instructions (textarea)
  to the Consolidation Settings section in bank-config-view.tsx
- Add CONSOLIDATION_PROMPT_MODE and CONSOLIDATION_CUSTOM_INSTRUCTIONS to
  configuration.md with prompt mode descriptions and custom mode example
- Update security model docs to list the new configurable fields
2026-02-21 13:44:09 +01:00
Nicolò Boschi 45367eb387 refactor(consolidation): merge into single message, drop basic mode
- Combine system+user into a single user message per prompt
- Remove "basic" mode; "standard" is now the default (was "basic")
- Rename prompts: STANDARD_CONSOLIDATION_PROMPT, CUSTOM_CONSOLIDATION_PROMPT
  (drop BASIC/SYSTEM/USER suffixes since there's one prompt per mode)
- Shared _DATA_SECTION and _OUTPUT_FORMAT templates composed into each prompt
2026-02-21 10:42:22 +01:00
Nicolò Boschi 96337e8eb8 feat(consolidation): add prompt modes (basic/standard/custom) following retain pattern
- Add consolidation_prompt_mode config flag (basic/standard/custom) with bank-level override support
- Add consolidation_custom_instructions config flag for custom mode
- Refactor prompts.py: BASIC_CONSOLIDATION_SYSTEM_PROMPT (minimal, new default),
  STANDARD_CONSOLIDATION_SYSTEM_PROMPT (original detailed prompt), and
  CUSTOM_CONSOLIDATION_SYSTEM_PROMPT (template with {custom_instructions})
- Pass resolved config through _process_memory to _consolidate_with_llm for prompt selection
- Add unit tests for prompt selection and config validation
- Add integration test for custom consolidation prompt mode
2026-02-21 09:50:10 +01:00
39 changed files with 2724 additions and 936 deletions
+1
View File
@@ -1332,6 +1332,7 @@ jobs:
HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }}
HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/tmp/gcp-credentials.json
HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=$HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID
HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true
EOF
- name: Start API server
+107 -26
View File
@@ -879,18 +879,103 @@ class CreateBankRequest(BaseModel):
model_config = ConfigDict(
json_schema_extra={
"example": {
"name": "Alice",
"disposition": {"skepticism": 3, "literalism": 3, "empathy": 3},
"mission": "I am a PM helping my engineering team stay organized",
"retain_mission": "Always include technical decisions and architectural trade-offs. Ignore meeting logistics.",
"observations_mission": "Observations are stable facts about people and projects. Always include preferences and skills.",
}
}
)
name: str | None = None
disposition: DispositionTraits | None = None
mission: str | None = Field(default=None, description="The agent's mission")
# Deprecated: use mission instead
background: str | None = Field(default=None, description="Deprecated: use mission instead")
# Deprecated fields — kept for backwards compatibility only
name: str | None = Field(default=None, description="Deprecated: display label only, not advertised")
disposition: DispositionTraits | None = Field(
default=None, description="Deprecated: use update_bank_config instead"
)
disposition_skepticism: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
disposition_literalism: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
disposition_empathy: int | None = Field(
default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead"
)
# Deprecated: use update_bank_config with reflect_mission instead
mission: str | None = Field(
default=None, description="Deprecated: use update_bank_config with reflect_mission instead"
)
# Deprecated alias for mission
background: str | None = Field(
default=None, description="Deprecated: use update_bank_config with reflect_mission instead"
)
# Reflect configuration
reflect_mission: str | None = Field(
default=None,
description="Mission/context for Reflect operations. Guides how Reflect interprets and uses memories.",
)
# Operational configuration (applied via config resolver)
retain_mission: str | None = Field(
default=None,
description="Steers what gets extracted during retain(). Injected alongside built-in extraction rules.",
)
retain_extraction_mode: str | None = Field(
default=None,
description="Fact extraction mode: 'concise' (default), 'verbose', or 'custom'.",
)
retain_custom_instructions: str | None = Field(
default=None,
description="Custom extraction prompt. Only active when retain_extraction_mode is 'custom'.",
)
retain_chunk_size: int | None = Field(
default=None,
description="Maximum token size for each content chunk during retain.",
)
enable_observations: bool | None = Field(
default=None,
description="Toggle automatic observation consolidation after retain().",
)
observations_mission: str | None = Field(
default=None,
description="Controls what gets synthesised into observations. Replaces built-in consolidation rules entirely.",
)
def get_config_updates(self) -> dict[str, Any]:
"""Return only the config fields that were explicitly set.
reflect_mission takes precedence over deprecated mission/background aliases.
Individual disposition_* fields take priority over the deprecated disposition dict.
"""
updates: dict[str, Any] = {}
# Resolve reflect mission: reflect_mission (new) > mission (deprecated) > background (deprecated)
resolved_reflect_mission = self.reflect_mission or self.mission or self.background
if resolved_reflect_mission is not None:
updates["reflect_mission"] = resolved_reflect_mission
# Disposition: individual fields take priority over legacy disposition dict
if self.disposition_skepticism is not None:
updates["disposition_skepticism"] = self.disposition_skepticism
elif self.disposition is not None:
updates["disposition_skepticism"] = self.disposition.skepticism
if self.disposition_literalism is not None:
updates["disposition_literalism"] = self.disposition_literalism
elif self.disposition is not None:
updates["disposition_literalism"] = self.disposition.literalism
if self.disposition_empathy is not None:
updates["disposition_empathy"] = self.disposition_empathy
elif self.disposition is not None:
updates["disposition_empathy"] = self.disposition.empathy
for field_name in (
"retain_mission",
"retain_extraction_mode",
"retain_custom_instructions",
"retain_chunk_size",
"enable_observations",
"observations_mission",
):
value = getattr(self, field_name)
if value is not None:
updates[field_name] = value
return updates
class BankConfigUpdate(BaseModel):
@@ -3203,6 +3288,7 @@ def _register_routes(app: FastAPI):
description="Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.",
operation_id="get_bank_profile",
tags=["Banks"],
deprecated=True,
)
async def api_get_bank_profile(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
"""Get memory bank profile (disposition + mission)."""
@@ -3238,6 +3324,7 @@ def _register_routes(app: FastAPI):
description="Update bank's disposition traits (skepticism, literalism, empathy)",
operation_id="update_bank_disposition",
tags=["Banks"],
deprecated=True,
)
async def api_update_bank_disposition(
bank_id: str, request: UpdateDispositionRequest, request_context: RequestContext = Depends(get_request_context)
@@ -3317,21 +3404,18 @@ def _register_routes(app: FastAPI):
# Ensure bank exists by getting profile (auto-creates with defaults)
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
# Update name and/or mission if provided (support both mission and deprecated background)
mission_value = request.mission or request.background
if request.name is not None or mission_value is not None:
# Update name if provided (stored in DB for display only, deprecated)
if request.name is not None:
await app.state.memory.update_bank(
bank_id,
name=request.name,
mission=mission_value,
request_context=request_context,
)
# Update disposition if provided
if request.disposition is not None:
await app.state.memory.update_bank_disposition(
bank_id, request.disposition.model_dump(), request_context=request_context
)
# Apply all config overrides (includes reflect_mission, disposition, retain settings)
config_updates = request.get_config_updates()
if config_updates:
await app.state.memory._config_resolver.update_bank_config(bank_id, config_updates, request_context)
# Get final profile
final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
@@ -3373,21 +3457,18 @@ def _register_routes(app: FastAPI):
# Ensure bank exists
await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
# Update name and/or mission if provided
mission_value = request.mission or request.background
if request.name is not None or mission_value is not None:
# Update name if provided (stored in DB for display only, deprecated)
if request.name is not None:
await app.state.memory.update_bank(
bank_id,
name=request.name,
mission=mission_value,
request_context=request_context,
)
# Update disposition if provided
if request.disposition is not None:
await app.state.memory.update_bank_disposition(
bank_id, request.disposition.model_dump(), request_context=request_context
)
# Apply all config overrides (includes reflect_mission, disposition, retain settings)
config_updates = request.get_config_updates()
if config_updates:
await app.state.memory._config_resolver.update_bank_config(bank_id, config_updates, request_context)
# Get final profile
final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context)
+46
View File
@@ -252,6 +252,7 @@ ENV_RETAIN_MAX_COMPLETION_TOKENS = "HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS"
ENV_RETAIN_CHUNK_SIZE = "HINDSIGHT_API_RETAIN_CHUNK_SIZE"
ENV_RETAIN_EXTRACT_CAUSAL_LINKS = "HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS"
ENV_RETAIN_EXTRACTION_MODE = "HINDSIGHT_API_RETAIN_EXTRACTION_MODE"
ENV_RETAIN_MISSION = "HINDSIGHT_API_RETAIN_MISSION"
ENV_RETAIN_CUSTOM_INSTRUCTIONS = "HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS"
ENV_RETAIN_BATCH_TOKENS = "HINDSIGHT_API_RETAIN_BATCH_TOKENS"
ENV_RETAIN_BATCH_ENABLED = "HINDSIGHT_API_RETAIN_BATCH_ENABLED"
@@ -281,6 +282,7 @@ ENV_FILE_DELETE_AFTER_RETAIN = "HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN"
ENV_ENABLE_OBSERVATIONS = "HINDSIGHT_API_ENABLE_OBSERVATIONS"
ENV_CONSOLIDATION_BATCH_SIZE = "HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE"
ENV_CONSOLIDATION_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS"
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
# Optimization flags
ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION"
@@ -306,6 +308,12 @@ ENV_WORKER_CONSOLIDATION_MAX_SLOTS = "HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLO
# Reflect agent settings
ENV_REFLECT_MAX_ITERATIONS = "HINDSIGHT_API_REFLECT_MAX_ITERATIONS"
ENV_REFLECT_MISSION = "HINDSIGHT_API_REFLECT_MISSION"
# Disposition settings
ENV_DISPOSITION_SKEPTICISM = "HINDSIGHT_API_DISPOSITION_SKEPTICISM"
ENV_DISPOSITION_LITERALISM = "HINDSIGHT_API_DISPOSITION_LITERALISM"
ENV_DISPOSITION_EMPATHY = "HINDSIGHT_API_DISPOSITION_EMPATHY"
# Default values
DEFAULT_DATABASE_URL = "pg0"
@@ -395,6 +403,7 @@ DEFAULT_RETAIN_CHUNK_SIZE = 3000 # Max chars per chunk for fact extraction
DEFAULT_RETAIN_EXTRACT_CAUSAL_LINKS = True # Extract causal links between facts
DEFAULT_RETAIN_EXTRACTION_MODE = "concise" # Extraction mode: "concise", "verbose", or "custom"
RETAIN_EXTRACTION_MODES = ("concise", "verbose", "custom") # Allowed extraction modes
DEFAULT_RETAIN_MISSION = None # Declarative spec of what to retain (injected into any extraction mode)
DEFAULT_RETAIN_CUSTOM_INSTRUCTIONS = None # Custom extraction guidelines (only used when mode="custom")
DEFAULT_RETAIN_BATCH_TOKENS = 10_000 # ~40KB of text # Max chars per sub-batch for async retain auto-splitting
DEFAULT_RETAIN_BATCH_ENABLED = False # Use LLM Batch API for fact extraction (only when async=True)
@@ -412,6 +421,7 @@ DEFAULT_FILE_DELETE_AFTER_RETAIN = True # Delete file bytes after retain (saves
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
DEFAULT_CONSOLIDATION_MAX_TOKENS = 1024 # Max tokens for recall when finding related observations
DEFAULT_OBSERVATIONS_MISSION = None # Declarative spec of what observations are for this bank
# Database migrations
DEFAULT_RUN_MIGRATIONS_ON_STARTUP = True
@@ -434,6 +444,11 @@ DEFAULT_WORKER_CONSOLIDATION_MAX_SLOTS = 2 # Max concurrent consolidation tasks
# Reflect agent settings
DEFAULT_REFLECT_MAX_ITERATIONS = 10 # Max tool call iterations before forcing response
# Disposition defaults (None = not set, fall back to bank DB value or 3)
DEFAULT_DISPOSITION_SKEPTICISM = None
DEFAULT_DISPOSITION_LITERALISM = None
DEFAULT_DISPOSITION_EMPATHY = None
# OpenTelemetry tracing configuration
DEFAULT_OTEL_TRACES_ENABLED = False # Disabled by default for backward compatibility
DEFAULT_OTEL_SERVICE_NAME = "hindsight-api"
@@ -627,6 +642,7 @@ class HindsightConfig:
retain_chunk_size: int
retain_extract_causal_links: bool
retain_extraction_mode: str
retain_mission: str | None
retain_custom_instructions: str | None
retain_batch_tokens: int
retain_batch_enabled: bool
@@ -656,6 +672,15 @@ class HindsightConfig:
enable_observations: bool
consolidation_batch_size: int
consolidation_max_tokens: int
observations_mission: str | None
# Reflect agent settings
reflect_mission: str | None
# Disposition settings (hierarchical - can be overridden per bank; None = fall back to DB)
disposition_skepticism: int | None
disposition_literalism: int | None
disposition_empathy: int | None
# Optimization flags
skip_llm_verification: bool
@@ -724,9 +749,17 @@ class HindsightConfig:
# Retention settings (behavioral)
"retain_chunk_size",
"retain_extraction_mode",
"retain_mission",
"retain_custom_instructions",
# Consolidation settings
"enable_observations",
"observations_mission",
# Reflect settings
"reflect_mission",
# Disposition settings
"disposition_skepticism",
"disposition_literalism",
"disposition_empathy",
}
@property
@@ -1013,6 +1046,7 @@ class HindsightConfig:
retain_extraction_mode=_validate_extraction_mode(
os.getenv(ENV_RETAIN_EXTRACTION_MODE, DEFAULT_RETAIN_EXTRACTION_MODE)
),
retain_mission=os.getenv(ENV_RETAIN_MISSION) or DEFAULT_RETAIN_MISSION,
retain_custom_instructions=os.getenv(ENV_RETAIN_CUSTOM_INSTRUCTIONS) or DEFAULT_RETAIN_CUSTOM_INSTRUCTIONS,
retain_batch_tokens=int(os.getenv(ENV_RETAIN_BATCH_TOKENS, str(DEFAULT_RETAIN_BATCH_TOKENS))),
retain_batch_enabled=os.getenv(ENV_RETAIN_BATCH_ENABLED, str(DEFAULT_RETAIN_BATCH_ENABLED)).lower()
@@ -1055,6 +1089,7 @@ class HindsightConfig:
consolidation_max_tokens=int(
os.getenv(ENV_CONSOLIDATION_MAX_TOKENS, str(DEFAULT_CONSOLIDATION_MAX_TOKENS))
),
observations_mission=os.getenv(ENV_OBSERVATIONS_MISSION) or DEFAULT_OBSERVATIONS_MISSION,
# Database migrations
run_migrations_on_startup=os.getenv(ENV_RUN_MIGRATIONS_ON_STARTUP, "true").lower() == "true",
# Database connection pool
@@ -1074,6 +1109,17 @@ class HindsightConfig:
),
# Reflect agent settings
reflect_max_iterations=int(os.getenv(ENV_REFLECT_MAX_ITERATIONS, str(DEFAULT_REFLECT_MAX_ITERATIONS))),
reflect_mission=os.getenv(ENV_REFLECT_MISSION) or None,
# Disposition settings (None = fall back to DB value)
disposition_skepticism=int(os.getenv(ENV_DISPOSITION_SKEPTICISM))
if os.getenv(ENV_DISPOSITION_SKEPTICISM)
else DEFAULT_DISPOSITION_SKEPTICISM,
disposition_literalism=int(os.getenv(ENV_DISPOSITION_LITERALISM))
if os.getenv(ENV_DISPOSITION_LITERALISM)
else DEFAULT_DISPOSITION_LITERALISM,
disposition_empathy=int(os.getenv(ENV_DISPOSITION_EMPATHY))
if os.getenv(ENV_DISPOSITION_EMPATHY)
else DEFAULT_DISPOSITION_EMPATHY,
# OpenTelemetry tracing configuration
otel_traces_enabled=os.getenv(ENV_OTEL_TRACES_ENABLED, str(DEFAULT_OTEL_TRACES_ENABLED)).lower()
in ("true", "1", "yes"),
@@ -23,10 +23,7 @@ from pydantic import BaseModel
from ...config import get_config
from ..memory_engine import fq_table
from ..retain import embedding_utils
from .prompts import (
CONSOLIDATION_SYSTEM_PROMPT,
CONSOLIDATION_USER_PROMPT,
)
from .prompts import build_consolidation_prompt
if TYPE_CHECKING:
from asyncpg import Connection
@@ -114,7 +111,7 @@ async def run_consolidation_job(
t0 = time.time()
bank_row = await conn.fetchrow(
f"""
SELECT bank_id, name, mission
SELECT bank_id, name
FROM {fq_table("banks")}
WHERE bank_id = $1
""",
@@ -125,7 +122,6 @@ async def run_consolidation_job(
logger.warning(f"Bank {bank_id} not found for consolidation")
return {"status": "bank_not_found", "bank_id": bank_id}
mission = bank_row["mission"] or "General memory consolidation"
perf.record_timing("fetch_bank", time.time() - t0)
# Count total unconsolidated memories for progress logging
@@ -205,9 +201,9 @@ async def run_consolidation_job(
memory_engine=memory_engine,
bank_id=bank_id,
memory=dict(memory),
mission=mission,
request_context=request_context,
perf=perf,
config=config,
)
# Mark memory as consolidated (committed immediately)
@@ -420,9 +416,9 @@ async def _process_memory(
memory_engine: "MemoryEngine",
bank_id: str,
memory: dict[str, Any],
mission: str,
request_context: "RequestContext",
perf: ConsolidationPerfLog | None = None,
config: Any = None,
) -> dict[str, Any]:
"""
Process a single memory for consolidation using a SINGLE LLM call.
@@ -476,7 +472,7 @@ async def _process_memory(
memory_engine=memory_engine,
fact_text=fact_text,
recall_result=recall_result,
mission=mission,
config=config,
)
if perf:
perf.record_timing("llm", time.time() - t0)
@@ -830,7 +826,7 @@ async def _consolidate_with_llm(
memory_engine: "MemoryEngine",
fact_text: str,
recall_result: "RecallResult",
mission: str,
config: Any = None,
) -> list[dict[str, Any]]:
"""
Single LLM call to extract durable knowledge and decide on consolidation actions.
@@ -859,24 +855,15 @@ async def _consolidate_with_llm(
else:
observations_text = "[]"
# Only include mission section if mission is set and not the default
mission_section = ""
if mission and mission != "General memory consolidation":
mission_section = f"""
MISSION CONTEXT: {mission}
Focus on DURABLE knowledge that serves this mission, not ephemeral state.
"""
user_prompt = CONSOLIDATION_USER_PROMPT.format(
mission_section=mission_section,
observations_mission = config.observations_mission if config is not None else None
prompt_template = build_consolidation_prompt(observations_mission)
prompt = prompt_template.format(
fact_text=fact_text,
observations_text=observations_text,
)
messages = [
{"role": "system", "content": CONSOLIDATION_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
{"role": "user", "content": prompt},
]
response: _ConsolidationResponse = await memory_engine._consolidation_llm_config.call(
@@ -1,53 +1,18 @@
"""Prompts for the consolidation engine."""
CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (observations) and merge with existing knowledge when appropriate.
# Output format instructions
_OUTPUT_FORMAT = """
Output a JSON object with an "actions" array:
{{"actions": [
{{"action": "update", "learning_id": "uuid-from-observations", "text": "...", "reason": "..."}},
{{"action": "create", "text": "...", "reason": "..."}}
]}}
You must output a JSON object with an "actions" array. The "text" field within each action should use markdown formatting (headers, lists, bold, etc.) for clarity and readability.
Return {{"actions": []}} if the fact contains no durable knowledge.
Do NOT include "tags" in output — tags are handled automatically."""
## EXTRACT DURABLE KNOWLEDGE, NOT EPHEMERAL STATE
Facts often describe events or actions. Extract the DURABLE KNOWLEDGE implied by the fact, not the transient state.
Examples of extracting durable knowledge:
- "User moved to Room 203" -> "Room 203 exists" (location exists, not where user is now)
- "User visited Acme Corp at Room 105" -> "Acme Corp is located in Room 105"
- "User took the elevator to floor 3" -> "Floor 3 is accessible by elevator"
- "User met Sarah at the lobby" -> "Sarah can be found at the lobby"
DO NOT track current user position/state as knowledge - that changes constantly.
DO track permanent facts learned from the user's actions.
## PRESERVE SPECIFIC DETAILS
Keep names, locations, numbers, and other specifics. Do NOT:
- Abstract into general principles
- Generate business insights
- Make knowledge generic
GOOD examples:
- Fact: "John likes pizza" -> "John likes pizza"
- Fact: "Alice works at Google" -> "Alice works at Google"
BAD examples:
- "John likes pizza" -> "Understanding dietary preferences helps..." (TOO ABSTRACT)
- "User is at Room 203" -> "User is currently at Room 203" (EPHEMERAL STATE)
## MERGE RULES (when comparing to existing observations):
1. REDUNDANT: Same information worded differently → update existing
2. CONTRADICTION: Opposite information about same topic → update with temporal markers showing change
Example: "Alex used to love pizza but now hates it" OR "Alex's pizza preference changed from love to hate"
3. UPDATE: New state replacing old state → update showing the transition with "used to", "now", "changed from X to Y"
## CRITICAL RULES:
- NEVER merge facts about DIFFERENT people
- NEVER merge unrelated topics (food preferences vs work vs hobbies)
- When merging contradictions, the "text" field MUST capture BOTH states with temporal markers:
* Use "used to X, now Y" OR "changed from X to Y" OR "X but now Y"
* DO NOT just state the new fact - you MUST show the change
- Keep observations focused on ONE specific topic per person
- The "text" field MUST contain durable knowledge, not ephemeral state
- Do NOT include "tags" in output - tags are handled automatically"""
CONSOLIDATION_USER_PROMPT = """Analyze this new fact and consolidate into knowledge.
{mission_section}
# Data section - holds the dynamic per-call data
_DATA_SECTION = """
NEW FACT: {fact_text}
EXISTING OBSERVATIONS (JSON array with source memories and dates):
@@ -57,28 +22,36 @@ Each observation includes:
- id: unique identifier for updating
- text: the observation content
- proof_count: number of supporting memories
- tags: visibility scope (handled automatically)
- occurred_start/occurred_end: temporal range of source facts
- source_memories: array of supporting facts with their text and dates
Instructions:
1. Extract DURABLE KNOWLEDGE from the new fact (not ephemeral state)
2. Review source_memories in existing observations to understand evidence
3. Check dates to detect contradictions or updates
4. Compare with observations:
- Same topic → UPDATE with learning_id
- New topic → CREATE new observation
- Purely ephemeral → return empty actions list
Compare the new fact against existing observations:
- Same topic → UPDATE with learning_id
- New topic → CREATE new observation
- Purely ephemeral → return empty actions list"""
Output a JSON object with an "actions" array (the "text" field should use markdown formatting for structure):
{{"actions": [
{{"action": "update", "learning_id": "uuid-from-observations", "text": "## Updated Knowledge\n\n**Key point**: details here\n\n- Supporting detail 1\n- Supporting detail 2", "reason": "..."}},
{{"action": "create", "text": "## New Durable Knowledge\n\nDescription with **emphasis** and proper structure", "reason": "..."}}
]}}
# Default rules used when no observations_mission is set
_DEFAULT_RULES = """Extract DURABLE KNOWLEDGE from facts — the stable truth implied by an event, not transient state.
Return {{"actions": []}} if fact contains no durable knowledge.
Example: "User moved to Room 203" → observe "Room 203 exists", not "User is in Room 203".
IMPORTANT: Format the "text" field with markdown for better readability:
- Use headers, lists, bold/italic, tables where appropriate
- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists)
- Ensure proper spacing for markdown to render correctly"""
Rules:
- Keep specifics: names, numbers, locations. Never abstract into general principles.
- NEVER merge observations about different people or unrelated topics.
- REDUNDANT: same info worded differently → update existing.
- CONTRADICTION/UPDATE: capture both states with temporal markers ("used to X, now Y")."""
def build_consolidation_prompt(observations_mission: str | None = None) -> str:
"""
Build the consolidation prompt.
If observations_mission is provided, it replaces the default durable-knowledge rules
with bank-specific instructions for what to synthesise. Otherwise the default rules apply.
"""
rules_section = f"## MISSION\n{observations_mission}" if observations_mission else _DEFAULT_RULES
return (
"You are a memory consolidation system. Synthesize facts into observations "
"and merge with existing observations when appropriate.\n\n" + rules_section + _DATA_SECTION + _OUTPUT_FORMAT
)
@@ -4095,12 +4095,28 @@ class MemoryEngine(MemoryEngineInterface):
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
profile = await bank_utils.get_bank_profile(pool, bank_id)
disposition = profile["disposition"]
# reflect_mission and disposition in config take precedence over the legacy DB columns
config_dict = await self._config_resolver.get_bank_config(bank_id, request_context)
mission = config_dict.get("reflect_mission") or profile["mission"]
# Overlay disposition from config if explicitly set; fall back to DB values
db_disp = profile["disposition"]
db_disp_dict = db_disp.model_dump() if hasattr(db_disp, "model_dump") else dict(db_disp)
cfg_skep = config_dict.get("disposition_skepticism")
cfg_lit = config_dict.get("disposition_literalism")
cfg_emp = config_dict.get("disposition_empathy")
disposition = {
"skepticism": cfg_skep if cfg_skep is not None else db_disp_dict["skepticism"],
"literalism": cfg_lit if cfg_lit is not None else db_disp_dict["literalism"],
"empathy": cfg_emp if cfg_emp is not None else db_disp_dict["empathy"],
}
return {
"bank_id": bank_id,
"name": profile["name"],
"disposition": disposition,
"mission": profile["mission"],
"mission": mission,
}
async def update_bank_disposition(
@@ -440,9 +440,7 @@ _BASE_FACT_EXTRACTION_PROMPT = """Extract SIGNIFICANT facts from text. Be SELECT
LANGUAGE: MANDATORY — Detect the language of the input text and produce ALL output in that EXACT same language. You are STRICTLY FORBIDDEN from translating or switching to any other language. Every single word of your output must be in the same language as the input. Do NOT output in a different language under any circumstance.
{fact_types_instruction}
{extraction_guidelines}
{retain_mission_section}{extraction_guidelines}
══════════════════════════════════════════════════════════════════════════
FACT FORMAT - BE CONCISE
@@ -549,16 +547,16 @@ about experiences ARE important to remember, even if they seem small (e.g., how
tasted, how someone looked, how loud music was). Extract these if they characterize
an experience or person."""
# Assembled concise prompt (backward compatible - exact same output as before)
# Assembled concise prompt
CONCISE_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format(
fact_types_instruction="{fact_types_instruction}",
retain_mission_section="{retain_mission_section}",
extraction_guidelines=_CONCISE_GUIDELINES,
examples=_CONCISE_EXAMPLES,
)
# Custom prompt uses same base but without examples
CUSTOM_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format(
fact_types_instruction="{fact_types_instruction}",
retain_mission_section="{retain_mission_section}",
extraction_guidelines="{custom_instructions}",
examples="", # No examples for custom mode
)
@@ -569,8 +567,6 @@ VERBOSE_FACT_EXTRACTION_PROMPT = """Extract facts from text into structured form
LANGUAGE: MANDATORY — Detect the language of the input text and produce ALL output in that EXACT same language. You are STRICTLY FORBIDDEN from translating or switching to any other language. Every single word of your output must be in the same language as the input. Do NOT output in a different language under any circumstance.
{fact_types_instruction}
══════════════════════════════════════════════════════════════════════════
FACT FORMAT - ALL FIVE DIMENSIONS REQUIRED - MAXIMUM VERBOSITY
══════════════════════════════════════════════════════════════════════════
@@ -701,27 +697,41 @@ def _build_extraction_prompt_and_schema(config) -> tuple[str, type]:
Returns:
Tuple of (prompt, response_schema)
"""
fact_types_instruction = "Extract ONLY 'world' and 'assistant' type facts."
extraction_mode = config.retain_extraction_mode
extract_causal_links = config.retain_extract_causal_links
# Build retain_mission section if set - injected before the mode-specific guidelines
retain_mission = getattr(config, "retain_mission", None)
if retain_mission:
retain_mission_section = (
f"══════════════════════════════════════════════════════════════════════════\n"
f"FOCUS — What to retain for this bank\n"
f"══════════════════════════════════════════════════════════════════════════\n\n"
f"{retain_mission}\n\n"
)
else:
retain_mission_section = ""
# Select base prompt based on extraction mode
if extraction_mode == "custom":
if not config.retain_custom_instructions:
base_prompt = CONCISE_FACT_EXTRACTION_PROMPT
prompt = base_prompt.format(fact_types_instruction=fact_types_instruction)
prompt = base_prompt.format(
retain_mission_section=retain_mission_section,
)
else:
base_prompt = CUSTOM_FACT_EXTRACTION_PROMPT
prompt = base_prompt.format(
fact_types_instruction=fact_types_instruction,
retain_mission_section=retain_mission_section,
custom_instructions=config.retain_custom_instructions,
)
elif extraction_mode == "verbose":
base_prompt = VERBOSE_FACT_EXTRACTION_PROMPT
prompt = base_prompt.format(fact_types_instruction=fact_types_instruction)
prompt = VERBOSE_FACT_EXTRACTION_PROMPT
else:
base_prompt = CONCISE_FACT_EXTRACTION_PROMPT
prompt = base_prompt.format(fact_types_instruction=fact_types_instruction)
prompt = base_prompt.format(
retain_mission_section=retain_mission_section,
)
# Add causal relationships section if enabled
if extract_causal_links:
+6
View File
@@ -246,6 +246,7 @@ def main():
retain_chunk_size=config.retain_chunk_size,
retain_extract_causal_links=config.retain_extract_causal_links,
retain_extraction_mode=config.retain_extraction_mode,
retain_mission=config.retain_mission,
retain_custom_instructions=config.retain_custom_instructions,
retain_batch_tokens=config.retain_batch_tokens,
retain_batch_enabled=config.retain_batch_enabled,
@@ -271,6 +272,7 @@ def main():
enable_observations=config.enable_observations,
consolidation_batch_size=config.consolidation_batch_size,
consolidation_max_tokens=config.consolidation_max_tokens,
observations_mission=config.observations_mission,
skip_llm_verification=config.skip_llm_verification,
lazy_reranker=config.lazy_reranker,
run_migrations_on_startup=config.run_migrations_on_startup,
@@ -286,6 +288,10 @@ def main():
worker_max_slots=config.worker_max_slots,
worker_consolidation_max_slots=config.worker_consolidation_max_slots,
reflect_max_iterations=config.reflect_max_iterations,
reflect_mission=config.reflect_mission,
disposition_skepticism=config.disposition_skepticism,
disposition_literalism=config.disposition_literalism,
disposition_empathy=config.disposition_empathy,
mental_model_refresh_concurrency=config.mental_model_refresh_concurrency,
otel_traces_enabled=config.otel_traces_enabled,
otel_exporter_otlp_endpoint=config.otel_exporter_otlp_endpoint,
+9 -9
View File
@@ -27,9 +27,9 @@ class TestAgentProfile:
assert "disposition" in profile
disposition = profile["disposition"]
assert disposition.skepticism == 3
assert disposition.literalism == 3
assert disposition.empathy == 3
assert disposition["skepticism"] == 3
assert disposition["literalism"] == 3
assert disposition["empathy"] == 3
@pytest.mark.asyncio
async def test_update_agent_disposition(self, memory: MemoryEngine, request_context):
@@ -37,7 +37,7 @@ class TestAgentProfile:
bank_id = unique_agent_id("test_profile_update")
profile = await memory.get_bank_profile(bank_id, request_context=request_context)
assert profile["disposition"].skepticism == 3
assert profile["disposition"]["skepticism"] == 3
new_disposition = {
"skepticism": 5,
@@ -48,9 +48,9 @@ class TestAgentProfile:
updated_profile = await memory.get_bank_profile(bank_id, request_context=request_context)
disposition = updated_profile["disposition"]
assert disposition.skepticism == new_disposition["skepticism"]
assert disposition.literalism == new_disposition["literalism"]
assert disposition.empathy == new_disposition["empathy"]
assert disposition["skepticism"] == new_disposition["skepticism"]
assert disposition["literalism"] == new_disposition["literalism"]
assert disposition["empathy"] == new_disposition["empathy"]
@pytest.mark.asyncio
async def test_list_agents(self, memory: MemoryEngine, request_context):
@@ -104,8 +104,8 @@ class TestAgentEndpoint:
final_profile = await memory.get_bank_profile(bank_id, request_context=request_context)
assert final_profile["disposition"].skepticism == 4
assert final_profile["disposition"].literalism == 5
assert final_profile["disposition"]["skepticism"] == 4
assert final_profile["disposition"]["literalism"] == 5
class TestAgentDispositionIntegration:
+97
View File
@@ -1990,3 +1990,100 @@ class TestMentalModelRefreshAfterConsolidation:
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
def test_consolidation_prompt_default():
"""Test that the default consolidation prompt contains the built-in durable-knowledge rules."""
from hindsight_api.engine.consolidation.prompts import build_consolidation_prompt
prompt = build_consolidation_prompt()
assert "DURABLE KNOWLEDGE" in prompt
assert "temporal markers" in prompt
assert "{fact_text}" in prompt
assert "{observations_text}" in prompt
def test_consolidation_prompt_observations_mission():
"""Test that observations_mission replaces the default rules."""
from hindsight_api.engine.consolidation.prompts import build_consolidation_prompt
spec = "Observations are weekly summaries of sprint outcomes and team dynamics."
prompt = build_consolidation_prompt(observations_mission=spec)
# Spec is injected
assert spec in prompt
# Default rules are NOT present
assert "EXTRACT DURABLE KNOWLEDGE" not in prompt
# Output format and data placeholders remain
assert "actions" in prompt
assert "{fact_text}" in prompt
assert "{observations_text}" in prompt
# Renders cleanly
rendered = prompt.format(fact_text="Alice fixed a bug.", observations_text="[]")
assert "{fact_text}" not in rendered
assert spec in rendered
def test_observations_mission_config():
"""Test that observations_mission is loaded from env and exposed as configurable."""
import os
from hindsight_api.config import HindsightConfig, _get_raw_config, clear_config_cache
original = os.getenv("HINDSIGHT_API_OBSERVATIONS_MISSION")
try:
os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = "Weekly sprint summaries only."
clear_config_cache()
config = _get_raw_config()
assert config.observations_mission == "Weekly sprint summaries only."
assert "observations_mission" in HindsightConfig.get_configurable_fields()
finally:
if original is None:
os.environ.pop("HINDSIGHT_API_OBSERVATIONS_MISSION", None)
else:
os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = original
clear_config_cache()
@pytest.mark.asyncio
async def test_consolidation_with_observations_mission(memory: "MemoryEngine", request_context):
"""Test that observations_mission is used during consolidation without errors."""
import os
from hindsight_api.config import _get_raw_config, clear_config_cache
original = os.getenv("HINDSIGHT_API_OBSERVATIONS_MISSION")
try:
os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = (
"Observations are summaries of programming language usage patterns."
)
clear_config_cache()
config = _get_raw_config()
bank_id = f"test-obs-spec-{uuid.uuid4().hex[:8]}"
original_global_config = memory._config_resolver._global_config
memory._config_resolver._global_config = config
try:
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
await memory.retain_async(
bank_id=bank_id,
content="Alice uses Python for data analysis and loves its simplicity.",
request_context=request_context,
)
async with memory._pool.acquire() as conn:
observations = await conn.fetch(
"SELECT id, text, fact_type FROM memory_units WHERE bank_id = $1 AND fact_type = 'observation'",
bank_id,
)
assert isinstance(observations, list)
finally:
memory._config_resolver._global_config = original_global_config
await memory.delete_bank(bank_id, request_context=request_context)
finally:
if original is None:
os.environ.pop("HINDSIGHT_API_OBSERVATIONS_MISSION", None)
else:
os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = original
clear_config_cache()
@@ -74,12 +74,18 @@ async def test_hierarchical_fields_categorization():
# Verify configurable fields include behavioral settings (safe to modify)
assert "retain_extraction_mode" in configurable
assert "enable_observations" in configurable
assert "retain_chunk_size" in configurable
assert "retain_mission" in configurable
assert "retain_custom_instructions" in configurable
assert "retain_chunk_size" in configurable
assert "enable_observations" in configurable
assert "observations_mission" in configurable
assert "reflect_mission" in configurable
assert "disposition_skepticism" in configurable
assert "disposition_literalism" in configurable
assert "disposition_empathy" in configurable
# Verify count is correct (only 4 fields)
assert len(configurable) == 4
# Verify count is correct
assert len(configurable) == 10
# Verify credential fields (NEVER exposed)
assert "llm_api_key" in credentials
+61
View File
@@ -2256,3 +2256,64 @@ async def test_retain_batch_with_per_item_tags_on_document(memory, request_conte
finally:
await memory.delete_bank(bank_id, request_context=request_context)
print(f"\n=== Cleaned up bank: {bank_id} ===")
def test_retain_mission_injected_into_prompt():
"""Test that retain_mission is injected as a FOCUS section into any extraction mode."""
from unittest.mock import MagicMock
from hindsight_api.engine.retain.fact_extraction import _build_extraction_prompt_and_schema
spec = "Focus on technical decisions and architecture choices only."
# Test with concise mode
config = MagicMock()
config.retain_extraction_mode = "concise"
config.retain_mission = spec
config.retain_custom_instructions = None
config.retain_extract_causal_links = False
prompt, _ = _build_extraction_prompt_and_schema(config)
assert spec in prompt
assert "FOCUS" in prompt
# retain_mission is present regardless of extraction mode (verbose has its own template, no spec injection)
config.retain_extraction_mode = "verbose"
prompt_verbose, _ = _build_extraction_prompt_and_schema(config)
# verbose uses its own template - spec not injected there
assert spec not in prompt_verbose
def test_retain_mission_absent_when_not_set():
"""Test that no FOCUS section appears when retain_mission is not set."""
from unittest.mock import MagicMock
from hindsight_api.engine.retain.fact_extraction import _build_extraction_prompt_and_schema
config = MagicMock()
config.retain_extraction_mode = "concise"
config.retain_mission = None
config.retain_custom_instructions = None
config.retain_extract_causal_links = False
prompt, _ = _build_extraction_prompt_and_schema(config)
assert "FOCUS" not in prompt
assert "retain_mission_section" not in prompt
def test_retain_mission_config_loaded_from_env():
"""Test that retain_mission is loaded from env and is a configurable field."""
import os
from hindsight_api.config import HindsightConfig, _get_raw_config, clear_config_cache
original = os.getenv("HINDSIGHT_API_RETAIN_MISSION")
try:
os.environ["HINDSIGHT_API_RETAIN_MISSION"] = "Only technical decisions."
clear_config_cache()
config = _get_raw_config()
assert config.retain_mission == "Only technical decisions."
assert "retain_mission" in HindsightConfig.get_configurable_fields()
finally:
if original is None:
os.environ.pop("HINDSIGHT_API_RETAIN_MISSION", None)
else:
os.environ["HINDSIGHT_API_RETAIN_MISSION"] = original
clear_config_cache()
+2
View File
@@ -127,6 +127,7 @@ impl ApiClient {
mission: None,
background: None,
disposition: None,
..Default::default()
};
let response = self.client.create_or_update_bank(agent_id, None, &request).await?;
Ok(response.into_inner())
@@ -436,6 +437,7 @@ impl ApiClient {
mission: Some(mission.to_string()),
background: None,
disposition: None,
..Default::default()
};
let response = self.client.update_bank(bank_id, None, &request).await?;
Ok(response.into_inner())
+2
View File
@@ -293,6 +293,7 @@ pub fn create(
mission: mission_text,
background: None,
disposition,
..Default::default()
};
let response = client.create_bank(bank_id, &request, verbose);
@@ -356,6 +357,7 @@ pub fn update(
mission: mission_text,
background: None,
disposition,
..Default::default()
};
let response = client.update_bank(bank_id, &request, verbose);
+42 -6
View File
@@ -1564,6 +1564,7 @@ paths:
- Operations
/v1/default/banks/{bank_id}/profile:
get:
deprecated: true
description: Get disposition traits and mission for a memory bank. Auto-creates
agent with defaults if not exists.
operationId: get_bank_profile
@@ -1601,6 +1602,7 @@ paths:
tags:
- Banks
put:
deprecated: true
description: "Update bank's disposition traits (skepticism, literalism, empathy)"
operationId: update_bank_disposition
parameters:
@@ -2616,24 +2618,58 @@ components:
CreateBankRequest:
description: Request model for creating/updating a bank.
example:
disposition:
empathy: 3
literalism: 3
skepticism: 3
mission: I am a PM helping my engineering team stay organized
name: Alice
observations_mission: Observations are stable facts about people and projects.
Always include preferences and skills.
retain_mission: Always include technical decisions and architectural trade-offs.
Ignore meeting logistics.
properties:
name:
nullable: true
type: string
disposition:
$ref: '#/components/schemas/DispositionTraits'
disposition_skepticism:
maximum: 5.0
minimum: 1.0
nullable: true
type: integer
disposition_literalism:
maximum: 5.0
minimum: 1.0
nullable: true
type: integer
disposition_empathy:
maximum: 5.0
minimum: 1.0
nullable: true
type: integer
mission:
nullable: true
type: string
background:
nullable: true
type: string
reflect_mission:
nullable: true
type: string
retain_mission:
nullable: true
type: string
retain_extraction_mode:
nullable: true
type: string
retain_custom_instructions:
nullable: true
type: string
retain_chunk_size:
nullable: true
type: integer
enable_observations:
nullable: true
type: boolean
observations_mission:
nullable: true
type: string
title: CreateBankRequest
CreateDirectiveRequest:
description: Request model for creating a directive.
+6
View File
@@ -804,6 +804,8 @@ Get disposition traits and mission for a memory bank. Auto-creates agent with de
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiGetBankProfileRequest
Deprecated
*/
func (a *BanksAPIService) GetBankProfile(ctx context.Context, bankId string) ApiGetBankProfileRequest {
return ApiGetBankProfileRequest{
@@ -815,6 +817,7 @@ func (a *BanksAPIService) GetBankProfile(ctx context.Context, bankId string) Api
// Execute executes the request
// @return BankProfileResponse
// Deprecated
func (a *BanksAPIService) GetBankProfileExecute(r ApiGetBankProfileRequest) (*BankProfileResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodGet
@@ -1560,6 +1563,8 @@ Update bank's disposition traits (skepticism, literalism, empathy)
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@return ApiUpdateBankDispositionRequest
Deprecated
*/
func (a *BanksAPIService) UpdateBankDisposition(ctx context.Context, bankId string) ApiUpdateBankDispositionRequest {
return ApiUpdateBankDispositionRequest{
@@ -1571,6 +1576,7 @@ func (a *BanksAPIService) UpdateBankDisposition(ctx context.Context, bankId stri
// Execute executes the request
// @return BankProfileResponse
// Deprecated
func (a *BanksAPIService) UpdateBankDispositionExecute(r ApiUpdateBankDispositionRequest) (*BankProfileResponse, *http.Response, error) {
var (
localVarHTTPMethod = http.MethodPut
@@ -21,8 +21,18 @@ var _ MappedNullable = &CreateBankRequest{}
type CreateBankRequest struct {
Name NullableString `json:"name,omitempty"`
Disposition NullableDispositionTraits `json:"disposition,omitempty"`
DispositionSkepticism NullableInt32 `json:"disposition_skepticism,omitempty"`
DispositionLiteralism NullableInt32 `json:"disposition_literalism,omitempty"`
DispositionEmpathy NullableInt32 `json:"disposition_empathy,omitempty"`
Mission NullableString `json:"mission,omitempty"`
Background NullableString `json:"background,omitempty"`
ReflectMission NullableString `json:"reflect_mission,omitempty"`
RetainMission NullableString `json:"retain_mission,omitempty"`
RetainExtractionMode NullableString `json:"retain_extraction_mode,omitempty"`
RetainCustomInstructions NullableString `json:"retain_custom_instructions,omitempty"`
RetainChunkSize NullableInt32 `json:"retain_chunk_size,omitempty"`
EnableObservations NullableBool `json:"enable_observations,omitempty"`
ObservationsMission NullableString `json:"observations_mission,omitempty"`
}
// NewCreateBankRequest instantiates a new CreateBankRequest object
@@ -126,6 +136,132 @@ func (o *CreateBankRequest) UnsetDisposition() {
o.Disposition.Unset()
}
// GetDispositionSkepticism returns the DispositionSkepticism field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetDispositionSkepticism() int32 {
if o == nil || IsNil(o.DispositionSkepticism.Get()) {
var ret int32
return ret
}
return *o.DispositionSkepticism.Get()
}
// GetDispositionSkepticismOk returns a tuple with the DispositionSkepticism field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetDispositionSkepticismOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.DispositionSkepticism.Get(), o.DispositionSkepticism.IsSet()
}
// HasDispositionSkepticism returns a boolean if a field has been set.
func (o *CreateBankRequest) HasDispositionSkepticism() bool {
if o != nil && o.DispositionSkepticism.IsSet() {
return true
}
return false
}
// SetDispositionSkepticism gets a reference to the given NullableInt32 and assigns it to the DispositionSkepticism field.
func (o *CreateBankRequest) SetDispositionSkepticism(v int32) {
o.DispositionSkepticism.Set(&v)
}
// SetDispositionSkepticismNil sets the value for DispositionSkepticism to be an explicit nil
func (o *CreateBankRequest) SetDispositionSkepticismNil() {
o.DispositionSkepticism.Set(nil)
}
// UnsetDispositionSkepticism ensures that no value is present for DispositionSkepticism, not even an explicit nil
func (o *CreateBankRequest) UnsetDispositionSkepticism() {
o.DispositionSkepticism.Unset()
}
// GetDispositionLiteralism returns the DispositionLiteralism field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetDispositionLiteralism() int32 {
if o == nil || IsNil(o.DispositionLiteralism.Get()) {
var ret int32
return ret
}
return *o.DispositionLiteralism.Get()
}
// GetDispositionLiteralismOk returns a tuple with the DispositionLiteralism field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetDispositionLiteralismOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.DispositionLiteralism.Get(), o.DispositionLiteralism.IsSet()
}
// HasDispositionLiteralism returns a boolean if a field has been set.
func (o *CreateBankRequest) HasDispositionLiteralism() bool {
if o != nil && o.DispositionLiteralism.IsSet() {
return true
}
return false
}
// SetDispositionLiteralism gets a reference to the given NullableInt32 and assigns it to the DispositionLiteralism field.
func (o *CreateBankRequest) SetDispositionLiteralism(v int32) {
o.DispositionLiteralism.Set(&v)
}
// SetDispositionLiteralismNil sets the value for DispositionLiteralism to be an explicit nil
func (o *CreateBankRequest) SetDispositionLiteralismNil() {
o.DispositionLiteralism.Set(nil)
}
// UnsetDispositionLiteralism ensures that no value is present for DispositionLiteralism, not even an explicit nil
func (o *CreateBankRequest) UnsetDispositionLiteralism() {
o.DispositionLiteralism.Unset()
}
// GetDispositionEmpathy returns the DispositionEmpathy field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetDispositionEmpathy() int32 {
if o == nil || IsNil(o.DispositionEmpathy.Get()) {
var ret int32
return ret
}
return *o.DispositionEmpathy.Get()
}
// GetDispositionEmpathyOk returns a tuple with the DispositionEmpathy field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetDispositionEmpathyOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.DispositionEmpathy.Get(), o.DispositionEmpathy.IsSet()
}
// HasDispositionEmpathy returns a boolean if a field has been set.
func (o *CreateBankRequest) HasDispositionEmpathy() bool {
if o != nil && o.DispositionEmpathy.IsSet() {
return true
}
return false
}
// SetDispositionEmpathy gets a reference to the given NullableInt32 and assigns it to the DispositionEmpathy field.
func (o *CreateBankRequest) SetDispositionEmpathy(v int32) {
o.DispositionEmpathy.Set(&v)
}
// SetDispositionEmpathyNil sets the value for DispositionEmpathy to be an explicit nil
func (o *CreateBankRequest) SetDispositionEmpathyNil() {
o.DispositionEmpathy.Set(nil)
}
// UnsetDispositionEmpathy ensures that no value is present for DispositionEmpathy, not even an explicit nil
func (o *CreateBankRequest) UnsetDispositionEmpathy() {
o.DispositionEmpathy.Unset()
}
// GetMission returns the Mission field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetMission() string {
if o == nil || IsNil(o.Mission.Get()) {
@@ -210,6 +346,300 @@ func (o *CreateBankRequest) UnsetBackground() {
o.Background.Unset()
}
// GetReflectMission returns the ReflectMission field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetReflectMission() string {
if o == nil || IsNil(o.ReflectMission.Get()) {
var ret string
return ret
}
return *o.ReflectMission.Get()
}
// GetReflectMissionOk returns a tuple with the ReflectMission field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetReflectMissionOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.ReflectMission.Get(), o.ReflectMission.IsSet()
}
// HasReflectMission returns a boolean if a field has been set.
func (o *CreateBankRequest) HasReflectMission() bool {
if o != nil && o.ReflectMission.IsSet() {
return true
}
return false
}
// SetReflectMission gets a reference to the given NullableString and assigns it to the ReflectMission field.
func (o *CreateBankRequest) SetReflectMission(v string) {
o.ReflectMission.Set(&v)
}
// SetReflectMissionNil sets the value for ReflectMission to be an explicit nil
func (o *CreateBankRequest) SetReflectMissionNil() {
o.ReflectMission.Set(nil)
}
// UnsetReflectMission ensures that no value is present for ReflectMission, not even an explicit nil
func (o *CreateBankRequest) UnsetReflectMission() {
o.ReflectMission.Unset()
}
// GetRetainMission returns the RetainMission field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetRetainMission() string {
if o == nil || IsNil(o.RetainMission.Get()) {
var ret string
return ret
}
return *o.RetainMission.Get()
}
// GetRetainMissionOk returns a tuple with the RetainMission field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetRetainMissionOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.RetainMission.Get(), o.RetainMission.IsSet()
}
// HasRetainMission returns a boolean if a field has been set.
func (o *CreateBankRequest) HasRetainMission() bool {
if o != nil && o.RetainMission.IsSet() {
return true
}
return false
}
// SetRetainMission gets a reference to the given NullableString and assigns it to the RetainMission field.
func (o *CreateBankRequest) SetRetainMission(v string) {
o.RetainMission.Set(&v)
}
// SetRetainMissionNil sets the value for RetainMission to be an explicit nil
func (o *CreateBankRequest) SetRetainMissionNil() {
o.RetainMission.Set(nil)
}
// UnsetRetainMission ensures that no value is present for RetainMission, not even an explicit nil
func (o *CreateBankRequest) UnsetRetainMission() {
o.RetainMission.Unset()
}
// GetRetainExtractionMode returns the RetainExtractionMode field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetRetainExtractionMode() string {
if o == nil || IsNil(o.RetainExtractionMode.Get()) {
var ret string
return ret
}
return *o.RetainExtractionMode.Get()
}
// GetRetainExtractionModeOk returns a tuple with the RetainExtractionMode field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetRetainExtractionModeOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.RetainExtractionMode.Get(), o.RetainExtractionMode.IsSet()
}
// HasRetainExtractionMode returns a boolean if a field has been set.
func (o *CreateBankRequest) HasRetainExtractionMode() bool {
if o != nil && o.RetainExtractionMode.IsSet() {
return true
}
return false
}
// SetRetainExtractionMode gets a reference to the given NullableString and assigns it to the RetainExtractionMode field.
func (o *CreateBankRequest) SetRetainExtractionMode(v string) {
o.RetainExtractionMode.Set(&v)
}
// SetRetainExtractionModeNil sets the value for RetainExtractionMode to be an explicit nil
func (o *CreateBankRequest) SetRetainExtractionModeNil() {
o.RetainExtractionMode.Set(nil)
}
// UnsetRetainExtractionMode ensures that no value is present for RetainExtractionMode, not even an explicit nil
func (o *CreateBankRequest) UnsetRetainExtractionMode() {
o.RetainExtractionMode.Unset()
}
// GetRetainCustomInstructions returns the RetainCustomInstructions field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetRetainCustomInstructions() string {
if o == nil || IsNil(o.RetainCustomInstructions.Get()) {
var ret string
return ret
}
return *o.RetainCustomInstructions.Get()
}
// GetRetainCustomInstructionsOk returns a tuple with the RetainCustomInstructions field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetRetainCustomInstructionsOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.RetainCustomInstructions.Get(), o.RetainCustomInstructions.IsSet()
}
// HasRetainCustomInstructions returns a boolean if a field has been set.
func (o *CreateBankRequest) HasRetainCustomInstructions() bool {
if o != nil && o.RetainCustomInstructions.IsSet() {
return true
}
return false
}
// SetRetainCustomInstructions gets a reference to the given NullableString and assigns it to the RetainCustomInstructions field.
func (o *CreateBankRequest) SetRetainCustomInstructions(v string) {
o.RetainCustomInstructions.Set(&v)
}
// SetRetainCustomInstructionsNil sets the value for RetainCustomInstructions to be an explicit nil
func (o *CreateBankRequest) SetRetainCustomInstructionsNil() {
o.RetainCustomInstructions.Set(nil)
}
// UnsetRetainCustomInstructions ensures that no value is present for RetainCustomInstructions, not even an explicit nil
func (o *CreateBankRequest) UnsetRetainCustomInstructions() {
o.RetainCustomInstructions.Unset()
}
// GetRetainChunkSize returns the RetainChunkSize field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetRetainChunkSize() int32 {
if o == nil || IsNil(o.RetainChunkSize.Get()) {
var ret int32
return ret
}
return *o.RetainChunkSize.Get()
}
// GetRetainChunkSizeOk returns a tuple with the RetainChunkSize field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetRetainChunkSizeOk() (*int32, bool) {
if o == nil {
return nil, false
}
return o.RetainChunkSize.Get(), o.RetainChunkSize.IsSet()
}
// HasRetainChunkSize returns a boolean if a field has been set.
func (o *CreateBankRequest) HasRetainChunkSize() bool {
if o != nil && o.RetainChunkSize.IsSet() {
return true
}
return false
}
// SetRetainChunkSize gets a reference to the given NullableInt32 and assigns it to the RetainChunkSize field.
func (o *CreateBankRequest) SetRetainChunkSize(v int32) {
o.RetainChunkSize.Set(&v)
}
// SetRetainChunkSizeNil sets the value for RetainChunkSize to be an explicit nil
func (o *CreateBankRequest) SetRetainChunkSizeNil() {
o.RetainChunkSize.Set(nil)
}
// UnsetRetainChunkSize ensures that no value is present for RetainChunkSize, not even an explicit nil
func (o *CreateBankRequest) UnsetRetainChunkSize() {
o.RetainChunkSize.Unset()
}
// GetEnableObservations returns the EnableObservations field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetEnableObservations() bool {
if o == nil || IsNil(o.EnableObservations.Get()) {
var ret bool
return ret
}
return *o.EnableObservations.Get()
}
// GetEnableObservationsOk returns a tuple with the EnableObservations field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetEnableObservationsOk() (*bool, bool) {
if o == nil {
return nil, false
}
return o.EnableObservations.Get(), o.EnableObservations.IsSet()
}
// HasEnableObservations returns a boolean if a field has been set.
func (o *CreateBankRequest) HasEnableObservations() bool {
if o != nil && o.EnableObservations.IsSet() {
return true
}
return false
}
// SetEnableObservations gets a reference to the given NullableBool and assigns it to the EnableObservations field.
func (o *CreateBankRequest) SetEnableObservations(v bool) {
o.EnableObservations.Set(&v)
}
// SetEnableObservationsNil sets the value for EnableObservations to be an explicit nil
func (o *CreateBankRequest) SetEnableObservationsNil() {
o.EnableObservations.Set(nil)
}
// UnsetEnableObservations ensures that no value is present for EnableObservations, not even an explicit nil
func (o *CreateBankRequest) UnsetEnableObservations() {
o.EnableObservations.Unset()
}
// GetObservationsMission returns the ObservationsMission field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *CreateBankRequest) GetObservationsMission() string {
if o == nil || IsNil(o.ObservationsMission.Get()) {
var ret string
return ret
}
return *o.ObservationsMission.Get()
}
// GetObservationsMissionOk returns a tuple with the ObservationsMission field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *CreateBankRequest) GetObservationsMissionOk() (*string, bool) {
if o == nil {
return nil, false
}
return o.ObservationsMission.Get(), o.ObservationsMission.IsSet()
}
// HasObservationsMission returns a boolean if a field has been set.
func (o *CreateBankRequest) HasObservationsMission() bool {
if o != nil && o.ObservationsMission.IsSet() {
return true
}
return false
}
// SetObservationsMission gets a reference to the given NullableString and assigns it to the ObservationsMission field.
func (o *CreateBankRequest) SetObservationsMission(v string) {
o.ObservationsMission.Set(&v)
}
// SetObservationsMissionNil sets the value for ObservationsMission to be an explicit nil
func (o *CreateBankRequest) SetObservationsMissionNil() {
o.ObservationsMission.Set(nil)
}
// UnsetObservationsMission ensures that no value is present for ObservationsMission, not even an explicit nil
func (o *CreateBankRequest) UnsetObservationsMission() {
o.ObservationsMission.Unset()
}
func (o CreateBankRequest) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
@@ -226,12 +656,42 @@ func (o CreateBankRequest) ToMap() (map[string]interface{}, error) {
if o.Disposition.IsSet() {
toSerialize["disposition"] = o.Disposition.Get()
}
if o.DispositionSkepticism.IsSet() {
toSerialize["disposition_skepticism"] = o.DispositionSkepticism.Get()
}
if o.DispositionLiteralism.IsSet() {
toSerialize["disposition_literalism"] = o.DispositionLiteralism.Get()
}
if o.DispositionEmpathy.IsSet() {
toSerialize["disposition_empathy"] = o.DispositionEmpathy.Get()
}
if o.Mission.IsSet() {
toSerialize["mission"] = o.Mission.Get()
}
if o.Background.IsSet() {
toSerialize["background"] = o.Background.Get()
}
if o.ReflectMission.IsSet() {
toSerialize["reflect_mission"] = o.ReflectMission.Get()
}
if o.RetainMission.IsSet() {
toSerialize["retain_mission"] = o.RetainMission.Get()
}
if o.RetainExtractionMode.IsSet() {
toSerialize["retain_extraction_mode"] = o.RetainExtractionMode.Get()
}
if o.RetainCustomInstructions.IsSet() {
toSerialize["retain_custom_instructions"] = o.RetainCustomInstructions.Get()
}
if o.RetainChunkSize.IsSet() {
toSerialize["retain_chunk_size"] = o.RetainChunkSize.Get()
}
if o.EnableObservations.IsSet() {
toSerialize["enable_observations"] = o.EnableObservations.Get()
}
if o.ObservationsMission.IsSet() {
toSerialize["observations_mission"] = o.ObservationsMission.Get()
}
return toSerialize, nil
}
@@ -79,6 +79,8 @@ class Hindsight:
config = hindsight_client_api.Configuration(host=base_url, access_token=api_key)
self._api_client = hindsight_client_api.ApiClient(config)
self._timeout = timeout
self._base_url = base_url.rstrip("/")
self._api_key = api_key
if api_key:
self._api_client.set_default_header("Authorization", f"Bearer {api_key}")
self._memory_api = memory_api.MemoryApi(self._api_client)
@@ -386,49 +388,124 @@ class Hindsight:
bank_id: str,
name: str | None = None,
mission: str | None = None,
disposition_skepticism: int | None = None,
disposition_literalism: int | None = None,
disposition_empathy: int | None = None,
disposition: dict[str, float] | None = None,
retain_mission: str | None = None,
retain_extraction_mode: str | None = None,
retain_custom_instructions: str | None = None,
retain_chunk_size: int | None = None,
enable_observations: bool | None = None,
observations_mission: str | None = None,
reflect_mission: str | None = None,
) -> BankProfileResponse:
"""Create or update a memory bank.
Args:
bank_id: Unique identifier for the bank
name: Human-readable display name
mission: Instructions guiding what Hindsight should learn and remember (for mental models)
disposition: Optional disposition traits (skepticism, literalism, empathy)
name: Deprecated. Display label only.
mission: Deprecated. Use reflect_mission instead.
disposition_skepticism: Deprecated. Use update_bank_config(disposition_skepticism=...) instead.
disposition_literalism: Deprecated. Use update_bank_config(disposition_literalism=...) instead.
disposition_empathy: Deprecated. Use update_bank_config(disposition_empathy=...) instead.
disposition: Deprecated. Use update_bank_config(disposition_skepticism=...) instead.
retain_mission: Steers what gets extracted during retain(). Injected alongside built-in rules.
retain_extraction_mode: Fact extraction mode: 'concise' (default), 'verbose', or 'custom'.
retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom').
retain_chunk_size: Maximum token size for each content chunk during retain.
enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations.
"""
from hindsight_client_api.models import create_bank_request, disposition_traits
disposition_obj = None
if disposition:
disposition_obj = disposition_traits.DispositionTraits(**disposition)
request_obj = create_bank_request.CreateBankRequest(
name=name,
mission=mission,
disposition=disposition_obj,
return _run_async(
self._acreate_bank(
bank_id,
name=name,
mission=mission,
reflect_mission=reflect_mission,
disposition_skepticism=disposition_skepticism,
disposition_literalism=disposition_literalism,
disposition_empathy=disposition_empathy,
disposition=disposition,
retain_mission=retain_mission,
retain_extraction_mode=retain_extraction_mode,
retain_custom_instructions=retain_custom_instructions,
retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations,
observations_mission=observations_mission,
)
)
return _run_async(self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout))
def set_mission(
async def _acreate_bank(
self,
bank_id: str,
mission: str,
name: str | None = None,
mission: str | None = None,
reflect_mission: str | None = None,
disposition_skepticism: int | None = None,
disposition_literalism: int | None = None,
disposition_empathy: int | None = None,
disposition: dict[str, float] | None = None,
retain_mission: str | None = None,
retain_extraction_mode: str | None = None,
retain_custom_instructions: str | None = None,
retain_chunk_size: int | None = None,
enable_observations: bool | None = None,
observations_mission: str | None = None,
) -> BankProfileResponse:
"""
Set or update the mission for a memory bank.
import aiohttp
Args:
bank_id: The memory bank ID
mission: The mission text describing the agent's purpose
body: dict[str, Any] = {}
if name is not None:
body["name"] = name
if mission is not None:
body["mission"] = mission
if reflect_mission is not None:
body["reflect_mission"] = reflect_mission
# Individual disposition fields take priority over legacy disposition dict
if disposition_skepticism is not None:
body["disposition_skepticism"] = disposition_skepticism
elif disposition is not None:
body["disposition_skepticism"] = disposition.get("skepticism")
if disposition_literalism is not None:
body["disposition_literalism"] = disposition_literalism
elif disposition is not None:
body["disposition_literalism"] = disposition.get("literalism")
if disposition_empathy is not None:
body["disposition_empathy"] = disposition_empathy
elif disposition is not None:
body["disposition_empathy"] = disposition.get("empathy")
if retain_mission is not None:
body["retain_mission"] = retain_mission
if retain_extraction_mode is not None:
body["retain_extraction_mode"] = retain_extraction_mode
if retain_custom_instructions is not None:
body["retain_custom_instructions"] = retain_custom_instructions
if retain_chunk_size is not None:
body["retain_chunk_size"] = retain_chunk_size
if enable_observations is not None:
body["enable_observations"] = enable_observations
if observations_mission is not None:
body["observations_mission"] = observations_mission
Returns:
BankProfileResponse with updated bank profile
"""
from hindsight_client_api.models import create_bank_request
url = f"{self._base_url}/v1/default/banks/{bank_id}"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
async with aiohttp.ClientSession() as session:
async with session.put(
url, json=body, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)
) as resp:
resp.raise_for_status()
data = await resp.json()
return BankProfileResponse.model_validate(data)
request_obj = create_bank_request.CreateBankRequest(mission=mission)
return _run_async(self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout))
def set_mission(self, bank_id: str, mission: str) -> dict[str, Any]:
"""Deprecated. Use update_bank_config(reflect_mission=...) instead."""
return self.create_bank(bank_id, mission=mission)
def set_reflect_mission(self, bank_id: str, reflect_mission: str) -> dict[str, Any]:
"""Deprecated alias for set_mission()."""
return self.set_mission(bank_id, reflect_mission)
# Async methods (native async, no _run_async wrapper)
@@ -437,49 +514,60 @@ class Hindsight:
bank_id: str,
name: str | None = None,
mission: str | None = None,
disposition_skepticism: int | None = None,
disposition_literalism: int | None = None,
disposition_empathy: int | None = None,
disposition: dict[str, float] | None = None,
retain_mission: str | None = None,
retain_extraction_mode: str | None = None,
retain_custom_instructions: str | None = None,
retain_chunk_size: int | None = None,
enable_observations: bool | None = None,
observations_mission: str | None = None,
reflect_mission: str | None = None,
) -> BankProfileResponse:
"""Create or update a memory bank (async).
Args:
bank_id: Unique identifier for the bank
name: Human-readable display name
mission: Instructions guiding what Hindsight should learn and remember (for mental models)
disposition: Optional disposition traits (skepticism, literalism, empathy)
name: Deprecated. Display label only.
mission: Deprecated. Use reflect_mission instead.
disposition_skepticism: Deprecated. Use update_bank_config(disposition_skepticism=...) instead.
disposition_literalism: Deprecated. Use update_bank_config(disposition_literalism=...) instead.
disposition_empathy: Deprecated. Use update_bank_config(disposition_empathy=...) instead.
disposition: Deprecated. Use update_bank_config(disposition_skepticism=...) instead.
retain_mission: Steers what gets extracted during retain(). Injected alongside built-in rules.
retain_extraction_mode: Fact extraction mode: 'concise' (default), 'verbose', or 'custom'.
retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom').
retain_chunk_size: Maximum token size for each content chunk during retain.
enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations.
"""
from hindsight_client_api.models import create_bank_request, disposition_traits
disposition_obj = None
if disposition:
disposition_obj = disposition_traits.DispositionTraits(**disposition)
request_obj = create_bank_request.CreateBankRequest(
return await self._acreate_bank(
bank_id,
name=name,
mission=mission,
disposition=disposition_obj,
reflect_mission=reflect_mission,
disposition_skepticism=disposition_skepticism,
disposition_literalism=disposition_literalism,
disposition_empathy=disposition_empathy,
disposition=disposition,
retain_mission=retain_mission,
retain_extraction_mode=retain_extraction_mode,
retain_custom_instructions=retain_custom_instructions,
retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations,
observations_mission=observations_mission,
)
return await self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout)
async def aset_mission(self, bank_id: str, mission: str) -> dict[str, Any]:
"""Deprecated. Use update_bank_config(reflect_mission=...) instead."""
return await self.acreate_bank(bank_id, mission=mission)
async def aset_mission(
self,
bank_id: str,
mission: str,
) -> BankProfileResponse:
"""
Set or update the mission for a memory bank (async).
Args:
bank_id: The memory bank ID
mission: The mission text describing the agent's purpose
Returns:
BankProfileResponse with updated bank profile
"""
from hindsight_client_api.models import create_bank_request
request_obj = create_bank_request.CreateBankRequest(mission=mission)
return await self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout)
async def aset_reflect_mission(self, bank_id: str, reflect_mission: str) -> dict[str, Any]:
"""Deprecated alias for aset_mission()."""
return await self.aset_mission(bank_id, reflect_mission)
async def aretain_batch(
self,
@@ -929,6 +1017,120 @@ class Hindsight:
"""
return _run_async(self._directives_api.delete_directive(bank_id, directive_id, _request_timeout=self._timeout))
def get_bank_config(self, bank_id: str) -> dict[str, Any]:
"""
Get the resolved configuration for a bank, including any bank-level overrides.
Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server.
Args:
bank_id: The memory bank ID
Returns:
dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (bank-level only)
"""
return _run_async(self._aget_bank_config(bank_id))
async def _aget_bank_config(self, bank_id: str) -> dict[str, Any]:
import aiohttp
url = f"{self._base_url}/v1/default/banks/{bank_id}/config"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
resp.raise_for_status()
return await resp.json()
def update_bank_config(
self,
bank_id: str,
*,
reflect_mission: str | None = None,
retain_mission: str | None = None,
retain_extraction_mode: str | None = None,
retain_custom_instructions: str | None = None,
retain_chunk_size: int | None = None,
enable_observations: bool | None = None,
observations_mission: str | None = None,
disposition_skepticism: int | None = None,
disposition_literalism: int | None = None,
disposition_empathy: int | None = None,
) -> dict[str, Any]:
"""
Update configuration overrides for a bank.
Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server.
Args:
bank_id: The memory bank ID
reflect_mission: Identity and reasoning framing for reflect().
retain_mission: Steers what gets extracted during retain().
retain_extraction_mode: Fact extraction mode: 'concise', 'verbose', or 'custom'.
retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom').
retain_chunk_size: Maximum token size for each content chunk during retain.
enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations.
disposition_skepticism: How skeptical vs trusting (1=trusting, 5=skeptical).
disposition_literalism: How literally to interpret information (1=flexible, 5=literal).
disposition_empathy: How much to consider emotional context (1=detached, 5=empathetic).
Returns:
dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (bank-level only)
"""
updates = {
k: v
for k, v in {
"reflect_mission": reflect_mission,
"retain_mission": retain_mission,
"retain_extraction_mode": retain_extraction_mode,
"retain_custom_instructions": retain_custom_instructions,
"retain_chunk_size": retain_chunk_size,
"enable_observations": enable_observations,
"observations_mission": observations_mission,
"disposition_skepticism": disposition_skepticism,
"disposition_literalism": disposition_literalism,
"disposition_empathy": disposition_empathy,
}.items()
if v is not None
}
return _run_async(self._aupdate_bank_config(bank_id, updates))
async def _aupdate_bank_config(self, bank_id: str, updates: dict[str, Any]) -> dict[str, Any]:
import aiohttp
url = f"{self._base_url}/v1/default/banks/{bank_id}/config"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
async with aiohttp.ClientSession() as session:
async with session.patch(
url, json={"updates": updates}, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)
) as resp:
resp.raise_for_status()
return await resp.json()
def reset_bank_config(self, bank_id: str) -> dict[str, Any]:
"""
Reset all bank-level configuration overrides, reverting to server defaults.
Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server.
Args:
bank_id: The memory bank ID
Returns:
dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (now empty)
"""
return _run_async(self._areset_bank_config(bank_id))
async def _areset_bank_config(self, bank_id: str) -> dict[str, Any]:
import aiohttp
url = f"{self._base_url}/v1/default/banks/{bank_id}/config"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
async with aiohttp.ClientSession() as session:
async with session.delete(url, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp:
resp.raise_for_status()
return await resp.json()
def delete_bank(self, bank_id: str):
"""
Delete a memory bank.
@@ -1793,7 +1793,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> BankProfileResponse:
"""Get memory bank profile
"""(Deprecated) Get memory bank profile
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
@@ -1822,6 +1822,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._get_bank_profile_serialize(
bank_id=bank_id,
@@ -1865,7 +1866,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[BankProfileResponse]:
"""Get memory bank profile
"""(Deprecated) Get memory bank profile
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
@@ -1894,6 +1895,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._get_bank_profile_serialize(
bank_id=bank_id,
@@ -1937,7 +1939,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Get memory bank profile
"""(Deprecated) Get memory bank profile
Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
@@ -1966,6 +1968,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._get_bank_profile_serialize(
bank_id=bank_id,
@@ -3503,7 +3506,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> BankProfileResponse:
"""Update memory bank disposition
"""(Deprecated) Update memory bank disposition
Update bank's disposition traits (skepticism, literalism, empathy)
@@ -3534,6 +3537,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._update_bank_disposition_serialize(
bank_id=bank_id,
@@ -3579,7 +3583,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[BankProfileResponse]:
"""Update memory bank disposition
"""(Deprecated) Update memory bank disposition
Update bank's disposition traits (skepticism, literalism, empathy)
@@ -3610,6 +3614,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._update_bank_disposition_serialize(
bank_id=bank_id,
@@ -3655,7 +3660,7 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Update memory bank disposition
"""(Deprecated) Update memory bank disposition
Update bank's disposition traits (skepticism, literalism, empathy)
@@ -3686,6 +3691,7 @@ class BanksApi:
:type _host_index: int, optional
:return: Returns the result object.
""" # noqa: E501
warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning)
_param = self._update_bank_disposition_serialize(
bank_id=bank_id,
@@ -17,8 +17,9 @@ import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, StrictStr
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing_extensions import Annotated
from hindsight_client_api.models.disposition_traits import DispositionTraits
from typing import Optional, Set
from typing_extensions import Self
@@ -29,9 +30,19 @@ class CreateBankRequest(BaseModel):
""" # noqa: E501
name: Optional[StrictStr] = None
disposition: Optional[DispositionTraits] = None
disposition_skepticism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
disposition_literalism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
disposition_empathy: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None
mission: Optional[StrictStr] = None
background: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["name", "disposition", "mission", "background"]
reflect_mission: Optional[StrictStr] = None
retain_mission: Optional[StrictStr] = None
retain_extraction_mode: Optional[StrictStr] = None
retain_custom_instructions: Optional[StrictStr] = None
retain_chunk_size: Optional[StrictInt] = None
enable_observations: Optional[StrictBool] = None
observations_mission: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["name", "disposition", "disposition_skepticism", "disposition_literalism", "disposition_empathy", "mission", "background", "reflect_mission", "retain_mission", "retain_extraction_mode", "retain_custom_instructions", "retain_chunk_size", "enable_observations", "observations_mission"]
model_config = ConfigDict(
populate_by_name=True,
@@ -85,6 +96,21 @@ class CreateBankRequest(BaseModel):
if self.disposition is None and "disposition" in self.model_fields_set:
_dict['disposition'] = None
# set to None if disposition_skepticism (nullable) is None
# and model_fields_set contains the field
if self.disposition_skepticism is None and "disposition_skepticism" in self.model_fields_set:
_dict['disposition_skepticism'] = None
# set to None if disposition_literalism (nullable) is None
# and model_fields_set contains the field
if self.disposition_literalism is None and "disposition_literalism" in self.model_fields_set:
_dict['disposition_literalism'] = None
# set to None if disposition_empathy (nullable) is None
# and model_fields_set contains the field
if self.disposition_empathy is None and "disposition_empathy" in self.model_fields_set:
_dict['disposition_empathy'] = None
# set to None if mission (nullable) is None
# and model_fields_set contains the field
if self.mission is None and "mission" in self.model_fields_set:
@@ -95,6 +121,41 @@ class CreateBankRequest(BaseModel):
if self.background is None and "background" in self.model_fields_set:
_dict['background'] = None
# set to None if reflect_mission (nullable) is None
# and model_fields_set contains the field
if self.reflect_mission is None and "reflect_mission" in self.model_fields_set:
_dict['reflect_mission'] = None
# set to None if retain_mission (nullable) is None
# and model_fields_set contains the field
if self.retain_mission is None and "retain_mission" in self.model_fields_set:
_dict['retain_mission'] = None
# set to None if retain_extraction_mode (nullable) is None
# and model_fields_set contains the field
if self.retain_extraction_mode is None and "retain_extraction_mode" in self.model_fields_set:
_dict['retain_extraction_mode'] = None
# set to None if retain_custom_instructions (nullable) is None
# and model_fields_set contains the field
if self.retain_custom_instructions is None and "retain_custom_instructions" in self.model_fields_set:
_dict['retain_custom_instructions'] = None
# set to None if retain_chunk_size (nullable) is None
# and model_fields_set contains the field
if self.retain_chunk_size is None and "retain_chunk_size" in self.model_fields_set:
_dict['retain_chunk_size'] = None
# set to None if enable_observations (nullable) is None
# and model_fields_set contains the field
if self.enable_observations is None and "enable_observations" in self.model_fields_set:
_dict['enable_observations'] = None
# set to None if observations_mission (nullable) is None
# and model_fields_set contains the field
if self.observations_mission is None and "observations_mission" in self.model_fields_set:
_dict['observations_mission'] = None
return _dict
@classmethod
@@ -109,8 +170,18 @@ class CreateBankRequest(BaseModel):
_obj = cls.model_validate({
"name": obj.get("name"),
"disposition": DispositionTraits.from_dict(obj["disposition"]) if obj.get("disposition") is not None else None,
"disposition_skepticism": obj.get("disposition_skepticism"),
"disposition_literalism": obj.get("disposition_literalism"),
"disposition_empathy": obj.get("disposition_empathy"),
"mission": obj.get("mission"),
"background": obj.get("background")
"background": obj.get("background"),
"reflect_mission": obj.get("reflect_mission"),
"retain_mission": obj.get("retain_mission"),
"retain_extraction_mode": obj.get("retain_extraction_mode"),
"retain_custom_instructions": obj.get("retain_custom_instructions"),
"retain_chunk_size": obj.get("retain_chunk_size"),
"enable_observations": obj.get("enable_observations"),
"observations_mission": obj.get("observations_mission")
})
return _obj
@@ -701,6 +701,8 @@ export const getOperationStatus = <ThrowOnError extends boolean = false>(
* Get memory bank profile
*
* Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.
*
* @deprecated
*/
export const getBankProfile = <ThrowOnError extends boolean = false>(
options: Options<GetBankProfileData, ThrowOnError>,
@@ -715,6 +717,8 @@ export const getBankProfile = <ThrowOnError extends boolean = false>(
* Update memory bank disposition
*
* Update bank's disposition traits (skepticism, literalism, empathy)
*
* @deprecated
*/
export const updateBankDisposition = <ThrowOnError extends boolean = false>(
options: Options<UpdateBankDispositionData, ThrowOnError>,
@@ -424,21 +424,86 @@ export type ConsolidationResponse = {
export type CreateBankRequest = {
/**
* Name
*
* Deprecated: display label only, not advertised
*/
name?: string | null;
/**
* Deprecated: use update_bank_config instead
*/
disposition?: DispositionTraits | null;
/**
* Disposition Skepticism
*
* Deprecated: use update_bank_config instead
*/
disposition_skepticism?: number | null;
/**
* Disposition Literalism
*
* Deprecated: use update_bank_config instead
*/
disposition_literalism?: number | null;
/**
* Disposition Empathy
*
* Deprecated: use update_bank_config instead
*/
disposition_empathy?: number | null;
/**
* Mission
*
* The agent's mission
* Deprecated: use update_bank_config with reflect_mission instead
*/
mission?: string | null;
/**
* Background
*
* Deprecated: use mission instead
* Deprecated: use update_bank_config with reflect_mission instead
*/
background?: string | null;
/**
* Reflect Mission
*
* Mission/context for Reflect operations. Guides how Reflect interprets and uses memories.
*/
reflect_mission?: string | null;
/**
* Retain Mission
*
* Steers what gets extracted during retain(). Injected alongside built-in extraction rules.
*/
retain_mission?: string | null;
/**
* Retain Extraction Mode
*
* Fact extraction mode: 'concise' (default), 'verbose', or 'custom'.
*/
retain_extraction_mode?: string | null;
/**
* Retain Custom Instructions
*
* Custom extraction prompt. Only active when retain_extraction_mode is 'custom'.
*/
retain_custom_instructions?: string | null;
/**
* Retain Chunk Size
*
* Maximum token size for each content chunk during retain.
*/
retain_chunk_size?: number | null;
/**
* Enable Observations
*
* Toggle automatic observation consolidation after retain().
*/
enable_observations?: boolean | null;
/**
* Observations Mission
*
* Controls what gets synthesised into observations. Replaces built-in consolidation rules entirely.
*/
observations_mission?: string | null;
};
/**
+1 -1
View File
@@ -6,5 +6,5 @@ module.exports = {
transform: {
'^.+\\.tsx?$': 'ts-jest',
},
testTimeout: 60000,
testTimeout: 120000,
};
+121 -7
View File
@@ -39,6 +39,7 @@ import type {
FileRetainResponse,
ListMemoryUnitsResponse,
BankProfileResponse,
BankConfigResponse,
CreateBankRequest,
Budget,
} from '../generated/types.gen';
@@ -347,25 +348,73 @@ export class HindsightClient {
}
/**
* Create or update a bank with disposition and background.
* Create or update a bank with disposition, missions, and operational configuration.
*/
async createBank(
bankId: string,
options: { name?: string; background?: string; disposition?: any }
options: {
/** @deprecated Display label only. */
name?: string;
/** @deprecated Use reflectMission instead. */
mission?: string;
/** Mission/context for Reflect operations. */
reflectMission?: string;
/** @deprecated Alias for mission. */
background?: string;
/** @deprecated Use dispositionSkepticism, dispositionLiteralism, dispositionEmpathy instead. */
disposition?: { skepticism: number; literalism: number; empathy: number };
/** @deprecated Use updateBankConfig({ dispositionSkepticism }) instead. */
dispositionSkepticism?: number;
/** @deprecated Use updateBankConfig({ dispositionLiteralism }) instead. */
dispositionLiteralism?: number;
/** @deprecated Use updateBankConfig({ dispositionEmpathy }) instead. */
dispositionEmpathy?: number;
/** Steers what gets extracted during retain(). Injected alongside built-in rules. */
retainMission?: string;
/** Fact extraction mode: 'concise' (default), 'verbose', or 'custom'. */
retainExtractionMode?: string;
/** Custom extraction prompt (only active when retainExtractionMode is 'custom'). */
retainCustomInstructions?: string;
/** Maximum token size for each content chunk during retain. */
retainChunkSize?: number;
/** Toggle automatic observation consolidation after retain(). */
enableObservations?: boolean;
/** Controls what gets synthesised into observations. Replaces built-in rules. */
observationsMission?: string;
} = {}
): Promise<BankProfileResponse> {
const response = await sdk.createOrUpdateBank({
client: this.client,
path: { bank_id: bankId },
body: {
name: options.name,
mission: options.mission,
reflect_mission: options.reflectMission,
background: options.background,
disposition: options.disposition,
disposition_skepticism: options.dispositionSkepticism,
disposition_literalism: options.dispositionLiteralism,
disposition_empathy: options.dispositionEmpathy,
retain_mission: options.retainMission,
retain_extraction_mode: options.retainExtractionMode,
retain_custom_instructions: options.retainCustomInstructions,
retain_chunk_size: options.retainChunkSize,
enable_observations: options.enableObservations,
observations_mission: options.observationsMission,
},
});
return this.validateResponse(response, 'createBank');
}
/**
* Set or update the reflect mission for a memory bank.
* @deprecated Use createBank({ reflectMission: '...' }) instead.
*/
async setMission(bankId: string, mission: string): Promise<BankProfileResponse> {
return this.createBank(bankId, { reflectMission: mission });
}
/**
* Get a bank's profile.
*/
@@ -378,17 +427,81 @@ export class HindsightClient {
return this.validateResponse(response, 'getBankProfile');
}
/**
* Set or update the mission for a memory bank.
* Get the resolved configuration for a bank, including any bank-level overrides.
*
* Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server.
*/
async setMission(bankId: string, mission: string): Promise<BankProfileResponse> {
const response = await sdk.createOrUpdateBank({
async getBankConfig(bankId: string): Promise<BankConfigResponse> {
const response = await sdk.getBankConfig({
client: this.client,
path: { bank_id: bankId },
body: { mission },
});
return this.validateResponse(response, 'setMission');
return this.validateResponse(response, 'getBankConfig');
}
/**
* Update configuration overrides for a bank.
*
* Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server.
*
* @param bankId - The memory bank ID
* @param options - Fields to override
*/
async updateBankConfig(
bankId: string,
options: {
reflectMission?: string;
retainMission?: string;
retainExtractionMode?: string;
retainCustomInstructions?: string;
retainChunkSize?: number;
enableObservations?: boolean;
observationsMission?: string;
/** How skeptical vs trusting (1=trusting, 5=skeptical). */
dispositionSkepticism?: number;
/** How literally to interpret information (1=flexible, 5=literal). */
dispositionLiteralism?: number;
/** How much to consider emotional context (1=detached, 5=empathetic). */
dispositionEmpathy?: number;
},
): Promise<BankConfigResponse> {
const updates: Record<string, unknown> = {};
if (options.reflectMission !== undefined) updates.reflect_mission = options.reflectMission;
if (options.retainMission !== undefined) updates.retain_mission = options.retainMission;
if (options.retainExtractionMode !== undefined) updates.retain_extraction_mode = options.retainExtractionMode;
if (options.retainCustomInstructions !== undefined)
updates.retain_custom_instructions = options.retainCustomInstructions;
if (options.retainChunkSize !== undefined) updates.retain_chunk_size = options.retainChunkSize;
if (options.enableObservations !== undefined) updates.enable_observations = options.enableObservations;
if (options.observationsMission !== undefined) updates.observations_mission = options.observationsMission;
if (options.dispositionSkepticism !== undefined) updates.disposition_skepticism = options.dispositionSkepticism;
if (options.dispositionLiteralism !== undefined) updates.disposition_literalism = options.dispositionLiteralism;
if (options.dispositionEmpathy !== undefined) updates.disposition_empathy = options.dispositionEmpathy;
const response = await sdk.updateBankConfig({
client: this.client,
path: { bank_id: bankId },
body: { updates },
});
return this.validateResponse(response, 'updateBankConfig');
}
/**
* Reset all bank-level configuration overrides, reverting to server defaults.
*
* Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server.
*/
async resetBankConfig(bankId: string): Promise<BankConfigResponse> {
const response = await sdk.resetBankConfig({
client: this.client,
path: { bank_id: bankId },
});
return this.validateResponse(response, 'resetBankConfig');
}
/**
@@ -623,6 +736,7 @@ export type {
FileRetainResponse,
ListMemoryUnitsResponse,
BankProfileResponse,
BankConfigResponse,
CreateBankRequest,
Budget,
};
+1 -1
View File
@@ -11,7 +11,7 @@
"public"
],
"scripts": {
"dev": "next dev --turbopack -p $(node -e \"const net=require('net');const s=net.createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})\")",
"dev": "next dev --turbopack -p 9999",
"build": "next build && npm run build:standalone",
"build:standalone": "rm -rf standalone && SERVER_JS=$(find .next/standalone -path '*/node_modules' -prune -o -name 'server.js' -print | head -1) && test -n \"$SERVER_JS\" || (echo 'Error: server.js not found in .next/standalone - standalone build failed' && exit 1) && STANDALONE_ROOT=$(dirname \"$SERVER_JS\") && cp -r \"$STANDALONE_ROOT\" standalone && cp -r .next/standalone/node_modules standalone/node_modules && mkdir -p standalone/.next && cp -r .next/static standalone/.next/static && mkdir -p standalone/public && (cp -r public/* standalone/public/ 2>/dev/null || true)",
"start": "next start",
@@ -36,7 +36,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Brain, Trash2, Loader2, MoreVertical, Pencil } from "lucide-react";
import { Brain, Trash2, Loader2, MoreVertical, Pencil, RotateCcw } from "lucide-react";
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
@@ -61,6 +61,8 @@ export default function BankPage() {
const [showClearObservationsDialog, setShowClearObservationsDialog] = useState(false);
const [isClearingObservations, setIsClearingObservations] = useState(false);
const [isConsolidating, setIsConsolidating] = useState(false);
const [showResetConfigDialog, setShowResetConfigDialog] = useState(false);
const [isResettingConfig, setIsResettingConfig] = useState(false);
const handleTabChange = (tab: NavItem) => {
router.push(`/banks/${bankId}?view=${tab}`);
@@ -108,6 +110,19 @@ export default function BankPage() {
}
};
const handleResetConfig = async () => {
if (!bankId) return;
setIsResettingConfig(true);
try {
await client.resetBankConfig(bankId);
setShowResetConfigDialog(false);
} catch {
// Error toast shown by API client interceptor
} finally {
setIsResettingConfig(false);
}
};
const handleTriggerConsolidation = async () => {
if (!bankId) return;
@@ -180,6 +195,14 @@ export default function BankPage() {
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setShowResetConfigDialog(true)}
className="text-amber-600 dark:text-amber-400 focus:text-amber-700 dark:focus:text-amber-300"
>
<RotateCcw className="w-4 h-4 mr-2" />
Reset Configuration
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => setShowDeleteDialog(true)}
className="text-red-600 dark:text-red-400 focus:text-red-700 dark:focus:text-red-300"
@@ -233,19 +256,13 @@ export default function BankPage() {
<div className="space-y-6">
<BankStatsView />
<BankOperationsView />
<BankProfileView hideReflectFields />
</div>
</div>
)}
{bankConfigTab === "configuration" && (
<div>
<p className="text-sm text-muted-foreground mb-6">
Configure disposition traits, mission, directives, and behavioral settings
for this bank.
</p>
<div className="space-y-6">
<BankProfileView />
{bankConfigEnabled && <BankConfigView />}
</div>
<div className="space-y-6">
<BankConfigView />
</div>
)}
</div>
@@ -481,6 +498,43 @@ export default function BankPage() {
</AlertDialogContent>
</AlertDialog>
{/* Reset Configuration Confirmation Dialog */}
<AlertDialog open={showResetConfigDialog} onOpenChange={setShowResetConfigDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset Configuration</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2 text-sm text-muted-foreground">
<p>
Are you sure you want to reset all configuration overrides for{" "}
<span className="font-semibold text-foreground">{bankId}</span>?
</p>
<p className="text-amber-600 dark:text-amber-400 font-medium">
All per-bank settings (retain, observations, reflect) will revert to server
defaults. This does not affect memories, entities, or the bank profile.
</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isResettingConfig}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleResetConfig} disabled={isResettingConfig}>
{isResettingConfig ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Resetting...
</>
) : (
<>
<RotateCcw className="w-4 h-4 mr-2" />
Reset Configuration
</>
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Clear Observations Confirmation Dialog */}
<AlertDialog open={showClearObservationsDialog} onOpenChange={setShowClearObservationsDialog}>
<AlertDialogContent>
@@ -1,11 +1,10 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo, type ReactNode } from "react";
import { useBank } from "@/lib/bank-context";
import { client } from "@/lib/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
@@ -14,154 +13,175 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Loader2, AlertCircle, CheckCircle2, Pencil, RotateCcw, MoreVertical } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Loader2, AlertCircle } from "lucide-react";
import { Card } from "@/components/ui/card";
// Field metadata for UI rendering
const FIELD_CATEGORIES = {
retention: {
title: "Retention Settings",
description: "Control how memories are extracted and stored",
fields: {
retain_chunk_size: {
label: "Chunk Size",
type: "number",
description: "Size of text chunks for processing (tokens)",
min: 500,
max: 8000,
},
retain_extraction_mode: {
label: "Extraction Mode",
type: "select",
description: "How to extract facts from content",
options: ["concise", "verbose", "custom"],
},
retain_custom_instructions: {
label: "Custom Instructions",
type: "textarea",
description:
"Custom instructions for fact extraction (requires retain_extraction_mode='custom')",
placeholder: "Focus on technical details and implementation specifics...",
rows: 3,
},
},
},
consolidation: {
title: "Consolidation Settings",
description: "Control observation synthesis",
fields: {
enable_observations: {
label: "Enable Observations",
type: "boolean",
description: "Enable automatic consolidation of facts into observations",
},
},
},
// ─── Types ────────────────────────────────────────────────────────────────────
interface ProfileData {
reflect_mission: string;
disposition_skepticism: number;
disposition_literalism: number;
disposition_empathy: number;
}
type RetainEdits = {
retain_chunk_size: number | null;
retain_extraction_mode: string | null;
retain_mission: string | null;
retain_custom_instructions: string | null;
};
type ObservationsEdits = {
enable_observations: boolean | null;
observations_mission: string | null;
};
// ─── Slice helpers ────────────────────────────────────────────────────────────
function retainSlice(config: Record<string, any>): RetainEdits {
return {
retain_chunk_size: config.retain_chunk_size ?? null,
retain_extraction_mode: config.retain_extraction_mode ?? null,
retain_mission: config.retain_mission ?? null,
retain_custom_instructions: config.retain_custom_instructions ?? null,
};
}
function observationsSlice(config: Record<string, any>): ObservationsEdits {
return {
enable_observations: config.enable_observations ?? null,
observations_mission: config.observations_mission ?? null,
};
}
const DEFAULT_PROFILE: ProfileData = {
reflect_mission: "",
disposition_skepticism: 3,
disposition_literalism: 3,
disposition_empathy: 3,
};
// ─── BankConfigView ───────────────────────────────────────────────────────────
export function BankConfigView() {
const { currentBank: bankId } = useBank();
const [loading, setLoading] = useState(true);
const [config, setConfig] = useState<Record<string, any>>({});
const [overrides, setOverrides] = useState<Record<string, any>>({});
const [showEditDialog, setShowEditDialog] = useState(false);
const [showResetDialog, setShowResetDialog] = useState(false);
const [resetting, setResetting] = useState(false);
// Source of truth
const [baseConfig, setBaseConfig] = useState<Record<string, any>>({});
const [baseProfile, setBaseProfile] = useState<ProfileData>(DEFAULT_PROFILE);
// Per-section local edits
const [retainEdits, setRetainEdits] = useState<RetainEdits>(retainSlice({}));
const [observationsEdits, setObservationsEdits] = useState<ObservationsEdits>(
observationsSlice({})
);
const [reflectEdits, setReflectEdits] = useState<ProfileData>(DEFAULT_PROFILE);
// Per-section saving/error state
const [retainSaving, setRetainSaving] = useState(false);
const [observationsSaving, setObservationsSaving] = useState(false);
const [reflectSaving, setReflectSaving] = useState(false);
const [retainError, setRetainError] = useState<string | null>(null);
const [observationsError, setObservationsError] = useState<string | null>(null);
const [reflectError, setReflectError] = useState<string | null>(null);
// Reset dialog
// Dirty tracking
const retainDirty = useMemo(
() => JSON.stringify(retainEdits) !== JSON.stringify(retainSlice(baseConfig)),
[retainEdits, baseConfig]
);
const observationsDirty = useMemo(
() => JSON.stringify(observationsEdits) !== JSON.stringify(observationsSlice(baseConfig)),
[observationsEdits, baseConfig]
);
const reflectDirty = useMemo(
() => JSON.stringify(reflectEdits) !== JSON.stringify(baseProfile),
[reflectEdits, baseProfile]
);
useEffect(() => {
if (bankId) {
loadConfig();
}
if (bankId) loadAll();
}, [bankId]);
const loadConfig = async () => {
const loadAll = async () => {
if (!bankId) return;
setLoading(true);
try {
const response = await client.getBankConfig(bankId);
setConfig(response.config);
setOverrides(response.overrides);
} catch (err: any) {
console.error("Failed to load config:", err);
const [configResp, profileResp] = await Promise.all([
client.getBankConfig(bankId),
client.getBankProfile(bankId),
]);
const cfg = configResp.config;
const prof: ProfileData = {
reflect_mission: profileResp.mission ?? "",
disposition_skepticism:
cfg.disposition_skepticism ?? profileResp.disposition?.skepticism ?? 3,
disposition_literalism:
cfg.disposition_literalism ?? profileResp.disposition?.literalism ?? 3,
disposition_empathy: cfg.disposition_empathy ?? profileResp.disposition?.empathy ?? 3,
};
setBaseConfig(cfg);
setBaseProfile(prof);
setRetainEdits(retainSlice(cfg));
setObservationsEdits(observationsSlice(cfg));
setReflectEdits(prof);
} catch (err) {
console.error("Failed to load bank data:", err);
} finally {
setLoading(false);
}
};
const handleReset = () => {
setShowResetDialog(true);
};
const confirmReset = async () => {
const saveRetain = async () => {
if (!bankId) return;
setResetting(true);
setRetainSaving(true);
setRetainError(null);
try {
await client.resetBankConfig(bankId);
await loadConfig();
setShowResetDialog(false);
await client.updateBankConfig(bankId, retainEdits);
setBaseConfig((prev) => ({ ...prev, ...retainEdits }));
} catch (err: any) {
// Error toast is shown automatically by the API client interceptor
setRetainError(err.message || "Failed to save retain settings");
} finally {
setResetting(false);
setRetainSaving(false);
}
};
const renderReadOnlyField = (fieldKey: string, fieldMeta: any) => {
const value = config[fieldKey];
const saveObservations = async () => {
if (!bankId) return;
setObservationsSaving(true);
setObservationsError(null);
try {
await client.updateBankConfig(bankId, observationsEdits);
setBaseConfig((prev) => ({ ...prev, ...observationsEdits }));
} catch (err: any) {
setObservationsError(err.message || "Failed to save observations settings");
} finally {
setObservationsSaving(false);
}
};
return (
<div
key={fieldKey}
className="flex items-start justify-between gap-4 p-3 border border-border rounded-lg bg-muted/30 hover:bg-muted/50 transition-colors"
>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium font-mono">{fieldKey}</div>
{fieldMeta.description && (
<p className="text-xs text-muted-foreground mt-0.5">{fieldMeta.description}</p>
)}
</div>
<div className="text-sm text-foreground font-mono flex-shrink-0">
{fieldMeta.type === "boolean" ? (
<span className={value ? "text-green-600" : "text-muted-foreground"}>
{value ? "Enabled" : "Disabled"}
</span>
) : fieldMeta.type === "textarea" ? (
<span className="text-muted-foreground italic">
{value ? `${value.substring(0, 50)}${value.length > 50 ? "..." : ""}` : "Not set"}
</span>
) : (
value || <span className="text-muted-foreground italic">Not set</span>
)}
</div>
</div>
);
const saveReflect = async () => {
if (!bankId) return;
setReflectSaving(true);
setReflectError(null);
try {
await client.updateBankConfig(bankId, {
reflect_mission: reflectEdits.reflect_mission || null,
disposition_skepticism: reflectEdits.disposition_skepticism,
disposition_literalism: reflectEdits.disposition_literalism,
disposition_empathy: reflectEdits.disposition_empathy,
});
setBaseProfile(reflectEdits);
} catch (err: any) {
setReflectError(err.message || "Failed to save reflect settings");
} finally {
setReflectSaving(false);
}
};
if (!bankId) {
@@ -182,298 +202,337 @@ export function BankConfigView() {
return (
<>
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-base">Configuration Settings</CardTitle>
<CardDescription className="text-xs">
Behavioral parameters for this memory bank
</CardDescription>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" disabled={resetting}>
{resetting ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MoreVertical className="h-4 w-4" />
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setShowEditDialog(true)}>
<Pencil className="h-4 w-4 mr-2" />
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={handleReset}>
<RotateCcw className="h-4 w-4 mr-2" />
Reset to Defaults
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent className="space-y-6">
{Object.entries(FIELD_CATEGORIES).map(([catKey, category]) => (
<div key={catKey}>
<div className="mb-3">
<h3 className="text-sm font-semibold">{category.title}</h3>
<p className="text-xs text-muted-foreground">{category.description}</p>
</div>
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
{Object.entries(category.fields).map(([fieldKey, fieldMeta]) =>
renderReadOnlyField(fieldKey, fieldMeta)
)}
</div>
</div>
))}
</CardContent>
</Card>
<div className="space-y-8">
{/* Retain Section */}
<ConfigSection
title="Retain"
description="Control what gets extracted and stored from content"
error={retainError}
dirty={retainDirty}
saving={retainSaving}
onSave={saveRetain}
>
<FieldRow
label="Chunk Size"
description="Size of text chunks for processing (characters)"
>
<Input
type="number"
min={500}
max={8000}
value={retainEdits.retain_chunk_size ?? ""}
onChange={(e) =>
setRetainEdits((prev) => ({
...prev,
retain_chunk_size: e.target.value ? parseFloat(e.target.value) : null,
}))
}
/>
</FieldRow>
<TextareaRow
label="Mission"
description="What this bank should pay attention to during extraction. Steers the LLM without replacing the extraction rules — works alongside any extraction mode."
value={retainEdits.retain_mission ?? ""}
onChange={(v) => setRetainEdits((prev) => ({ ...prev, retain_mission: v || null }))}
placeholder="e.g. Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics, greetings, and social exchanges."
rows={3}
/>
<FieldRow
label="Extraction Mode"
description="How aggressively to extract facts: concise (default, selective), verbose (capture everything), custom (write your own extraction rules)"
>
<Select
value={retainEdits.retain_extraction_mode ?? ""}
onValueChange={(val) =>
setRetainEdits((prev) => ({ ...prev, retain_extraction_mode: val }))
}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{["concise", "verbose", "custom"].map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
</FieldRow>
{retainEdits.retain_extraction_mode === "custom" && (
<TextareaRow
label="Custom Extraction Prompt"
description="Replaces the built-in extraction rules entirely. Only active when Extraction Mode is set to custom."
value={retainEdits.retain_custom_instructions ?? ""}
onChange={(v) =>
setRetainEdits((prev) => ({ ...prev, retain_custom_instructions: v || null }))
}
rows={5}
/>
)}
</ConfigSection>
{showEditDialog && (
<ConfigEditDialog
bankId={bankId}
initialConfig={config}
overrides={overrides}
onClose={() => setShowEditDialog(false)}
onSaved={() => {
loadConfig();
setShowEditDialog(false);
}}
/>
)}
{/* Observations Section */}
<ConfigSection
title="Observations"
description="Control how facts are synthesized into durable observations"
error={observationsError}
dirty={observationsDirty}
saving={observationsSaving}
onSave={saveObservations}
>
<FieldRow
label="Enable Observations"
description="Enable automatic consolidation of facts into observations"
>
<div className="flex justify-end">
<Toggle
value={observationsEdits.enable_observations ?? false}
onChange={(v) =>
setObservationsEdits((prev) => ({ ...prev, enable_observations: v }))
}
/>
</div>
</FieldRow>
<TextareaRow
label="Mission"
description="What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave blank to use the server default."
value={observationsEdits.observations_mission ?? ""}
onChange={(v) =>
setObservationsEdits((prev) => ({ ...prev, observations_mission: v || null }))
}
placeholder="e.g. Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events and ephemeral state."
rows={3}
/>
</ConfigSection>
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Reset Configuration</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to reset all configuration overrides to defaults? This action
cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={resetting}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmReset} disabled={resetting}>
{resetting ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Resetting...
</>
) : (
"Reset to Defaults"
)}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Reflect Section */}
<ConfigSection
title="Reflect"
description="Shape how the bank reasons and responds in reflect operations"
error={reflectError}
dirty={reflectDirty}
saving={reflectSaving}
onSave={saveReflect}
>
<TextareaRow
label="Mission"
description="Agent identity and purpose. Used as framing context in reflect."
value={reflectEdits.reflect_mission}
onChange={(v) => setReflectEdits((prev) => ({ ...prev, reflect_mission: v }))}
placeholder="e.g. You are a senior engineering assistant. Always ground answers in documented decisions and rationale. Ignore speculation. Be direct and precise."
rows={3}
/>
<TraitRow
label="Skepticism"
description="How skeptical vs trusting when evaluating claims"
lowLabel="Trusting"
highLabel="Skeptical"
value={reflectEdits.disposition_skepticism}
onChange={(v) => setReflectEdits((prev) => ({ ...prev, disposition_skepticism: v }))}
/>
<TraitRow
label="Literalism"
description="How literally to interpret information"
lowLabel="Flexible"
highLabel="Literal"
value={reflectEdits.disposition_literalism}
onChange={(v) => setReflectEdits((prev) => ({ ...prev, disposition_literalism: v }))}
/>
<TraitRow
label="Empathy"
description="How much to weight emotional context"
lowLabel="Detached"
highLabel="Empathetic"
value={reflectEdits.disposition_empathy}
onChange={(v) => setReflectEdits((prev) => ({ ...prev, disposition_empathy: v }))}
/>
</ConfigSection>
</div>
</>
);
}
// Edit dialog component
function ConfigEditDialog({
bankId,
initialConfig,
overrides,
onClose,
onSaved,
// ─── ConfigSection ────────────────────────────────────────────────────────────
function ConfigSection({
title,
description,
children,
error,
dirty,
saving,
onSave,
}: {
bankId: string;
initialConfig: Record<string, any>;
overrides: Record<string, any>;
onClose: () => void;
onSaved: () => void;
title: string;
description: string;
children: ReactNode;
error: string | null;
dirty: boolean;
saving: boolean;
onSave: () => void;
}) {
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [config, setConfig] = useState(initialConfig);
const handleFieldChange = (field: string, value: any) => {
setConfig({ ...config, [field]: value });
};
const handleSave = async () => {
setSaving(true);
setError(null);
try {
const updates: Record<string, any> = {};
Object.keys(config).forEach((key) => {
const isConfigurable = Object.values(FIELD_CATEGORIES).some((cat) =>
Object.keys(cat.fields).includes(key)
);
if (isConfigurable) {
updates[key] = config[key];
}
});
await client.updateBankConfig(bankId, updates);
onSaved();
} catch (err: any) {
console.error("Failed to save config:", err);
setError(err.message || "Failed to save configuration");
setSaving(false);
}
};
const renderField = (fieldKey: string, fieldMeta: any) => {
const value = config[fieldKey];
if (fieldMeta.type === "boolean") {
return (
<div key={fieldKey} className="space-y-2">
<div className="flex items-center justify-between">
<div>
<Label htmlFor={fieldKey} className="font-mono">
{fieldKey}
</Label>
{fieldMeta.description && (
<p className="text-xs text-muted-foreground mt-1">{fieldMeta.description}</p>
)}
</div>
<button
onClick={() => handleFieldChange(fieldKey, !value)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
value ? "bg-primary" : "bg-muted"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
value ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
</div>
</div>
);
}
if (fieldMeta.type === "select") {
return (
<div key={fieldKey} className="space-y-2">
<Label htmlFor={fieldKey} className="font-mono">
{fieldKey}
</Label>
{fieldMeta.description && (
<p className="text-xs text-muted-foreground mt-1">{fieldMeta.description}</p>
)}
<Select
value={value?.toString()}
onValueChange={(val) => handleFieldChange(fieldKey, val)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{fieldMeta.options.map((opt: string) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
if (fieldMeta.type === "textarea") {
return (
<div key={fieldKey} className="space-y-2">
<Label htmlFor={fieldKey} className="font-mono">
{fieldKey}
</Label>
{fieldMeta.description && (
<p className="text-xs text-muted-foreground mt-1">{fieldMeta.description}</p>
)}
<Textarea
id={fieldKey}
value={value || ""}
onChange={(e) => handleFieldChange(fieldKey, e.target.value || null)}
placeholder={fieldMeta.placeholder}
rows={fieldMeta.rows || 3}
className="font-mono text-sm"
/>
</div>
);
}
// number or text
return (
<div key={fieldKey} className="space-y-2">
<Label htmlFor={fieldKey} className="font-mono">
{fieldKey}
</Label>
{fieldMeta.description && (
<p className="text-xs text-muted-foreground mt-1">{fieldMeta.description}</p>
)}
<Input
id={fieldKey}
type={fieldMeta.type || "text"}
value={value ?? ""}
onChange={(e) =>
handleFieldChange(
fieldKey,
fieldMeta.type === "number" ? parseFloat(e.target.value) : e.target.value
)
}
min={fieldMeta.min}
max={fieldMeta.max}
/>
</div>
);
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Edit Configuration</DialogTitle>
<DialogDescription>
Customize behavioral settings for this bank. Changes only affect this bank and override
global defaults.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{error && (
<section className="space-y-3">
<div>
<h2 className="text-lg font-semibold">{title}</h2>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
<Card className="bg-muted/20 border-border/40">
<div className="divide-y divide-border/40">{children}</div>
{error && (
<div className="px-6 pb-2 pt-2">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{Object.entries(FIELD_CATEGORIES).map(([catKey, category]) => (
<div key={catKey} className="space-y-4">
<div>
<h3 className="text-sm font-semibold">{category.title}</h3>
<p className="text-xs text-muted-foreground">{category.description}</p>
</div>
<div className="grid gap-4">
{Object.entries(category.fields).map(([fieldKey, fieldMeta]) =>
renderField(fieldKey, fieldMeta)
)}
</div>
</div>
))}
</div>
<DialogFooter>
<Button onClick={onClose} variant="outline" disabled={saving}>
Cancel
</Button>
<Button onClick={handleSave} disabled={saving}>
</div>
)}
<div className="px-6 py-4 flex justify-end border-t border-border/40">
<Button size="sm" disabled={!dirty || saving} onClick={onSave}>
{saving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Saving...
</>
) : (
"Save Changes"
"Save changes"
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Card>
</section>
);
}
// ─── FieldRow (2-column layout for number / select / boolean) ─────────────────
function FieldRow({
label,
description,
children,
}: {
label: string;
description?: string;
children: ReactNode;
}) {
return (
<div className="px-6 py-4">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<div className="flex-1">
<p className="text-sm font-medium">{label}</p>
{description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}
</div>
<div className="md:w-64 shrink-0">{children}</div>
</div>
</div>
);
}
// ─── TextareaRow (stacked layout) ─────────────────────────────────────────────
function TextareaRow({
label,
description,
value,
onChange,
placeholder,
rows,
}: {
label: string;
description?: string;
value: string;
onChange: (v: string) => void;
placeholder?: string;
rows?: number;
}) {
return (
<div className="px-6 py-4">
<div className="space-y-2">
<div>
<p className="text-sm font-medium">{label}</p>
{description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}
</div>
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
rows={rows ?? 3}
className="font-mono text-sm"
/>
</div>
</div>
);
}
// ─── TraitRow (stacked layout with 15 selector) ──────────────────────────────
function TraitRow({
label,
description,
lowLabel,
highLabel,
value,
onChange,
}: {
label: string;
description?: string;
lowLabel?: string;
highLabel?: string;
value: number;
onChange: (v: number) => void;
}) {
return (
<div className="px-6 py-4">
<div className="space-y-3">
<div>
<p className="text-sm font-medium">{label}</p>
{description && <p className="text-xs text-muted-foreground mt-0.5">{description}</p>}
</div>
<div className="flex items-center gap-1.5">
{lowLabel && (
<span className="text-xs text-muted-foreground w-16 text-right shrink-0">
{lowLabel}
</span>
)}
<div className="flex gap-0.5">
{[1, 2, 3, 4, 5].map((n) => (
<button
key={n}
type="button"
onClick={() => onChange(n)}
className={`w-4 h-4 rounded-full transition-colors hover:opacity-80 ${
n <= value ? "bg-primary" : "bg-muted"
}`}
/>
))}
</div>
{highLabel && (
<span className="text-xs text-muted-foreground w-20 shrink-0">{highLabel}</span>
)}
<span className="text-xs font-mono text-muted-foreground ml-1 shrink-0">{value}/5</span>
</div>
</div>
</div>
);
}
// ─── Toggle ───────────────────────────────────────────────────────────────────
function Toggle({ value, onChange }: { value: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
onClick={() => onChange(!value)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
value ? "bg-primary" : "bg-muted"
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
value ? "translate-x-6" : "translate-x-1"
}`}
/>
</button>
);
}
@@ -152,7 +152,7 @@ const TRAIT_LABELS: Record<
},
};
export function BankProfileView() {
export function BankProfileView({ hideReflectFields = false }: { hideReflectFields?: boolean }) {
const router = useRouter();
const { currentBank, setCurrentBank, loadBanks } = useBank();
const { features } = useFeatures();
@@ -391,161 +391,159 @@ export function BankProfileView() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Disposition Chart */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Brain className="w-5 h-5 text-primary" />
Disposition Profile
</CardTitle>
<CardDescription>Traits that shape the reasoning and perspective</CardDescription>
{!hideReflectFields && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Disposition Chart */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Brain className="w-5 h-5 text-primary" />
Disposition Profile
</CardTitle>
<CardDescription>Traits that shape the reasoning and perspective</CardDescription>
</div>
<Button onClick={() => setShowDispositionDialog(true)} variant="ghost" size="sm">
<Pencil className="h-4 w-4" />
</Button>
</div>
<Button onClick={() => setShowDispositionDialog(true)} variant="ghost" size="sm">
<Pencil className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
{profile && (
<div className="space-y-4">
{(Object.keys(TRAIT_LABELS) as Array<keyof DispositionTraits>).map((trait) => (
<div key={trait} className="space-y-2">
<div className="flex justify-between items-center">
<div>
<label className="text-sm font-medium text-foreground">
{TRAIT_LABELS[trait].label}
</label>
<p className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].description}
</p>
</CardHeader>
<CardContent>
{profile && (
<div className="space-y-4">
{(Object.keys(TRAIT_LABELS) as Array<keyof DispositionTraits>).map((trait) => (
<div key={trait} className="space-y-2">
<div className="flex justify-between items-center">
<div>
<label className="text-sm font-medium text-foreground">
{TRAIT_LABELS[trait].label}
</label>
<p className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].description}
</p>
</div>
<span className="text-sm font-bold text-primary">
{profile.disposition[trait]}/5
</span>
</div>
<span className="text-sm font-bold text-primary">
{profile.disposition[trait]}/5
</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].lowLabel}
</span>
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${((profile.disposition[trait] - 1) / 4) * 100}%` }}
/>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].lowLabel}
</span>
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${((profile.disposition[trait] - 1) / 4) * 100}%` }}
/>
</div>
<span className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].highLabel}
</span>
</div>
<span className="text-xs text-muted-foreground">
{TRAIT_LABELS[trait].highLabel}
</span>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
</div>
)}
</CardContent>
</Card>
{/* Mission */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Target className="w-5 h-5 text-primary" />
Mission
</CardTitle>
<CardDescription>
Affects how observations, reflect, and mental models are created
</CardDescription>
{/* Mission */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<Target className="w-5 h-5 text-primary" />
Mission
</CardTitle>
<CardDescription>
Affects how observations, reflect, and mental models are created
</CardDescription>
</div>
<Button onClick={() => setShowMissionDialog(true)} variant="ghost" size="sm">
<Pencil className="h-4 w-4" />
</Button>
</div>
<Button onClick={() => setShowMissionDialog(true)} variant="ghost" size="sm">
<Pencil className="h-4 w-4" />
</Button>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
{profile?.mission ||
"No mission set. Set a mission to derive structural mental models and personalize reflect responses."}
</p>
</CardContent>
</Card>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed">
{profile?.mission ||
"No mission set. Set a mission to derive structural mental models and personalize reflect responses."}
</p>
</CardContent>
</Card>
</div>
)}
{/* Directives Section */}
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2 text-lg">
<AlertTriangle className="w-5 h-5 text-rose-500" />
Directives
</CardTitle>
<CardDescription>Hard rules that must be followed during reflect</CardDescription>
</div>
<Button
onClick={() => setShowCreateDirective(true)}
variant="outline"
size="sm"
className="h-8"
>
<Plus className="w-4 h-4 mr-1" />
Add
</Button>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-lg font-semibold flex items-center gap-2">
<AlertTriangle className="w-5 h-5 text-rose-500" />
Directives
</h3>
<p className="text-sm text-muted-foreground">
Hard rules that must be followed during reflect
</p>
</div>
</CardHeader>
<CardContent>
{directives.length > 0 ? (
<div className="grid grid-cols-2 gap-3">
{directives.map((d) => (
<Card
key={d.id}
className={`cursor-pointer transition-colors border-rose-500/30 ${
selectedDirective?.id === d.id
? "bg-rose-500/10 border-rose-500"
: "hover:bg-rose-500/5"
}`}
onClick={() => setSelectedDirective(d)}
>
<CardContent className="p-3">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-rose-500 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<span className="font-medium text-sm text-foreground">{d.name}</span>
<p className="text-xs text-muted-foreground line-clamp-2 mt-1">
{d.content}
</p>
{d.tags && d.tags.length > 0 && (
<div className="flex items-center gap-1 mt-2 flex-wrap">
<Tag className="w-3 h-3 text-muted-foreground" />
{d.tags.map((tag) => (
<span
key={tag}
className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
<Button
onClick={() => setShowCreateDirective(true)}
variant="outline"
size="sm"
className="h-8"
>
<Plus className="w-4 h-4 mr-1" />
Add
</Button>
</div>
{directives.length > 0 ? (
<div className="grid grid-cols-2 gap-3">
{directives.map((d) => (
<Card
key={d.id}
className={`cursor-pointer transition-colors border-rose-500/30 ${
selectedDirective?.id === d.id
? "bg-rose-500/10 border-rose-500"
: "hover:bg-rose-500/5"
}`}
onClick={() => setSelectedDirective(d)}
>
<CardContent className="p-3">
<div className="flex items-start gap-2">
<AlertTriangle className="w-4 h-4 text-rose-500 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<span className="font-medium text-sm text-foreground">{d.name}</span>
<p className="text-xs text-muted-foreground line-clamp-2 mt-1">{d.content}</p>
{d.tags && d.tags.length > 0 && (
<div className="flex items-center gap-1 mt-2 flex-wrap">
<Tag className="w-3 h-3 text-muted-foreground" />
{d.tags.map((tag) => (
<span
key={tag}
className="text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground"
>
{tag}
</span>
))}
</div>
)}
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="p-6 border border-dashed border-rose-500/30 rounded-lg text-center">
<AlertTriangle className="w-6 h-6 mx-auto mb-2 text-rose-500/50" />
<p className="text-sm text-muted-foreground">
No directives yet. Directives are hard rules that must be followed during reflect.
</p>
</div>
)}
</CardContent>
</Card>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<div className="p-6 border border-dashed border-rose-500/30 rounded-lg text-center">
<AlertTriangle className="w-6 h-6 mx-auto mb-2 text-rose-500/50" />
<p className="text-sm text-muted-foreground">
No directives yet. Directives are hard rules that must be followed during reflect.
</p>
</div>
)}
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
@@ -744,8 +742,10 @@ function DispositionEditDialog({
setSaving(true);
try {
await client.updateBankProfile(currentBank, {
disposition: editDisposition,
await client.updateBankConfig(currentBank, {
disposition_skepticism: editDisposition.skepticism,
disposition_literalism: editDisposition.literalism,
disposition_empathy: editDisposition.empathy,
});
onSaved();
} catch (error) {
@@ -834,8 +834,8 @@ function MissionEditDialog({
setSaving(true);
try {
await client.updateBankProfile(currentBank, {
mission: editMission,
await client.updateBankConfig(currentBank, {
reflect_mission: editMission || null,
});
onSaved();
} catch (error) {
@@ -0,0 +1,15 @@
/**
* Next.js instrumentation file - runs exactly once at server startup.
* https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation
*/
export async function register() {
const dataplaneUrl = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
const apiKey = process.env.HINDSIGHT_CP_DATAPLANE_API_KEY || "";
console.log(`[Control Plane] Connecting to dataplane at: ${dataplaneUrl}`);
if (apiKey) {
console.log("[Control Plane] Using API key authentication");
} else {
console.log("[Control Plane] No API key configured (public access)");
}
}
@@ -14,14 +14,6 @@ import {
export const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
const DATAPLANE_API_KEY = process.env.HINDSIGHT_CP_DATAPLANE_API_KEY || "";
// Log dataplane configuration at startup
console.log(`[Control Plane] Connecting to dataplane at: ${DATAPLANE_URL}`);
if (DATAPLANE_API_KEY) {
console.log("[Control Plane] Using API key authentication");
} else {
console.log("[Control Plane] No API key configured (public access)");
}
/**
* Auth headers for direct fetch calls to the dataplane API.
*/
@@ -46,49 +46,166 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
<TabItem value="cli" label="CLI">
```bash
# Set mission
hindsight bank mission my-bank "I am a research assistant specializing in ML"
# Set disposition
hindsight bank disposition my-bank \
--skepticism 4 \
--literalism 3 \
--empathy 3
hindsight bank create my-bank
```
</TabItem>
</Tabs>
## Mission and Disposition
## Bank Configuration
Mission and disposition are optional settings that influence how the bank reasons during [reflect](./reflect) operations.
Each memory bank can be configured independently per operation. Configuration can be set via the [bank config API](#updating-configuration), the [Control Plane UI](/developer/index), or [server-wide environment variables](/developer/configuration).
:::info
Mission and disposition only affect the `reflect` operation. They do not impact `retain`, `recall`, or other memory operations.
:::
### retain_mission
### Mission
A plain-language description of what this bank should pay attention to during extraction. The mission is injected into the extraction prompt alongside the built-in rules — it steers focus without replacing the extraction logic.
The mission is a first-person narrative providing context for reasoning:
```
e.g. Always include technical decisions, API design choices, and architectural trade-offs.
Ignore meeting logistics, greetings, and social exchanges.
```
Works alongside any extraction mode. Leave blank for general-purpose extraction.
### retain_extraction_mode
Controls how aggressively facts are extracted:
| Mode | Description |
|------|-------------|
| `concise` *(default)* | Selective — only facts worth remembering long-term |
| `verbose` | Captures more detail per fact; slower and uses more tokens |
| `custom` | Write your own extraction rules via `retain_custom_instructions` |
### retain_custom_instructions
Only active when `retain_extraction_mode` is `custom`. Replaces the built-in extraction rules entirely with your own instructions.
See [Retain configuration](/developer/configuration#retain) for environment variable names and defaults.
### enable_observations
Toggles automatic observation consolidation on or off. Defaults to `true` when the observations feature is enabled on the server.
### observations_mission
Defines what this bank should synthesise into durable observations. Replaces the built-in consolidation rules entirely — leave blank to use the server default.
```
e.g. Observations are stable facts about people and projects.
Always include preferences, skills, and recurring patterns.
Ignore one-off events and ephemeral state.
```
See [Observations configuration](/developer/configuration#observations) for environment variable names and defaults.
### mission
A first-person narrative that provides identity and framing context for `reflect`. The agent uses this to ground its reasoning and apply a consistent perspective.
```
e.g. You are a senior engineering assistant.
Always ground answers in documented decisions and rationale.
Ignore speculation. Be direct and precise.
```
### disposition_skepticism
How skeptical vs trusting the bank is when evaluating claims during `reflect`. Scale 15.
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={memoryBanksPy} section="bank-mission" language="python" />
<CodeSnippet code={memoryBanksPy} section="bank-with-disposition" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={memoryBanksMjs} section="bank-mission" language="javascript" />
<CodeSnippet code={memoryBanksMjs} section="bank-with-disposition" language="javascript" />
</TabItem>
</Tabs>
### Disposition Traits
| Value | Behaviour |
|-------|-----------|
| `1` | Trusting — accepts information at face value |
| `3` *(default)* | Balanced |
| `5` | Skeptical — questions and doubts claims |
Disposition traits influence how reasoning is performed during reflection. Each trait is scored 1 to 5:
### disposition_literalism
| Trait | Low (1) | High (5) |
|-------|---------|----------|
| **Skepticism** | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
| **Literalism** | Flexible interpretation, reads between the lines | Literal interpretation, takes things exactly as stated |
| **Empathy** | Detached, focuses on facts and logic | Empathetic, considers emotional context |
How literally to interpret information during `reflect`. Scale 15.
| Value | Behaviour |
|-------|-----------|
| `1` | Flexible — reads between the lines, considers context |
| `3` *(default)* | Balanced |
| `5` | Literal — takes things exactly as stated |
### disposition_empathy
How much to weight emotional context when reasoning during `reflect`. Scale 15.
| Value | Behaviour |
|-------|-----------|
| `1` | Detached — focuses on facts and logic |
| `3` *(default)* | Balanced |
| `5` | Empathetic — considers emotional context |
:::info
Disposition traits and `mission` only affect the `reflect` operation. `retain_mission` and `observations_mission` are separate per-operation settings.
:::
---
## Updating Configuration
Bank configuration fields (retain mission, extraction mode, observations mission, etc.) are managed via a **separate config API**, not the `create_bank` call. This lets you change operational settings independently from the bank's identity and disposition.
:::note
The bank config API must be enabled on the server with `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`.
:::
### Setting Configuration Overrides
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={memoryBanksPy} section="update-bank-config" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={memoryBanksMjs} section="update-bank-config" language="javascript" />
</TabItem>
</Tabs>
You can update any subset of fields — only the keys you provide are changed.
### Reading the Current Configuration
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={memoryBanksPy} section="get-bank-config" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={memoryBanksMjs} section="get-bank-config" language="javascript" />
</TabItem>
</Tabs>
The response distinguishes:
- **`config`** — the fully resolved configuration (server defaults merged with bank overrides)
- **`overrides`** — only the fields explicitly overridden for this bank
### Resetting to Defaults
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={memoryBanksPy} section="reset-bank-config" language="python" />
</TabItem>
<TabItem value="node" label="Node.js">
<CodeSnippet code={memoryBanksMjs} section="reset-bank-config" language="javascript" />
</TabItem>
</Tabs>
This removes all bank-level overrides. The bank reverts to server-wide defaults (set via environment variables).
You can also update configuration directly from the [Control Plane UI](/developer/index) — navigate to a bank and open the **Configuration** tab.
---
## Directives
+80 -20
View File
@@ -540,42 +540,51 @@ Controls the retain (memory ingestion) pipeline.
| `HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS` | Max completion tokens for fact extraction LLM calls | `64000` |
| `HINDSIGHT_API_RETAIN_CHUNK_SIZE` | Max characters per chunk for fact extraction. Larger chunks extract fewer LLM calls but may lose context. | `3000` |
| `HINDSIGHT_API_RETAIN_EXTRACTION_MODE` | Fact extraction mode: `concise`, `verbose`, or `custom` | `concise` |
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Custom extraction guidelines (only used when mode is `custom`) | - |
| `HINDSIGHT_API_RETAIN_MISSION` | What this bank should pay attention to during extraction. Steers the LLM without replacing the extraction rules — works alongside any extraction mode. | - |
| `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` | Full prompt override for fact extraction (only used when mode is `custom`). Replaces built-in extraction rules entirely. | - |
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
| `HINDSIGHT_API_RETAIN_BATCH_ENABLED` | Use LLM Batch API for fact extraction (50% cost savings, only with async operations) | `false` |
| `HINDSIGHT_API_RETAIN_BATCH_POLL_INTERVAL_SECONDS` | Batch API polling interval in seconds | `60` |
#### Extraction Modes
#### Customizing retain: when to use what
The extraction mode controls how aggressively facts are extracted from content:
There are three levels of customization for the retain pipeline. Start with the simplest that covers your needs:
- **`concise`** (default): Selective extraction that focuses on significant, long-term valuable facts. Filters out greetings, filler, and trivial information. Produces fewer but higher-quality facts with better performance.
| Goal | Use |
|------|-----|
| Steer what topics to focus on or deprioritize | `HINDSIGHT_API_RETAIN_MISSION` |
| Extract more detail per fact | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose` |
| Completely replace the extraction rules | `HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` + `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` |
- **`verbose`**: Detailed extraction that captures every piece of information with maximum verbosity. Produces more facts with extensive detail but slower performance and higher token usage.
**`HINDSIGHT_API_RETAIN_MISSION` — steer extraction without replacing it (recommended starting point)**
- **`custom`**: Inject your own extraction guidelines while keeping the structural parts of the prompt (output format, coreference resolution, temporal handling, etc.) intact. Useful for A/B testing different extraction strategies or domain-specific customization.
**Example: Custom Extraction Mode**
Tell the bank what to pay attention to during extraction, in plain language. The mission is injected into the extraction prompt alongside the built-in rules — it narrows focus without replacing the underlying logic. Works with any extraction mode (`concise`, `verbose`, `custom`).
```bash
# Set mode to custom
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
export HINDSIGHT_API_RETAIN_MISSION="Focus on technical decisions, architecture choices, and team member expertise. Deprioritize social or personal information."
```
# Define custom guidelines (multi-line is fine)
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=verbose` — more detail per fact**
Use when you need richer facts with full context, relationships, and verbosity. Slower and uses more tokens than `concise`.
**`HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom` + `HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS` — full control**
Replaces the built-in selectivity rules entirely. The structural parts of the prompt (output format, temporal handling, coreference resolution) remain intact — only the extraction guidelines are replaced.
Use this when `retain_mission` isn't sufficient and you need strict inclusion/exclusion logic.
```bash
export HINDSIGHT_API_RETAIN_EXTRACTION_MODE=custom
export HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS="ONLY extract facts that are:
✅ Technical decisions and their rationale
✅ Architecture patterns and design choices
✅ Performance metrics and benchmarks
✅ Code reviews and feedback
DO NOT extract:
❌ Generic greetings or pleasantries
❌ Greetings or social conversation
❌ Process chatter (\"let me check\", \"one moment\")
Repeated information already captured
CONSOLIDATE related technical discussions into ONE fact when possible.
Ask yourself: 'Would this technical context be useful in 6 months?' If no, skip it."
Anything that would not be useful in 6 months"
```
### File Processing
@@ -717,12 +726,63 @@ Observations are consolidated knowledge synthesized from facts.
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
| `HINDSIGHT_API_OBSERVATIONS_MISSION` | What this bank should synthesise into durable observations. Replaces the built-in consolidation rules — leave unset to use the server default. | - |
#### Customizing observations: when to use what
| Goal | Use |
|------|-----|
| Default behavior: durable specific facts, no ephemeral state | Leave unset |
| Change what observations *are* for this bank (different shape, different purpose) | `HINDSIGHT_API_OBSERVATIONS_MISSION` |
**`HINDSIGHT_API_OBSERVATIONS_MISSION` — redefine what this bank synthesises**
By default, observations are durable, specific facts synthesized from memories — the kind of knowledge that stays true over time (preferences, skills, relationships, recurring patterns). Ephemeral state is filtered out. Contradictions are tracked with temporal markers.
Set `HINDSIGHT_API_OBSERVATIONS_MISSION` to replace this definition entirely. Write a plain-language description of what observations should be for your use case. The LLM will use this instead of the default rules when deciding what to create or update. Leave it unset to keep the server default.
:::tip When to use observations_mission
Use it when the default durable-knowledge behavior doesn't match your use case. Common scenarios:
- You want **broader event summaries** rather than isolated facts
- You want observations **grouped by time period** (weekly, monthly)
- You want a **different granularity** (one observation per project rather than per fact)
- You have a **domain-specific** notion of what's worth remembering
:::
**Example: Weekly event summaries**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are broad summaries of project events grouped by week. Each observation should capture what happened, what was decided, and what was blocked — not individual facts. Merge related events into cohesive weekly narratives."
```
**Example: Person-centric knowledge**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are durable facts about specific named people: their preferences, skills, relationships, and behavioral patterns. Only create observations for facts that are stable over time and tied to a named individual."
```
**Example: Support ticket patterns**
```bash
export HINDSIGHT_API_OBSERVATIONS_MISSION="Observations are recurring patterns in customer support interactions: common failure modes, frequently requested features, and pain points that appear across multiple tickets."
```
### Reflect
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_REFLECT_MAX_ITERATIONS` | Max tool call iterations before forcing a response | `10` |
| `HINDSIGHT_API_REFLECT_MISSION` | Global reflect mission (identity and reasoning framing). Overridden per bank via config API. | - |
#### Disposition
Disposition traits control how the bank reasons during reflect operations. Each trait is on a scale of 15. These are hierarchical — they can be overridden per bank via the [config API](./configuration.md#hierarchical-configuration).
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DISPOSITION_SKEPTICISM` | How skeptical vs trusting (1=trusting, 5=skeptical) | `3` |
| `HINDSIGHT_API_DISPOSITION_LITERALISM` | How literally to interpret information (1=flexible, 5=literal) | `3` |
| `HINDSIGHT_API_DISPOSITION_EMPATHY` | How much to consider emotional context (1=detached, 5=empathetic) | `3` |
### MCP Server
@@ -913,8 +973,8 @@ This design prevents bugs where global defaults are used instead of bank overrid
Configuration fields are categorized for security:
1. **Configurable Fields** - Safe behavioral settings that can be customized per-bank:
- Retention: `retain_chunk_size`, `retain_extraction_mode`, `retain_custom_instructions`
- Consolidation: `enable_observations`
- Retention: `retain_chunk_size`, `retain_extraction_mode`, `retain_mission`, `retain_custom_instructions`
- Observations: `enable_observations`, `observations_mission`
2. **Credential Fields** - NEVER exposed or configurable via API:
- API keys: `*_api_key` (all LLM API keys)
+16 -14
View File
@@ -131,26 +131,28 @@ This ensures responses stay accurate even as the underlying data changes.
---
## Mission-Oriented Consolidation
## Observations Mission
The bank's **mission** directly influences what knowledge gets consolidated into observations. When you set a mission on your memory bank, the consolidation engine focuses on extracting knowledge that serves that mission.
You can define exactly what this bank should synthesise by setting an **observations mission** (`observations_mission`). This replaces the built-in durable-knowledge rules with your own instructions, letting you control what shape observations take.
**Example:**
```
e.g. Observations are stable facts about people and projects.
Always include preferences, skills, and recurring patterns.
Ignore one-off events and ephemeral state.
```
<CodeSnippet code={memoryBanksPy} section="bank-support-agent" language="python" />
Leave it blank to use the server default — durable, specific facts that stay true over time (preferences, skills, relationships, recurring patterns), with ephemeral state filtered out.
With this mission, the consolidation engine will:
- **Prioritize** customer preferences, issue patterns, and communication styles
- **Skip** ephemeral details that don't serve support goals
- **Synthesize** observations focused on helping customers
**Examples:**
Without a mission, the engine performs general-purpose consolidation. With a mission, it becomes focused and efficient — extracting only knowledge that matters for your use case.
| `observations_mission` | What gets synthesised |
|------------------------|----------------------|
| *(unset — default)* | Durable facts: preferences, skills, relationships, recurring patterns |
| *"Observations are weekly summaries of sprint outcomes and blockers"* | Broad event summaries grouped by time period |
| *"Observations are stable facts about named individuals only"* | Person-centric knowledge, tied to specific people |
| *"Observations are recurring patterns in customer support interactions"* | Failure modes, common requests, pain points |
| Mission | Observations Focus |
|---------|-------------------|
| *Customer support agent* | Customer preferences, issue patterns, resolution history |
| *Code review assistant* | Coding patterns, team conventions, common mistakes |
| *Research assistant* | Topic expertise, source reliability, methodology preferences |
Set `observations_mission` via the [bank config API](/developer/api/memory-banks#observations-configuration) or the [`HINDSIGHT_API_OBSERVATIONS_MISSION`](/developer/configuration#observations) environment variable.
---
+9 -5
View File
@@ -117,14 +117,18 @@ When you create a memory bank, you can configure its disposition using three tra
### Mission: Natural Language Identity
Beyond numeric traits, you can provide a natural language **mission** that describes the bank's identity:
Beyond numeric traits, you can provide a natural language **mission** that describes the bank's identity and reasoning context:
<CodeSnippet code={memoryBanksPy} section="bank-with-disposition" language="python" />
The mission tells Hindsight what knowledge to prioritize and shapes how disposition traits are applied:
- "keep track of system designs" → focuses consolidation on architectural decisions
- "prefer simplicity over cutting-edge" + high skepticism → questions complex solutions
- Explicit guidance → consistent memory focus across conversations
The reflect mission frames how the agent reasons and responds:
- Provides identity context: who the agent is and what it cares about
- Shapes how disposition traits are applied in practice
- Keeps reasoning consistent across conversations
:::info Per-operation missions
The reflect mission only affects `reflect()`. To steer what gets extracted during `retain()`, use [`retain_mission`](/developer/api/memory-banks#retain-configuration). To control what gets synthesised into observations, use [`observations_mission`](/developer/api/memory-banks#observations-configuration).
:::
---
+23
View File
@@ -176,6 +176,29 @@ All stored in your isolated **memory bank**, ready for `recall()` and `reflect()
---
## Steering Extraction with a Mission
By default, `retain()` extracts all significant facts from the content. You can narrow this focus with a **retain mission** (`retain_mission`) — a plain-language description of what this bank should pay attention to.
```
e.g. Always include technical decisions, API design choices, and architectural trade-offs.
Ignore meeting logistics, greetings, and social exchanges.
```
The mission is injected into the extraction prompt alongside the built-in rules — it steers the LLM without replacing the extraction logic. It works with any extraction mode (`concise`, `verbose`, `custom`).
For finer control, you can also change the **extraction mode**:
| Mode | When to use |
|------|-------------|
| `concise` *(default)* | General-purpose — selective, fast |
| `verbose` | When you need richer facts with full context and relationships |
| `custom` | When you want to write your own extraction rules entirely |
Set `retain_mission` and `retain_extraction_mode` via the [bank config API](/developer/api/memory-banks#retain-configuration) or the [`HINDSIGHT_API_RETAIN_MISSION`](/developer/configuration#retain) environment variable.
---
## Observation Consolidation
After `retain()` completes, Hindsight automatically triggers **observation consolidation** in the background. This process:
+35 -24
View File
@@ -17,42 +17,53 @@ const client = new HindsightClient({ baseUrl: HINDSIGHT_URL });
// =============================================================================
// [docs:create-bank]
await client.createBank('my-bank', {
name: 'Research Assistant',
mission: 'I am a research assistant specializing in machine learning',
disposition: {
skepticism: 4,
literalism: 3,
empathy: 3
}
});
await client.createBank('my-bank');
// [/docs:create-bank]
// [docs:bank-mission]
await client.createBank('financial-advisor', {
name: 'Financial Advisor',
mission: `I am a conservative financial advisor with 20 years of experience.
I prioritize capital preservation over aggressive growth.
I have seen multiple market crashes and believe in diversification.`
// [docs:bank-with-disposition]
await client.createBank('architect-bank');
await client.updateBankConfig('architect-bank', {
reflectMission: "You're a senior software architect - keep track of system designs, technology decisions, and architectural patterns.",
dispositionSkepticism: 4, // Questions new technologies
dispositionLiteralism: 4, // Focuses on concrete specs
dispositionEmpathy: 2, // Prioritizes technical facts
});
// [/docs:bank-mission]
// [/docs:bank-with-disposition]
// [docs:bank-background]
// Legacy snippet for v0.3 docs (background renamed to mission in v0.4)
await client.createBank('legacy-bank', {
name: 'Legacy Example',
mission: `I'm a personal assistant helping a software engineer. I should track their
project preferences, coding style, and technology choices.`
// [docs:update-bank-config]
// Requires HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true on the server
await client.updateBankConfig('my-bank', {
retainMission: 'Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics and social exchanges.',
retainExtractionMode: 'verbose',
observationsMission: 'Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events.',
dispositionSkepticism: 4,
dispositionLiteralism: 4,
dispositionEmpathy: 2,
});
// [/docs:bank-background]
// [/docs:update-bank-config]
// [docs:get-bank-config]
// Returns resolved config (server defaults merged with bank overrides) and the raw overrides
const { config, overrides } = await client.getBankConfig('my-bank');
// config — full resolved configuration
// overrides — only fields overridden at the bank level
// [/docs:get-bank-config]
// [docs:reset-bank-config]
// Remove all bank-level overrides, reverting to server defaults
await client.resetBankConfig('my-bank');
// [/docs:reset-bank-config]
// =============================================================================
// Cleanup (not shown in docs)
// =============================================================================
await fetch(`${HINDSIGHT_URL}/v1/default/banks/my-bank`, { method: 'DELETE' });
await fetch(`${HINDSIGHT_URL}/v1/default/banks/financial-advisor`, { method: 'DELETE' });
await fetch(`${HINDSIGHT_URL}/v1/default/banks/architect-bank`, { method: 'DELETE' });
console.log('memory-banks.mjs: All examples passed');
+34 -47
View File
@@ -20,69 +20,56 @@ client = Hindsight(base_url=HINDSIGHT_URL)
# =============================================================================
# [docs:create-bank]
client.create_bank(
bank_id="my-bank",
name="Research Assistant",
mission="You're a research assistant specializing in machine learning - keep track of papers, methods, and findings.",
disposition={
"skepticism": 4,
"literalism": 3,
"empathy": 3
}
)
client.create_bank(bank_id="my-bank")
# [/docs:create-bank]
# [docs:bank-mission]
client.create_bank(
bank_id="financial-advisor",
name="Financial Advisor",
mission="""You're a conservative financial advisor - keep track of client risk tolerance,
investment preferences, and market conditions. Prioritize capital preservation over growth."""
)
# [/docs:bank-mission]
# [docs:bank-background]
# Legacy snippet for v0.3 docs (background renamed to mission in v0.4)
client.create_bank(
bank_id="legacy-bank",
name="Legacy Example",
mission="""I'm a personal assistant helping a software engineer. I should track their
project preferences, coding style, and technology choices."""
)
# [/docs:bank-background]
# [docs:bank-with-disposition]
client.create_bank(
bank_id="architect-bank",
mission="You're a senior software architect - keep track of system designs, "
client.create_bank(bank_id="architect-bank")
client.update_bank_config(
"architect-bank",
reflect_mission="You're a senior software architect - keep track of system designs, "
"technology decisions, and architectural patterns. Prefer simplicity over cutting-edge.",
disposition={
"skepticism": 4, # Questions new technologies
"literalism": 4, # Focuses on concrete specs
"empathy": 2 # Prioritizes technical facts
}
disposition_skepticism=4, # Questions new technologies
disposition_literalism=4, # Focuses on concrete specs
disposition_empathy=2, # Prioritizes technical facts
)
# [/docs:bank-with-disposition]
# [docs:bank-support-agent]
client.create_bank(
bank_id="support-agent",
mission="You're a customer support agent - keep track of "
"customer preferences, past issues, and communication styles."
# [docs:update-bank-config]
# Requires HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true on the server
client.update_bank_config(
"my-bank",
retain_mission="Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics and social exchanges.",
retain_extraction_mode="verbose",
observations_mission="Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events.",
disposition_skepticism=4,
disposition_literalism=4,
disposition_empathy=2,
)
# [/docs:bank-support-agent]
# [/docs:update-bank-config]
# [docs:get-bank-config]
# Returns resolved config (server defaults merged with bank overrides) and the raw overrides
data = client.get_bank_config("my-bank")
# data["config"] — full resolved configuration
# data["overrides"] — only fields overridden at the bank level
# [/docs:get-bank-config]
# [docs:reset-bank-config]
# Remove all bank-level overrides, reverting to server defaults
client.reset_bank_config("my-bank")
# [/docs:reset-bank-config]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/my-bank")
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/financial-advisor")
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/architect-bank")
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/support-agent")
print("memory-banks.py: All examples passed")
+136 -11
View File
@@ -2309,6 +2309,7 @@
"summary": "Get memory bank profile",
"description": "Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.",
"operationId": "get_bank_profile",
"deprecated": true,
"parameters": [
{
"name": "bank_id",
@@ -2366,6 +2367,7 @@
"summary": "Update memory bank disposition",
"description": "Update bank's disposition traits (skepticism, literalism, empathy)",
"operationId": "update_bank_disposition",
"deprecated": true,
"parameters": [
{
"name": "bank_id",
@@ -3872,7 +3874,8 @@
"type": "null"
}
],
"title": "Name"
"title": "Name",
"description": "Deprecated: display label only, not advertised"
},
"disposition": {
"anyOf": [
@@ -3882,7 +3885,50 @@
{
"type": "null"
}
]
],
"description": "Deprecated: use update_bank_config instead"
},
"disposition_skepticism": {
"anyOf": [
{
"type": "integer",
"maximum": 5.0,
"minimum": 1.0
},
{
"type": "null"
}
],
"title": "Disposition Skepticism",
"description": "Deprecated: use update_bank_config instead"
},
"disposition_literalism": {
"anyOf": [
{
"type": "integer",
"maximum": 5.0,
"minimum": 1.0
},
{
"type": "null"
}
],
"title": "Disposition Literalism",
"description": "Deprecated: use update_bank_config instead"
},
"disposition_empathy": {
"anyOf": [
{
"type": "integer",
"maximum": 5.0,
"minimum": 1.0
},
{
"type": "null"
}
],
"title": "Disposition Empathy",
"description": "Deprecated: use update_bank_config instead"
},
"mission": {
"anyOf": [
@@ -3894,7 +3940,7 @@
}
],
"title": "Mission",
"description": "The agent's mission"
"description": "Deprecated: use update_bank_config with reflect_mission instead"
},
"background": {
"anyOf": [
@@ -3906,20 +3952,99 @@
}
],
"title": "Background",
"description": "Deprecated: use mission instead"
"description": "Deprecated: use update_bank_config with reflect_mission instead"
},
"reflect_mission": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Reflect Mission",
"description": "Mission/context for Reflect operations. Guides how Reflect interprets and uses memories."
},
"retain_mission": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Retain Mission",
"description": "Steers what gets extracted during retain(). Injected alongside built-in extraction rules."
},
"retain_extraction_mode": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Retain Extraction Mode",
"description": "Fact extraction mode: 'concise' (default), 'verbose', or 'custom'."
},
"retain_custom_instructions": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Retain Custom Instructions",
"description": "Custom extraction prompt. Only active when retain_extraction_mode is 'custom'."
},
"retain_chunk_size": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Retain Chunk Size",
"description": "Maximum token size for each content chunk during retain."
},
"enable_observations": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Enable Observations",
"description": "Toggle automatic observation consolidation after retain()."
},
"observations_mission": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Observations Mission",
"description": "Controls what gets synthesised into observations. Replaces built-in consolidation rules entirely."
}
},
"type": "object",
"title": "CreateBankRequest",
"description": "Request model for creating/updating a bank.",
"example": {
"disposition": {
"empathy": 3,
"literalism": 3,
"skepticism": 3
},
"mission": "I am a PM helping my engineering team stay organized",
"name": "Alice"
"observations_mission": "Observations are stable facts about people and projects. Always include preferences and skills.",
"retain_mission": "Always include technical decisions and architectural trade-offs. Ignore meeting logistics."
}
},
"CreateDirectiveRequest": {
+87
View File
@@ -0,0 +1,87 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Load .env to pick up HINDSIGHT_API_PORT if set
ROOT_DIR="$(git rev-parse --show-toplevel)"
if [ -f "$ROOT_DIR/.env" ]; then
set -a
source "$ROOT_DIR/.env"
set +a
fi
API_PORT="${HINDSIGHT_API_PORT:-8888}"
PIDS=()
kill_tree() {
local pid=$1
local children
children=$(pgrep -P "$pid" 2>/dev/null) || true
for child in $children; do
kill_tree "$child"
done
kill "$pid" 2>/dev/null || true
}
cleanup() {
echo ""
echo "Shutting down..."
for pid in "${PIDS[@]}"; do
kill_tree "$pid"
done
wait 2>/dev/null || true
}
trap cleanup EXIT INT TERM
# Start API
echo "Starting API server..."
"$SCRIPT_DIR/start-api.sh" &
API_PID=$!
PIDS+=($API_PID)
# Wait for API to be ready
echo "Waiting for API to be ready..."
API_READY=false
for i in {1..60}; do
if curl -sf "http://localhost:${API_PORT}/health" &>/dev/null; then
echo "API is ready"
API_READY=true
break
fi
if ! kill -0 "$API_PID" 2>/dev/null; then
echo "API process exited unexpectedly"
exit 1
fi
sleep 1
done
if [ "$API_READY" = false ]; then
echo "API did not become ready in time"
exit 1
fi
# Start Control Plane
echo ""
"$SCRIPT_DIR/start-control-plane.sh" &
CP_PID=$!
PIDS+=($CP_PID)
echo ""
echo "Hindsight is running!"
echo ""
echo " API: http://localhost:${API_PORT}"
echo " Control Plane: http://localhost:9999"
echo ""
echo "Press Ctrl+C to stop both services."
echo ""
# Poll until any service exits (wait -n requires bash 4.3+, not available on macOS)
while true; do
for pid in "${PIDS[@]}"; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "A service exited unexpectedly (PID $pid)"
exit 1
fi
done
sleep 2
done