feat(sdk-python): accept scope kwargs on memory set/get/delete and reconcile memory.search#707
Conversation
…rrect README search API Add optional scope/scope_id kwargs to app.memory.set/get/delete so the developer-facing MemoryInterface matches what humans and LLMs naturally guess (app.memory.set(key, data, scope="global")) and mirrors the TypeScript SDK, which already accepts scope positionally. scope=None keeps today's hierarchical behavior unchanged; explicit scopes route to the existing accessor clients. Invalid scopes raise a ValueError listing valid scopes; non-global scopes without a scope_id raise a clear ValueError. No general memory search endpoint exists on the control plane (only /api/v1/memory/vector/search, already exposed as similarity_search), so the README's advertised app.memory.search(...) is corrected to similarity_search and the scope names are corrected from 'agent/run' to 'actor/workflow'.
Performance
⚠ Regression detected:
|
📊 Coverage gateThresholds from
✅ Gate passedNo surface regressed past the allowed threshold and the aggregate stayed above the floor. |
📐 Patch coverage gateThreshold: 80% on lines this PR touches vs
✅ Patch gate passedEvery surface whose lines were touched by this PR has patch coverage at or above the threshold. |
Finish the memory-scope DX work for #712: the developer-facing MemoryInterface.similarity_search now accepts the same optional scope/scope_id kwargs as set/get/delete, dispatching through the shared _resolve_scope_target helper. This matches the TypeScript SDK, whose searchVector already takes scope/scopeId options. scope=None keeps today's behavior exactly. Also loosen ScopedMemoryClient/ScopedMemoryEventClient scope_id type hints to Optional[str] - the context-derived explicit-scope path passes None by design - and apply ruff format to memory_events.py. Verified end-to-end against a local control plane (local mode): set/get/delete with scope kwargs across global/session/workflow, hierarchical get, accessor-style reads, context-derived scope ids, similarity_search with scope="global", and ValueError on invalid scopes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Taking this over to land it.
Testing: all memory tests pass ( Also verified end-to-end against a live local control plane: |
|
recheck |
Fixes #712
Motivation
During tutorial writing, four independent LLM writers all guessed the same non-existent API:
That signature does not exist. The Python
MemoryInterfaceonly exposed scoped access through accessors (app.memory.global_scope.set(...),app.memory.session(id).set(...), etc.), while the TypeScript SDK already acceptsscope(andscopeId) positionally onset/get/delete. When four writers and four models independently reach for the same shape, the API should match the guess. This PR makes the Python API match both the natural guess and the TS SDK.What changed
Three changes, nothing else.
1. Scope kwargs on
MemoryInterface.set/get/deleteset,get, anddeletenow accept optionalscopeandscope_idkeyword arguments.scope=None(default) keeps today's behavior exactly:setuses automatic context scoping,getperforms hierarchical lookup (workflow -> session -> actor -> global),deleteuses the current scope. Zero behavior change for existing code.scope="global"delegates toglobal_scope;scope="session"|"actor"|"workflow"delegates to the corresponding accessor and requiresscope_id.ValueErrorlisting the valid scopes. A non-global scope without ascope_idderives the id from the current execution context headers (same as the accessor path when called mid-execution).GlobalMemoryClient/ScopedMemoryClient) via a single private dispatch helper, so there is one code path.2. Reconcile
memory.searchThe README advertised
app.memory.search(...), which does not exist. Corrected to the real method and fixed the scope names.What was verified against the control plane
control-plane/internal/server/routes_memory.go:31-41registers/memory/set,/memory/get,/memory/delete,/memory/list, and vector routes. The only search endpoint isPOST /memory/vector/search(routes_memory.go:40). There is no general/memory/search.control-plane/internal/server/apicatalog/catalog_entries.go:96-107lists set/get/delete/list plusvector/search("Similarity search over vectors"), and no plain memory search.workflow,session,actor,global—control-plane/internal/handlers/memory.go:273(scopes := []string{"workflow", "session", "actor", "global"}) and thegetScopeIDswitch atcontrol-plane/internal/handlers/memory.go:422-433. There is noagentorrunscope. The server derivesscope_idfrom theX-Workflow-ID/X-Session-ID/X-Actor-IDheaders (resolveScope,memory.go:402-418), which the PythonMemoryClient._build_headersalready sets fromscope_id.sdk/typescript/src/memory/MemoryInterface.ts:41-117—set/get/delete/exists/listKeysall takescopeandscopeIdpositionally, andMemoryScopeis'workflow' | 'session' | 'actor' | 'global'(sdk/typescript/src/types/agent.ts:72).Change 2 outcome: the server endpoint does NOT exist. Per scope, I did not invent a
searchmethod. Instead I fixed the README:app.memory.set() / .get() / .search()->.similarity_search()(the real method onMemoryInterface).app.memory.search(embedding, top_k=5)->app.memory.similarity_search(embedding, top_k=5).Global, agent, session, run->Global, actor, session, workflow(the actual scope names).The low-level Python
MemoryClientalready implementssimilarity_searchagainst/memory/vector/search, so no new SDK method was needed.3. Scope kwargs on
MemoryInterface.similarity_searchTo finish the reconciliation, the developer-facing
similarity_searchaccepts the same optionalscope/scope_idkwargs asset/get/delete, dispatched through the same_resolve_scope_targethelper. This matches the TypeScript SDK'ssearchVector, which already takesscope/scopeIdoptions, and the low-level PythonMemoryClient.similarity_search, which already supported them.scope=Nonekeeps today's behavior exactly.Test plan
New file
sdk/python/tests/test_memory_scope_kwargs.py(all@pytest.mark.unit, mocking the low-level client the same way neighboring memory tests do):scope=Nonedefault behavior unchanged forset/get/deletescope="global"delegation forset/get/delete(andscope_idignored for global)scope="session"|"actor"|"workflow"delegation with explicitscope_idforset/get/deletescope_iddelegate through the current execution contextValueErrorlisting valid scopes (onset/get/delete)scope=Nonedefault behavior unchanged forsimilarity_searchscopekwarg delegation forsimilarity_search(global, session/actor/workflow with and withoutscope_id, invalid scope)Results (run in
sdk/python):ruff checkandruff formatpass on the touched files.Follow-ups filed separately
Out of scope for this PR, noted for later:
Resultcost field parityon_changeparityExecutionFilterversion fieldCross-SDK memory check
set/get/deletealready derived scoped headers from metadata whenscopeIdwas omitted.ExecutionContext, and the control-plane backend intentionally omits empty scope headers without client-side failure.MemoryEventClient.history: event history filtering now derivesscope_idfrom metadata whenscopeIdis omitted, matching the scalar memory client behavior and the control-plane history query contract.Additional verification: