Skip to content

feat(sdk): ship the integration layer, so memory is automatic instead of remembered - #24

Merged
rrader26 merged 1 commit into
mainfrom
feat/memory-integration-layer
Sep 1, 2026
Merged

feat(sdk): ship the integration layer, so memory is automatic instead of remembered#24
rrader26 merged 1 commit into
mainfrom
feat/memory-integration-layer

Conversation

@rrader26

@rrader26 rrader26 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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.

const openai = withMemory(new OpenAI(), mm, { subject, sessionId })

Every existing call site is unchanged and now retrieves relevant memory, injects it within a token budget, runs, and writes the turn back.

Coverage

Client How
OpenAI withOpenAI / withMemory — context as a system message
Anthropic withAnthropic — context on system, since {role:'system'} in messages is rejected by that API
Vercel AI SDK memoryMiddleware() for wrapLanguageModel
Anything else MemoryMiddleware — the provider-agnostic core

Client 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

  • Injected context lands after the caller's own system prompt. Their system prompt is their product; memory is context, not policy. A memory block above it reads to the model as the higher-priority instruction.
  • Streaming captures only the user half. The assistant text doesn't 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, rather than appearing to work.

The proxy, and why every function is bound

Provider SDKs use real #private class fields. Those are keyed to the instance, so a method invoked with this set to a proxy throws TypeError: Cannot read private member — and the natural implementation (return methods unbound, or pass the proxy as the Reflect.get receiver) does exactly that, while typechecking perfectly.

There's a test for it against a fake client with a real #private field.

Also here

  • context.forConversation() — 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.
  • Two docstrings corrected because they made false claims:
    • memory.delete was "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.
    • subject was "not needed with text (the engine resolves subjects during extraction)". It did not.

⚠️ Do not publish ahead of the server

Requires POST /memory/context and POST /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.

… 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.
@rrader26
rrader26 merged commit c6f2169 into main Sep 1, 2026
2 checks passed
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.

2 participants