feat(sdk): ship the integration layer, so memory is automatic instead of remembered - #24
Merged
Merged
Conversation
… of remembered
This SDK has 14 resources and ~40 memory methods. It is a very complete API
client and it has no integration layer at all — no OpenAI/Anthropic/AI-SDK
wrapper, no per-turn hook. The examples are demos, not integrations.
That means every customer writes the same forty lines of glue (pull a query
out of the conversation, call search, decide how much fits, prepend a system
message, remember to write the turn back), and memory quality varies with how
well each of them wrote it. The most common version of those forty lines is
none, which is how a real deployment reaches 182k memories against 98
subjects. Handing the hard part to the caller is what makes memory a feature
people have to remember to use rather than something that just works.
const openai = withMemory(new OpenAI(), mm, { subject, sessionId })
Every existing call site is unchanged and now retrieves, injects within a
token budget, runs, and writes the turn back. Anthropic gets the same
treatment (context goes on `system`, since a {role:'system'} entry in
`messages` is rejected by that API), the Vercel AI SDK gets a
wrapLanguageModel middleware, and MemoryMiddleware is the provider-agnostic
core for anything else.
Client detection is structural, not instanceof — depending on three provider
packages to support three provider packages is how an integration layer
becomes unusable to everyone on the fourth.
IT CANNOT BREAK THE HOST'S TURN. Every memory call is wrapped: an outage,
timeout, 5xx or malformed response degrades to "no context this turn" and the
model runs exactly as it would have without any of this. Failures surface
through onError, never as a throw on the caller's request path. That is the
contract, not defensive coding — the moment adding memory can take an app
down, nobody leaves it on.
Two things the wrapper deliberately does not pretend to do. Injected context
lands AFTER the caller's own system prompt, because their system prompt is
their product and memory is context, not policy. And a streaming call captures
only the user half, because the assistant text does not exist when the call
returns and consuming the stream to get it would break the caller's — the
README says so and shows the tee.
Also here:
- context.forConversation(), the client for the new POST /memory/context. The
subject-keyed build() needs you to know WHO before you can ask what we know;
this answers "what should the model know before it replies to this".
- admin.merge(), dry-run by default — the measurement that gates whether
cluster merging is safe to enable server-side.
- Corrected two docstrings that made false claims. `memory.delete` is
PROJECT-scoped, not "one of your own memory items": your API key can delete
any memory in its project, which is right for a backend and wrong behind an
end user's "forget this" button. And `subject` was documented as "not needed
with text (the engine resolves subjects during extraction)", which it did
not do.
Requires the server-side POST /memory/context and /admin/memory/merge
endpoints; do not publish ahead of those. Minor version bump: new public
surface, nothing removed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
This SDK has 14 resources and ~40 memory methods. It is a very complete API client and it has no integration layer at all — no OpenAI/Anthropic/AI-SDK wrapper, no per-turn hook. The examples are demos, not integrations.
So every customer writes the same forty lines of glue: pull a query out of the conversation, call search, decide how much fits, prepend a system message, remember to write the turn back. Memory quality then varies with how well each of them wrote it — and the most common version of those forty lines is none, which is how a real deployment reaches 182k memories against 98 subjects.
Handing the hard part to the caller is what makes memory a feature people have to remember to use, rather than something that works.
Every existing call site is unchanged and now retrieves relevant memory, injects it within a token budget, runs, and writes the turn back.
Coverage
withOpenAI/withMemory— context as a system messagewithAnthropic— context onsystem, since{role:'system'}inmessagesis rejected by that APImemoryMiddleware()forwrapLanguageModelMemoryMiddleware— the provider-agnostic coreClient detection is structural, not
instanceof. Depending on three provider packages in order to support three provider packages is how an integration layer becomes unusable to everyone on the fourth. No new dependencies.It cannot break the host's turn
Every memory call is wrapped. An outage, timeout, 5xx or malformed response degrades to "no context this turn" and the model runs exactly as it would have. Failures surface through
onError, never as a throw on the caller's request path.That's the contract, not defensive coding — the moment adding memory can take an app down, nobody leaves it switched on.
Two things it deliberately does not pretend to do
The proxy, and why every function is bound
Provider SDKs use real
#privateclass fields. Those are keyed to the instance, so a method invoked withthisset to a proxy throwsTypeError: Cannot read private member— and the natural implementation (return methods unbound, or pass the proxy as theReflect.getreceiver) does exactly that, while typechecking perfectly.There's a test for it against a fake client with a real
#privatefield.Also here
context.forConversation()— client for the newPOST /memory/context. The subject-keyedbuild()needs you to know who before you can ask what we know; this answers "what should the model know before it replies to this".admin.merge(), dry-run by default — the measurement that gates whether cluster merging is safe to enable server-side.memory.deletewas"Delete one of your own memory items". It is project-scoped: your API key can delete any memory in its project. Right for a backend, wrong behind an end user's "forget this" button.subjectwas"not needed with text (the engine resolves subjects during extraction)". It did not.Requires
POST /memory/contextandPOST /admin/memory/merge— ThinkfleetAI/memory-thinkfleet#410 and #409.Tests
11 integration checks via
npm run test:integrations— injection, capture of both halves, system-prompt ordering, the private-field trap, and the failure contract in both phases. Runs against fake clients, so no network or key needed.Version 0.9.0 → 0.10.0: new public surface, nothing removed.