Skip to content

th-ebe27d: let a host supply the turn's memory (C#, Python, TS, Go) - #562

Merged
brentrager merged 1 commit into
mainfrom
th-mem-fanout
Aug 30, 2026
Merged

brentrager merged 1 commit into
mainfrom
th-mem-fanout

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

The gap

Rust #330 put memory_for_access on StorageAdapter and had the server runner thread the result into the engine's agent options — the seam that lights up Big Smooth's durable auto-recall. The four sibling servers never did.

What makes this worth fixing now: both ends were already built. All five engine cores implement Memory and already recall relevant entries into context. The only thing missing was the wire between them — so no matter what store a deployment had, every turn on these servers ran without auto-recall. A fully-implemented capability, unreachable.

What changed

Each server now takes a MemoryProvider (IMemoryProvider in C#) with one method — memoryForAccess(access) — resolved per turn and handed to the engine as AgentOptions.memory:

seam how a host installs it
C# IMemoryProvider DI — services.GetService<IMemoryProvider>()
Python MemoryProvider ServerState.memory_provider
TypeScript MemoryProvider serve({ memoryProvider })
Go MemoryProvider WithMemoryProvider(...)

access is threaded exactly as it is for knowledge, so a multi-tenant host can bind memory to the requester's org/user. Single-tenant hosts — Big Smooth's daemon, the reason this seam exists — ignore it, so each language also ships a StaticMemoryProvider over one store.

Go and Python attach the store post-construction (alongside hooks) rather than growing an 11- and 15-parameter constructor further. That is the pattern those files already use, and for this exact reason.

Opting out is the default, and it is tested

No provider — or a provider that returns nothing for this caller — leaves the turn byte-for-byte what it was. That is a test in every language, not a claim. Five per language, named after their Rust counterparts in rust/smooth-operator-server/tests/injection_seams.rs:

  1. no provider ⇒ nothing injected
  2. provider declines this caller ⇒ nothing injected (the seam must not fabricate a store just because one was installed)
  3. attached memory ⇒ recalled into the turn
  4. unrelated message ⇒ recalls nothing (relevance-gated by the engine, not a blanket dump of every stored memory into every turn)
  5. the provider actually receives the caller's access

All four mutation-checked — dropping the single line that hands memory to the engine fails them, so they are not vacuous.

Verification

result
C# 412 passed
Python 389 passed, 21 skipped; ruff clean
TypeScript 43 files passed; tsc clean
Go full go/server suite green

Two things worth knowing

  • The recall block's header text is deliberately not asserted. The five cores inject three different strings for it — Rust [Recalled memories], Python/Go/TS Relevant memory (things you remember about this user/context):, C# Relevant memory: — filed as th-ffaeae. The tests assert the recalled content reaches the model, which is the behavior the seam exists for. A conformance scenario for auto-recall is blocked on unifying that string.
  • The bundled lexical scorer counts raw token overlap with no stopword filter. A single shared "the" is enough to score a hit — my first draft of test 4 failed for exactly that reason. Worth knowing before trusting recall precision in production; it is documented as test-grade in the core.

Notes

🤖 Generated with Claude Code

… servers

Rust #330 put memory_for_access on StorageAdapter and had the server runner
thread the result into the engine's agent options — the seam that lights up Big
Smooth's durable auto-recall. The four sibling servers never did.

The gap was invisible because BOTH ends were already built: all five engine
cores implement Memory and already recall relevant entries into context. What
was missing was the wire between them, so no matter what store a deployment
had, every turn on these servers ran without auto-recall.

Each server now takes a MemoryProvider (IMemoryProvider in C#) with one method,
memory_for_access(access), resolved per turn and passed to the engine as
AgentOptions.memory:

  C#          IMemoryProvider        DI (services.GetService<IMemoryProvider>())
  Python      MemoryProvider         ServerState.memory_provider
  TypeScript  MemoryProvider         serve({ memoryProvider })
  Go          MemoryProvider         WithMemoryProvider(...)

access is threaded exactly as it is for knowledge, so a multi-tenant host can
bind memory to the requester's org/user; single-tenant hosts (Big Smooth's
daemon, the reason the seam exists) ignore it, so each language also ships a
StaticMemoryProvider over one store.

Nothing changes for anyone who does not opt in, and that is a test rather than a
claim: each language asserts BOTH the no-provider and the declining-provider
paths inject nothing, alongside the positive case, a relevance case (an
unrelated message recalls nothing — not a blanket dump of every memory into
every turn), and one proving the access argument actually reaches the provider.

Five tests per language, named after their Rust counterparts in
rust/smooth-operator-server/tests/injection_seams.rs. All four mutation-checked:
dropping the single line that hands memory to the engine fails them.

Go and Python attach the store post-construction (alongside hooks) rather than
growing an 11- and 15-parameter constructor further — the pattern those files
already use for exactly this reason.

Verified: C# 412 passed; Python 389 passed / 21 skipped, ruff clean; TypeScript
43 files passed, tsc clean; Go full server suite green.

Two notes for whoever picks this up next. The recall block's header text is
deliberately NOT asserted — the five cores inject three different strings for it
(th-ffaeae) — so the tests assert the recalled CONTENT reaches the model, which
is the behavior the seam exists for. And the bundled lexical scorer counts raw
token overlap with no stopword filter, so a single shared "the" scores a hit;
worth knowing before trusting recall precision in production.

No wire-protocol change.
@changeset-bot

changeset-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c834fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@smooai/smooth-operator Patch
@smooai/smooth-operator-web-chat-example Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager
brentrager merged commit 6bfc3ed into main Aug 30, 2026
7 checks passed
@brentrager
brentrager deleted the th-mem-fanout branch September 6, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant