Skip to content

Commit e897d9f

Browse files
author
SIN-Agent
committed
feat(bundle): add Honcho behavioral-memory backend (lazy, graceful)
Implements strategic architecture: SCKG (code) + Honcho (behavioral) + SQLite (fallback). Honcho is a battle-tested behavioral-memory service (honcho-ai 2.1.2) that stores CONVERSATIONS, PREFERENCES, and PEER-MODELS — complementary to SCKG which only knows about CODE STRUCTURE. **New: HonchoBackend class in memory.py** - Lazy initialization (no import cost if unused) - 1s timeout + 0 retries for fast-fail - Graceful degradation: returns False/None on any failure - Optional dependency: honcho-ai (not required) **SINMemory now accepts honcho_workspace + honcho_base_url** **New method: get_context_for_query(query)** The killer feature: combines SCKG code knowledge + Honcho behavioral insights into a single structured dict for LLM prompt injection. **Updated: get_stats() now includes honcho status** **New CLI sub-commands:** sin memory honcho-status # check if Honcho is reachable sin memory honcho-retain # store a message in Honcho sin memory honcho-chat # ask Honcho peer a question sin context query <query> # unified code + behavioral context **Tests: 157 → 163 (+6 new, 0 regressions)** - test_memory_with_honcho_unavailable - test_honcho_backend_init_lazy - test_honcho_backend_get_status - test_honcho_retain_message_unavailable - test_memory_get_context_for_query - test_memory_stats_includes_honcho **memory.doc.md updated with Honcho section + combined-stack example. **AGENTS.md updated (both copies): - New Priority -9.5 HONCHO PFLICHT mandate - Documents complementarity to SCKG - Lists trigger phrases and install instructions **Compatibility:** - 163/163 tests pass - 0 breaking changes - All previous commands still work - Honcho completely optional Bundle now exposes 3 distinct memory layers: 1. SQLite (always) — durable facts 2. SCKG (if installed) — code knowledge graph 3. Honcho (if reachable) — behavioral + session history This is the foundation for cross-session agent intelligence.
1 parent 0badfa9 commit e897d9f

4 files changed

Lines changed: 614 additions & 20 deletions

File tree

