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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 34 additions & 29 deletions hindsight-all/hindsight/embedded.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@
logger = logging.getLogger(__name__)


def _set_if_truthy(target: dict[str, str]):
"""Return a setter that only writes non-empty values to *target*.

Callers that rely on shell environment variables
(``os.environ``) should not have those silently overridden by a
constructor default of ``\"\"``. See issue #3253.
"""

def _set(key: str, value: str) -> None:
if value:
target[key] = value

return _set


class HindsightEmbedded:
"""
Hindsight client with automatic daemon lifecycle management.
Expand Down Expand Up @@ -110,20 +125,20 @@ def __init__(
"""
self.profile = profile

# Build config dict for daemon (matches CLI format)
self.config = {
"HINDSIGHT_API_LLM_PROVIDER": llm_provider,
"HINDSIGHT_API_LLM_API_KEY": llm_api_key,
"HINDSIGHT_API_LLM_MODEL": llm_model,
"HINDSIGHT_API_LOG_LEVEL": log_level,
"HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT": str(idle_timeout),
}

if llm_base_url:
self.config["HINDSIGHT_API_LLM_BASE_URL"] = llm_base_url

if database_url:
self.config["HINDSIGHT_EMBED_API_DATABASE_URL"] = database_url
# Build config dict for daemon (matches CLI format).
# Only include keys with actual values so that callers relying on shell
# environment variables (os.environ) aren't silently overridden by empty
# defaults. See issue #3253.
self.config: dict[str, str] = {}
_set_if = _set_if_truthy(self.config)

_set_if("HINDSIGHT_API_LLM_PROVIDER", llm_provider)
_set_if("HINDSIGHT_API_LLM_API_KEY", llm_api_key)
_set_if("HINDSIGHT_API_LLM_MODEL", llm_model)
_set_if("HINDSIGHT_API_LOG_LEVEL", log_level)
_set_if("HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT", str(idle_timeout) if idle_timeout else "")
_set_if("HINDSIGHT_API_LLM_BASE_URL", llm_base_url or "")
_set_if("HINDSIGHT_EMBED_API_DATABASE_URL", database_url or "")

self._ui = ui
self._ui_port = ui_port
Expand Down Expand Up @@ -175,17 +190,13 @@ def _ensure_started(self):
self._started = False

if self._closed:
raise RuntimeError(
"Cannot use HindsightEmbedded after it has been closed"
)
raise RuntimeError("Cannot use HindsightEmbedded after it has been closed")

# Use embed manager interface for daemon management
logger.info(f"Ensuring daemon is running for profile '{self.profile}'...")
success = self._manager.ensure_running(self.config, self.profile)
if not success:
raise RuntimeError(
f"Failed to start daemon for profile '{self.profile}'"
)
raise RuntimeError(f"Failed to start daemon for profile '{self.profile}'")

# Get daemon URL and create client
daemon_url = self._manager.get_url(self.profile)
Expand All @@ -196,9 +207,7 @@ def _ensure_started(self):
# Start UI if requested
if self._ui:
logger.info(f"Starting UI for profile '{self.profile}'...")
ui_started = self._manager.start_ui(
self.profile, self._ui_port, self._ui_hostname
)
ui_started = self._manager.start_ui(self.profile, self._ui_port, self._ui_hostname)
if not ui_started:
logger.warning(f"Failed to start UI for profile '{self.profile}'")

Expand All @@ -219,8 +228,7 @@ def _cleanup(self, stop_daemon_on_close: bool = False):
# Mark closed to prevent new operations but skip shared-state
# teardown — the daemon's idle timeout handles the rest.
logger.warning(
"Cleanup lock acquisition timed out for profile '%s'; "
"marking closed, daemon will idle-stop on its own",
"Cleanup lock acquisition timed out for profile '%s'; marking closed, daemon will idle-stop on its own",
self.profile,
)
self._closed = True
Expand Down Expand Up @@ -433,10 +441,7 @@ def url(self) -> str:
def is_running(self) -> bool:
"""Check if the client is initialized and the daemon is responsive."""
return (
self._started
and not self._closed
and self._client is not None
and self._manager.is_running(self.profile)
self._started and not self._closed and self._client is not None and self._manager.is_running(self.profile)
)

@property
Expand Down
2 changes: 1 addition & 1 deletion hindsight-embed/hindsight_embed/daemon_embed_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ def _start_daemon_locked(
# HINDSIGHT_API_EMBEDDINGS_PROVIDER) are silently dropped because the
# whitelist above only covers LLM/log/idle_timeout keys.
for key, value in config.items():
if key.startswith("HINDSIGHT_") and value is not None:
if key.startswith("HINDSIGHT_") and value:
env[key] = str(value)

# Use profile-specific database (check config for override)
Expand Down
85 changes: 85 additions & 0 deletions skills/hindsight-docs/references/sdks/integrations/agent-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

# Agent Plugins

Portable long-term memory for any [Agent Plugins](https://agent-plugins.org) client, powered by [Hindsight](https://vectorize.io/hindsight).

[Agent Plugins](https://agent-plugins.org) is the vendor-neutral open standard (developed with Amazon, Cursor, Microsoft, OpenAI, and Vercel) for packaging **Agent Skills + MCP servers** into a single distributable plugin. Instead of a separate integration per tool, Hindsight ships **one** plugin that every compatible client can load — at launch: **ChatGPT / Codex, Cursor, GitHub Copilot, Kiro, and VS Code**.

## Quick Start

> **💡 Recommended: Hindsight Cloud**
>
[Sign up free](https://ui.hindsight.vectorize.io/signup) for a Hindsight Cloud API key — no self-hosting, no local daemon to manage.
1. Get your `hsk_...` API key from [ui.hindsight.vectorize.io/connect](https://ui.hindsight.vectorize.io/connect).
2. Set the environment variables the plugin reads:

```bash
export HINDSIGHT_API_KEY="hsk_your_token"
export HINDSIGHT_BANK_ID="my-project" # optional; defaults to "default"
```

3. Install the plugin in your client (through its plugin/MCP UI, or by pointing it at the plugin directory — installation is client-specific per the standard).

Once installed, ask the agent something that depends on past context, or tell it a durable preference — it calls `recall` and `retain` automatically, guided by the bundled skill.

## What's in the plugin

The plugin is a thin, transport-only wrapper — all memory logic stays server-side in Hindsight. It follows the Agent Plugins `1.0.0` layout:

```
agent-plugin/
├── plugin.json # manifest ($schema + name + metadata)
├── mcp.json # Hindsight MCP server (Streamable HTTP)
└── skills/
└── hindsight-memory/
└── SKILL.md # teaches the agent when to recall / retain / reflect
```

- **`mcp.json`** connects the client to Hindsight's built-in [MCP server](../../developer/mcp-server.md) over Streamable HTTP.
- **`skills/hindsight-memory/SKILL.md`** is loaded into the agent's context so it knows *when* to reach for memory, not just that the tools exist.

## Memory tools

Via the MCP server, the agent gets Hindsight's full memory surface. The three it reaches for most:

| Tool | When | What it does |
|------|------|--------------|
| `recall` | Before answering, when past context could help | Semantic + keyword + graph + temporal retrieval over the bank |
| `retain` | After learning a durable, reusable fact | Stores the fact for future sessions |
| `reflect` | When a lookup is too shallow and you need synthesized reasoning | Disposition-aware reasoning over everything remembered |

Additional tools (knowledge pages, mental models, documents, tags) are exposed too — see the [MCP Server reference](../../developer/mcp-server.md).

## Configuration

The plugin reads two environment variables, interpolated into `mcp.json`:

| Setting | Env Var | Default | Description |
|---------|---------|---------|-------------|
| API key | `HINDSIGHT_API_KEY` | — | Your `hsk_...` key. Sent as `Authorization: Bearer`. Required for Hindsight Cloud. |
| Memory bank | `HINDSIGHT_BANK_ID` | `default` | Bank to read from and write to (sent as `X-Bank-Id`). Use one bank per user, project, or team for isolation. |

> **📝 Env-var syntax varies by client**
>
Most clients substitute `${VAR}`; some (VS Code, Cursor) use `${env:VAR}`. If your client doesn't interpolate, paste the literal key and bank id into `mcp.json`.
**Self-hosting:** replace the host in `mcp.json` (`https://api.hindsight.vectorize.io`) with your deployment's URL. A local server with the MCP endpoint open needs no API key.

## Explicit tools vs. automatic capture

Agent Plugins `1.0.0` standardizes **Skills + MCP**, not session lifecycle hooks. This plugin therefore delivers **explicit, tool-driven** memory that works identically across every supported client.

For the fully automatic experience — recall injected before every prompt and transcripts retained on session end — use the native, hook-based integration built for your specific tool, such as [Claude Code](claude-code.md) or [Codex](codex.md). Both share the same Hindsight banks, so memory captured by the hook-based integration is recalled through the Agent Plugin, and vice versa.

## Troubleshooting

**No memories recalled**: `recall` returns results only after something has been retained. Retain a fact first, or seed the bank via the [API](../../developer/api/quickstart.md).

**401 Unauthorized**: Check `HINDSIGHT_API_KEY` is set and your client is interpolating it into the `Authorization` header (see the env-var syntax note above).

**Wrong or empty memory**: Confirm `HINDSIGHT_BANK_ID` points at the bank you expect. Different tools writing to different banks won't share memory.

## Learn more

- [Agent Plugins standard](https://agent-plugins.org)
- [Hindsight MCP Server reference](../../developer/mcp-server.md)
- [Hindsight Cloud sign-up](https://ui.hindsight.vectorize.io/signup)