‎src/sin_code_bundle/cli.py‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,6 +1601,107 @@ def memory_forget(
16011601
raise typer.Exit(code=1)
16021602

16031603

1604+
@memory_app.command("honcho-status")
1605+
def memory_honcho_status(
1606+
repo: str = typer.Option(".", "--repo", help="Repo root"),
1607+
base_url: str = typer.Option("http://localhost:8000", "--base-url", help="Honcho server URL"),
1608+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
1609+
):
1610+
"""Check Honcho behavioral-memory backend status."""
1611+
from sin_code_bundle.memory import SINMemory
1612+
mem = SINMemory(Path(repo), honcho_base_url=base_url)
1613+
status = mem.honcho.get_status()
1614+
if json_out:
1615+
typer.echo(json.dumps(status, indent=2))
1616+
else:
1617+
typer.echo("Honcho backend status:")
1618+
typer.echo(f" available: {status['available']}")
1619+
typer.echo(f" workspace_id: {status['workspace_id']}")
1620+
typer.echo(f" base_url: {status['base_url']}")
1621+
if status.get("error"):
1622+
typer.echo(f" error: {status['error']}")
1623+
if not status["available"]:
1624+
typer.echo("")
1625+
typer.echo(" Install: pip install honcho-ai")
1626+
typer.echo(" Serve: honcho serve (or set HONCHO_BASE_URL)")
1627+
1628+
1629+
@memory_app.command("honcho-retain")
1630+
def memory_honcho_retain(
1631+
peer: str = typer.Option("coding-agent", "--peer", help="Peer name (e.g., 'user', 'coding-agent')"),
1632+
content: str = typer.Argument(..., help="Message content to store"),
1633+
role: str = typer.Option("user", "--role", help="Message role: 'user' or 'assistant'"),
1634+
session: str = typer.Option("", "--session", help="Session name (optional)"),
1635+
repo: str = typer.Option(".", "--repo", help="Repo root"),
1636+
base_url: str = typer.Option("http://localhost:8000", "--base-url", help="Honcho server URL"),
1637+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
1638+
):
1639+
"""Store a message in Honcho (behavioral memory)."""
1640+
from sin_code_bundle.memory import SINMemory
1641+
mem = SINMemory(Path(repo), honcho_base_url=base_url)
1642+
if not mem.honcho.is_available():
1643+
typer.echo("ERROR: Honcho not available. Check 'sin memory honcho-status'", err=True)
1644+
raise typer.Exit(code=1)
1645+
result = mem.honcho.retain_message(
1646+
peer_name=peer,
1647+
content=content,
1648+
role=role,
1649+
session_name=session or None,
1650+
)
1651+
if json_out:
1652+
typer.echo(json.dumps(result, indent=2))
1653+
else:
1654+
if result and result.get("success"):
1655+
typer.echo(f"Stored: peer={result['peer']}, session={result.get('session', '-')}")
1656+
else:
1657+
typer.echo(f"ERROR: {result}", err=True)
1658+
raise typer.Exit(code=1)
1659+
1660+
1661+
@memory_app.command("honcho-chat")
1662+
def memory_honcho_chat(
1663+
query: str = typer.Argument(..., help="Question to ask Honcho peer"),
1664+
peer: str = typer.Option("coding-agent", "--peer", help="Peer name"),
1665+
repo: str = typer.Option(".", "--repo", help="Repo root"),
1666+
base_url: str = typer.Option("http://localhost:8000", "--base-url", help="Honcho server URL"),
1667+
):
1668+
"""Ask a Honcho peer a question (dialectic)."""
1669+
from sin_code_bundle.memory import SINMemory
1670+
mem = SINMemory(Path(repo), honcho_base_url=base_url)
1671+
if not mem.honcho.is_available():
1672+
typer.echo("ERROR: Honcho not available", err=True)
1673+
raise typer.Exit(code=1)
1674+
response = mem.honcho.chat(peer, query)
1675+
if response is None:
1676+
typer.echo("(no response from Honcho)", err=True)
1677+
raise typer.Exit(code=1)
1678+
typer.echo(response)
1679+
1680+
1681+
context_app = typer.Typer(help="Unified context: SCKG code + Honcho behavioral")
1682+
app.add_typer(context_app, name="context")
1683+
1684+
1685+
@context_app.command("query")
1686+
def context_query(
1687+
query: str = typer.Argument(..., help="Natural-language query"),
1688+
repo: str = typer.Option(".", "--repo", help="Repo root"),
1689+
honcho_url: str = typer.Option("http://localhost:8000", "--honcho-url", help="Honcho server URL"),
1690+
json_out: bool = typer.Option(False, "--json", help="JSON output"),
1691+
):
1692+
"""Get unified context (code + behavioral) for a query."""
1693+
from sin_code_bundle.memory import SINMemory
1694+
mem = SINMemory(Path(repo), honcho_base_url=honcho_url)
1695+
result = mem.get_context_for_query(query)
1696+
if json_out:
1697+
typer.echo(json.dumps(result, indent=2))
1698+
else:
1699+
typer.echo(f"Query: {query}")
1700+
typer.echo(f"Backends: {result['backends']}")
1701+
typer.echo("")
1702+
typer.echo(result["synthesis"])
1703+
1704+
16041705
ast_app = typer.Typer(help="AST-based code editing (requires tree-sitter)")
16051706
app.add_typer(ast_app, name="ast")
16061707

‎src/sin_code_bundle/memory.doc.md‎

Lines changed: 117 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
11
# memory.py
22

3-
Persistent memory layer with a best-effort **SCKG** semantic backend and
4-
a durable **SQLite** store. Inspired by the retain / recall / reflect
5-
patterns from Hindsight and Letta. SCKG is an *optional* dependency;
6-
SQLite is always available.
3+
Persistent memory layer with three stacked backends:
4+
5+
1. **SQLite** — always-on durable store (facts, tags, context).
6+
2. **SCKG** — optional code-knowledge graph (`sin_code_sckg`).
7+
3. **Honcho** — optional behavioral-memory layer (`honcho-ai`).
8+
9+
Inspired by the retain / recall / reflect patterns from Hindsight and
10+
Letta. SCKG and Honcho are both *optional* dependencies and gracefully
11+
degrade to no-ops when the package is missing or the server is
12+
unreachable. SQLite is the durable source of truth.
713

814
## Dependencies
915

1016
- stdlib: `sqlite3`, `json`, `datetime`, `pathlib`
1117
- optional: `sin_code_sckg` (`KnowledgeGraph` with `add_node`)
18+
- optional: `honcho-ai` 2.1+ (`Honcho`, `Honcho.peer`, `Honcho.session`)
1219

1320
## Touched by
1421

1522
- (none yet — this is a leaf module exposed for agent use)
1623

1724
## What it does
1825

19-
1. **`SINMemory(repo_root, db_path)`** — opens a SQLite DB at
20-
`<repo_root>/.sin_memory.db` (overridable), then *attempts* to
21-
construct a `sin_code_sckg.KnowledgeGraph` rooted at `repo_root`.
22-
If SCKG is missing or its init throws, the instance still works
23-
in SQLite-only mode.
26+
1. **`SINMemory(repo_root, db_path, honcho_workspace, honcho_base_url)`**
27+
— opens a SQLite DB at `<repo_root>/.sin_memory.db` (overridable),
28+
then *attempts* to construct a `sin_code_sckg.KnowledgeGraph` rooted
29+
at `repo_root`, then attaches a `HonchoBackend`. All optional
30+
backends degrade silently on failure.
2431
2. **`retain(fact, context, tags)`** — inserts a row into
2532
`memories(id, fact, context, tags, created_at)`, then best-effort
2633
adds a `memory:<id>` node to SCKG. SQLite is the source of truth.
@@ -31,15 +38,100 @@ SQLite is always available.
3138
fixed 0.5 confidence and a hint to use an LLM for real synthesis.
3239
5. **`forget(memory_id)`** — deletes a single row by id, returns
3340
whether a row was actually removed.
34-
6. **`get_stats()`** — returns `{total_facts, tags, backend}` where
35-
`backend` is `"SQLite + SCKG"` or `"SQLite only"`.
41+
6. **`get_context_for_query(query)`** — unified context retrieval
42+
that fans out to SCKG (code knowledge) and Honcho (behavioral
43+
insights) and produces a single `synthesis` string suitable for
44+
LLM prompt injection. Always returns a well-formed dict.
45+
7. **`get_stats()`** — returns `{total_facts, tags, backend, honcho}`
46+
where `backend` is `"SQLite + SCKG"` or `"SQLite only"`, and
47+
`honcho` is the `HonchoBackend.get_status()` payload.
48+
49+
## Honcho Backend (Behavioral Memory)
50+
51+
Honcho is a **behavioral memory** service — it remembers things about
52+
*peers* (users, agents) and their *interactions*: preferences, tone,
53+
recurring mistakes, and peer-specific context. It is the
54+
*complementary* half of the memory stack:
55+
56+
| | SCKG (Code) | Honcho (Behavioral) |
57+
|---|---|---|
58+
| **Stores** | Module / File / Function graph | Conversations, preferences, peer-models |
59+
| **Use** | "Which modules are affected?" | "How does this user react to errors?" |
60+
| **Granularity** | Code | Session, peer |
61+
62+
`HonchoBackend` exposes:
63+
64+
- `is_available()` — cheap, cached connectivity check.
65+
- `get_status()` — `{available, workspace_id, base_url, error}`.
66+
- `get_or_create_peer(name)` / `get_or_create_session(name)`.
67+
- `retain_message(peer, content, role, session, metadata)` — stores a
68+
message; optional session attachment.
69+
- `get_session_context(name)` — dialectic context for a session.
70+
- `chat(peer, query)` — ask the peer a question (dialectic).
71+
- `search(query, peer_name=...)` — semantic search across memory.
72+
73+
### Graceful degradation
74+
75+
Honcho is **optional** in every direction:
76+
77+
- If `honcho-ai` is not installed → `is_available()` returns `False`
78+
and every other method returns `None` / `[]` / `{"error": ...}`.
79+
- If the server is unreachable → same behaviour; the failure is
80+
cached in `_init_error` and surfaced via `get_status()`.
81+
- A 2-second `timeout` is the default; we never want a Honcho outage
82+
to block `retain` / `recall` / agent timeouts.
83+
- `SINMemory` is fully usable with no Honcho at all — `retain` still
84+
writes to SQLite, and `get_context_for_query` returns
85+
`{"synthesis": "No context available.", ...}`.
86+
87+
### Setting up the Honcho server
88+
89+
```bash
90+
# Install (already a transitive dep in many envs)
91+
pip install honcho-ai
92+
93+
# Start the local server (default: http://localhost:8000)
94+
honcho serve
95+
96+
# Or set a custom URL
97+
export HONCHO_BASE_URL="https://honcho.example.com"
98+
```
99+
100+
If you don't run the server, `HonchoBackend` will detect the
101+
connection error on first use and silently turn into a no-op.
102+
103+
### Using the combined stack
104+
105+
```python
106+
from pathlib import Path
107+
from sin_code_bundle.memory import SINMemory
108+
109+
mem = SINMemory(
110+
Path("/path/to/repo"),
111+
honcho_workspace="my-team", # optional, defaults to f"sin-bundle-{repo_name}"
112+
honcho_base_url="http://localhost:8000",
113+
)
114+
115+
# Backend status
116+
print(mem.get_stats())
117+
# → {"total_facts": 0, "tags": [], "backend": "SQLite + SCKG", "honcho": {...}}
118+
119+
# Unified context (SCKG + Honcho + SQLite-synthesis)
120+
ctx = mem.get_context_for_query("How should we handle auth errors?")
121+
print(ctx["synthesis"])
122+
print(ctx["backends"]) # {"sqlite": True, "sckg": True, "honcho": False}
123+
```
36124

37125
## Important config
38126

39127
- `db_path` — defaults to `<repo_root>/.sin_memory.db`. Pass an
40128
explicit path to keep multiple memory stores side-by-side.
41129
- `repo_root` — defaults to `Path.cwd()`. Used as the SCKG root and
42130
as the parent for the default DB path.
131+
- `honcho_workspace` — defaults to `f"sin-bundle-{repo_root.name}".
132+
Distinct workspaces isolate peer/session namespaces in Honcho.
133+
- `honcho_base_url` — defaults to `http://localhost:8000`. Honcho is
134+
treated as a local sidecar in dev.
43135
- `tags` in `retain` — stored as a comma-joined string, so a tag
44136
cannot contain a comma. `recall(tags=[...])` uses
45137
`LIKE '%tag%'` per tag (AND-combined), so substring collisions
@@ -49,26 +141,38 @@ SQLite is always available.
49141

50142
```python
51143
from pathlib import Path
52-
from sin_code_bundle.memory import SINMemory
144+
from sin_code_bundle.memory import SINMemory, HonchoBackend
53145

146+
# Full stack (SCKG + Honcho, if both are installed/reachable)
54147
mem = SINMemory(Path("/path/to/repo"))
55148
result = mem.retain("User prefers TypeScript", tags=["preference"])
56-
print(result["id"], result["stored_in"]) # → "SQLite only" if no SCKG
149+
print(result["id"], result["stored_in"]) # → "SQLite only" or "SQLite + SCKG"
57150

58151
hits = mem.recall("typescript", limit=5)
59152
for h in hits:
60153
print(h["id"], h["fact"], h["tags"])
61154

62155
print(mem.reflect("typescript")["confidence"]) # 0.0 or 0.5
63156
print(mem.get_stats())
157+
158+
# Honcho-only usage (rarely needed; usually go through SINMemory)
159+
hb = HonchoBackend(workspace_id="my-ws")
160+
if hb.is_available():
161+
hb.retain_message("coding-agent", "Likes small PRs", role="user")
162+
print(hb.chat("coding-agent", "What does the user prefer?"))
64163
```
65164

66165
## Known caveats
67166

68167
- Search is **LIKE-based**, not semantic — substring matches only.
69168
SCKG integration exists for *graph* queries, not for vector search.
169+
- Honcho peer models are *evolving* — early sessions yield thin
170+
insights until the peer has seen enough interactions. Don't
171+
treat `behavioral_insights` as authoritative on day one.
70172
- No schema migrations yet — adding columns requires manual ALTER.
71173
- Reflect is intentionally dumb; a real synthesizer (LLM) should
72174
replace it before relying on `reflect()` for decisions.
73175
- SQLite writes are not WAL-mode here; high-frequency retains from
74176
many threads should switch to WAL or move to a real server.
177+
- Honcho is a **sidecar**, not a source of truth. If the server is
178+
wiped, behavioral insights are lost; durable facts live in SQLite.

0 commit comments

Comments
 (0)